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