// Cedar parser -- policy-level recursive descent with ArrayView threading.
///|
/// Parse one or more Cedar policies from source text.
/// Each policy is validated immediately after parsing.
pub fn parse_policies(src : String) -> Array[@ast.Policy] raise ParseError {
let tokens = tokenize(src)
let policies : Array[@ast.Policy] = []
let mut auto_id = 0
let _ = for rest = tokens[:] {
match rest {
[{ kind: EOF, .. }] => break rest
_ => {
let (raw, rest) = parse_policy(rest)
let mut policy = raw
if policy.id == "" {
let n = if auto_id == 0 { "" } else { auto_id.to_string() }
policy = { ..policy, id: "policy\{n}" }
auto_id = auto_id + 1
}
policies.push(policy)
continue rest
}
}
}
policies
}
///|
/// Parse a single policy: @annotations effect(principal, action, resource) conditions;
fn parse_policy(
tokens : ArrayView[Token],
) -> (@ast.Policy, ArrayView[Token]) raise ParseError {
// annotations: @key(value) @key ...
let (anns, rest) = parse_annotations(tokens)
// Extract @id annotation as the policy id, remove from annotations
let (id, anns) = extract_id(anns)
// effect: permit | forbid
let (effect, rest) = parse_effect(rest)
// scope: ( principal, action, resource )
guard rest is [{ kind: Bracket('('), .. }, .. rest] else {
raise ParseError("expected '(' after effect", rest[0].pos)
}
let (principal, rest) = parse_scope_principal(rest)
guard rest is [{ kind: Symbol(","), .. }, .. rest] else {
raise ParseError("expected ','", rest[0].pos)
}
let (action, rest) = parse_scope_action(rest)
guard rest is [{ kind: Symbol(","), .. }, .. rest] else {
raise ParseError("expected ','", rest[0].pos)
}
let (resource, rest) = parse_scope_resource(rest)
// optional trailing comma
let rest = if rest is [{ kind: Symbol(","), .. }, .. rest] {
rest
} else {
rest
}
guard rest is [{ kind: Bracket(')'), .. }, .. rest] else {
raise ParseError("expected ')'", rest[0].pos)
}
// conditions
let (conds, rest) = parse_conditions(rest)
// terminator
guard rest is [{ kind: Symbol(";"), .. }, .. rest] else {
raise ParseError("expected ';'", rest[0].pos)
}
let policy : @ast.Policy = {
id,
effect,
annotations: anns,
principal,
action,
resource,
conditions: conds,
}
// Semantic validation: check scope constraint validity per PARC position
@ast.validate_policy(policy) catch {
@ast.ValidationError(msg, _) =>
raise ParseError(msg, @ast.Position::{
filename: "",
offset: 0,
line: 0,
column: 0,
})
}
(policy, rest)
}
// ---------------------------------------------------------------------------
// Annotations: @key(value) @key ...
// ---------------------------------------------------------------------------
///|
fn parse_annotations(
tokens : ArrayView[Token],
) -> (Array[@ast.Annotation], ArrayView[Token]) raise ParseError {
let annotations : Array[@ast.Annotation] = []
let rest = for rest = tokens {
match rest {
[{ kind: Symbol("@"), .. }, .. rest] => {
let (ann, rest) = parse_annotation(rest)
annotations.push(ann)
continue rest
}
_ => break rest
}
}
(annotations, rest)
}
///|
fn parse_annotation(
tokens : ArrayView[Token],
) -> (@ast.Annotation, ArrayView[Token]) raise ParseError {
guard tokens is [head, .. rest] else {
raise ParseError("expected annotation key", tokens[0].pos)
}
let key = match head.kind {
Ident(k) => k
Keyword(k) => keyword_string(k)
_ => raise ParseError("expected annotation key", head.pos)
}
match rest {
[{ kind: Bracket('('), .. }, .. rest] => {
guard rest is [{ kind: String(v), .. }, .. r3] else {
raise ParseError("expected annotation string value", rest[0].pos)
}
guard r3 is [{ kind: Bracket(')'), .. }, .. r4] else {
raise ParseError("expected ')' in annotation", r3[0].pos)
}
({ key, value: v }, r4)
}
_ => ({ key, value: "" }, rest)
}
}
///|
/// Extract @id annotation value as the policy id, removing it from annotations.
/// Returns ("", anns) if no @id annotation is present.
fn extract_id(
anns : Array[@ast.Annotation],
) -> (String, Array[@ast.Annotation]) {
let mut matched_id = ""
let filtered = []
for ann in anns {
if ann.key == "id" && matched_id == "" {
matched_id = ann.value
} else {
filtered.push(ann)
}
}
(matched_id, filtered)
}
// ---------------------------------------------------------------------------
// Effect: permit | forbid
// ---------------------------------------------------------------------------
///|
fn parse_effect(
tokens : ArrayView[Token],
) -> (@ast.PolicyEffect, ArrayView[Token]) raise ParseError {
guard tokens is [head, .. rest] else {
raise ParseError("expected 'permit' or 'forbid'", tokens[0].pos)
}
match head.kind {
Keyword(Permit) => (@ast.Permit, rest)
Keyword(Forbid) => (@ast.Forbid, rest)
_ => raise ParseError("expected 'permit' or 'forbid'", head.pos)
}
}
// ---------------------------------------------------------------------------
// Scope: principal, action, resource
// ---------------------------------------------------------------------------
///|
fn parse_scope_principal(
tokens : ArrayView[Token],
) -> (@ast.ScopeConstraint, ArrayView[Token]) raise ParseError {
guard tokens is [{ kind: Ident("principal"), .. }, .. rest] else {
raise ParseError("expected 'principal'", tokens[0].pos)
}
parse_scope_op(rest)
}
///|
fn parse_scope_action(
tokens : ArrayView[Token],
) -> (@ast.ScopeConstraint, ArrayView[Token]) raise ParseError {
guard tokens is [{ kind: Ident("action"), .. }, .. rest] else {
raise ParseError("expected 'action'", tokens[0].pos)
}
parse_scope_op(rest)
}
///|
fn parse_scope_resource(
tokens : ArrayView[Token],
) -> (@ast.ScopeConstraint, ArrayView[Token]) raise ParseError {
guard tokens is [{ kind: Ident("resource"), .. }, .. rest] else {
raise ParseError("expected 'resource'", tokens[0].pos)
}
parse_scope_op(rest)
}
///|
fn parse_scope_op(
tokens : ArrayView[Token],
) -> (@ast.ScopeConstraint, ArrayView[Token]) raise ParseError {
match tokens {
[{ kind: Op(Eq), .. }, .. rest] => {
let (entity, rest) = parse_entity_uid(rest)
(@ast.Eq(entity), rest)
}
[{ kind: Keyword(In), .. }, .. rest] =>
// InSet: action in [EntityUID, EntityUID, ...]
if rest.length() > 0 && rest[0].kind == Bracket('[') {
let (entities, rest) = parse_entity_uid_list(rest[1:])
(@ast.InSet(entities), rest)
} else {
let (entity, rest) = parse_entity_uid(rest)
(@ast.In(entity), rest)
}
[{ kind: Keyword(Is), .. }, .. rest] => {
let (etype, rest) = parse_entity_type(rest)
match rest {
[{ kind: Keyword(In), .. }, .. rest] => {
let (entity, rest) = parse_entity_uid(rest)
(@ast.IsIn(etype, entity), rest)
}
_ => (@ast.Is(etype), rest)
}
}
_ => (@ast.All, tokens)
}
}
// ---------------------------------------------------------------------------
// Entity UIDs and types
// ---------------------------------------------------------------------------
///|
fn parse_entity_uid(
tokens : ArrayView[Token],
) -> (@ast.EntityUID, ArrayView[Token]) raise ParseError {
guard tokens is [{ kind: Ident(first), .. }, .. rest] else {
raise ParseError("expected entity type (uppercase)", tokens[0].pos)
}
let mut etype = first
for rest = rest {
match rest {
[{ kind: Symbol("::"), .. }, .. rest] =>
match rest {
[{ kind: Ident(next), .. }, .. r3] => {
etype = "\{etype}::\{next}"
continue r3
}
[{ kind: String(id), .. }, .. r3] => return ({ type_: etype, id }, r3)
[t, ..] => raise ParseError("expected type or id after ::", t.pos)
[] => raise ParseError("expected type or id after ::", tokens[0].pos)
}
_ =>
raise ParseError("expected '::' in entity UID", @ast.Position::{
filename: "",
offset: 0,
line: 0,
column: 0,
})
}
}
raise ParseError("expected '::' in entity UID", @ast.Position::{
filename: "",
offset: 0,
line: 0,
column: 0,
})
}
///|
/// Parse a comma-separated list of entity UIDs: EntityUID, EntityUID, ... ]
/// The '[' is already consumed by the caller.
fn parse_entity_uid_list(
tokens : ArrayView[Token],
) -> (Array[@ast.EntityUID], ArrayView[Token]) raise ParseError {
let entities : Array[@ast.EntityUID] = []
let rest = for rest = tokens {
match rest {
[{ kind: Bracket(']'), .. }, .. r1] => break r1
_ => {
let (entity, r2) = parse_entity_uid(rest)
entities.push(entity)
continue match r2 {
[{ kind: Symbol(","), .. }, .. r3] => r3
_ => r2
}
}
}
}
(entities, rest)
}
// ---------------------------------------------------------------------------
// Conditions
// ---------------------------------------------------------------------------
///|
fn parse_conditions(
tokens : ArrayView[Token],
) -> (Array[@ast.Condition], ArrayView[Token]) raise ParseError {
let conds : Array[@ast.Condition] = []
let rest = for rest = tokens {
match rest {
[{ kind: Keyword(When), .. }, .. rest] => {
let (expr, rest) = parse_cond_body(rest)
conds.push({ kind: @ast.When, body: expr })
continue rest
}
[{ kind: Keyword(Unless), .. }, .. rest] => {
let (expr, rest) = parse_cond_body(rest)
conds.push({ kind: @ast.Unless, body: expr })
continue rest
}
_ => break rest
}
}
(conds, rest)
}
///|
fn parse_cond_body(
tokens : ArrayView[Token],
) -> (@ast.Expr, ArrayView[Token]) raise ParseError {
guard tokens is [{ kind: Bracket('{'), .. }, .. rest] else {
raise ParseError("expected '{' in condition", tokens[0].pos)
}
let (expr, rest) = parse_expr(rest)
if rest.length() > 0 && rest[0].kind == Bracket('}') {
(expr, rest[1:])
} else {
raise ParseError(
"expected '}' after condition",
if rest.length() > 0 {
rest[0].pos
} else {
tokens[0].pos
},
)
}
}
///|
fn keyword_string(k : Keyword) -> String {
match k {
Permit => "permit"
Forbid => "forbid"
When => "when"
Unless => "unless"
In => "in"
Has => "has"
Like => "like"
Is => "is"
If => "if"
Then => "then"
Else => "else"
}
}