Skip to main content

allium_parser/
parser.rs

1//! Recursive descent parser for Allium.
2//!
3//! Expressions use a Pratt parser (precedence climbing). Declarations and block
4//! bodies use direct recursive descent. Multi-line clause values are detected
5//! by comparing the line/column of the next token against the clause keyword.
6
7use crate::ast::*;
8use crate::diagnostic::Diagnostic;
9use crate::lexer::{lex, SourceMap, Token, TokenKind};
10use crate::Span;
11
12// ---------------------------------------------------------------------------
13// Public API
14// ---------------------------------------------------------------------------
15
16#[derive(Debug)]
17pub struct ParseResult {
18    pub module: Module,
19    pub diagnostics: Vec<Diagnostic>,
20}
21
22/// Parse an Allium source file into a [`Module`].
23pub fn parse(source: &str) -> ParseResult {
24    let tokens = lex(source);
25    let source_map = SourceMap::new(source);
26    let mut p = Parser {
27        source,
28        tokens,
29        pos: 0,
30        source_map,
31        diagnostics: Vec::new(),
32    };
33    let module = p.parse_module();
34    ParseResult {
35        module,
36        diagnostics: p.diagnostics,
37    }
38}
39
40// ---------------------------------------------------------------------------
41// Parser state
42// ---------------------------------------------------------------------------
43
44struct Parser<'s> {
45    source: &'s str,
46    tokens: Vec<Token>,
47    pos: usize,
48    source_map: SourceMap,
49    diagnostics: Vec<Diagnostic>,
50}
51
52// ---------------------------------------------------------------------------
53// Navigation helpers
54// ---------------------------------------------------------------------------
55
56impl<'s> Parser<'s> {
57    fn peek(&self) -> Token {
58        self.tokens[self.pos]
59    }
60
61    fn peek_kind(&self) -> TokenKind {
62        self.tokens[self.pos].kind
63    }
64
65    fn peek_at(&self, offset: usize) -> Token {
66        let idx = (self.pos + offset).min(self.tokens.len() - 1);
67        self.tokens[idx]
68    }
69
70    fn advance(&mut self) -> Token {
71        let tok = self.tokens[self.pos];
72        if tok.kind != TokenKind::Eof {
73            self.pos += 1;
74        }
75        tok
76    }
77
78    fn at(&self, kind: TokenKind) -> bool {
79        self.peek_kind() == kind
80    }
81
82    fn at_eof(&self) -> bool {
83        self.at(TokenKind::Eof)
84    }
85
86    fn eat(&mut self, kind: TokenKind) -> Option<Token> {
87        if self.at(kind) {
88            Some(self.advance())
89        } else {
90            None
91        }
92    }
93
94    fn expect(&mut self, kind: TokenKind) -> Option<Token> {
95        if self.at(kind) {
96            Some(self.advance())
97        } else {
98            self.error(
99                self.peek().span,
100                format!("expected {kind}, found {}", self.peek_kind()),
101            );
102            None
103        }
104    }
105
106    fn text(&self, span: Span) -> &'s str {
107        &self.source[span.start..span.end]
108    }
109
110    fn line_of(&self, span: Span) -> u32 {
111        self.source_map.line_col(span.start).0
112    }
113
114    fn col_of(&self, span: Span) -> u32 {
115        self.source_map.line_col(span.start).1
116    }
117
118    fn error(&mut self, span: Span, msg: impl Into<String>) {
119        self.diagnostics.push(Diagnostic::error(span, msg));
120    }
121
122    /// Consume and return an [`Ident`] from any word token.
123    fn parse_ident(&mut self) -> Option<Ident> {
124        self.parse_ident_in("identifier")
125    }
126
127    /// Consume and return an [`Ident`] with a context-specific label for errors.
128    fn parse_ident_in(&mut self, context: &str) -> Option<Ident> {
129        let tok = self.peek();
130        if tok.kind.is_word() {
131            self.advance();
132            Some(Ident {
133                span: tok.span,
134                name: self.text(tok.span).to_string(),
135            })
136        } else {
137            self.error(
138                tok.span,
139                format!("expected {context}, found {}", tok.kind),
140            );
141            None
142        }
143    }
144
145    /// Consume a string token and produce a [`StringLiteral`].
146    fn parse_string(&mut self) -> Option<StringLiteral> {
147        let tok = self.expect(TokenKind::String)?;
148        let raw = self.text(tok.span);
149        // Strip surrounding quotes
150        let inner = &raw[1..raw.len() - 1];
151        let parts = parse_string_parts(inner, tok.span.start + 1);
152        Some(StringLiteral {
153            span: tok.span,
154            parts,
155        })
156    }
157}
158
159/// Split the inner content of a string literal into text and interpolation
160/// parts. `base_offset` is the byte offset of the first character after the
161/// opening quote in the source file.
162fn parse_string_parts(inner: &str, base_offset: usize) -> Vec<StringPart> {
163    let mut parts = Vec::new();
164    let mut buf = String::new();
165    let bytes = inner.as_bytes();
166    let mut i = 0;
167    while i < bytes.len() {
168        if bytes[i] == b'\\' && i + 1 < bytes.len() {
169            buf.push(bytes[i + 1] as char);
170            i += 2;
171        } else if bytes[i] == b'{' {
172            if !buf.is_empty() {
173                parts.push(StringPart::Text(std::mem::take(&mut buf)));
174            }
175            i += 1; // skip {
176            let start = i;
177            while i < bytes.len() && bytes[i] != b'}' {
178                i += 1;
179            }
180            let name = std::str::from_utf8(&bytes[start..i]).unwrap_or("").to_string();
181            let span_start = base_offset + start;
182            let span_end = base_offset + i;
183            parts.push(StringPart::Interpolation(Ident {
184                span: Span::new(span_start, span_end),
185                name,
186            }));
187            if i < bytes.len() {
188                i += 1; // skip }
189            }
190        } else {
191            buf.push(bytes[i] as char);
192            i += 1;
193        }
194    }
195    if !buf.is_empty() {
196        parts.push(StringPart::Text(buf));
197    }
198    parts
199}
200
201// ---------------------------------------------------------------------------
202// Clause-keyword recognition
203// ---------------------------------------------------------------------------
204
205/// Returns true for identifiers that act as clause keywords inside blocks.
206/// These are parsed as `Clause` items rather than `Assignment` items.
207fn is_clause_keyword(text: &str) -> bool {
208    matches!(
209        text,
210        "when"
211            | "requires"
212            | "ensures"
213            | "trigger"
214            | "facing"
215            | "context"
216            | "exposes"
217            | "provides"
218            | "related"
219            | "timeout"
220            | "guarantee"
221            | "guidance"
222            | "identified_by"
223            | "within"
224            | "tags"
225            | "invariant"
226    )
227}
228
229/// True for clause keywords whose value can start with a `name: expr` binding.
230fn clause_allows_binding(keyword: &str) -> bool {
231    matches!(keyword, "when")
232}
233
234/// True for keywords that use `keyword name: value` syntax (no colon after the
235/// keyword). These directly embed a binding.
236fn is_binding_clause_keyword(text: &str) -> bool {
237    matches!(text, "facing" | "context")
238}
239
240/// True if the current token is a keyword that begins a clause.
241fn token_is_clause_keyword(kind: TokenKind) -> bool {
242    matches!(
243        kind,
244        TokenKind::When | TokenKind::Requires | TokenKind::Ensures | TokenKind::Within
245    )
246}
247
248// ---------------------------------------------------------------------------
249// Module parsing
250// ---------------------------------------------------------------------------
251
252impl<'s> Parser<'s> {
253    fn parse_module(&mut self) -> Module {
254        let start = self.peek().span;
255        // Version marker is a comment: `-- allium: N`. Detect it from the raw
256        // source before the lexer strips it.
257        let version = detect_version(self.source);
258
259        let mut decls = Vec::new();
260        while !self.at_eof() {
261            if let Some(d) = self.parse_decl() {
262                decls.push(d);
263            } else {
264                // Recovery: skip one token and try again
265                self.advance();
266            }
267        }
268        let end = self.peek().span;
269        Module {
270            span: start.merge(end),
271            version,
272            declarations: decls,
273        }
274    }
275}
276
277fn detect_version(source: &str) -> Option<u32> {
278    for line in source.lines() {
279        let trimmed = line.trim();
280        if trimmed.is_empty() {
281            continue;
282        }
283        if let Some(rest) = trimmed.strip_prefix("--") {
284            let rest = rest.trim();
285            if let Some(ver) = rest.strip_prefix("allium:") {
286                return ver.trim().parse().ok();
287            }
288        }
289        break; // only check leading lines
290    }
291    None
292}
293
294// ---------------------------------------------------------------------------
295// Declaration parsing
296// ---------------------------------------------------------------------------
297
298impl<'s> Parser<'s> {
299    fn parse_decl(&mut self) -> Option<Decl> {
300        match self.peek_kind() {
301            TokenKind::Use => self.parse_use_decl().map(Decl::Use),
302            TokenKind::Rule => self.parse_block(BlockKind::Rule).map(Decl::Block),
303            TokenKind::Entity => self.parse_block(BlockKind::Entity).map(Decl::Block),
304            TokenKind::External => {
305                let start = self.advance().span;
306                if self.at(TokenKind::Entity) {
307                    self.parse_block_from(start, BlockKind::ExternalEntity)
308                        .map(Decl::Block)
309                } else {
310                    self.error(self.peek().span, "expected 'entity' after 'external'");
311                    None
312                }
313            }
314            TokenKind::Value => self.parse_block(BlockKind::Value).map(Decl::Block),
315            TokenKind::Enum => self.parse_block(BlockKind::Enum).map(Decl::Block),
316            TokenKind::Given => self.parse_anonymous_block(BlockKind::Given).map(Decl::Block),
317            TokenKind::Config => self.parse_anonymous_block(BlockKind::Config).map(Decl::Block),
318            TokenKind::Surface => self.parse_block(BlockKind::Surface).map(Decl::Block),
319            TokenKind::Actor => self.parse_block(BlockKind::Actor).map(Decl::Block),
320            TokenKind::Default => self.parse_default_decl().map(Decl::Default),
321            TokenKind::Variant => self.parse_variant_decl().map(Decl::Variant),
322            TokenKind::Deferred => self.parse_deferred_decl().map(Decl::Deferred),
323            TokenKind::Open => self.parse_open_question_decl().map(Decl::OpenQuestion),
324            TokenKind::Module => self.parse_module_decl(),
325            // Qualified config: `alias/config { ... }`
326            TokenKind::Ident
327                if self.peek_at(1).kind == TokenKind::Slash
328                    && self.text(self.peek_at(2).span) == "config" =>
329            {
330                self.parse_qualified_config().map(Decl::Block)
331            }
332            _ => {
333                self.error(
334                    self.peek().span,
335                    format!(
336                        "expected declaration (entity, rule, enum, value, config, surface, actor, \
337                         given, default, variant, deferred, use, open question), found {}",
338                        self.peek_kind(),
339                    ),
340                );
341                None
342            }
343        }
344    }
345
346    // -- module declaration -----------------------------------------------
347
348    fn parse_module_decl(&mut self) -> Option<Decl> {
349        let start = self.expect(TokenKind::Module)?.span;
350        let name = self.parse_ident_in("module name")?;
351        Some(Decl::ModuleDecl(ModuleDecl {
352            span: start.merge(name.span),
353            name,
354        }))
355    }
356
357    // -- use declaration ------------------------------------------------
358
359    fn parse_use_decl(&mut self) -> Option<UseDecl> {
360        let start = self.expect(TokenKind::Use)?.span;
361        let path = self.parse_string()?;
362        let alias = if self.eat(TokenKind::As).is_some() {
363            Some(self.parse_ident_in("import alias")?)
364        } else {
365            None
366        };
367        let end = alias
368            .as_ref()
369            .map(|a| a.span)
370            .unwrap_or(path.span);
371        Some(UseDecl {
372            span: start.merge(end),
373            path,
374            alias,
375        })
376    }
377
378    // -- named block: `keyword Name { ... }` ----------------------------
379
380    fn parse_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
381        let start = self.advance().span; // consume keyword
382        self.parse_block_from(start, kind)
383    }
384
385    fn parse_block_from(&mut self, start: Span, kind: BlockKind) -> Option<BlockDecl> {
386        // For ExternalEntity the keyword was already consumed by the caller;
387        // here we consume Entity.
388        if kind == BlockKind::ExternalEntity {
389            self.expect(TokenKind::Entity)?;
390        }
391        let context = match kind {
392            BlockKind::Entity | BlockKind::ExternalEntity => "entity name",
393            BlockKind::Rule => "rule name",
394            BlockKind::Surface => "surface name",
395            BlockKind::Actor => "actor name",
396            BlockKind::Value => "value type name",
397            BlockKind::Enum => "enum name",
398            _ => "block name",
399        };
400        let name = Some(self.parse_ident_in(context)?);
401        self.expect(TokenKind::LBrace)?;
402        let items = if kind == BlockKind::Enum {
403            self.parse_enum_body()
404        } else {
405            self.parse_block_items()
406        };
407        let end = self.expect(TokenKind::RBrace)?.span;
408        Some(BlockDecl {
409            span: start.merge(end),
410            kind,
411            name,
412            items,
413        })
414    }
415
416    // -- anonymous block: `keyword { ... }` -----------------------------
417
418    /// Parse enum body: pipe-separated variant names.
419    /// `{ pending | shipped | delivered }`
420    fn parse_enum_body(&mut self) -> Vec<BlockItem> {
421        let mut items = Vec::new();
422        while !self.at(TokenKind::RBrace) && !self.at_eof() {
423            if self.eat(TokenKind::Pipe).is_some() {
424                continue;
425            }
426            if let Some(ident) = self.parse_ident_in("enum variant") {
427                items.push(BlockItem {
428                    span: ident.span,
429                    kind: BlockItemKind::EnumVariant { name: ident },
430                });
431            } else {
432                self.advance(); // skip unrecognised token
433            }
434        }
435        items
436    }
437
438    fn parse_anonymous_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
439        let start = self.advance().span;
440        self.expect(TokenKind::LBrace)?;
441        let items = self.parse_block_items();
442        let end = self.expect(TokenKind::RBrace)?.span;
443        Some(BlockDecl {
444            span: start.merge(end),
445            kind,
446            name: None,
447            items,
448        })
449    }
450
451    // -- qualified config: `alias/config { ... }` -----------------------
452
453    fn parse_qualified_config(&mut self) -> Option<BlockDecl> {
454        let alias = self.parse_ident_in("config qualifier")?;
455        let start = alias.span;
456        self.expect(TokenKind::Slash)?;
457        self.advance(); // consume "config" ident
458        self.expect(TokenKind::LBrace)?;
459        let items = self.parse_block_items();
460        let end = self.expect(TokenKind::RBrace)?.span;
461        Some(BlockDecl {
462            span: start.merge(end),
463            kind: BlockKind::Config,
464            name: Some(alias),
465            items,
466        })
467    }
468
469    // -- default declaration -------------------------------------------
470
471    fn parse_default_decl(&mut self) -> Option<DefaultDecl> {
472        let start = self.expect(TokenKind::Default)?.span;
473
474        // `default [TypeName] instanceName = value`
475        // The type name is optional. If the next two tokens are both words
476        // and the second is followed by `=`, the first is the type.
477        let (type_name, name) = if self.peek_kind().is_word()
478            && self.peek_at(1).kind.is_word()
479            && self.peek_at(2).kind == TokenKind::Eq
480        {
481            let t = self.parse_ident_in("type name")?;
482            let n = self.parse_ident_in("default name")?;
483            (Some(t), n)
484        } else {
485            (None, self.parse_ident_in("default name")?)
486        };
487
488        self.expect(TokenKind::Eq)?;
489        let value = self.parse_expr(0)?;
490        Some(DefaultDecl {
491            span: start.merge(value.span()),
492            type_name,
493            name,
494            value,
495        })
496    }
497
498    // -- variant declaration -------------------------------------------
499
500    fn parse_variant_decl(&mut self) -> Option<VariantDecl> {
501        let start = self.expect(TokenKind::Variant)?.span;
502        let name = self.parse_ident_in("variant name")?;
503        self.expect(TokenKind::Colon)?;
504        let base = self.parse_expr(0)?;
505
506        let items = if self.eat(TokenKind::LBrace).is_some() {
507            let items = self.parse_block_items();
508            self.expect(TokenKind::RBrace)?;
509            items
510        } else {
511            Vec::new()
512        };
513
514        let end = if let Some(last) = items.last() {
515            last.span
516        } else {
517            base.span()
518        };
519        Some(VariantDecl {
520            span: start.merge(end),
521            name,
522            base,
523            items,
524        })
525    }
526
527    // -- deferred declaration ------------------------------------------
528
529    fn parse_deferred_decl(&mut self) -> Option<DeferredDecl> {
530        let start = self.expect(TokenKind::Deferred)?.span;
531        let path = self.parse_expr(0)?;
532        Some(DeferredDecl {
533            span: start.merge(path.span()),
534            path,
535        })
536    }
537
538    // -- open question --------------------------------------------------
539
540    fn parse_open_question_decl(&mut self) -> Option<OpenQuestionDecl> {
541        let start = self.expect(TokenKind::Open)?.span;
542        self.expect(TokenKind::Question)?;
543        let text = self.parse_string()?;
544        Some(OpenQuestionDecl {
545            span: start.merge(text.span),
546            text,
547        })
548    }
549}
550
551// ---------------------------------------------------------------------------
552// Block item parsing
553// ---------------------------------------------------------------------------
554
555impl<'s> Parser<'s> {
556    fn parse_block_items(&mut self) -> Vec<BlockItem> {
557        let mut items = Vec::new();
558        while !self.at(TokenKind::RBrace) && !self.at_eof() {
559            if let Some(item) = self.parse_block_item() {
560                items.push(item);
561            } else {
562                // Recovery: skip one token
563                self.advance();
564            }
565        }
566        items
567    }
568
569    fn parse_block_item(&mut self) -> Option<BlockItem> {
570        let start = self.peek().span;
571
572        // `let name = value`
573        if self.at(TokenKind::Let) {
574            return self.parse_let_item(start);
575        }
576
577        // `for binding in collection [where filter]: ...` at block level
578        if self.at(TokenKind::For) {
579            return self.parse_for_block_item(start);
580        }
581
582        // `open question "text"` (inside a block)
583        if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
584            self.advance(); // open
585            self.advance(); // question
586            let text = self.parse_string()?;
587            return Some(BlockItem {
588                span: start.merge(text.span),
589                kind: BlockItemKind::OpenQuestion { text },
590            });
591        }
592
593        // Everything else: `name: value` or `keyword: value` or
594        // `name(params): value`
595        if self.peek_kind().is_word() {
596            // `facing name: Type` / `context name: Type [where ...]` — binding
597            // clause keywords that don't use `:` after the keyword itself.
598            if is_binding_clause_keyword(self.text(self.peek().span))
599                && self.peek_at(1).kind.is_word()
600                && self.peek_at(2).kind == TokenKind::Colon
601            {
602                return self.parse_binding_clause_item(start);
603            }
604
605            // Check for `name(` — potential parameterised assignment
606            if self.peek_at(1).kind == TokenKind::LParen {
607                return self.parse_param_or_clause_item(start);
608            }
609
610            // Check for `name:` — assignment or clause
611            if self.peek_at(1).kind == TokenKind::Colon {
612                return self.parse_assign_or_clause_item(start);
613            }
614        }
615
616        // For clauses whose keyword is a separate TokenKind (when, requires, etc.)
617        if token_is_clause_keyword(self.peek_kind()) && self.peek_at(1).kind == TokenKind::Colon {
618            return self.parse_assign_or_clause_item(start);
619        }
620
621        self.error(
622            start,
623            format!(
624                "expected block item (name: value, let name = value, when:/requires:/ensures: clause, \
625                 for ... in ...:, or open question), found {}",
626                self.peek_kind(),
627            ),
628        );
629        None
630    }
631
632    fn parse_let_item(&mut self, start: Span) -> Option<BlockItem> {
633        self.advance(); // consume `let`
634        let name = self.parse_ident_in("binding name")?;
635        self.expect(TokenKind::Eq)?;
636        let value = self.parse_clause_value(start)?;
637        Some(BlockItem {
638            span: start.merge(value.span()),
639            kind: BlockItemKind::Let { name, value },
640        })
641    }
642
643    /// Parse `facing name: Type` or `context name: Type [where ...]`.
644    /// These keywords don't take `:` after the keyword — they embed a binding directly.
645    fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
646        let keyword_tok = self.advance(); // consume facing/context
647        let keyword = self.text(keyword_tok.span).to_string();
648        let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
649        self.advance(); // consume ':'
650        let type_expr = self.parse_clause_value(start)?;
651        let value_span = type_expr.span();
652        let value = Expr::Binding {
653            span: binding_name.span.merge(value_span),
654            name: binding_name,
655            value: Box::new(type_expr),
656        };
657        Some(BlockItem {
658            span: start.merge(value_span),
659            kind: BlockItemKind::Clause { keyword, value },
660        })
661    }
662
663    /// Parse `for binding in collection [where filter]:` at block level.
664    /// The body is a set of nested block items (let, requires, ensures, etc.).
665    fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
666        self.advance(); // consume `for`
667        let binding = self.parse_ident_in("loop variable")?;
668        self.expect(TokenKind::In)?;
669
670        let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
671
672        let filter = if self.eat(TokenKind::Where).is_some() {
673            // Parse filter at min_bp 0 — colon terminates naturally since
674            // it's not an expression operator.
675            Some(self.parse_expr(0)?)
676        } else {
677            None
678        };
679
680        self.expect(TokenKind::Colon)?;
681
682        // The body contains nested block items at higher indentation.
683        let for_line = self.line_of(start);
684        let next_line = self.line_of(self.peek().span);
685
686        let items = if next_line > for_line {
687            let base_col = self.col_of(self.peek().span);
688            self.parse_indented_block_items(base_col)
689        } else {
690            // Single-line for: parse one block item
691            let mut items = Vec::new();
692            if let Some(item) = self.parse_block_item() {
693                items.push(item);
694            }
695            items
696        };
697
698        let end = items
699            .last()
700            .map(|i| i.span)
701            .unwrap_or(start);
702
703        Some(BlockItem {
704            span: start.merge(end),
705            kind: BlockItemKind::ForBlock {
706                binding,
707                collection,
708                filter,
709                items,
710            },
711        })
712    }
713
714    /// Collect block items at column >= `base_col` (for indented for-block bodies).
715    fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
716        let mut items = Vec::new();
717        while !self.at_eof()
718            && !self.at(TokenKind::RBrace)
719            && self.col_of(self.peek().span) >= base_col
720        {
721            if let Some(item) = self.parse_block_item() {
722                items.push(item);
723            } else {
724                self.advance();
725                break;
726            }
727        }
728        items
729    }
730
731    fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
732        let name_tok = self.advance(); // consume name/keyword
733        let name_text = self.text(name_tok.span).to_string();
734        self.advance(); // consume ':'
735
736        let allows_binding = clause_allows_binding(&name_text);
737        let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
738        let value_span = value.span();
739
740        let kind = if is_clause_keyword(&name_text) {
741            BlockItemKind::Clause {
742                keyword: name_text,
743                value,
744            }
745        } else {
746            BlockItemKind::Assignment {
747                name: Ident {
748                    span: name_tok.span,
749                    name: name_text,
750                },
751                value,
752            }
753        };
754
755        Some(BlockItem {
756            span: start.merge(value_span),
757            kind,
758        })
759    }
760
761    fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
762        // Could be `name(params): value` (param assignment) or
763        // `name(args)` which is an expression that happens to start a clause
764        // value. Peek far enough to see if `)` is followed by `:`.
765        let saved_pos = self.pos;
766        let _name_tok = self.advance();
767        self.advance(); // (
768
769        // Try to scan past balanced parens
770        let mut depth = 1u32;
771        while !self.at_eof() && depth > 0 {
772            match self.peek_kind() {
773                TokenKind::LParen => {
774                    depth += 1;
775                    self.advance();
776                }
777                TokenKind::RParen => {
778                    depth -= 1;
779                    self.advance();
780                }
781                _ => {
782                    self.advance();
783                }
784            }
785        }
786
787        if self.at(TokenKind::Colon) {
788            // It's a parameterised assignment: restore and parse properly
789            self.pos = saved_pos;
790            let name = self.parse_ident_in("derived value name")?;
791            self.expect(TokenKind::LParen)?;
792            let params = self.parse_ident_list()?;
793            self.expect(TokenKind::RParen)?;
794            self.expect(TokenKind::Colon)?;
795            let value = self.parse_clause_value(start)?;
796            Some(BlockItem {
797                span: start.merge(value.span()),
798                kind: BlockItemKind::ParamAssignment {
799                    name,
800                    params,
801                    value,
802                },
803            })
804        } else {
805            // Not a param assignment — restore and fall through to assignment
806            self.pos = saved_pos;
807            // Check for regular `name: value`
808            if self.peek_at(1).kind == TokenKind::Colon {
809                // Nope, the (1) is LParen not Colon. Re-examine.
810            }
811            // Fall back: treat as `name: value` where value starts with a call
812            self.parse_assign_or_clause_item(start)
813        }
814    }
815
816    fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
817        let mut params = Vec::new();
818        if !self.at(TokenKind::RParen) {
819            params.push(self.parse_ident_in("parameter name")?);
820            while self.eat(TokenKind::Comma).is_some() {
821                params.push(self.parse_ident_in("parameter name")?);
822            }
823        }
824        Some(params)
825    }
826
827    /// Parse a clause value, optionally checking for a `name: expr` binding
828    /// pattern at the start. Used for when, facing and context clauses where
829    /// the first `ident:` is a binding rather than a nested assignment.
830    fn parse_clause_value_maybe_binding(
831        &mut self,
832        clause_start: Span,
833        allow_binding: bool,
834    ) -> Option<Expr> {
835        if allow_binding
836            && self.peek_kind().is_word()
837            && self.peek_at(1).kind == TokenKind::Colon
838        {
839            // Check this isn't at the start of a new block item on the next line.
840            // Bindings only apply on the same line or the immediate indented value.
841            let clause_line = self.line_of(clause_start);
842            let next_line = self.line_of(self.peek().span);
843            let colon_is_block_item = next_line > clause_line
844                && self.peek_at(2).kind != TokenKind::Eof
845                && self.line_of(self.peek_at(2).span) == next_line;
846
847            if next_line == clause_line || colon_is_block_item {
848                let name = self.parse_ident_in("binding name")?;
849                self.advance(); // consume ':'
850                let inner = self.parse_clause_value(clause_start)?;
851                return Some(Expr::Binding {
852                    span: name.span.merge(inner.span()),
853                    name,
854                    value: Box::new(inner),
855                });
856            }
857        }
858        self.parse_clause_value(clause_start)
859    }
860
861    /// Parse a clause value. If the next token is on a new line (indented),
862    /// collect a multi-line block. Otherwise parse a single expression.
863    fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
864        let clause_line = self.line_of(clause_start);
865        let next = self.peek();
866        let next_line = self.line_of(next.span);
867
868        if next_line > clause_line {
869            // Multi-line block
870            let base_col = self.col_of(next.span);
871            self.parse_indented_block(base_col)
872        } else {
873            // Single-line — parse primary expression, then check for suffix predicate
874            let expr = self.parse_expr(0)?;
875            self.maybe_wrap_suffix_predicate(expr, clause_line)
876        }
877    }
878
879    /// If there are remaining tokens on `clause_line` after the primary
880    /// expression, collect them as a suffix predicate tail.
881    fn maybe_wrap_suffix_predicate(&mut self, expr: Expr, clause_line: u32) -> Option<Expr> {
882        if self.at_eof()
883            || self.at(TokenKind::RBrace)
884            || self.line_of(self.peek().span) != clause_line
885        {
886            return Some(expr);
887        }
888
889        let mut tail = Vec::new();
890        while !self.at_eof()
891            && !self.at(TokenKind::RBrace)
892            && self.line_of(self.peek().span) == clause_line
893        {
894            if let Some(e) = self.try_parse_expr(0) {
895                tail.push(e);
896            } else {
897                // Unrecognised token — consume as raw identifier to avoid stalling.
898                let tok = self.advance();
899                tail.push(Expr::Ident(Ident {
900                    span: tok.span,
901                    name: self.text(tok.span).to_string(),
902                }));
903            }
904        }
905
906        if tail.is_empty() {
907            Some(expr)
908        } else {
909            let end = tail.last().unwrap().span();
910            Some(Expr::Predicate {
911                span: expr.span().merge(end),
912                subject: Box::new(expr),
913                tail,
914            })
915        }
916    }
917
918    /// Collect expressions that start at column >= `base_col` into a block.
919    /// Also handles `let name = value` bindings inside clause value blocks.
920    fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
921        let start = self.peek().span;
922        let mut items = Vec::new();
923
924        while !self.at_eof()
925            && !self.at(TokenKind::RBrace)
926            && self.col_of(self.peek().span) >= base_col
927        {
928            // Handle `let name = value` inside expression blocks
929            if self.at(TokenKind::Let) {
930                let let_start = self.advance().span;
931                if let Some(name) = self.parse_ident_in("binding name") {
932                    if self.expect(TokenKind::Eq).is_some() {
933                        if let Some(value) = self.parse_expr(0) {
934                            items.push(Expr::LetExpr {
935                                span: let_start.merge(value.span()),
936                                name,
937                                value: Box::new(value),
938                            });
939                            continue;
940                        }
941                    }
942                }
943                break;
944            }
945
946            if let Some(expr) = self.parse_expr(0) {
947                items.push(expr);
948            } else {
949                self.advance();
950                break;
951            }
952        }
953
954        if items.len() == 1 {
955            Some(items.pop().unwrap())
956        } else {
957            let end = items.last().map(|e| e.span()).unwrap_or(start);
958            Some(Expr::Block {
959                span: start.merge(end),
960                items,
961            })
962        }
963    }
964}
965
966// ---------------------------------------------------------------------------
967// Expression parsing — Pratt parser
968// ---------------------------------------------------------------------------
969
970// Binding powers (even = left, odd = right for right-associative)
971const BP_LAMBDA: u8 = 4;
972const BP_WHEN_GUARD: u8 = 5;
973const BP_OR: u8 = 10;
974const BP_AND: u8 = 20;
975const BP_COMPARE: u8 = 30;
976const BP_TRANSITION: u8 = 32;
977const BP_WITH_WHERE: u8 = 35;
978const BP_PROJECTION: u8 = 37;
979const BP_NULL_COALESCE: u8 = 40;
980const BP_ADD: u8 = 50;
981const BP_MUL: u8 = 60;
982const BP_PIPE: u8 = 65;
983const BP_PREFIX: u8 = 70;
984const BP_POSTFIX: u8 = 80;
985
986impl<'s> Parser<'s> {
987    pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
988        let mut lhs = self.parse_prefix()?;
989
990        loop {
991            if let Some((l_bp, r_bp)) = self.infix_bp() {
992                if l_bp < min_bp {
993                    break;
994                }
995                lhs = self.parse_infix(lhs, r_bp)?;
996            } else if let Some(l_bp) = self.postfix_bp() {
997                if l_bp < min_bp {
998                    break;
999                }
1000                lhs = self.parse_postfix(lhs)?;
1001            } else {
1002                break;
1003            }
1004        }
1005
1006        Some(lhs)
1007    }
1008
1009    /// Try to parse an expression without emitting diagnostics on failure.
1010    /// Restores position if parsing fails.
1011    fn try_parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1012        let saved_pos = self.pos;
1013        let saved_diag_count = self.diagnostics.len();
1014        match self.parse_expr(min_bp) {
1015            Some(expr) => Some(expr),
1016            None => {
1017                self.pos = saved_pos;
1018                self.diagnostics.truncate(saved_diag_count);
1019                None
1020            }
1021        }
1022    }
1023
1024    // -- prefix ---------------------------------------------------------
1025
1026    fn parse_prefix(&mut self) -> Option<Expr> {
1027        match self.peek_kind() {
1028            TokenKind::Not => {
1029                let start = self.advance().span;
1030                if self.at(TokenKind::Exists) {
1031                    self.advance();
1032                    let operand = self.parse_expr(BP_PREFIX)?;
1033                    Some(Expr::NotExists {
1034                        span: start.merge(operand.span()),
1035                        operand: Box::new(operand),
1036                    })
1037                } else {
1038                    let operand = self.parse_expr(BP_PREFIX)?;
1039                    Some(Expr::Not {
1040                        span: start.merge(operand.span()),
1041                        operand: Box::new(operand),
1042                    })
1043                }
1044            }
1045            TokenKind::Exists => {
1046                // When `exists` is not followed by an expression-start token,
1047                // treat it as a plain identifier (e.g. `label: exists`).
1048                let next = self.peek_at(1).kind;
1049                if matches!(
1050                    next,
1051                    TokenKind::RParen
1052                        | TokenKind::RBrace
1053                        | TokenKind::RBracket
1054                        | TokenKind::Comma
1055                        | TokenKind::Eof
1056                ) {
1057                    let id = self.parse_ident()?;
1058                    return Some(Expr::Ident(id));
1059                }
1060                let start = self.advance().span;
1061                let operand = self.parse_expr(BP_PREFIX)?;
1062                Some(Expr::Exists {
1063                    span: start.merge(operand.span()),
1064                    operand: Box::new(operand),
1065                })
1066            }
1067            TokenKind::If => self.parse_if_expr(),
1068            TokenKind::For => self.parse_for_expr(),
1069            TokenKind::LBrace => self.parse_brace_expr(),
1070            TokenKind::LBracket => self.parse_list_literal(),
1071            TokenKind::LParen => self.parse_paren_expr(),
1072            TokenKind::Number => {
1073                let t = self.advance();
1074                Some(Expr::NumberLiteral {
1075                    span: t.span,
1076                    value: self.text(t.span).to_string(),
1077                })
1078            }
1079            TokenKind::Duration => {
1080                let t = self.advance();
1081                Some(Expr::DurationLiteral {
1082                    span: t.span,
1083                    value: self.text(t.span).to_string(),
1084                })
1085            }
1086            TokenKind::String => {
1087                let sl = self.parse_string()?;
1088                Some(Expr::StringLiteral(sl))
1089            }
1090            TokenKind::True => {
1091                let t = self.advance();
1092                Some(Expr::BoolLiteral {
1093                    span: t.span,
1094                    value: true,
1095                })
1096            }
1097            TokenKind::False => {
1098                let t = self.advance();
1099                Some(Expr::BoolLiteral {
1100                    span: t.span,
1101                    value: false,
1102                })
1103            }
1104            TokenKind::Null => {
1105                let t = self.advance();
1106                Some(Expr::Null { span: t.span })
1107            }
1108            TokenKind::Now => {
1109                let t = self.advance();
1110                Some(Expr::Now { span: t.span })
1111            }
1112            TokenKind::This => {
1113                let t = self.advance();
1114                Some(Expr::This { span: t.span })
1115            }
1116            TokenKind::Within => {
1117                let t = self.advance();
1118                Some(Expr::Within { span: t.span })
1119            }
1120            k if k.is_word() => {
1121                let id = self.parse_ident()?;
1122                Some(Expr::Ident(id))
1123            }
1124            TokenKind::Minus => {
1125                // Unary minus: -expr → BinaryOp(0, Sub, expr)
1126                let start = self.advance().span;
1127                let operand = self.parse_expr(BP_PREFIX)?;
1128                Some(Expr::BinaryOp {
1129                    span: start.merge(operand.span()),
1130                    left: Box::new(Expr::NumberLiteral {
1131                        span: start,
1132                        value: "0".into(),
1133                    }),
1134                    op: BinaryOp::Sub,
1135                    right: Box::new(operand),
1136                })
1137            }
1138            _ => {
1139                self.error(
1140                    self.peek().span,
1141                    format!(
1142                        "expected expression (identifier, number, string, true/false, null, \
1143                         if/for/not/exists, '(', '{{', '['), found {}",
1144                        self.peek_kind(),
1145                    ),
1146                );
1147                None
1148            }
1149        }
1150    }
1151
1152    // -- infix binding powers -------------------------------------------
1153
1154    fn infix_bp(&self) -> Option<(u8, u8)> {
1155        match self.peek_kind() {
1156            TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), // right-assoc
1157            // `when` as an inline guard on provides/related items
1158            TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1159            TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1160            TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1161            TokenKind::And => Some((BP_AND, BP_AND + 1)),
1162            TokenKind::Eq | TokenKind::EqEq | TokenKind::BangEq => {
1163                Some((BP_COMPARE, BP_COMPARE + 1))
1164            }
1165            TokenKind::Lt => {
1166                // If `<` is immediately adjacent to a word token (no space),
1167                // treat as generic type postfix, not comparison infix.
1168                if self.pos > 0 {
1169                    let prev = self.tokens[self.pos - 1];
1170                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1171                        return None;
1172                    }
1173                }
1174                Some((BP_COMPARE, BP_COMPARE + 1))
1175            }
1176            TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1177                Some((BP_COMPARE, BP_COMPARE + 1))
1178            }
1179            TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1180            // `not in` — only when followed by `in`
1181            TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1182                Some((BP_COMPARE, BP_COMPARE + 1))
1183            }
1184            TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1185            TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1186            TokenKind::Includes => Some((BP_COMPARE, BP_COMPARE + 1)),
1187            TokenKind::Excludes => Some((BP_COMPARE, BP_COMPARE + 1)),
1188            TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1189            TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1190            TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1191            TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1192            TokenKind::DotDot => Some((BP_ADD + 1, BP_ADD + 2)), // tighter than +/-
1193            TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1194            TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1195            _ => None,
1196        }
1197    }
1198
1199    fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1200        let op_tok = self.advance();
1201        match op_tok.kind {
1202            TokenKind::FatArrow => {
1203                let body = self.parse_expr(r_bp)?;
1204                Some(Expr::Lambda {
1205                    span: lhs.span().merge(body.span()),
1206                    param: Box::new(lhs),
1207                    body: Box::new(body),
1208                })
1209            }
1210            TokenKind::Pipe => {
1211                let rhs = self.parse_expr(r_bp)?;
1212                Some(Expr::Pipe {
1213                    span: lhs.span().merge(rhs.span()),
1214                    left: Box::new(lhs),
1215                    right: Box::new(rhs),
1216                })
1217            }
1218            TokenKind::Or => {
1219                let rhs = self.parse_expr(r_bp)?;
1220                Some(Expr::LogicalOp {
1221                    span: lhs.span().merge(rhs.span()),
1222                    left: Box::new(lhs),
1223                    op: LogicalOp::Or,
1224                    right: Box::new(rhs),
1225                })
1226            }
1227            TokenKind::And => {
1228                let rhs = self.parse_expr(r_bp)?;
1229                Some(Expr::LogicalOp {
1230                    span: lhs.span().merge(rhs.span()),
1231                    left: Box::new(lhs),
1232                    op: LogicalOp::And,
1233                    right: Box::new(rhs),
1234                })
1235            }
1236            TokenKind::Eq | TokenKind::EqEq => {
1237                let rhs = self.parse_expr(r_bp)?;
1238                Some(Expr::Comparison {
1239                    span: lhs.span().merge(rhs.span()),
1240                    left: Box::new(lhs),
1241                    op: ComparisonOp::Eq,
1242                    right: Box::new(rhs),
1243                })
1244            }
1245            TokenKind::BangEq => {
1246                let rhs = self.parse_expr(r_bp)?;
1247                Some(Expr::Comparison {
1248                    span: lhs.span().merge(rhs.span()),
1249                    left: Box::new(lhs),
1250                    op: ComparisonOp::NotEq,
1251                    right: Box::new(rhs),
1252                })
1253            }
1254            TokenKind::Lt => {
1255                let rhs = self.parse_expr(r_bp)?;
1256                Some(Expr::Comparison {
1257                    span: lhs.span().merge(rhs.span()),
1258                    left: Box::new(lhs),
1259                    op: ComparisonOp::Lt,
1260                    right: Box::new(rhs),
1261                })
1262            }
1263            TokenKind::LtEq => {
1264                let rhs = self.parse_expr(r_bp)?;
1265                Some(Expr::Comparison {
1266                    span: lhs.span().merge(rhs.span()),
1267                    left: Box::new(lhs),
1268                    op: ComparisonOp::LtEq,
1269                    right: Box::new(rhs),
1270                })
1271            }
1272            TokenKind::Gt => {
1273                let rhs = self.parse_expr(r_bp)?;
1274                Some(Expr::Comparison {
1275                    span: lhs.span().merge(rhs.span()),
1276                    left: Box::new(lhs),
1277                    op: ComparisonOp::Gt,
1278                    right: Box::new(rhs),
1279                })
1280            }
1281            TokenKind::GtEq => {
1282                let rhs = self.parse_expr(r_bp)?;
1283                Some(Expr::Comparison {
1284                    span: lhs.span().merge(rhs.span()),
1285                    left: Box::new(lhs),
1286                    op: ComparisonOp::GtEq,
1287                    right: Box::new(rhs),
1288                })
1289            }
1290            TokenKind::In => {
1291                let rhs = self.parse_expr(r_bp)?;
1292                Some(Expr::In {
1293                    span: lhs.span().merge(rhs.span()),
1294                    element: Box::new(lhs),
1295                    collection: Box::new(rhs),
1296                })
1297            }
1298            TokenKind::Not => {
1299                // `not in`
1300                self.expect(TokenKind::In)?;
1301                let rhs = self.parse_expr(r_bp)?;
1302                Some(Expr::NotIn {
1303                    span: lhs.span().merge(rhs.span()),
1304                    element: Box::new(lhs),
1305                    collection: Box::new(rhs),
1306                })
1307            }
1308            TokenKind::Where => {
1309                let rhs = self.parse_expr(r_bp)?;
1310                Some(Expr::Where {
1311                    span: lhs.span().merge(rhs.span()),
1312                    source: Box::new(lhs),
1313                    condition: Box::new(rhs),
1314                })
1315            }
1316            TokenKind::With => {
1317                let rhs = self.parse_expr(r_bp)?;
1318                Some(Expr::With {
1319                    span: lhs.span().merge(rhs.span()),
1320                    source: Box::new(lhs),
1321                    predicate: Box::new(rhs),
1322                })
1323            }
1324            TokenKind::QuestionQuestion => {
1325                let rhs = self.parse_expr(r_bp)?;
1326                Some(Expr::NullCoalesce {
1327                    span: lhs.span().merge(rhs.span()),
1328                    left: Box::new(lhs),
1329                    right: Box::new(rhs),
1330                })
1331            }
1332            TokenKind::Plus => {
1333                let rhs = self.parse_expr(r_bp)?;
1334                Some(Expr::BinaryOp {
1335                    span: lhs.span().merge(rhs.span()),
1336                    left: Box::new(lhs),
1337                    op: BinaryOp::Add,
1338                    right: Box::new(rhs),
1339                })
1340            }
1341            TokenKind::Minus => {
1342                let rhs = self.parse_expr(r_bp)?;
1343                Some(Expr::BinaryOp {
1344                    span: lhs.span().merge(rhs.span()),
1345                    left: Box::new(lhs),
1346                    op: BinaryOp::Sub,
1347                    right: Box::new(rhs),
1348                })
1349            }
1350            TokenKind::Star => {
1351                let rhs = self.parse_expr(r_bp)?;
1352                Some(Expr::BinaryOp {
1353                    span: lhs.span().merge(rhs.span()),
1354                    left: Box::new(lhs),
1355                    op: BinaryOp::Mul,
1356                    right: Box::new(rhs),
1357                })
1358            }
1359            TokenKind::Slash => {
1360                // Check for qualified name: `alias/Name` or `alias/config`
1361                // Qualified if the LHS is a bare identifier and the RHS is a
1362                // word that either starts with uppercase or is a block keyword
1363                // (like `config`).
1364                if let Expr::Ident(ref id) = lhs {
1365                    if self.peek_kind().is_word() {
1366                        let next_text = self.text(self.peek().span);
1367                        let is_qualified = next_text
1368                            .chars()
1369                            .next()
1370                            .is_some_and(|c| c.is_uppercase())
1371                            || matches!(
1372                                self.peek_kind(),
1373                                TokenKind::Config | TokenKind::Entity | TokenKind::Value
1374                            );
1375                        if is_qualified {
1376                            let name_tok = self.advance();
1377                            return Some(Expr::QualifiedName(QualifiedName {
1378                                span: lhs.span().merge(name_tok.span),
1379                                qualifier: Some(id.name.clone()),
1380                                name: self.text(name_tok.span).to_string(),
1381                            }));
1382                        }
1383                    }
1384                }
1385                let rhs = self.parse_expr(r_bp)?;
1386                Some(Expr::BinaryOp {
1387                    span: lhs.span().merge(rhs.span()),
1388                    left: Box::new(lhs),
1389                    op: BinaryOp::Div,
1390                    right: Box::new(rhs),
1391                })
1392            }
1393            TokenKind::ThinArrow => {
1394                let field = self.parse_ident_in("projection field")?;
1395                Some(Expr::ProjectionMap {
1396                    span: lhs.span().merge(field.span),
1397                    source: Box::new(lhs),
1398                    field,
1399                })
1400            }
1401            TokenKind::TransitionsTo => {
1402                let rhs = self.parse_expr(r_bp)?;
1403                Some(Expr::TransitionsTo {
1404                    span: lhs.span().merge(rhs.span()),
1405                    subject: Box::new(lhs),
1406                    new_state: Box::new(rhs),
1407                })
1408            }
1409            TokenKind::Becomes => {
1410                let rhs = self.parse_expr(r_bp)?;
1411                Some(Expr::Becomes {
1412                    span: lhs.span().merge(rhs.span()),
1413                    subject: Box::new(lhs),
1414                    new_state: Box::new(rhs),
1415                })
1416            }
1417            TokenKind::Includes => {
1418                let rhs = self.parse_expr(r_bp)?;
1419                Some(Expr::Includes {
1420                    span: lhs.span().merge(rhs.span()),
1421                    collection: Box::new(lhs),
1422                    element: Box::new(rhs),
1423                })
1424            }
1425            TokenKind::Excludes => {
1426                let rhs = self.parse_expr(r_bp)?;
1427                Some(Expr::Excludes {
1428                    span: lhs.span().merge(rhs.span()),
1429                    collection: Box::new(lhs),
1430                    element: Box::new(rhs),
1431                })
1432            }
1433            TokenKind::When => {
1434                // Inline guard: `action when condition`
1435                let rhs = self.parse_expr(r_bp)?;
1436                Some(Expr::WhenGuard {
1437                    span: lhs.span().merge(rhs.span()),
1438                    action: Box::new(lhs),
1439                    condition: Box::new(rhs),
1440                })
1441            }
1442            TokenKind::DotDot => {
1443                let rhs = self.parse_expr(r_bp)?;
1444                Some(Expr::Range {
1445                    span: lhs.span().merge(rhs.span()),
1446                    start: Box::new(lhs),
1447                    end: Box::new(rhs),
1448                })
1449            }
1450            _ => {
1451                self.error(
1452                    op_tok.span,
1453                    format!("unexpected infix operator {}", op_tok.kind),
1454                );
1455                None
1456            }
1457        }
1458    }
1459
1460    // -- postfix --------------------------------------------------------
1461
1462    fn postfix_bp(&self) -> Option<u8> {
1463        match self.peek_kind() {
1464            TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
1465            TokenKind::QuestionMark => Some(BP_POSTFIX),
1466            // `<` for generic types like `Set<T>`, `List<T>` — only treated
1467            // as postfix when it immediately follows a word with no space.
1468            TokenKind::Lt => {
1469                if self.pos > 0 {
1470                    let prev = self.tokens[self.pos - 1];
1471                    // Only if `<` starts immediately after the previous token
1472                    // (no whitespace gap) to distinguish from comparisons.
1473                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1474                        return Some(BP_POSTFIX);
1475                    }
1476                }
1477                None
1478            }
1479            TokenKind::LParen => Some(BP_POSTFIX),
1480            TokenKind::LBrace => {
1481                // Join lookup: only when preceded by something that looks
1482                // like an entity name (handled generically — any expr can
1483                // be followed by { for join lookup in expression position).
1484                // But only if the { is on the same line to avoid consuming
1485                // a block body.
1486                let next = self.peek();
1487                let prev_end = if self.pos > 0 {
1488                    self.tokens[self.pos - 1].span.end
1489                } else {
1490                    0
1491                };
1492                // Same line check
1493                if self.line_of(Span::new(prev_end, prev_end))
1494                    == self.line_of(next.span)
1495                {
1496                    Some(BP_POSTFIX)
1497                } else {
1498                    None
1499                }
1500            }
1501            _ => None,
1502        }
1503    }
1504
1505    fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
1506        match self.peek_kind() {
1507            TokenKind::QuestionMark => {
1508                let end = self.advance().span;
1509                Some(Expr::TypeOptional {
1510                    span: lhs.span().merge(end),
1511                    inner: Box::new(lhs),
1512                })
1513            }
1514            TokenKind::Lt => {
1515                // Generic type: `Set<T>`, `List<Node?>`
1516                self.advance(); // consume <
1517                let mut args = Vec::new();
1518                // Parse args above comparison BP so `>` isn't consumed as infix
1519                while !self.at(TokenKind::Gt) && !self.at_eof() {
1520                    args.push(self.parse_expr(BP_COMPARE + 1)?);
1521                    self.eat(TokenKind::Comma);
1522                }
1523                let end = self.expect(TokenKind::Gt)?.span;
1524                Some(Expr::GenericType {
1525                    span: lhs.span().merge(end),
1526                    name: Box::new(lhs),
1527                    args,
1528                })
1529            }
1530            TokenKind::Dot => {
1531                self.advance();
1532                let field = self.parse_ident_in("field name")?;
1533                Some(Expr::MemberAccess {
1534                    span: lhs.span().merge(field.span),
1535                    object: Box::new(lhs),
1536                    field,
1537                })
1538            }
1539            TokenKind::QuestionDot => {
1540                self.advance();
1541                let field = self.parse_ident_in("field name")?;
1542                Some(Expr::OptionalAccess {
1543                    span: lhs.span().merge(field.span),
1544                    object: Box::new(lhs),
1545                    field,
1546                })
1547            }
1548            TokenKind::LParen => {
1549                self.advance();
1550                let args = self.parse_call_args()?;
1551                let end = self.expect(TokenKind::RParen)?.span;
1552                Some(Expr::Call {
1553                    span: lhs.span().merge(end),
1554                    function: Box::new(lhs),
1555                    args,
1556                })
1557            }
1558            TokenKind::LBrace => {
1559                self.advance();
1560                let fields = self.parse_join_fields()?;
1561                let end = self.expect(TokenKind::RBrace)?.span;
1562                Some(Expr::JoinLookup {
1563                    span: lhs.span().merge(end),
1564                    entity: Box::new(lhs),
1565                    fields,
1566                })
1567            }
1568            _ => None,
1569        }
1570    }
1571
1572    // -- call arguments -------------------------------------------------
1573
1574    fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
1575        let mut args = Vec::new();
1576        while !self.at(TokenKind::RParen) && !self.at_eof() {
1577            // Check for named argument: `name: value`
1578            if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1579                let name = self.parse_ident_in("argument name")?;
1580                self.advance(); // :
1581                let value = self.parse_expr(0)?;
1582                args.push(CallArg::Named(NamedArg {
1583                    span: name.span.merge(value.span()),
1584                    name,
1585                    value,
1586                }));
1587            } else {
1588                let expr = self.parse_expr(0)?;
1589                args.push(CallArg::Positional(expr));
1590            }
1591            self.eat(TokenKind::Comma);
1592        }
1593        Some(args)
1594    }
1595
1596    // -- join fields ----------------------------------------------------
1597
1598    fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
1599        let mut fields = Vec::new();
1600        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1601            let field = self.parse_ident_in("join field name")?;
1602            let value = if self.eat(TokenKind::Colon).is_some() {
1603                Some(self.parse_expr(0)?)
1604            } else {
1605                None
1606            };
1607            fields.push(JoinField {
1608                span: field.span.merge(
1609                    value
1610                        .as_ref()
1611                        .map(|v| v.span())
1612                        .unwrap_or(field.span),
1613                ),
1614                field,
1615                value,
1616            });
1617            self.eat(TokenKind::Comma);
1618        }
1619        Some(fields)
1620    }
1621
1622    // -- if expression --------------------------------------------------
1623
1624    fn parse_if_expr(&mut self) -> Option<Expr> {
1625        let start = self.advance().span; // consume `if`
1626        let mut branches = Vec::new();
1627
1628        // First branch
1629        let condition = self.parse_expr(0)?;
1630        self.expect(TokenKind::Colon)?;
1631        let body = self.parse_branch_body(start)?;
1632        branches.push(CondBranch {
1633            span: start.merge(body.span()),
1634            condition,
1635            body,
1636        });
1637
1638        // else if / else
1639        let mut else_body = None;
1640        while self.at(TokenKind::Else) {
1641            let else_tok = self.advance();
1642            if self.at(TokenKind::If) {
1643                let if_start = self.advance().span;
1644                let cond = self.parse_expr(0)?;
1645                self.expect(TokenKind::Colon)?;
1646                let body = self.parse_branch_body(else_tok.span)?;
1647                branches.push(CondBranch {
1648                    span: if_start.merge(body.span()),
1649                    condition: cond,
1650                    body,
1651                });
1652            } else {
1653                self.expect(TokenKind::Colon)?;
1654                let body = self.parse_branch_body(else_tok.span)?;
1655                else_body = Some(Box::new(body));
1656                break;
1657            }
1658        }
1659
1660        let end = else_body
1661            .as_ref()
1662            .map(|b| b.span())
1663            .or_else(|| branches.last().map(|b| b.body.span()))
1664            .unwrap_or(start);
1665
1666        Some(Expr::Conditional {
1667            span: start.merge(end),
1668            branches,
1669            else_body,
1670        })
1671    }
1672
1673    fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
1674        let keyword_line = self.line_of(keyword_span);
1675        let next_line = self.line_of(self.peek().span);
1676
1677        if next_line > keyword_line {
1678            let base_col = self.col_of(self.peek().span);
1679            self.parse_indented_block(base_col)
1680        } else {
1681            self.parse_expr(0)
1682        }
1683    }
1684
1685    // -- for expression -------------------------------------------------
1686
1687    fn parse_for_expr(&mut self) -> Option<Expr> {
1688        let start = self.advance().span; // consume `for`
1689        let binding = self.parse_ident_in("loop variable")?;
1690        self.expect(TokenKind::In)?;
1691
1692        // Parse collection, stopping before `where` and `:`
1693        let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1694
1695        let filter = if self.eat(TokenKind::Where).is_some() {
1696            // Parse filter at min_bp 0 — colon terminates naturally.
1697            Some(Box::new(self.parse_expr(0)?))
1698        } else {
1699            None
1700        };
1701
1702        self.expect(TokenKind::Colon)?;
1703        let body = self.parse_branch_body(start)?;
1704
1705        Some(Expr::For {
1706            span: start.merge(body.span()),
1707            binding,
1708            collection: Box::new(collection),
1709            filter,
1710            body: Box::new(body),
1711        })
1712    }
1713
1714    // -- brace expressions: set literal or object literal ---------------
1715
1716    fn parse_brace_expr(&mut self) -> Option<Expr> {
1717        let start = self.advance().span; // consume {
1718
1719        if self.at(TokenKind::RBrace) {
1720            let end = self.advance().span;
1721            return Some(Expr::SetLiteral {
1722                span: start.merge(end),
1723                elements: Vec::new(),
1724            });
1725        }
1726
1727        // Peek: if first item is `ident:`, it's an object literal
1728        if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1729            return self.parse_object_literal(start);
1730        }
1731
1732        // Otherwise set literal
1733        self.parse_set_literal(start)
1734    }
1735
1736    fn parse_list_literal(&mut self) -> Option<Expr> {
1737        let start = self.advance().span; // consume [
1738        let mut elements = Vec::new();
1739        while !self.at(TokenKind::RBracket) && !self.at_eof() {
1740            elements.push(self.parse_expr(0)?);
1741            self.eat(TokenKind::Comma);
1742        }
1743        let end = self.expect(TokenKind::RBracket)?.span;
1744        Some(Expr::ListLiteral {
1745            span: start.merge(end),
1746            elements,
1747        })
1748    }
1749
1750    fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
1751        let mut fields = Vec::new();
1752        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1753            let name = self.parse_ident_in("field name")?;
1754            self.expect(TokenKind::Colon)?;
1755            let value = self.parse_expr(0)?;
1756            fields.push(NamedArg {
1757                span: name.span.merge(value.span()),
1758                name,
1759                value,
1760            });
1761            self.eat(TokenKind::Comma);
1762        }
1763        let end = self.expect(TokenKind::RBrace)?.span;
1764        Some(Expr::ObjectLiteral {
1765            span: start.merge(end),
1766            fields,
1767        })
1768    }
1769
1770    fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
1771        let mut elements = Vec::new();
1772        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1773            elements.push(self.parse_expr(0)?);
1774            self.eat(TokenKind::Comma);
1775        }
1776        let end = self.expect(TokenKind::RBrace)?.span;
1777        Some(Expr::SetLiteral {
1778            span: start.merge(end),
1779            elements,
1780        })
1781    }
1782
1783    // -- parenthesised expression ---------------------------------------
1784
1785    fn parse_paren_expr(&mut self) -> Option<Expr> {
1786        self.advance(); // (
1787        let expr = self.parse_expr(0)?;
1788        self.expect(TokenKind::RParen)?;
1789        Some(expr)
1790    }
1791}
1792
1793// ---------------------------------------------------------------------------
1794// Tests
1795// ---------------------------------------------------------------------------
1796
1797#[cfg(test)]
1798mod tests {
1799    use super::*;
1800
1801    fn parse_ok(src: &str) -> ParseResult {
1802        let result = parse(src);
1803        if !result.diagnostics.is_empty() {
1804            for d in &result.diagnostics {
1805                eprintln!(
1806                    "  [{:?}] {} ({}..{})",
1807                    d.severity, d.message, d.span.start, d.span.end
1808                );
1809            }
1810        }
1811        result
1812    }
1813
1814    #[test]
1815    fn version_marker() {
1816        let r = parse_ok("-- allium: 1\n");
1817        assert_eq!(r.module.version, Some(1));
1818    }
1819
1820    #[test]
1821    fn empty_entity() {
1822        let r = parse_ok("entity User {}");
1823        assert_eq!(r.diagnostics.len(), 0);
1824        assert_eq!(r.module.declarations.len(), 1);
1825        match &r.module.declarations[0] {
1826            Decl::Block(b) => {
1827                assert_eq!(b.kind, BlockKind::Entity);
1828                assert_eq!(b.name.as_ref().unwrap().name, "User");
1829            }
1830            other => panic!("expected Block, got {other:?}"),
1831        }
1832    }
1833
1834    #[test]
1835    fn entity_with_fields() {
1836        let src = r#"entity Order {
1837    customer: Customer
1838    status: pending | active | completed
1839    total: Decimal
1840}"#;
1841        let r = parse_ok(src);
1842        assert_eq!(r.diagnostics.len(), 0);
1843        match &r.module.declarations[0] {
1844            Decl::Block(b) => {
1845                assert_eq!(b.items.len(), 3);
1846            }
1847            other => panic!("expected Block, got {other:?}"),
1848        }
1849    }
1850
1851    #[test]
1852    fn use_declaration() {
1853        let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
1854        assert_eq!(r.diagnostics.len(), 0);
1855        match &r.module.declarations[0] {
1856            Decl::Use(u) => {
1857                assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
1858            }
1859            other => panic!("expected Use, got {other:?}"),
1860        }
1861    }
1862
1863    #[test]
1864    fn enum_declaration() {
1865        let src = "enum OrderStatus { pending | shipped | delivered }";
1866        let r = parse_ok(src);
1867        assert_eq!(r.diagnostics.len(), 0);
1868    }
1869
1870    #[test]
1871    fn config_block() {
1872        let src = r#"config {
1873    max_retries: Integer = 3
1874    timeout: Duration = 24.hours
1875}"#;
1876        // Config entries are `name: Type = default`. The parser sees
1877        // `name: Type = default` as an assignment where the value is
1878        // `Type = default` (comparison with Eq). That's fine for the
1879        // parse tree — semantic pass separates type from default.
1880        let r = parse_ok(src);
1881        assert_eq!(r.diagnostics.len(), 0);
1882    }
1883
1884    #[test]
1885    fn rule_declaration() {
1886        let src = r#"rule PlaceOrder {
1887    when: CustomerPlacesOrder(customer, items, total)
1888    requires: total > 0
1889    ensures: Order.created(customer: customer, status: pending, total: total)
1890}"#;
1891        let r = parse_ok(src);
1892        assert_eq!(r.diagnostics.len(), 0);
1893        match &r.module.declarations[0] {
1894            Decl::Block(b) => {
1895                assert_eq!(b.kind, BlockKind::Rule);
1896                assert_eq!(b.items.len(), 3);
1897            }
1898            other => panic!("expected Block, got {other:?}"),
1899        }
1900    }
1901
1902    #[test]
1903    fn expression_precedence() {
1904        let r = parse_ok("rule T { v: a + b * c }");
1905        // The value should be Add(a, Mul(b, c))
1906        match &r.module.declarations[0] {
1907            Decl::Block(b) => match &b.items[0].kind {
1908                BlockItemKind::Assignment { value, .. } => match value {
1909                    Expr::BinaryOp { op, right, .. } => {
1910                        assert_eq!(*op, BinaryOp::Add);
1911                        assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
1912                    }
1913                    other => panic!("expected BinaryOp, got {other:?}"),
1914                },
1915                other => panic!("expected Assignment, got {other:?}"),
1916            },
1917            other => panic!("expected Block, got {other:?}"),
1918        }
1919    }
1920
1921    #[test]
1922    fn default_declaration() {
1923        let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
1924        let r = parse_ok(src);
1925        assert_eq!(r.diagnostics.len(), 0);
1926    }
1927
1928    #[test]
1929    fn open_question() {
1930        let src = r#"open question "Should admins be role-specific?""#;
1931        let r = parse_ok(src);
1932        assert_eq!(r.diagnostics.len(), 0);
1933    }
1934
1935    #[test]
1936    fn external_entity() {
1937        let src = "external entity Customer { email: String }";
1938        let r = parse_ok(src);
1939        assert_eq!(r.diagnostics.len(), 0);
1940        match &r.module.declarations[0] {
1941            Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
1942            other => panic!("expected Block, got {other:?}"),
1943        }
1944    }
1945
1946    #[test]
1947    fn where_expression() {
1948        let src = "entity E { active: items where status = active }";
1949        let r = parse_ok(src);
1950        assert_eq!(r.diagnostics.len(), 0);
1951    }
1952
1953    #[test]
1954    fn with_expression() {
1955        let src = "entity E { slots: InterviewSlot with candidacy = this }";
1956        let r = parse_ok(src);
1957        assert_eq!(r.diagnostics.len(), 0);
1958    }
1959
1960    #[test]
1961    fn lambda_expression() {
1962        let src = "entity E { v: items.any(i => i.active) }";
1963        let r = parse_ok(src);
1964        assert_eq!(r.diagnostics.len(), 0);
1965    }
1966
1967    #[test]
1968    fn deferred() {
1969        let src = "deferred InterviewerMatching.suggest";
1970        let r = parse_ok(src);
1971        assert_eq!(r.diagnostics.len(), 0);
1972    }
1973
1974    #[test]
1975    fn variant_declaration() {
1976        let src = "variant Email : Notification { subject: String }";
1977        let r = parse_ok(src);
1978        assert_eq!(r.diagnostics.len(), 0);
1979    }
1980
1981    // -- projection mapping -----------------------------------------------
1982
1983    #[test]
1984    fn projection_arrow() {
1985        let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
1986        let r = parse_ok(src);
1987        assert_eq!(r.diagnostics.len(), 0);
1988    }
1989
1990    // -- transitions_to / becomes ------------------------------------------
1991
1992    #[test]
1993    fn transitions_to_trigger() {
1994        let src = "rule R { when: Interview.status transitions_to scheduled\n    ensures: Notification.created() }";
1995        let r = parse_ok(src);
1996        assert_eq!(r.diagnostics.len(), 0);
1997    }
1998
1999    #[test]
2000    fn becomes_trigger() {
2001        let src = "rule R { when: Interview.status becomes scheduled\n    ensures: Notification.created() }";
2002        let r = parse_ok(src);
2003        assert_eq!(r.diagnostics.len(), 0);
2004    }
2005
2006    // -- binding colon in clause values ------------------------------------
2007
2008    #[test]
2009    fn when_binding() {
2010        let src = "rule R {\n    when: interview: Interview.status transitions_to scheduled\n    ensures: Notification.created()\n}";
2011        let r = parse_ok(src);
2012        assert_eq!(r.diagnostics.len(), 0);
2013        // The when clause value should be a Binding wrapping a TransitionsTo
2014        let decl = &r.module.declarations[0];
2015        if let Decl::Block(b) = decl {
2016            if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2017                assert_eq!(keyword, "when");
2018                assert!(matches!(value, Expr::Binding { .. }));
2019            } else {
2020                panic!("expected clause");
2021            }
2022        } else {
2023            panic!("expected block decl");
2024        }
2025    }
2026
2027    #[test]
2028    fn when_binding_temporal() {
2029        let src = "rule R {\n    when: invitation: Invitation.expires_at <= now\n    ensures: Invitation.expired()\n}";
2030        let r = parse_ok(src);
2031        assert_eq!(r.diagnostics.len(), 0);
2032    }
2033
2034    #[test]
2035    fn when_binding_created() {
2036        let src = "rule R {\n    when: batch: DigestBatch.created\n    ensures: Email.created()\n}";
2037        let r = parse_ok(src);
2038        assert_eq!(r.diagnostics.len(), 0);
2039    }
2040
2041    #[test]
2042    fn facing_binding() {
2043        let src = "surface S {\n    facing viewer: Interviewer\n    exposes: InterviewList\n}";
2044        let r = parse_ok(src);
2045        assert_eq!(r.diagnostics.len(), 0);
2046    }
2047
2048    #[test]
2049    fn context_binding() {
2050        let src = "surface S {\n    facing viewer: Interviewer\n    context assignment: SlotConfirmation where interviewer = viewer\n}";
2051        let r = parse_ok(src);
2052        assert_eq!(r.diagnostics.len(), 0);
2053    }
2054
2055    // -- rule-level for block item -----------------------------------------
2056
2057    #[test]
2058    fn rule_level_for() {
2059        let src = r#"rule ProcessDigests {
2060    when: schedule: DigestSchedule.next_run_at <= now
2061    for user in Users where notification_setting.digest_enabled:
2062        ensures: DigestBatch.created(user: user)
2063}"#;
2064        let r = parse_ok(src);
2065        assert_eq!(r.diagnostics.len(), 0);
2066        if let Decl::Block(b) = &r.module.declarations[0] {
2067            // Should have when clause + for block item
2068            assert!(b.items.len() >= 2);
2069            assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2070        } else {
2071            panic!("expected block decl");
2072        }
2073    }
2074
2075    // -- let inside ensures blocks -----------------------------------------
2076
2077    #[test]
2078    fn let_in_ensures_block() {
2079        let src = r#"rule R {
2080    when: ScheduleInterview(candidacy, time, interviewers)
2081    ensures:
2082        let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2083        for interviewer in interviewers:
2084            SlotConfirmation.created(slot: slot, interviewer: interviewer)
2085}"#;
2086        let r = parse_ok(src);
2087        assert_eq!(r.diagnostics.len(), 0);
2088    }
2089
2090    // -- when guard on provides items --------------------------------------
2091
2092    #[test]
2093    fn provides_when_guard() {
2094        let src = "surface S {\n    facing viewer: Interviewer\n    provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2095        let r = parse_ok(src);
2096        assert_eq!(r.diagnostics.len(), 0);
2097    }
2098
2099    // -- optional type suffix ----------------------------------------------
2100
2101    #[test]
2102    fn optional_type_suffix() {
2103        let src = "entity E { locked_until: Timestamp? }";
2104        let r = parse_ok(src);
2105        assert_eq!(r.diagnostics.len(), 0);
2106    }
2107
2108    #[test]
2109    fn optional_trigger_param() {
2110        let src = "rule R { when: Report(interviewer, interview, reason, details?)\n    ensures: Done() }";
2111        let r = parse_ok(src);
2112        assert_eq!(r.diagnostics.len(), 0);
2113    }
2114
2115    // -- qualified name with config ----------------------------------------
2116
2117    #[test]
2118    fn qualified_config_access() {
2119        let src = "entity E { duration: oauth/config.session_duration }";
2120        let r = parse_ok(src);
2121        assert_eq!(r.diagnostics.len(), 0);
2122    }
2123
2124    // -- comprehensive integration test ------------------------------------
2125
2126    #[test]
2127    fn realistic_spec() {
2128        let src = r#"-- allium: 1
2129
2130enum OrderStatus { pending | shipped | delivered }
2131
2132external entity Customer {
2133    email: String
2134    name: String
2135}
2136
2137entity Order {
2138    customer: Customer
2139    status: OrderStatus
2140    total: Decimal
2141    items: OrderItem with order = this
2142    shipped_items: items where status = shipped
2143    confirmed_items: items where status = confirmed -> item
2144    is_complete: status = delivered
2145    locked_until: Timestamp?
2146}
2147
2148config {
2149    max_retries: Integer = 3
2150    timeout: Duration = 24.hours
2151}
2152
2153rule PlaceOrder {
2154    when: CustomerPlacesOrder(customer, items, total)
2155    requires: total > 0
2156    ensures: Order.created(customer: customer, status: pending, total: total)
2157}
2158
2159rule ShipOrder {
2160    when: order: Order.status transitions_to shipped
2161    ensures: Email.created(to: order.customer.email, template: order_shipped)
2162}
2163
2164open question "How do we handle partial shipments?"
2165"#;
2166        let r = parse_ok(src);
2167        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2168        assert_eq!(r.module.version, Some(1));
2169        assert_eq!(r.module.declarations.len(), 7);
2170    }
2171
2172    #[test]
2173    fn extension_behaviour_excerpt() {
2174        // Exercises: inline enums, generic types, or-triggers, named call
2175        // args, config with typed defaults, module declaration.
2176        let src = r#"value Document {
2177    uri: String
2178    text: String
2179}
2180
2181entity Finding {
2182    code: String
2183    severity: error | warning | info
2184    range: FindingRange
2185}
2186
2187entity DiagnosticsMode {
2188    value: strict | relaxed
2189}
2190
2191config {
2192    duplicateKey: String = "allium.config.duplicateKey"
2193}
2194
2195rule RefreshDiagnostics {
2196    when: DocumentOpened(document) or DocumentChanged(document)
2197    requires: document.language_id = "allium"
2198    ensures: FindingsComputed(document)
2199}
2200
2201surface DiagnosticsDashboard {
2202    facing viewer: Developer
2203    context doc: Document where viewer.active_document = doc
2204    provides: RunChecks(viewer) when doc.language_id = "allium"
2205    exposes: FindingList
2206}
2207
2208rule ProcessDigests {
2209    when: schedule: DigestSchedule.next_run_at <= now
2210    for user in Users where notification_setting.digest_enabled:
2211        let settings = user.notification_setting
2212        ensures: DigestBatch.created(user: user)
2213}
2214"#;
2215        let r = parse_ok(src);
2216        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2217        // value + entity + entity + config + rule + surface + rule = 7
2218        assert_eq!(r.module.declarations.len(), 7);
2219    }
2220
2221    #[test]
2222    fn suffix_predicate_simple() {
2223        let src = r#"rule R {
2224    when: RenameRequested(symbol)
2225    requires: symbol resolves_to_single_definition
2226    ensures: RenameApplied()
2227}"#;
2228        let r = parse_ok(src);
2229        assert_eq!(r.diagnostics.len(), 0);
2230        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2231        // when + requires + ensures = 3 items
2232        assert_eq!(b.items.len(), 3);
2233        // The requires value should be a Predicate
2234        let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
2235        assert_eq!(keyword, "requires");
2236        assert!(matches!(value, Expr::Predicate { .. }));
2237    }
2238
2239    #[test]
2240    fn suffix_predicate_with_arg() {
2241        let src = r#"rule R {
2242    when: X()
2243    requires: finding.code starts_with "allium."
2244}"#;
2245        let r = parse_ok(src);
2246        assert_eq!(r.diagnostics.len(), 0);
2247        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2248        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
2249        if let Expr::Predicate { subject, tail, .. } = value {
2250            assert!(matches!(subject.as_ref(), Expr::MemberAccess { .. }));
2251            assert_eq!(tail.len(), 2); // starts_with + "allium."
2252        } else {
2253            panic!("expected Predicate, got {:?}", value);
2254        }
2255    }
2256
2257    #[test]
2258    fn suffix_predicate_with_comparison() {
2259        let src = r#"rule R {
2260    when: X()
2261    requires: clause compares_literals = true
2262}"#;
2263        let r = parse_ok(src);
2264        assert_eq!(r.diagnostics.len(), 0);
2265        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2266        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
2267        // Should be Predicate(clause, [Comparison(compares_literals, Eq, true)])
2268        if let Expr::Predicate { tail, .. } = value {
2269            assert_eq!(tail.len(), 1);
2270            assert!(matches!(tail[0], Expr::Comparison { .. }));
2271        } else {
2272            panic!("expected Predicate");
2273        }
2274    }
2275
2276    #[test]
2277    fn range_literal_in_list() {
2278        let src = r#"rule R {
2279    when: X()
2280    requires: width in [1..8]
2281}"#;
2282        let r = parse_ok(src);
2283        assert_eq!(r.diagnostics.len(), 0);
2284        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2285        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
2286        // Should be In(width, ListLiteral([Range(1, 8)]))
2287        if let Expr::In { collection, .. } = value {
2288            if let Expr::ListLiteral { elements, .. } = collection.as_ref() {
2289                assert_eq!(elements.len(), 1);
2290                assert!(matches!(elements[0], Expr::Range { .. }));
2291            } else {
2292                panic!("expected ListLiteral");
2293            }
2294        } else {
2295            panic!("expected In");
2296        }
2297    }
2298
2299    #[test]
2300    fn exists_as_identifier() {
2301        let src = r#"rule R {
2302    when: X()
2303    ensures: CompletionItemAvailable(label: exists)
2304}"#;
2305        let r = parse_ok(src);
2306        assert_eq!(r.diagnostics.len(), 0);
2307    }
2308
2309    // -- pipe precedence: tighter than boolean ops ----------------------------
2310
2311    #[test]
2312    fn pipe_binds_tighter_than_or() {
2313        // `a or b | c` should parse as `a or (b | c)`, not `(a or b) | c`
2314        let src = "entity E { v: a or b | c }";
2315        let r = parse_ok(src);
2316        assert_eq!(r.diagnostics.len(), 0);
2317        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2318        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2319        // Top-level should be LogicalOp(Or)
2320        let Expr::LogicalOp { op, right, .. } = value else {
2321            panic!("expected LogicalOp, got {value:?}");
2322        };
2323        assert_eq!(*op, LogicalOp::Or);
2324        // Right side should be Pipe(b, c)
2325        assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2326    }
2327
2328    // -- variant with expression base -----------------------------------------
2329
2330    #[test]
2331    fn variant_with_pipe_base() {
2332        let src = "variant Mixed : TypeA | TypeB";
2333        let r = parse_ok(src);
2334        assert_eq!(r.diagnostics.len(), 0);
2335        let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2336        assert!(matches!(v.base, Expr::Pipe { .. }));
2337    }
2338
2339    // -- trigger clause keyword -----------------------------------------------
2340
2341    #[test]
2342    fn trigger_clause() {
2343        let src = "rule R { trigger: ExternalEvent(data)\n    ensures: Done() }";
2344        let r = parse_ok(src);
2345        assert_eq!(r.diagnostics.len(), 0);
2346        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2347        let BlockItemKind::Clause { keyword, .. } = &b.items[0].kind else { panic!() };
2348        assert_eq!(keyword, "trigger");
2349    }
2350
2351    // -- for-block with comparison in where filter ----------------------------
2352
2353    #[test]
2354    fn for_block_where_comparison() {
2355        let src = r#"rule R {
2356    when: X()
2357    for item in Items where item.status = active:
2358        ensures: Processed(item: item)
2359}"#;
2360        let r = parse_ok(src);
2361        assert_eq!(r.diagnostics.len(), 0);
2362        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2363        let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2364        assert!(filter.is_some());
2365        assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2366    }
2367
2368    // -- for-expression with comparison in where filter -----------------------
2369
2370    #[test]
2371    fn for_expr_where_comparison() {
2372        let src = r#"rule R {
2373    when: X()
2374    ensures:
2375        for item in Items where item.active = true:
2376            Processed(item: item)
2377}"#;
2378        let r = parse_ok(src);
2379        assert_eq!(r.diagnostics.len(), 0);
2380    }
2381
2382    // -- if/else if/else chain ------------------------------------------------
2383
2384    #[test]
2385    fn if_else_if_else() {
2386        let src = r#"rule R {
2387    when: X(v)
2388    ensures:
2389        if v < 10: Small()
2390        else if v < 100: Medium()
2391        else: Large()
2392}"#;
2393        let r = parse_ok(src);
2394        assert_eq!(r.diagnostics.len(), 0);
2395    }
2396
2397    // -- null coalescing and optional chaining --------------------------------
2398
2399    #[test]
2400    fn null_coalesce_and_optional_chain() {
2401        let src = "entity E { v: a?.b ?? fallback }";
2402        let r = parse_ok(src);
2403        assert_eq!(r.diagnostics.len(), 0);
2404        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2405        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2406        // Top-level should be NullCoalesce
2407        assert!(matches!(value, Expr::NullCoalesce { .. }));
2408    }
2409
2410    // -- generic types --------------------------------------------------------
2411
2412    #[test]
2413    fn generic_type_nested() {
2414        let src = "entity E { v: List<Set<String>> }";
2415        let r = parse_ok(src);
2416        assert_eq!(r.diagnostics.len(), 0);
2417    }
2418
2419    // -- set literal, list literal, object literal ----------------------------
2420
2421    #[test]
2422    fn collection_literals() {
2423        let src = r#"rule R {
2424    when: X()
2425    ensures:
2426        let s = {a, b, c}
2427        let l = [1, 2, 3]
2428        let o = {name: "test", count: 42}
2429        Done()
2430}"#;
2431        let r = parse_ok(src);
2432        assert_eq!(r.diagnostics.len(), 0);
2433    }
2434
2435    // -- given block ----------------------------------------------------------
2436
2437    #[test]
2438    fn given_block() {
2439        let src = "given { viewer: User\n    time: Timestamp }";
2440        let r = parse_ok(src);
2441        assert_eq!(r.diagnostics.len(), 0);
2442        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2443        assert_eq!(b.kind, BlockKind::Given);
2444        assert!(b.name.is_none());
2445    }
2446
2447    // -- actor block ----------------------------------------------------------
2448
2449    #[test]
2450    fn actor_block() {
2451        let src = "actor Admin { identified_by: User where role = admin }";
2452        let r = parse_ok(src);
2453        assert_eq!(r.diagnostics.len(), 0);
2454        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2455        assert_eq!(b.kind, BlockKind::Actor);
2456    }
2457
2458    // -- join lookup ----------------------------------------------------------
2459
2460    #[test]
2461    fn join_lookup() {
2462        let src = "entity E { match: Other{field_a, field_b: value} }";
2463        let r = parse_ok(src);
2464        assert_eq!(r.diagnostics.len(), 0);
2465        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2466        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2467        assert!(matches!(value, Expr::JoinLookup { .. }));
2468    }
2469
2470    // -- includes / excludes --------------------------------------------------
2471
2472    #[test]
2473    fn includes_excludes() {
2474        let src = r#"rule R {
2475    when: X(a, b)
2476    requires: a.items includes b
2477    requires: a.items excludes b
2478    ensures: Done()
2479}"#;
2480        let r = parse_ok(src);
2481        assert_eq!(r.diagnostics.len(), 0);
2482    }
2483
2484    // -- in / not in with set literal -----------------------------------------
2485
2486    #[test]
2487    fn in_not_in_set() {
2488        let src = r#"rule R {
2489    when: X(s)
2490    requires: s in {a, b, c}
2491    requires: s not in {d, e}
2492    ensures: Done()
2493}"#;
2494        let r = parse_ok(src);
2495        assert_eq!(r.diagnostics.len(), 0);
2496    }
2497
2498    // -- comprehensive fixture file -------------------------------------------
2499
2500    #[test]
2501    fn comprehensive_fixture() {
2502        let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
2503        let r = parse(src);
2504        assert_eq!(
2505            r.diagnostics.len(),
2506            0,
2507            "expected no errors in comprehensive fixture, got: {:?}",
2508            r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
2509        );
2510        assert!(r.module.declarations.len() > 30, "expected many declarations");
2511    }
2512
2513    // -- error message quality ------------------------------------------------
2514
2515    #[test]
2516    fn error_expected_declaration() {
2517        let r = parse("+ invalid");
2518        assert!(r.diagnostics.len() >= 1);
2519        let msg = &r.diagnostics[0].message;
2520        assert!(msg.contains("expected declaration"), "got: {msg}");
2521        assert!(msg.contains("entity"), "should list valid options, got: {msg}");
2522        assert!(msg.contains("rule"), "should list valid options, got: {msg}");
2523    }
2524
2525    #[test]
2526    fn error_expected_expression() {
2527        let r = parse("entity E { v: }");
2528        assert!(r.diagnostics.len() >= 1);
2529        let msg = &r.diagnostics[0].message;
2530        assert!(msg.contains("expected expression"), "got: {msg}");
2531        assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
2532    }
2533
2534    #[test]
2535    fn error_expected_block_item() {
2536        let r = parse("entity E { + }");
2537        assert!(r.diagnostics.len() >= 1);
2538        let msg = &r.diagnostics[0].message;
2539        assert!(msg.contains("expected block item"), "got: {msg}");
2540    }
2541
2542    #[test]
2543    fn error_expected_identifier() {
2544        let r = parse("entity 123 {}");
2545        assert!(r.diagnostics.len() >= 1);
2546        let msg = &r.diagnostics[0].message;
2547        // Context-aware: says "entity name" not generic "identifier"
2548        assert!(msg.contains("expected entity name"), "got: {msg}");
2549        // Human-friendly: says "number" not "Number" or "TokenKind::Number"
2550        assert!(msg.contains("number"), "should say what was found, got: {msg}");
2551    }
2552
2553    #[test]
2554    fn error_missing_brace() {
2555        let r = parse("entity E {");
2556        assert!(r.diagnostics.len() >= 1);
2557        let msg = &r.diagnostics[0].message;
2558        assert!(msg.contains("expected"), "got: {msg}");
2559    }
2560
2561    #[test]
2562    fn error_recovery_multiple() {
2563        // Parser should recover and report multiple errors
2564        let r = parse("entity E { + }\nentity F { - }");
2565        assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
2566    }
2567}