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