Skip to main content

allium_parser/
ast.rs

1//! AST for the Allium specification language.
2//!
3//! The parse tree uses a uniform block-item representation for declaration
4//! bodies: every `name: value`, `keyword: value` and `let name = value` within
5//! braces is a [`BlockItem`]. Semantic classification into entity fields vs
6//! relationships vs derived values, or trigger types, happens in a later pass.
7//!
8//! Expressions are fully typed — the parser produces the rich [`Expr`] tree
9//! directly.
10
11use serde::Serialize;
12
13use crate::Span;
14
15// ---------------------------------------------------------------------------
16// Top level
17// ---------------------------------------------------------------------------
18
19/// A parsed `.allium` file.
20#[derive(Debug, Clone, Serialize)]
21pub struct Module {
22    pub span: Span,
23    /// Extracted from `-- allium: N` if present.
24    pub version: Option<u32>,
25    pub declarations: Vec<Decl>,
26}
27
28// ---------------------------------------------------------------------------
29// Declarations
30// ---------------------------------------------------------------------------
31
32#[derive(Debug, Clone, Serialize)]
33pub enum Decl {
34    Use(UseDecl),
35    Block(BlockDecl),
36    Default(DefaultDecl),
37    Variant(VariantDecl),
38    Deferred(DeferredDecl),
39    OpenQuestion(OpenQuestionDecl),
40    Invariant(InvariantDecl),
41}
42
43/// `use "path" as alias`
44#[derive(Debug, Clone, Serialize)]
45pub struct UseDecl {
46    pub span: Span,
47    pub path: StringLiteral,
48    pub alias: Option<Ident>,
49}
50
51/// A named or anonymous block: `entity User { ... }`, `config { ... }`, etc.
52#[derive(Debug, Clone, Serialize)]
53pub struct BlockDecl {
54    pub span: Span,
55    pub kind: BlockKind,
56    /// `None` for `given` and local `config` blocks.
57    pub name: Option<Ident>,
58    pub items: Vec<BlockItem>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
62pub enum BlockKind {
63    Entity,
64    ExternalEntity,
65    Value,
66    Enum,
67    Given,
68    Config,
69    Rule,
70    Surface,
71    Actor,
72    Contract,
73    Invariant,
74}
75
76/// `default [Type] name = value`
77#[derive(Debug, Clone, Serialize)]
78pub struct DefaultDecl {
79    pub span: Span,
80    pub type_name: Option<Ident>,
81    pub name: Ident,
82    pub value: Expr,
83}
84
85/// `variant Name : Type { ... }`
86#[derive(Debug, Clone, Serialize)]
87pub struct VariantDecl {
88    pub span: Span,
89    pub name: Ident,
90    pub base: Expr,
91    pub items: Vec<BlockItem>,
92}
93
94/// `deferred Name.field` / `deferred alias/Name.field`, with an optional
95/// trailing quoted location hint: `deferred Foo.bar "detailed/foo.allium"`.
96///
97/// The path is constrained to a dotted name with an optional `alias/Name`
98/// qualifier, but stays an [`Expr`] (`Ident`, `QualifiedName`, or
99/// `MemberAccess` chains over them) so qualified-reference collection and the
100/// WASM AST mirror consume it unchanged.
101#[derive(Debug, Clone, Serialize)]
102pub struct DeferredDecl {
103    pub span: Span,
104    pub path: Expr,
105    pub location_hint: Option<StringLiteral>,
106}
107
108/// `open question "text"`
109#[derive(Debug, Clone, Serialize)]
110pub struct OpenQuestionDecl {
111    pub span: Span,
112    pub text: StringLiteral,
113}
114
115/// `invariant Name { expr }` — top-level expression-bearing invariant
116#[derive(Debug, Clone, Serialize)]
117pub struct InvariantDecl {
118    pub span: Span,
119    pub name: Ident,
120    pub body: Expr,
121}
122
123// ---------------------------------------------------------------------------
124// Transition graphs (v3)
125// ---------------------------------------------------------------------------
126
127/// A directed edge in a transition graph: `from -> to`.
128#[derive(Debug, Clone, Serialize)]
129pub struct TransitionEdge {
130    pub span: Span,
131    pub from: Ident,
132    pub to: Ident,
133}
134
135/// A transition graph block: `transitions field_name { edges..., terminal: states }`.
136#[derive(Debug, Clone, Serialize)]
137pub struct TransitionGraph {
138    pub span: Span,
139    pub field: Ident,
140    pub edges: Vec<TransitionEdge>,
141    pub terminal: Vec<Ident>,
142}
143
144// ---------------------------------------------------------------------------
145// When clauses (v3)
146// ---------------------------------------------------------------------------
147
148/// A `when` clause on a field declaration: `when status = shipped | delivered`.
149#[derive(Debug, Clone, Serialize)]
150pub struct WhenClause {
151    pub span: Span,
152    pub status_field: Ident,
153    pub qualifying_states: Vec<Ident>,
154}
155
156// ---------------------------------------------------------------------------
157// Block items — uniform representation for declaration bodies
158// ---------------------------------------------------------------------------
159
160#[derive(Debug, Clone, Serialize)]
161pub struct BlockItem {
162    pub span: Span,
163    pub kind: BlockItemKind,
164}
165
166#[derive(Debug, Clone, Serialize)]
167pub enum BlockItemKind {
168    /// `keyword: value` — when:, requires:, ensures:, facing:, etc.
169    Clause { keyword: String, value: Expr },
170    /// `name: value` — field, relationship, projection, derived value.
171    Assignment { name: Ident, value: Expr },
172    /// `name(params): value` — parameterised derived value.
173    ParamAssignment {
174        name: Ident,
175        params: Vec<Ident>,
176        value: Expr,
177    },
178    /// `let name = value`
179    Let { name: Ident, value: Expr },
180    /// Bare name inside an enum body — `pending`, `shipped`, `` `de-CH-1996` ``, etc.
181    EnumVariant { name: Ident, backtick_quoted: bool },
182    /// `for binding in collection [where filter]: ...` at block level (rule iteration)
183    ForBlock {
184        binding: ForBinding,
185        collection: Expr,
186        filter: Option<Expr>,
187        items: Vec<BlockItem>,
188    },
189    /// `if condition: ... else if ...: ... else: ...` at block level
190    IfBlock {
191        branches: Vec<CondBlockBranch>,
192        else_items: Option<Vec<BlockItem>>,
193    },
194    /// `Shard.shard_cache: value` — dot-path reverse relationship
195    PathAssignment { path: Expr, value: Expr },
196    /// `open question "text"` (nested within a block)
197    OpenQuestion { text: StringLiteral },
198    /// `contracts:` clause in a surface body
199    ContractsClause {
200        entries: Vec<ContractBinding>,
201    },
202    /// `@invariant`, `@guidance`, `@guarantee` prose annotation
203    Annotation(Annotation),
204    /// `invariant Name { expr }` inside an entity/value block
205    InvariantBlock { name: Ident, body: Expr },
206    /// `transitions field { ... }` — transition graph declaration inside an entity
207    TransitionsBlock(TransitionGraph),
208    /// `name: Type when status_field = state1 | state2` — field with lifecycle-dependent presence
209    FieldWithWhen {
210        name: Ident,
211        value: Expr,
212        when_clause: WhenClause,
213    },
214}
215
216// ---------------------------------------------------------------------------
217// Contract bindings (ALP-15)
218// ---------------------------------------------------------------------------
219
220/// Direction marker for contract bindings in surfaces.
221#[derive(Debug, Clone, Serialize)]
222pub enum ContractDirection {
223    Demands,
224    Fulfils,
225}
226
227/// A single entry in a `contracts:` clause.
228#[derive(Debug, Clone, Serialize)]
229pub struct ContractBinding {
230    pub direction: ContractDirection,
231    /// Module alias when the contract is imported from another spec
232    /// (`fulfils base/MyContract`), mirroring `QualifiedName`.
233    pub qualifier: Option<String>,
234    pub name: Ident,
235    pub span: Span,
236}
237
238// ---------------------------------------------------------------------------
239// Annotations (ALP-16)
240// ---------------------------------------------------------------------------
241
242/// Prose annotation kinds.
243#[derive(Debug, Clone, Serialize)]
244pub enum AnnotationKind {
245    Invariant,
246    Guidance,
247    Guarantee,
248}
249
250/// A prose annotation: `@invariant Name`, `@guidance`, `@guarantee Name`.
251#[derive(Debug, Clone, Serialize)]
252pub struct Annotation {
253    pub kind: AnnotationKind,
254    pub name: Option<Ident>,
255    pub body: Vec<String>,
256    pub span: Span,
257}
258
259// ---------------------------------------------------------------------------
260// Expressions
261// ---------------------------------------------------------------------------
262
263#[derive(Debug, Clone, Serialize)]
264pub enum Expr {
265    /// `identifier` or `_`
266    Ident(Ident),
267
268    /// `"text"` possibly with `{interpolation}`
269    StringLiteral(StringLiteral),
270
271    /// `` `de-CH-1996` `` — backtick-quoted enum literal
272    BacktickLiteral { span: Span, value: String },
273
274    /// `42`, `100_000`, `3.14`
275    NumberLiteral { span: Span, value: String },
276
277    /// `true`, `false`
278    BoolLiteral { span: Span, value: bool },
279
280    /// `null`
281    Null { span: Span },
282
283    /// `now`
284    Now { span: Span },
285
286    /// `this`
287    This { span: Span },
288
289    /// `within`
290    Within { span: Span },
291
292    /// `24.hours`, `7.days`
293    DurationLiteral { span: Span, value: String },
294
295    /// `{ a, b, c }` — set literal
296    SetLiteral { span: Span, elements: Vec<Expr> },
297
298    /// `{ key: value, ... }` — object literal
299    ObjectLiteral { span: Span, fields: Vec<NamedArg> },
300
301    /// `Set<T>`, `List<T>` — generic type annotation
302    GenericType {
303        span: Span,
304        name: Box<Expr>,
305        args: Vec<Expr>,
306    },
307
308    /// `a.b`
309    MemberAccess {
310        span: Span,
311        object: Box<Expr>,
312        field: Ident,
313    },
314
315    /// `a?.b`
316    OptionalAccess {
317        span: Span,
318        object: Box<Expr>,
319        field: Ident,
320    },
321
322    /// `a ?? b`
323    NullCoalesce {
324        span: Span,
325        left: Box<Expr>,
326        right: Box<Expr>,
327    },
328
329    /// `func(args)` or `entity.method(args)`
330    Call {
331        span: Span,
332        function: Box<Expr>,
333        args: Vec<CallArg>,
334    },
335
336    /// `Entity{field1, field2}` or `Entity{field: value}`
337    JoinLookup {
338        span: Span,
339        entity: Box<Expr>,
340        fields: Vec<JoinField>,
341    },
342
343    /// `a + b`, `a - b`, `a * b`, `a / b`
344    BinaryOp {
345        span: Span,
346        left: Box<Expr>,
347        op: BinaryOp,
348        right: Box<Expr>,
349    },
350
351    /// `a = b`, `a != b`, `a < b`, `a <= b`, `a > b`, `a >= b`
352    Comparison {
353        span: Span,
354        left: Box<Expr>,
355        op: ComparisonOp,
356        right: Box<Expr>,
357    },
358
359    /// `a and b`, `a or b`
360    LogicalOp {
361        span: Span,
362        left: Box<Expr>,
363        op: LogicalOp,
364        right: Box<Expr>,
365    },
366
367    /// `not expr`
368    Not { span: Span, operand: Box<Expr> },
369
370    /// `x in collection`
371    In {
372        span: Span,
373        element: Box<Expr>,
374        collection: Box<Expr>,
375    },
376
377    /// `x not in collection`
378    NotIn {
379        span: Span,
380        element: Box<Expr>,
381        collection: Box<Expr>,
382    },
383
384    /// `exists expr`
385    Exists { span: Span, operand: Box<Expr> },
386
387    /// `not exists expr`
388    NotExists { span: Span, operand: Box<Expr> },
389
390    /// `collection where condition`
391    Where {
392        span: Span,
393        source: Box<Expr>,
394        condition: Box<Expr>,
395    },
396
397    /// `collection with predicate` (in relationship declarations)
398    With {
399        span: Span,
400        source: Box<Expr>,
401        predicate: Box<Expr>,
402    },
403
404    /// `a | b` — pipe, used for inline enums and sum type discriminators
405    Pipe {
406        span: Span,
407        left: Box<Expr>,
408        right: Box<Expr>,
409    },
410
411    /// `x => body`
412    Lambda {
413        span: Span,
414        param: Box<Expr>,
415        body: Box<Expr>,
416    },
417
418    /// `if cond: a else if cond: b else: c`
419    Conditional {
420        span: Span,
421        branches: Vec<CondBranch>,
422        else_body: Option<Box<Expr>>,
423    },
424
425    /// `for x in collection [where cond]: body`
426    For {
427        span: Span,
428        binding: ForBinding,
429        collection: Box<Expr>,
430        filter: Option<Box<Expr>>,
431        body: Box<Expr>,
432    },
433
434    /// `collection where cond -> field` — projection mapping
435    ProjectionMap {
436        span: Span,
437        source: Box<Expr>,
438        field: Ident,
439    },
440
441    /// `Entity.status transitions_to state`
442    TransitionsTo {
443        span: Span,
444        subject: Box<Expr>,
445        new_state: Box<Expr>,
446    },
447
448    /// `Entity.status becomes state`
449    Becomes {
450        span: Span,
451        subject: Box<Expr>,
452        new_state: Box<Expr>,
453    },
454
455    /// `name: expr` — binding inside a clause value (when triggers, facing, context)
456    Binding {
457        span: Span,
458        name: Ident,
459        value: Box<Expr>,
460    },
461
462    /// `action when condition` — guard on a provides/related item
463    WhenGuard {
464        span: Span,
465        action: Box<Expr>,
466        condition: Box<Expr>,
467    },
468
469    /// `T?` — optional type annotation
470    TypeOptional {
471        span: Span,
472        inner: Box<Expr>,
473    },
474
475    /// `let name = value` inside an expression block (ensures, provides, etc.)
476    LetExpr {
477        span: Span,
478        name: Ident,
479        value: Box<Expr>,
480    },
481
482    /// `oauth/Session` — qualified name with module prefix
483    QualifiedName(QualifiedName),
484
485    /// A sequence of expressions from a multi-line block.
486    Block { span: Span, items: Vec<Expr> },
487
488}
489
490impl Expr {
491    pub fn span(&self) -> Span {
492        match self {
493            Expr::Ident(id) => id.span,
494            Expr::StringLiteral(s) => s.span,
495            Expr::BacktickLiteral { span, .. }
496            | Expr::NumberLiteral { span, .. }
497            | Expr::BoolLiteral { span, .. }
498            | Expr::Null { span }
499            | Expr::Now { span }
500            | Expr::This { span }
501            | Expr::Within { span }
502            | Expr::DurationLiteral { span, .. }
503            | Expr::SetLiteral { span, .. }
504            | Expr::ObjectLiteral { span, .. }
505            | Expr::GenericType { span, .. }
506            | Expr::MemberAccess { span, .. }
507            | Expr::OptionalAccess { span, .. }
508            | Expr::NullCoalesce { span, .. }
509            | Expr::Call { span, .. }
510            | Expr::JoinLookup { span, .. }
511            | Expr::BinaryOp { span, .. }
512            | Expr::Comparison { span, .. }
513            | Expr::LogicalOp { span, .. }
514            | Expr::Not { span, .. }
515            | Expr::In { span, .. }
516            | Expr::NotIn { span, .. }
517            | Expr::Exists { span, .. }
518            | Expr::NotExists { span, .. }
519            | Expr::Where { span, .. }
520            | Expr::With { span, .. }
521            | Expr::Pipe { span, .. }
522            | Expr::Lambda { span, .. }
523            | Expr::Conditional { span, .. }
524            | Expr::For { span, .. }
525            | Expr::ProjectionMap { span, .. }
526            | Expr::TransitionsTo { span, .. }
527            | Expr::Becomes { span, .. }
528            | Expr::Binding { span, .. }
529            | Expr::WhenGuard { span, .. }
530            | Expr::TypeOptional { span, .. }
531            | Expr::LetExpr { span, .. }
532            | Expr::Block { span, .. } => *span,
533            Expr::QualifiedName(q) => q.span,
534        }
535    }
536}
537
538#[derive(Debug, Clone, Serialize)]
539pub struct CondBranch {
540    pub span: Span,
541    pub condition: Expr,
542    pub body: Expr,
543}
544
545/// A branch of a block-level `if`/`else if` chain.
546#[derive(Debug, Clone, Serialize)]
547pub struct CondBlockBranch {
548    pub span: Span,
549    pub condition: Expr,
550    pub items: Vec<BlockItem>,
551}
552
553/// Binding in a `for` loop — either a single identifier or a
554/// destructured tuple like `(a, b)`.
555#[derive(Debug, Clone, Serialize)]
556pub enum ForBinding {
557    Single(Ident),
558    Destructured(Vec<Ident>, Span),
559}
560
561// ---------------------------------------------------------------------------
562// Shared types
563// ---------------------------------------------------------------------------
564
565#[derive(Debug, Clone, Serialize)]
566pub struct Ident {
567    pub span: Span,
568    pub name: String,
569}
570
571#[derive(Debug, Clone, Serialize)]
572pub struct QualifiedName {
573    pub span: Span,
574    pub qualifier: Option<String>,
575    pub name: String,
576}
577
578#[derive(Debug, Clone, Serialize)]
579pub struct StringLiteral {
580    pub span: Span,
581    pub parts: Vec<StringPart>,
582}
583
584impl StringLiteral {
585    /// Extract the plain text content, dropping any interpolation segments.
586    /// For use paths this is safe since interpolation in `use` declarations is
587    /// not a supported pattern.
588    pub fn text(&self) -> String {
589        let mut s = String::new();
590        for part in &self.parts {
591            if let StringPart::Text(t) = part {
592                s.push_str(t);
593            }
594        }
595        s
596    }
597}
598
599#[derive(Debug, Clone, Serialize)]
600pub enum StringPart {
601    Text(String),
602    Interpolation(Ident),
603}
604
605#[derive(Debug, Clone, Serialize)]
606pub struct NamedArg {
607    pub span: Span,
608    pub name: Ident,
609    pub value: Expr,
610}
611
612#[derive(Debug, Clone, Serialize)]
613pub enum CallArg {
614    Positional(Expr),
615    Named(NamedArg),
616}
617
618#[derive(Debug, Clone, Serialize)]
619pub struct JoinField {
620    pub span: Span,
621    pub field: Ident,
622    /// If absent, matches a local variable with the same name.
623    pub value: Option<Expr>,
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
627pub enum BinaryOp {
628    Add,
629    Sub,
630    Mul,
631    Div,
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
635pub enum ComparisonOp {
636    Eq,
637    NotEq,
638    Lt,
639    LtEq,
640    Gt,
641    GtEq,
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
645pub enum LogicalOp {
646    And,
647    Or,
648    Implies,
649}