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        let line = self.source_map.line_col(span.start).0;
120        if let Some(last) = self.diagnostics.last() {
121            if last.severity == crate::diagnostic::Severity::Error
122                && self.source_map.line_col(last.span.start).0 == line
123            {
124                return;
125            }
126        }
127        self.diagnostics.push(Diagnostic::error(span, msg));
128    }
129
130    /// Consume and return an [`Ident`] from any word token.
131    fn parse_ident(&mut self) -> Option<Ident> {
132        self.parse_ident_in("identifier")
133    }
134
135    /// Consume and return an [`Ident`] with a context-specific label for errors.
136    fn parse_ident_in(&mut self, context: &str) -> Option<Ident> {
137        let tok = self.peek();
138        if tok.kind.is_word() {
139            self.advance();
140            Some(Ident {
141                span: tok.span,
142                name: self.text(tok.span).to_string(),
143            })
144        } else {
145            self.error(
146                tok.span,
147                format!("expected {context}, found {}", tok.kind),
148            );
149            None
150        }
151    }
152
153    /// Consume a string token and produce a [`StringLiteral`].
154    fn parse_string(&mut self) -> Option<StringLiteral> {
155        let tok = self.expect(TokenKind::String)?;
156        let raw = self.text(tok.span);
157        // Strip surrounding quotes
158        let inner = &raw[1..raw.len() - 1];
159        let parts = parse_string_parts(inner, tok.span.start + 1);
160        Some(StringLiteral {
161            span: tok.span,
162            parts,
163        })
164    }
165}
166
167/// Split the inner content of a string literal into text and interpolation
168/// parts. `base_offset` is the byte offset of the first character after the
169/// opening quote in the source file.
170fn parse_string_parts(inner: &str, base_offset: usize) -> Vec<StringPart> {
171    let mut parts = Vec::new();
172    let mut buf = String::new();
173    let bytes = inner.as_bytes();
174    let mut i = 0;
175    while i < bytes.len() {
176        if bytes[i] == b'\\' && i + 1 < bytes.len() {
177            buf.push(bytes[i + 1] as char);
178            i += 2;
179        } else if bytes[i] == b'{' {
180            if !buf.is_empty() {
181                parts.push(StringPart::Text(std::mem::take(&mut buf)));
182            }
183            i += 1; // skip {
184            let start = i;
185            while i < bytes.len() && bytes[i] != b'}' {
186                i += 1;
187            }
188            let name = std::str::from_utf8(&bytes[start..i]).unwrap_or("").to_string();
189            let span_start = base_offset + start;
190            let span_end = base_offset + i;
191            parts.push(StringPart::Interpolation(Ident {
192                span: Span::new(span_start, span_end),
193                name,
194            }));
195            if i < bytes.len() {
196                i += 1; // skip }
197            }
198        } else {
199            buf.push(bytes[i] as char);
200            i += 1;
201        }
202    }
203    if !buf.is_empty() {
204        parts.push(StringPart::Text(buf));
205    }
206    parts
207}
208
209// ---------------------------------------------------------------------------
210// Clause-keyword recognition
211// ---------------------------------------------------------------------------
212
213/// Returns true for identifiers that act as clause keywords inside blocks.
214/// These are parsed as `Clause` items rather than `Assignment` items.
215fn is_clause_keyword(text: &str) -> bool {
216    matches!(
217        text,
218        "when"
219            | "requires"
220            | "ensures"
221            | "facing"
222            | "context"
223            | "exposes"
224            | "provides"
225            | "related"
226            | "timeout"
227            | "guarantee"
228            | "guidance"
229            | "identified_by"
230            | "within"
231    )
232}
233
234/// True for clause keywords whose value can start with a `name: expr` binding.
235fn clause_allows_binding(keyword: &str) -> bool {
236    matches!(keyword, "when")
237}
238
239/// True for keywords that use `keyword name: value` syntax (no colon after the
240/// keyword). These directly embed a binding.
241fn is_binding_clause_keyword(text: &str) -> bool {
242    matches!(text, "facing" | "context")
243}
244
245/// True if the current token is a keyword that begins a clause.
246fn token_is_clause_keyword(kind: TokenKind) -> bool {
247    matches!(
248        kind,
249        TokenKind::When | TokenKind::Requires | TokenKind::Ensures | TokenKind::Within
250    )
251}
252
253// ---------------------------------------------------------------------------
254// Module parsing
255// ---------------------------------------------------------------------------
256
257impl<'s> Parser<'s> {
258    fn parse_module(&mut self) -> Module {
259        let start = self.peek().span;
260        // Version marker is a comment: `-- allium: N`. Detect it from the raw
261        // source before the lexer strips it.
262        let version = detect_version(self.source);
263
264        match version {
265            None => {
266                self.diagnostics.push(Diagnostic::warning(
267                    start,
268                    "missing version marker; expected '-- allium: 1' as the first line",
269                ));
270            }
271            Some(1) => {}
272            Some(v) => {
273                self.diagnostics.push(Diagnostic::error(
274                    start,
275                    format!("unsupported allium version {v}; this parser supports version 1"),
276                ));
277            }
278        }
279
280        let mut decls = Vec::new();
281        while !self.at_eof() {
282            if let Some(d) = self.parse_decl() {
283                decls.push(d);
284            } else {
285                // Recovery: skip one token and try again
286                self.advance();
287            }
288        }
289        let end = self.peek().span;
290        Module {
291            span: start.merge(end),
292            version,
293            declarations: decls,
294        }
295    }
296}
297
298fn detect_version(source: &str) -> Option<u32> {
299    for line in source.lines() {
300        let trimmed = line.trim();
301        if trimmed.is_empty() {
302            continue;
303        }
304        if let Some(rest) = trimmed.strip_prefix("--") {
305            let rest = rest.trim();
306            if let Some(ver) = rest.strip_prefix("allium:") {
307                return ver.trim().parse().ok();
308            }
309        }
310        break; // only check leading lines
311    }
312    None
313}
314
315// ---------------------------------------------------------------------------
316// Declaration parsing
317// ---------------------------------------------------------------------------
318
319impl<'s> Parser<'s> {
320    fn parse_decl(&mut self) -> Option<Decl> {
321        match self.peek_kind() {
322            TokenKind::Use => self.parse_use_decl().map(Decl::Use),
323            TokenKind::Rule => self.parse_block(BlockKind::Rule).map(Decl::Block),
324            TokenKind::Entity => self.parse_block(BlockKind::Entity).map(Decl::Block),
325            TokenKind::External => {
326                let start = self.advance().span;
327                if self.at(TokenKind::Entity) {
328                    self.parse_block_from(start, BlockKind::ExternalEntity)
329                        .map(Decl::Block)
330                } else {
331                    self.error(self.peek().span, "expected 'entity' after 'external'");
332                    None
333                }
334            }
335            TokenKind::Value => self.parse_block(BlockKind::Value).map(Decl::Block),
336            TokenKind::Enum => self.parse_block(BlockKind::Enum).map(Decl::Block),
337            TokenKind::Given => self.parse_anonymous_block(BlockKind::Given).map(Decl::Block),
338            TokenKind::Config => self.parse_anonymous_block(BlockKind::Config).map(Decl::Block),
339            TokenKind::Surface => self.parse_block(BlockKind::Surface).map(Decl::Block),
340            TokenKind::Actor => self.parse_block(BlockKind::Actor).map(Decl::Block),
341            TokenKind::Default => self.parse_default_decl().map(Decl::Default),
342            TokenKind::Variant => self.parse_variant_decl().map(Decl::Variant),
343            TokenKind::Deferred => self.parse_deferred_decl().map(Decl::Deferred),
344            TokenKind::Open => self.parse_open_question_decl().map(Decl::OpenQuestion),
345            // Qualified config: `alias/config { ... }`
346            TokenKind::Ident
347                if self.peek_at(1).kind == TokenKind::Slash
348                    && self.text(self.peek_at(2).span) == "config" =>
349            {
350                self.parse_qualified_config().map(Decl::Block)
351            }
352            _ => {
353                self.error(
354                    self.peek().span,
355                    format!(
356                        "expected declaration (entity, rule, enum, value, config, surface, actor, \
357                         given, default, variant, deferred, use, open question), found {}",
358                        self.peek_kind(),
359                    ),
360                );
361                None
362            }
363        }
364    }
365
366    // -- module declaration -----------------------------------------------
367
368    // -- use declaration ------------------------------------------------
369
370    fn parse_use_decl(&mut self) -> Option<UseDecl> {
371        let start = self.expect(TokenKind::Use)?.span;
372        let path = self.parse_string()?;
373        let alias = if self.eat(TokenKind::As).is_some() {
374            Some(self.parse_ident_in("import alias")?)
375        } else {
376            None
377        };
378        let end = alias
379            .as_ref()
380            .map(|a| a.span)
381            .unwrap_or(path.span);
382        Some(UseDecl {
383            span: start.merge(end),
384            path,
385            alias,
386        })
387    }
388
389    // -- named block: `keyword Name { ... }` ----------------------------
390
391    fn parse_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
392        let start = self.advance().span; // consume keyword
393        self.parse_block_from(start, kind)
394    }
395
396    fn parse_block_from(&mut self, start: Span, kind: BlockKind) -> Option<BlockDecl> {
397        // For ExternalEntity the keyword was already consumed by the caller;
398        // here we consume Entity.
399        if kind == BlockKind::ExternalEntity {
400            self.expect(TokenKind::Entity)?;
401        }
402        let context = match kind {
403            BlockKind::Entity | BlockKind::ExternalEntity => "entity name",
404            BlockKind::Rule => "rule name",
405            BlockKind::Surface => "surface name",
406            BlockKind::Actor => "actor name",
407            BlockKind::Value => "value type name",
408            BlockKind::Enum => "enum name",
409            _ => "block name",
410        };
411        let name = Some(self.parse_ident_in(context)?);
412        self.expect(TokenKind::LBrace)?;
413        let items = if kind == BlockKind::Enum {
414            self.parse_enum_body()
415        } else {
416            self.parse_block_items()
417        };
418        let end = self.expect(TokenKind::RBrace)?.span;
419        Some(BlockDecl {
420            span: start.merge(end),
421            kind,
422            name,
423            items,
424        })
425    }
426
427    // -- anonymous block: `keyword { ... }` -----------------------------
428
429    /// Parse enum body: pipe-separated variant names.
430    /// `{ pending | shipped | delivered }`
431    fn parse_enum_body(&mut self) -> Vec<BlockItem> {
432        let mut items = Vec::new();
433        while !self.at(TokenKind::RBrace) && !self.at_eof() {
434            if self.eat(TokenKind::Pipe).is_some() {
435                continue;
436            }
437            if let Some(ident) = self.parse_ident_in("enum variant") {
438                items.push(BlockItem {
439                    span: ident.span,
440                    kind: BlockItemKind::EnumVariant { name: ident },
441                });
442            } else {
443                self.advance(); // skip unrecognised token
444            }
445        }
446        items
447    }
448
449    fn parse_anonymous_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
450        let start = self.advance().span;
451        self.expect(TokenKind::LBrace)?;
452        let items = self.parse_block_items();
453        let end = self.expect(TokenKind::RBrace)?.span;
454        Some(BlockDecl {
455            span: start.merge(end),
456            kind,
457            name: None,
458            items,
459        })
460    }
461
462    // -- qualified config: `alias/config { ... }` -----------------------
463
464    fn parse_qualified_config(&mut self) -> Option<BlockDecl> {
465        let alias = self.parse_ident_in("config qualifier")?;
466        let start = alias.span;
467        self.expect(TokenKind::Slash)?;
468        self.advance(); // consume "config" ident
469        self.expect(TokenKind::LBrace)?;
470        let items = self.parse_block_items();
471        let end = self.expect(TokenKind::RBrace)?.span;
472        Some(BlockDecl {
473            span: start.merge(end),
474            kind: BlockKind::Config,
475            name: Some(alias),
476            items,
477        })
478    }
479
480    // -- default declaration -------------------------------------------
481
482    fn parse_default_decl(&mut self) -> Option<DefaultDecl> {
483        let start = self.expect(TokenKind::Default)?.span;
484
485        // `default [TypeName] instanceName = value`
486        // The type name is optional. If the next two tokens are both words
487        // and the second is followed by `=`, the first is the type.
488        let (type_name, name) = if self.peek_kind().is_word()
489            && self.peek_at(1).kind.is_word()
490            && self.peek_at(2).kind == TokenKind::Eq
491        {
492            let t = self.parse_ident_in("type name")?;
493            let n = self.parse_ident_in("default name")?;
494            (Some(t), n)
495        } else {
496            (None, self.parse_ident_in("default name")?)
497        };
498
499        self.expect(TokenKind::Eq)?;
500        let value = self.parse_expr(0)?;
501        Some(DefaultDecl {
502            span: start.merge(value.span()),
503            type_name,
504            name,
505            value,
506        })
507    }
508
509    // -- variant declaration -------------------------------------------
510
511    fn parse_variant_decl(&mut self) -> Option<VariantDecl> {
512        let start = self.expect(TokenKind::Variant)?.span;
513        let name = self.parse_ident_in("variant name")?;
514        self.expect(TokenKind::Colon)?;
515        let base = self.parse_expr(0)?;
516
517        let items = if self.eat(TokenKind::LBrace).is_some() {
518            let items = self.parse_block_items();
519            self.expect(TokenKind::RBrace)?;
520            items
521        } else {
522            Vec::new()
523        };
524
525        let end = if let Some(last) = items.last() {
526            last.span
527        } else {
528            base.span()
529        };
530        Some(VariantDecl {
531            span: start.merge(end),
532            name,
533            base,
534            items,
535        })
536    }
537
538    // -- deferred declaration ------------------------------------------
539
540    fn parse_deferred_decl(&mut self) -> Option<DeferredDecl> {
541        let start = self.expect(TokenKind::Deferred)?.span;
542        let path = self.parse_expr(0)?;
543        Some(DeferredDecl {
544            span: start.merge(path.span()),
545            path,
546        })
547    }
548
549    // -- open question --------------------------------------------------
550
551    fn parse_open_question_decl(&mut self) -> Option<OpenQuestionDecl> {
552        let start = self.expect(TokenKind::Open)?.span;
553        self.expect(TokenKind::Question)?;
554        let text = self.parse_string()?;
555        Some(OpenQuestionDecl {
556            span: start.merge(text.span),
557            text,
558        })
559    }
560
561    // -- guidance declaration ----------------------------------------------
562
563}
564
565// ---------------------------------------------------------------------------
566// Block item parsing
567// ---------------------------------------------------------------------------
568
569impl<'s> Parser<'s> {
570    fn parse_block_items(&mut self) -> Vec<BlockItem> {
571        let mut items = Vec::new();
572        while !self.at(TokenKind::RBrace) && !self.at_eof() {
573            if let Some(item) = self.parse_block_item() {
574                items.push(item);
575            } else {
576                // Recovery: skip one token
577                self.advance();
578            }
579        }
580        items
581    }
582
583    fn parse_block_item(&mut self) -> Option<BlockItem> {
584        let start = self.peek().span;
585
586        // `let name = value`
587        if self.at(TokenKind::Let) {
588            return self.parse_let_item(start);
589        }
590
591        // `for binding in collection [where filter]: ...` at block level
592        if self.at(TokenKind::For) {
593            return self.parse_for_block_item(start);
594        }
595
596        // `if condition: ... [else if ...: ...] [else: ...]` at block level
597        if self.at(TokenKind::If) {
598            return self.parse_if_block_item(start);
599        }
600
601        // `open question "text"` (inside a block)
602        if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
603            self.advance(); // open
604            self.advance(); // question
605            let text = self.parse_string()?;
606            return Some(BlockItem {
607                span: start.merge(text.span),
608                kind: BlockItemKind::OpenQuestion { text },
609            });
610        }
611
612        // Everything else: `name: value` or `keyword: value` or
613        // `name(params): value`
614        if self.peek_kind().is_word() {
615            // `facing name: Type` / `context name: Type [where ...]` — binding
616            // clause keywords that don't use `:` after the keyword itself.
617            if is_binding_clause_keyword(self.text(self.peek().span))
618                && self.peek_at(1).kind.is_word()
619                && self.peek_at(2).kind == TokenKind::Colon
620            {
621                return self.parse_binding_clause_item(start);
622            }
623
624            // Check for `Name.field:` — dot-path reverse relationship
625            if self.peek_at(1).kind == TokenKind::Dot
626                && self.peek_at(2).kind.is_word()
627                && self.peek_at(3).kind == TokenKind::Colon
628            {
629                return self.parse_path_assignment_item(start);
630            }
631
632            // Check for `name(` — potential parameterised assignment
633            if self.peek_at(1).kind == TokenKind::LParen {
634                return self.parse_param_or_clause_item(start);
635            }
636
637            // Check for `name:` — assignment or clause
638            if self.peek_at(1).kind == TokenKind::Colon {
639                return self.parse_assign_or_clause_item(start);
640            }
641        }
642
643        // For clauses whose keyword is a separate TokenKind (when, requires, etc.)
644        if token_is_clause_keyword(self.peek_kind()) && self.peek_at(1).kind == TokenKind::Colon {
645            return self.parse_assign_or_clause_item(start);
646        }
647
648        self.error(
649            start,
650            format!(
651                "expected block item (name: value, let name = value, when:/requires:/ensures: clause, \
652                 for ... in ...:, or open question), found {}",
653                self.peek_kind(),
654            ),
655        );
656        None
657    }
658
659    fn parse_let_item(&mut self, start: Span) -> Option<BlockItem> {
660        self.advance(); // consume `let`
661        let name = self.parse_ident_in("binding name")?;
662        self.expect(TokenKind::Eq)?;
663        let value = self.parse_clause_value(start)?;
664        Some(BlockItem {
665            span: start.merge(value.span()),
666            kind: BlockItemKind::Let { name, value },
667        })
668    }
669
670    /// Parse `facing name: Type` or `context name: Type [where ...]`.
671    /// These keywords don't take `:` after the keyword — they embed a binding directly.
672    fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
673        let keyword_tok = self.advance(); // consume facing/context
674        let keyword = self.text(keyword_tok.span).to_string();
675        let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
676        self.advance(); // consume ':'
677        let type_expr = self.parse_clause_value(start)?;
678        let value_span = type_expr.span();
679        let value = Expr::Binding {
680            span: binding_name.span.merge(value_span),
681            name: binding_name,
682            value: Box::new(type_expr),
683        };
684        Some(BlockItem {
685            span: start.merge(value_span),
686            kind: BlockItemKind::Clause { keyword, value },
687        })
688    }
689
690    /// Parse `for binding in collection [where filter]:` at block level.
691    /// The body is a set of nested block items (let, requires, ensures, etc.).
692    fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
693        self.advance(); // consume `for`
694        let binding = self.parse_for_binding()?;
695        self.expect(TokenKind::In)?;
696
697        let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
698
699        let filter = if self.eat(TokenKind::Where).is_some() {
700            // Parse filter at min_bp 0 — colon terminates naturally since
701            // it's not an expression operator.
702            Some(self.parse_expr(0)?)
703        } else {
704            None
705        };
706
707        self.expect(TokenKind::Colon)?;
708
709        // The body contains nested block items at higher indentation.
710        let for_line = self.line_of(start);
711        let next_line = self.line_of(self.peek().span);
712
713        let items = if next_line > for_line {
714            let base_col = self.col_of(self.peek().span);
715            self.parse_indented_block_items(base_col)
716        } else {
717            // Single-line for: parse one block item
718            let mut items = Vec::new();
719            if let Some(item) = self.parse_block_item() {
720                items.push(item);
721            }
722            items
723        };
724
725        let end = items
726            .last()
727            .map(|i| i.span)
728            .unwrap_or(start);
729
730        Some(BlockItem {
731            span: start.merge(end),
732            kind: BlockItemKind::ForBlock {
733                binding,
734                collection,
735                filter,
736                items,
737            },
738        })
739    }
740
741    /// Collect block items at column >= `base_col` (for indented for-block bodies).
742    fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
743        let mut items = Vec::new();
744        while !self.at_eof()
745            && !self.at(TokenKind::RBrace)
746            && self.col_of(self.peek().span) >= base_col
747        {
748            if let Some(item) = self.parse_block_item() {
749                items.push(item);
750            } else {
751                self.advance();
752                break;
753            }
754        }
755        items
756    }
757
758    /// Parse `if condition: ... [else if ...: ...] [else: ...]` at block level.
759    fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
760        self.advance(); // consume `if`
761        let mut branches = Vec::new();
762
763        // First branch
764        let condition = self.parse_expr(0)?;
765        self.expect(TokenKind::Colon)?;
766        let if_line = self.line_of(start);
767        let items = self.parse_if_block_body(if_line);
768        branches.push(CondBlockBranch {
769            span: start.merge(items.last().map(|i| i.span).unwrap_or(start)),
770            condition,
771            items,
772        });
773
774        // else if / else
775        let mut else_items = None;
776        while self.at(TokenKind::Else) {
777            let else_tok = self.advance();
778            if self.at(TokenKind::If) {
779                let if_start = self.advance().span;
780                let cond = self.parse_expr(0)?;
781                self.expect(TokenKind::Colon)?;
782                let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
783                branches.push(CondBlockBranch {
784                    span: if_start.merge(body_items.last().map(|i| i.span).unwrap_or(if_start)),
785                    condition: cond,
786                    items: body_items,
787                });
788            } else {
789                self.expect(TokenKind::Colon)?;
790                let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
791                else_items = Some(body_items);
792                break;
793            }
794        }
795
796        let end = else_items
797            .as_ref()
798            .and_then(|items| items.last().map(|i| i.span))
799            .or_else(|| branches.last().and_then(|b| b.items.last().map(|i| i.span)))
800            .unwrap_or(start);
801
802        Some(BlockItem {
803            span: start.merge(end),
804            kind: BlockItemKind::IfBlock {
805                branches,
806                else_items,
807            },
808        })
809    }
810
811    /// Parse the body of an if/else if/else block branch.
812    fn parse_if_block_body(&mut self, keyword_line: u32) -> Vec<BlockItem> {
813        let next_line = self.line_of(self.peek().span);
814        if next_line > keyword_line {
815            let base_col = self.col_of(self.peek().span);
816            self.parse_indented_block_items(base_col)
817        } else {
818            // Single-line: parse one block item
819            let mut items = Vec::new();
820            if let Some(item) = self.parse_block_item() {
821                items.push(item);
822            }
823            items
824        }
825    }
826
827    fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
828        let name_tok = self.advance(); // consume name/keyword
829        let name_text = self.text(name_tok.span).to_string();
830        self.advance(); // consume ':'
831
832        let allows_binding = clause_allows_binding(&name_text);
833        let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
834        let value_span = value.span();
835
836        let kind = if is_clause_keyword(&name_text) {
837            BlockItemKind::Clause {
838                keyword: name_text,
839                value,
840            }
841        } else {
842            BlockItemKind::Assignment {
843                name: Ident {
844                    span: name_tok.span,
845                    name: name_text,
846                },
847                value,
848            }
849        };
850
851        Some(BlockItem {
852            span: start.merge(value_span),
853            kind,
854        })
855    }
856
857    /// Parse `Entity.field: value` — a dot-path reverse relationship declaration.
858    fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
859        let obj_tok = self.advance(); // consume first ident
860        self.advance(); // consume '.'
861        let field = self.parse_ident_in("field name")?;
862        self.advance(); // consume ':'
863
864        let path = Expr::MemberAccess {
865            span: obj_tok.span.merge(field.span),
866            object: Box::new(Expr::Ident(Ident {
867                span: obj_tok.span,
868                name: self.text(obj_tok.span).to_string(),
869            })),
870            field,
871        };
872
873        let value = self.parse_clause_value(start)?;
874        let value_span = value.span();
875        Some(BlockItem {
876            span: start.merge(value_span),
877            kind: BlockItemKind::PathAssignment { path, value },
878        })
879    }
880
881    fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
882        // Could be `name(params): value` (param assignment) or
883        // `name(args)` which is an expression that happens to start a clause
884        // value. Peek far enough to see if `)` is followed by `:`.
885        let saved_pos = self.pos;
886        let _name_tok = self.advance();
887        self.advance(); // (
888
889        // Try to scan past balanced parens
890        let mut depth = 1u32;
891        while !self.at_eof() && depth > 0 {
892            match self.peek_kind() {
893                TokenKind::LParen => {
894                    depth += 1;
895                    self.advance();
896                }
897                TokenKind::RParen => {
898                    depth -= 1;
899                    self.advance();
900                }
901                _ => {
902                    self.advance();
903                }
904            }
905        }
906
907        if self.at(TokenKind::Colon) {
908            // It's a parameterised assignment: restore and parse properly
909            self.pos = saved_pos;
910            let name = self.parse_ident_in("derived value name")?;
911            self.expect(TokenKind::LParen)?;
912            let params = self.parse_ident_list()?;
913            self.expect(TokenKind::RParen)?;
914            self.expect(TokenKind::Colon)?;
915            let value = self.parse_clause_value(start)?;
916            Some(BlockItem {
917                span: start.merge(value.span()),
918                kind: BlockItemKind::ParamAssignment {
919                    name,
920                    params,
921                    value,
922                },
923            })
924        } else {
925            // Not a param assignment — restore and fall through to assignment
926            self.pos = saved_pos;
927            // Check for regular `name: value`
928            if self.peek_at(1).kind == TokenKind::Colon {
929                // Nope, the (1) is LParen not Colon. Re-examine.
930            }
931            // Fall back: treat as `name: value` where value starts with a call
932            self.parse_assign_or_clause_item(start)
933        }
934    }
935
936    fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
937        let mut params = Vec::new();
938        if !self.at(TokenKind::RParen) {
939            params.push(self.parse_ident_in("parameter name")?);
940            while self.eat(TokenKind::Comma).is_some() {
941                params.push(self.parse_ident_in("parameter name")?);
942            }
943        }
944        Some(params)
945    }
946
947    /// Parse a for-loop binding: either a single ident or `(a, b)` destructuring.
948    fn parse_for_binding(&mut self) -> Option<ForBinding> {
949        if self.at(TokenKind::LParen) {
950            let start = self.advance().span; // consume '('
951            let mut idents = Vec::new();
952            idents.push(self.parse_ident_in("loop variable")?);
953            while self.eat(TokenKind::Comma).is_some() {
954                idents.push(self.parse_ident_in("loop variable")?);
955            }
956            let end = self.expect(TokenKind::RParen)?.span;
957            Some(ForBinding::Destructured(idents, start.merge(end)))
958        } else {
959            let ident = self.parse_ident_in("loop variable")?;
960            Some(ForBinding::Single(ident))
961        }
962    }
963
964    /// Parse a clause value, optionally checking for a `name: expr` binding
965    /// pattern at the start. Used for when, facing and context clauses where
966    /// the first `ident:` is a binding rather than a nested assignment.
967    fn parse_clause_value_maybe_binding(
968        &mut self,
969        clause_start: Span,
970        allow_binding: bool,
971    ) -> Option<Expr> {
972        if allow_binding
973            && self.peek_kind().is_word()
974            && self.peek_at(1).kind == TokenKind::Colon
975        {
976            // Check this isn't at the start of a new block item on the next line.
977            // Bindings only apply on the same line or the immediate indented value.
978            let clause_line = self.line_of(clause_start);
979            let next_line = self.line_of(self.peek().span);
980            let colon_is_block_item = next_line > clause_line
981                && self.peek_at(2).kind != TokenKind::Eof
982                && self.line_of(self.peek_at(2).span) == next_line;
983
984            if next_line == clause_line || colon_is_block_item {
985                let name = self.parse_ident_in("binding name")?;
986                self.advance(); // consume ':'
987                let inner = self.parse_clause_value(clause_start)?;
988                return Some(Expr::Binding {
989                    span: name.span.merge(inner.span()),
990                    name,
991                    value: Box::new(inner),
992                });
993            }
994        }
995        self.parse_clause_value(clause_start)
996    }
997
998    /// Parse a clause value. If the next token is on a new line (indented),
999    /// collect a multi-line block. Otherwise parse a single expression.
1000    fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1001        let clause_line = self.line_of(clause_start);
1002        let next = self.peek();
1003        let next_line = self.line_of(next.span);
1004
1005        if next_line > clause_line {
1006            // Multi-line block — but only if the next token is actually
1007            // indented past the clause keyword. When a clause has only a
1008            // comment as its value (stripped by the lexer), the next visible
1009            // token is a sibling at the same indentation.
1010            let base_col = self.col_of(next.span);
1011            let clause_col = self.col_of(clause_start);
1012            if base_col <= clause_col {
1013                return Some(Expr::Block {
1014                    span: clause_start,
1015                    items: Vec::new(),
1016                });
1017            }
1018            self.parse_indented_block(base_col)
1019        } else {
1020            // Single-line clause value
1021            self.parse_expr(0)
1022        }
1023    }
1024
1025    /// Collect expressions that start at column >= `base_col` into a block.
1026    /// Also handles `let name = value` bindings inside clause value blocks.
1027    fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1028        let start = self.peek().span;
1029        let mut items = Vec::new();
1030
1031        while !self.at_eof()
1032            && !self.at(TokenKind::RBrace)
1033            && self.col_of(self.peek().span) >= base_col
1034        {
1035            // Handle `let name = value` inside expression blocks
1036            if self.at(TokenKind::Let) {
1037                let let_start = self.advance().span;
1038                if let Some(name) = self.parse_ident_in("binding name") {
1039                    if self.expect(TokenKind::Eq).is_some() {
1040                        if let Some(value) = self.parse_expr(0) {
1041                            items.push(Expr::LetExpr {
1042                                span: let_start.merge(value.span()),
1043                                name,
1044                                value: Box::new(value),
1045                            });
1046                            continue;
1047                        }
1048                    }
1049                }
1050                break;
1051            }
1052
1053            if let Some(expr) = self.parse_expr(0) {
1054                items.push(expr);
1055            } else {
1056                self.advance();
1057                break;
1058            }
1059        }
1060
1061        if items.len() == 1 {
1062            Some(items.pop().unwrap())
1063        } else {
1064            let end = items.last().map(|e| e.span()).unwrap_or(start);
1065            Some(Expr::Block {
1066                span: start.merge(end),
1067                items,
1068            })
1069        }
1070    }
1071}
1072
1073// ---------------------------------------------------------------------------
1074// Expression parsing — Pratt parser
1075// ---------------------------------------------------------------------------
1076
1077// Binding powers (even = left, odd = right for right-associative)
1078const BP_LAMBDA: u8 = 4;
1079const BP_WHEN_GUARD: u8 = 5;
1080const BP_PROJECTION: u8 = 6;
1081const BP_WITH_WHERE: u8 = 7;
1082const BP_OR: u8 = 10;
1083const BP_AND: u8 = 20;
1084const BP_COMPARE: u8 = 30;
1085const BP_TRANSITION: u8 = 32;
1086const BP_NULL_COALESCE: u8 = 40;
1087const BP_ADD: u8 = 50;
1088const BP_MUL: u8 = 60;
1089const BP_PIPE: u8 = 65;
1090const BP_PREFIX: u8 = 70;
1091const BP_POSTFIX: u8 = 80;
1092
1093impl<'s> Parser<'s> {
1094    pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1095        let mut lhs = self.parse_prefix()?;
1096
1097        loop {
1098            if let Some((l_bp, r_bp)) = self.infix_bp() {
1099                if l_bp < min_bp {
1100                    break;
1101                }
1102                lhs = self.parse_infix(lhs, r_bp)?;
1103            } else if let Some(l_bp) = self.postfix_bp() {
1104                if l_bp < min_bp {
1105                    break;
1106                }
1107                lhs = self.parse_postfix(lhs)?;
1108            } else {
1109                break;
1110            }
1111        }
1112
1113        Some(lhs)
1114    }
1115
1116    // -- prefix ---------------------------------------------------------
1117
1118    fn parse_prefix(&mut self) -> Option<Expr> {
1119        match self.peek_kind() {
1120            TokenKind::Not => {
1121                let start = self.advance().span;
1122                if self.at(TokenKind::Exists) {
1123                    self.advance();
1124                    let operand = self.parse_expr(BP_PREFIX)?;
1125                    Some(Expr::NotExists {
1126                        span: start.merge(operand.span()),
1127                        operand: Box::new(operand),
1128                    })
1129                } else {
1130                    let operand = self.parse_expr(BP_PREFIX)?;
1131                    Some(Expr::Not {
1132                        span: start.merge(operand.span()),
1133                        operand: Box::new(operand),
1134                    })
1135                }
1136            }
1137            TokenKind::Exists => {
1138                // When `exists` is not followed by an expression-start token,
1139                // treat it as a plain identifier (e.g. `label: exists`).
1140                let next = self.peek_at(1).kind;
1141                if matches!(
1142                    next,
1143                    TokenKind::RParen
1144                        | TokenKind::RBrace
1145                        | TokenKind::RBracket
1146                        | TokenKind::Comma
1147                        | TokenKind::Eof
1148                ) {
1149                    let id = self.parse_ident()?;
1150                    return Some(Expr::Ident(id));
1151                }
1152                let start = self.advance().span;
1153                let operand = self.parse_expr(BP_PREFIX)?;
1154                Some(Expr::Exists {
1155                    span: start.merge(operand.span()),
1156                    operand: Box::new(operand),
1157                })
1158            }
1159            TokenKind::If => self.parse_if_expr(),
1160            TokenKind::For => self.parse_for_expr(),
1161            TokenKind::LBrace => self.parse_brace_expr(),
1162            TokenKind::LBracket => {
1163                let t = self.advance();
1164                self.error(t.span, "list literals `[...]` are not supported; use `Set<T>` type annotation or `{...}` set literal");
1165                None
1166            }
1167            TokenKind::LParen => self.parse_paren_expr(),
1168            TokenKind::Number => {
1169                let t = self.advance();
1170                Some(Expr::NumberLiteral {
1171                    span: t.span,
1172                    value: self.text(t.span).to_string(),
1173                })
1174            }
1175            TokenKind::Duration => {
1176                let t = self.advance();
1177                Some(Expr::DurationLiteral {
1178                    span: t.span,
1179                    value: self.text(t.span).to_string(),
1180                })
1181            }
1182            TokenKind::String => {
1183                let sl = self.parse_string()?;
1184                Some(Expr::StringLiteral(sl))
1185            }
1186            TokenKind::True => {
1187                let t = self.advance();
1188                Some(Expr::BoolLiteral {
1189                    span: t.span,
1190                    value: true,
1191                })
1192            }
1193            TokenKind::False => {
1194                let t = self.advance();
1195                Some(Expr::BoolLiteral {
1196                    span: t.span,
1197                    value: false,
1198                })
1199            }
1200            TokenKind::Null => {
1201                let t = self.advance();
1202                Some(Expr::Null { span: t.span })
1203            }
1204            TokenKind::Now => {
1205                let t = self.advance();
1206                Some(Expr::Now { span: t.span })
1207            }
1208            TokenKind::This => {
1209                let t = self.advance();
1210                Some(Expr::This { span: t.span })
1211            }
1212            TokenKind::Within => {
1213                let t = self.advance();
1214                Some(Expr::Within { span: t.span })
1215            }
1216            k if k.is_word() => {
1217                let id = self.parse_ident()?;
1218                Some(Expr::Ident(id))
1219            }
1220            TokenKind::Star => {
1221                // Wildcard `*` in type position (e.g. `Codec<*>`)
1222                let t = self.advance();
1223                Some(Expr::Ident(Ident {
1224                    span: t.span,
1225                    name: "*".into(),
1226                }))
1227            }
1228            TokenKind::Minus => {
1229                // Unary minus: -expr → BinaryOp(0, Sub, expr)
1230                let start = self.advance().span;
1231                let operand = self.parse_expr(BP_PREFIX)?;
1232                Some(Expr::BinaryOp {
1233                    span: start.merge(operand.span()),
1234                    left: Box::new(Expr::NumberLiteral {
1235                        span: start,
1236                        value: "0".into(),
1237                    }),
1238                    op: BinaryOp::Sub,
1239                    right: Box::new(operand),
1240                })
1241            }
1242            _ => {
1243                self.error(
1244                    self.peek().span,
1245                    format!(
1246                        "expected expression (identifier, number, string, true/false, null, \
1247                         if/for/not/exists, '(', '{{', '['), found {}",
1248                        self.peek_kind(),
1249                    ),
1250                );
1251                None
1252            }
1253        }
1254    }
1255
1256    // -- infix binding powers -------------------------------------------
1257
1258    fn infix_bp(&self) -> Option<(u8, u8)> {
1259        match self.peek_kind() {
1260            TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), // right-assoc
1261            // `when` as an inline guard on provides/related items
1262            TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1263            TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1264            TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1265            TokenKind::And => Some((BP_AND, BP_AND + 1)),
1266            TokenKind::Eq | TokenKind::BangEq => {
1267                Some((BP_COMPARE, BP_COMPARE + 1))
1268            }
1269            TokenKind::Lt => {
1270                // If `<` is immediately adjacent to a word token (no space),
1271                // treat as generic type postfix, not comparison infix.
1272                if self.pos > 0 {
1273                    let prev = self.tokens[self.pos - 1];
1274                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1275                        return None;
1276                    }
1277                }
1278                Some((BP_COMPARE, BP_COMPARE + 1))
1279            }
1280            TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1281                Some((BP_COMPARE, BP_COMPARE + 1))
1282            }
1283            TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1284            // `not in` — only when followed by `in`
1285            TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1286                Some((BP_COMPARE, BP_COMPARE + 1))
1287            }
1288            TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1289            TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1290            TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1291            TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1292            TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1293            TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1294            TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1295            TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1296            _ => None,
1297        }
1298    }
1299
1300    fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1301        let op_tok = self.advance();
1302        match op_tok.kind {
1303            TokenKind::FatArrow => {
1304                let body = self.parse_expr(r_bp)?;
1305                Some(Expr::Lambda {
1306                    span: lhs.span().merge(body.span()),
1307                    param: Box::new(lhs),
1308                    body: Box::new(body),
1309                })
1310            }
1311            TokenKind::Pipe => {
1312                let rhs = self.parse_expr(r_bp)?;
1313                Some(Expr::Pipe {
1314                    span: lhs.span().merge(rhs.span()),
1315                    left: Box::new(lhs),
1316                    right: Box::new(rhs),
1317                })
1318            }
1319            TokenKind::Or => {
1320                let rhs = self.parse_expr(r_bp)?;
1321                Some(Expr::LogicalOp {
1322                    span: lhs.span().merge(rhs.span()),
1323                    left: Box::new(lhs),
1324                    op: LogicalOp::Or,
1325                    right: Box::new(rhs),
1326                })
1327            }
1328            TokenKind::And => {
1329                let rhs = self.parse_expr(r_bp)?;
1330                Some(Expr::LogicalOp {
1331                    span: lhs.span().merge(rhs.span()),
1332                    left: Box::new(lhs),
1333                    op: LogicalOp::And,
1334                    right: Box::new(rhs),
1335                })
1336            }
1337            TokenKind::Eq => {
1338                let rhs = self.parse_expr(r_bp)?;
1339                Some(Expr::Comparison {
1340                    span: lhs.span().merge(rhs.span()),
1341                    left: Box::new(lhs),
1342                    op: ComparisonOp::Eq,
1343                    right: Box::new(rhs),
1344                })
1345            }
1346            TokenKind::BangEq => {
1347                let rhs = self.parse_expr(r_bp)?;
1348                Some(Expr::Comparison {
1349                    span: lhs.span().merge(rhs.span()),
1350                    left: Box::new(lhs),
1351                    op: ComparisonOp::NotEq,
1352                    right: Box::new(rhs),
1353                })
1354            }
1355            TokenKind::Lt => {
1356                let rhs = self.parse_expr(r_bp)?;
1357                Some(Expr::Comparison {
1358                    span: lhs.span().merge(rhs.span()),
1359                    left: Box::new(lhs),
1360                    op: ComparisonOp::Lt,
1361                    right: Box::new(rhs),
1362                })
1363            }
1364            TokenKind::LtEq => {
1365                let rhs = self.parse_expr(r_bp)?;
1366                Some(Expr::Comparison {
1367                    span: lhs.span().merge(rhs.span()),
1368                    left: Box::new(lhs),
1369                    op: ComparisonOp::LtEq,
1370                    right: Box::new(rhs),
1371                })
1372            }
1373            TokenKind::Gt => {
1374                let rhs = self.parse_expr(r_bp)?;
1375                Some(Expr::Comparison {
1376                    span: lhs.span().merge(rhs.span()),
1377                    left: Box::new(lhs),
1378                    op: ComparisonOp::Gt,
1379                    right: Box::new(rhs),
1380                })
1381            }
1382            TokenKind::GtEq => {
1383                let rhs = self.parse_expr(r_bp)?;
1384                Some(Expr::Comparison {
1385                    span: lhs.span().merge(rhs.span()),
1386                    left: Box::new(lhs),
1387                    op: ComparisonOp::GtEq,
1388                    right: Box::new(rhs),
1389                })
1390            }
1391            TokenKind::In => {
1392                let rhs = self.parse_expr(r_bp)?;
1393                Some(Expr::In {
1394                    span: lhs.span().merge(rhs.span()),
1395                    element: Box::new(lhs),
1396                    collection: Box::new(rhs),
1397                })
1398            }
1399            TokenKind::Not => {
1400                // `not in`
1401                self.expect(TokenKind::In)?;
1402                let rhs = self.parse_expr(r_bp)?;
1403                Some(Expr::NotIn {
1404                    span: lhs.span().merge(rhs.span()),
1405                    element: Box::new(lhs),
1406                    collection: Box::new(rhs),
1407                })
1408            }
1409            TokenKind::Where => {
1410                let rhs = self.parse_expr(r_bp)?;
1411                Some(Expr::Where {
1412                    span: lhs.span().merge(rhs.span()),
1413                    source: Box::new(lhs),
1414                    condition: Box::new(rhs),
1415                })
1416            }
1417            TokenKind::With => {
1418                let rhs = self.parse_expr(r_bp)?;
1419                Some(Expr::With {
1420                    span: lhs.span().merge(rhs.span()),
1421                    source: Box::new(lhs),
1422                    predicate: Box::new(rhs),
1423                })
1424            }
1425            TokenKind::QuestionQuestion => {
1426                let rhs = self.parse_expr(r_bp)?;
1427                Some(Expr::NullCoalesce {
1428                    span: lhs.span().merge(rhs.span()),
1429                    left: Box::new(lhs),
1430                    right: Box::new(rhs),
1431                })
1432            }
1433            TokenKind::Plus => {
1434                let rhs = self.parse_expr(r_bp)?;
1435                Some(Expr::BinaryOp {
1436                    span: lhs.span().merge(rhs.span()),
1437                    left: Box::new(lhs),
1438                    op: BinaryOp::Add,
1439                    right: Box::new(rhs),
1440                })
1441            }
1442            TokenKind::Minus => {
1443                let rhs = self.parse_expr(r_bp)?;
1444                Some(Expr::BinaryOp {
1445                    span: lhs.span().merge(rhs.span()),
1446                    left: Box::new(lhs),
1447                    op: BinaryOp::Sub,
1448                    right: Box::new(rhs),
1449                })
1450            }
1451            TokenKind::Star => {
1452                let rhs = self.parse_expr(r_bp)?;
1453                Some(Expr::BinaryOp {
1454                    span: lhs.span().merge(rhs.span()),
1455                    left: Box::new(lhs),
1456                    op: BinaryOp::Mul,
1457                    right: Box::new(rhs),
1458                })
1459            }
1460            TokenKind::Slash => {
1461                // Check for qualified name: `alias/Name` or `alias/config`
1462                // Qualified if the LHS is a bare identifier and the RHS is a
1463                // word that either starts with uppercase or is a block keyword
1464                // (like `config`).
1465                if let Expr::Ident(ref id) = lhs {
1466                    if self.peek_kind().is_word() {
1467                        let next_text = self.text(self.peek().span);
1468                        let is_qualified = next_text
1469                            .chars()
1470                            .next()
1471                            .is_some_and(|c| c.is_uppercase())
1472                            || matches!(
1473                                self.peek_kind(),
1474                                TokenKind::Config | TokenKind::Entity | TokenKind::Value
1475                            );
1476                        if is_qualified {
1477                            let name_tok = self.advance();
1478                            return Some(Expr::QualifiedName(QualifiedName {
1479                                span: lhs.span().merge(name_tok.span),
1480                                qualifier: Some(id.name.clone()),
1481                                name: self.text(name_tok.span).to_string(),
1482                            }));
1483                        }
1484                    }
1485                }
1486                let rhs = self.parse_expr(r_bp)?;
1487                Some(Expr::BinaryOp {
1488                    span: lhs.span().merge(rhs.span()),
1489                    left: Box::new(lhs),
1490                    op: BinaryOp::Div,
1491                    right: Box::new(rhs),
1492                })
1493            }
1494            TokenKind::ThinArrow => {
1495                let field = self.parse_ident_in("projection field")?;
1496                Some(Expr::ProjectionMap {
1497                    span: lhs.span().merge(field.span),
1498                    source: Box::new(lhs),
1499                    field,
1500                })
1501            }
1502            TokenKind::TransitionsTo => {
1503                let rhs = self.parse_expr(r_bp)?;
1504                Some(Expr::TransitionsTo {
1505                    span: lhs.span().merge(rhs.span()),
1506                    subject: Box::new(lhs),
1507                    new_state: Box::new(rhs),
1508                })
1509            }
1510            TokenKind::Becomes => {
1511                let rhs = self.parse_expr(r_bp)?;
1512                Some(Expr::Becomes {
1513                    span: lhs.span().merge(rhs.span()),
1514                    subject: Box::new(lhs),
1515                    new_state: Box::new(rhs),
1516                })
1517            }
1518            TokenKind::When => {
1519                // Inline guard: `action when condition`
1520                let rhs = self.parse_expr(r_bp)?;
1521                Some(Expr::WhenGuard {
1522                    span: lhs.span().merge(rhs.span()),
1523                    action: Box::new(lhs),
1524                    condition: Box::new(rhs),
1525                })
1526            }
1527            _ => {
1528                self.error(
1529                    op_tok.span,
1530                    format!("unexpected infix operator {}", op_tok.kind),
1531                );
1532                None
1533            }
1534        }
1535    }
1536
1537    // -- postfix --------------------------------------------------------
1538
1539    fn postfix_bp(&self) -> Option<u8> {
1540        match self.peek_kind() {
1541            TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
1542            TokenKind::QuestionMark => Some(BP_POSTFIX),
1543            // `<` for generic types like `Set<T>`, `List<T>` — only treated
1544            // as postfix when it immediately follows a word with no space.
1545            TokenKind::Lt => {
1546                if self.pos > 0 {
1547                    let prev = self.tokens[self.pos - 1];
1548                    // Only if `<` starts immediately after the previous token
1549                    // (no whitespace gap) to distinguish from comparisons.
1550                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1551                        return Some(BP_POSTFIX);
1552                    }
1553                }
1554                None
1555            }
1556            TokenKind::LParen => Some(BP_POSTFIX),
1557            TokenKind::LBrace => {
1558                // Join lookup: only when preceded by something that looks
1559                // like an entity name (handled generically — any expr can
1560                // be followed by { for join lookup in expression position).
1561                // But only if the { is on the same line to avoid consuming
1562                // a block body.
1563                let next = self.peek();
1564                let prev_end = if self.pos > 0 {
1565                    self.tokens[self.pos - 1].span.end
1566                } else {
1567                    0
1568                };
1569                // Same line check
1570                if self.line_of(Span::new(prev_end, prev_end))
1571                    == self.line_of(next.span)
1572                {
1573                    Some(BP_POSTFIX)
1574                } else {
1575                    None
1576                }
1577            }
1578            _ => None,
1579        }
1580    }
1581
1582    fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
1583        match self.peek_kind() {
1584            TokenKind::QuestionMark => {
1585                let end = self.advance().span;
1586                Some(Expr::TypeOptional {
1587                    span: lhs.span().merge(end),
1588                    inner: Box::new(lhs),
1589                })
1590            }
1591            TokenKind::Lt => {
1592                // Generic type: `Set<T>`, `List<Node?>`
1593                self.advance(); // consume <
1594                let mut args = Vec::new();
1595                // Parse args above comparison BP so `>` isn't consumed as infix
1596                while !self.at(TokenKind::Gt) && !self.at_eof() {
1597                    args.push(self.parse_expr(BP_COMPARE + 1)?);
1598                    self.eat(TokenKind::Comma);
1599                }
1600                let end = self.expect(TokenKind::Gt)?.span;
1601                Some(Expr::GenericType {
1602                    span: lhs.span().merge(end),
1603                    name: Box::new(lhs),
1604                    args,
1605                })
1606            }
1607            TokenKind::Dot => {
1608                self.advance();
1609                let field = self.parse_ident_in("field name")?;
1610                Some(Expr::MemberAccess {
1611                    span: lhs.span().merge(field.span),
1612                    object: Box::new(lhs),
1613                    field,
1614                })
1615            }
1616            TokenKind::QuestionDot => {
1617                self.advance();
1618                let field = self.parse_ident_in("field name")?;
1619                Some(Expr::OptionalAccess {
1620                    span: lhs.span().merge(field.span),
1621                    object: Box::new(lhs),
1622                    field,
1623                })
1624            }
1625            TokenKind::LParen => {
1626                self.advance();
1627                let args = self.parse_call_args()?;
1628                let end = self.expect(TokenKind::RParen)?.span;
1629                Some(Expr::Call {
1630                    span: lhs.span().merge(end),
1631                    function: Box::new(lhs),
1632                    args,
1633                })
1634            }
1635            TokenKind::LBrace => {
1636                self.advance();
1637                let fields = self.parse_join_fields()?;
1638                let end = self.expect(TokenKind::RBrace)?.span;
1639                Some(Expr::JoinLookup {
1640                    span: lhs.span().merge(end),
1641                    entity: Box::new(lhs),
1642                    fields,
1643                })
1644            }
1645            _ => None,
1646        }
1647    }
1648
1649    // -- call arguments -------------------------------------------------
1650
1651    fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
1652        let mut args = Vec::new();
1653        while !self.at(TokenKind::RParen) && !self.at_eof() {
1654            // Check for named argument: `name: value`
1655            if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1656                let name = self.parse_ident_in("argument name")?;
1657                self.advance(); // :
1658                let value = self.parse_expr(0)?;
1659                args.push(CallArg::Named(NamedArg {
1660                    span: name.span.merge(value.span()),
1661                    name,
1662                    value,
1663                }));
1664            } else {
1665                let expr = self.parse_expr(0)?;
1666                args.push(CallArg::Positional(expr));
1667            }
1668            self.eat(TokenKind::Comma);
1669        }
1670        Some(args)
1671    }
1672
1673    // -- join fields ----------------------------------------------------
1674
1675    fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
1676        let mut fields = Vec::new();
1677        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1678            let field = self.parse_ident_in("join field name")?;
1679            let value = if self.eat(TokenKind::Colon).is_some() {
1680                Some(self.parse_expr(0)?)
1681            } else {
1682                None
1683            };
1684            fields.push(JoinField {
1685                span: field.span.merge(
1686                    value
1687                        .as_ref()
1688                        .map(|v| v.span())
1689                        .unwrap_or(field.span),
1690                ),
1691                field,
1692                value,
1693            });
1694            self.eat(TokenKind::Comma);
1695        }
1696        Some(fields)
1697    }
1698
1699    // -- if expression --------------------------------------------------
1700
1701    fn parse_if_expr(&mut self) -> Option<Expr> {
1702        let start = self.advance().span; // consume `if`
1703        let mut branches = Vec::new();
1704
1705        // First branch
1706        let condition = self.parse_expr(0)?;
1707        self.expect(TokenKind::Colon)?;
1708        let body = self.parse_branch_body(start)?;
1709        branches.push(CondBranch {
1710            span: start.merge(body.span()),
1711            condition,
1712            body,
1713        });
1714
1715        // else if / else
1716        let mut else_body = None;
1717        while self.at(TokenKind::Else) {
1718            let else_tok = self.advance();
1719            if self.at(TokenKind::If) {
1720                let if_start = self.advance().span;
1721                let cond = self.parse_expr(0)?;
1722                self.expect(TokenKind::Colon)?;
1723                let body = self.parse_branch_body(else_tok.span)?;
1724                branches.push(CondBranch {
1725                    span: if_start.merge(body.span()),
1726                    condition: cond,
1727                    body,
1728                });
1729            } else {
1730                self.expect(TokenKind::Colon)?;
1731                let body = self.parse_branch_body(else_tok.span)?;
1732                else_body = Some(Box::new(body));
1733                break;
1734            }
1735        }
1736
1737        let end = else_body
1738            .as_ref()
1739            .map(|b| b.span())
1740            .or_else(|| branches.last().map(|b| b.body.span()))
1741            .unwrap_or(start);
1742
1743        Some(Expr::Conditional {
1744            span: start.merge(end),
1745            branches,
1746            else_body,
1747        })
1748    }
1749
1750    fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
1751        let keyword_line = self.line_of(keyword_span);
1752        let next_line = self.line_of(self.peek().span);
1753
1754        if next_line > keyword_line {
1755            let base_col = self.col_of(self.peek().span);
1756            self.parse_indented_block(base_col)
1757        } else {
1758            self.parse_expr(0)
1759        }
1760    }
1761
1762    // -- for expression -------------------------------------------------
1763
1764    fn parse_for_expr(&mut self) -> Option<Expr> {
1765        let start = self.advance().span; // consume `for`
1766        let binding = self.parse_for_binding()?;
1767        self.expect(TokenKind::In)?;
1768
1769        // Parse collection, stopping before `where` and `:`
1770        let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1771
1772        let filter = if self.eat(TokenKind::Where).is_some() {
1773            // Parse filter at min_bp 0 — colon terminates naturally.
1774            Some(Box::new(self.parse_expr(0)?))
1775        } else {
1776            None
1777        };
1778
1779        self.expect(TokenKind::Colon)?;
1780        let body = self.parse_branch_body(start)?;
1781
1782        Some(Expr::For {
1783            span: start.merge(body.span()),
1784            binding,
1785            collection: Box::new(collection),
1786            filter,
1787            body: Box::new(body),
1788        })
1789    }
1790
1791    // -- brace expressions: set literal or object literal ---------------
1792
1793    fn parse_brace_expr(&mut self) -> Option<Expr> {
1794        let start = self.advance().span; // consume {
1795
1796        if self.at(TokenKind::RBrace) {
1797            let end = self.advance().span;
1798            return Some(Expr::SetLiteral {
1799                span: start.merge(end),
1800                elements: Vec::new(),
1801            });
1802        }
1803
1804        // Peek: if first item is `ident:`, it's an object literal
1805        if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1806            return self.parse_object_literal(start);
1807        }
1808
1809        // Otherwise set literal
1810        self.parse_set_literal(start)
1811    }
1812
1813
1814    fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
1815        let mut fields = Vec::new();
1816        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1817            let name = self.parse_ident_in("field name")?;
1818            self.expect(TokenKind::Colon)?;
1819            let value = self.parse_expr(0)?;
1820            fields.push(NamedArg {
1821                span: name.span.merge(value.span()),
1822                name,
1823                value,
1824            });
1825            self.eat(TokenKind::Comma);
1826        }
1827        let end = self.expect(TokenKind::RBrace)?.span;
1828        Some(Expr::ObjectLiteral {
1829            span: start.merge(end),
1830            fields,
1831        })
1832    }
1833
1834    fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
1835        let mut elements = Vec::new();
1836        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1837            elements.push(self.parse_expr(0)?);
1838            self.eat(TokenKind::Comma);
1839        }
1840        let end = self.expect(TokenKind::RBrace)?.span;
1841        Some(Expr::SetLiteral {
1842            span: start.merge(end),
1843            elements,
1844        })
1845    }
1846
1847    // -- parenthesised expression ---------------------------------------
1848
1849    fn parse_paren_expr(&mut self) -> Option<Expr> {
1850        self.advance(); // (
1851        let expr = self.parse_expr(0)?;
1852        self.expect(TokenKind::RParen)?;
1853        Some(expr)
1854    }
1855}
1856
1857// ---------------------------------------------------------------------------
1858// Tests
1859// ---------------------------------------------------------------------------
1860
1861#[cfg(test)]
1862mod tests {
1863    use super::*;
1864    use crate::diagnostic::Severity;
1865
1866    fn parse_ok(src: &str) -> ParseResult {
1867        // Prefix with version marker if not already present, to avoid
1868        // spurious "missing version marker" warnings in every test.
1869        let owned;
1870        let input = if src.starts_with("-- allium:") {
1871            src
1872        } else {
1873            owned = format!("-- allium: 1\n{src}");
1874            &owned
1875        };
1876        let result = parse(input);
1877        if !result.diagnostics.is_empty() {
1878            for d in &result.diagnostics {
1879                eprintln!(
1880                    "  [{:?}] {} ({}..{})",
1881                    d.severity, d.message, d.span.start, d.span.end
1882                );
1883            }
1884        }
1885        result
1886    }
1887
1888    #[test]
1889    fn version_marker() {
1890        let r = parse_ok("-- allium: 1\n");
1891        assert_eq!(r.module.version, Some(1));
1892        assert_eq!(r.diagnostics.len(), 0);
1893    }
1894
1895    #[test]
1896    fn version_missing_warns() {
1897        let r = parse("entity User {}");
1898        assert_eq!(r.module.version, None);
1899        assert_eq!(r.diagnostics.len(), 1);
1900        assert_eq!(r.diagnostics[0].severity, Severity::Warning);
1901        assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
1902    }
1903
1904    #[test]
1905    fn version_unsupported_errors() {
1906        let r = parse("-- allium: 99\nentity User {}");
1907        assert_eq!(r.module.version, Some(99));
1908        assert!(r.diagnostics.iter().any(|d|
1909            d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
1910        ), "expected unsupported version error, got: {:?}", r.diagnostics);
1911    }
1912
1913    #[test]
1914    fn empty_entity() {
1915        let r = parse_ok("entity User {}");
1916        assert_eq!(r.diagnostics.len(), 0);
1917        assert_eq!(r.module.declarations.len(), 1);
1918        match &r.module.declarations[0] {
1919            Decl::Block(b) => {
1920                assert_eq!(b.kind, BlockKind::Entity);
1921                assert_eq!(b.name.as_ref().unwrap().name, "User");
1922            }
1923            other => panic!("expected Block, got {other:?}"),
1924        }
1925    }
1926
1927    #[test]
1928    fn entity_with_fields() {
1929        let src = r#"entity Order {
1930    customer: Customer
1931    status: pending | active | completed
1932    total: Decimal
1933}"#;
1934        let r = parse_ok(src);
1935        assert_eq!(r.diagnostics.len(), 0);
1936        match &r.module.declarations[0] {
1937            Decl::Block(b) => {
1938                assert_eq!(b.items.len(), 3);
1939            }
1940            other => panic!("expected Block, got {other:?}"),
1941        }
1942    }
1943
1944    #[test]
1945    fn use_declaration() {
1946        let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
1947        assert_eq!(r.diagnostics.len(), 0);
1948        match &r.module.declarations[0] {
1949            Decl::Use(u) => {
1950                assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
1951            }
1952            other => panic!("expected Use, got {other:?}"),
1953        }
1954    }
1955
1956    #[test]
1957    fn enum_declaration() {
1958        let src = "enum OrderStatus { pending | shipped | delivered }";
1959        let r = parse_ok(src);
1960        assert_eq!(r.diagnostics.len(), 0);
1961    }
1962
1963    #[test]
1964    fn config_block() {
1965        let src = r#"config {
1966    max_retries: Integer = 3
1967    timeout: Duration = 24.hours
1968}"#;
1969        // Config entries are `name: Type = default`. The parser sees
1970        // `name: Type = default` as an assignment where the value is
1971        // `Type = default` (comparison with Eq). That's fine for the
1972        // parse tree — semantic pass separates type from default.
1973        let r = parse_ok(src);
1974        assert_eq!(r.diagnostics.len(), 0);
1975    }
1976
1977    #[test]
1978    fn rule_declaration() {
1979        let src = r#"rule PlaceOrder {
1980    when: CustomerPlacesOrder(customer, items, total)
1981    requires: total > 0
1982    ensures: Order.created(customer: customer, status: pending, total: total)
1983}"#;
1984        let r = parse_ok(src);
1985        assert_eq!(r.diagnostics.len(), 0);
1986        match &r.module.declarations[0] {
1987            Decl::Block(b) => {
1988                assert_eq!(b.kind, BlockKind::Rule);
1989                assert_eq!(b.items.len(), 3);
1990            }
1991            other => panic!("expected Block, got {other:?}"),
1992        }
1993    }
1994
1995    #[test]
1996    fn expression_precedence() {
1997        let r = parse_ok("rule T { v: a + b * c }");
1998        // The value should be Add(a, Mul(b, c))
1999        match &r.module.declarations[0] {
2000            Decl::Block(b) => match &b.items[0].kind {
2001                BlockItemKind::Assignment { value, .. } => match value {
2002                    Expr::BinaryOp { op, right, .. } => {
2003                        assert_eq!(*op, BinaryOp::Add);
2004                        assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2005                    }
2006                    other => panic!("expected BinaryOp, got {other:?}"),
2007                },
2008                other => panic!("expected Assignment, got {other:?}"),
2009            },
2010            other => panic!("expected Block, got {other:?}"),
2011        }
2012    }
2013
2014    #[test]
2015    fn default_declaration() {
2016        let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2017        let r = parse_ok(src);
2018        assert_eq!(r.diagnostics.len(), 0);
2019    }
2020
2021    #[test]
2022    fn open_question() {
2023        let src = r#"open question "Should admins be role-specific?""#;
2024        let r = parse_ok(src);
2025        assert_eq!(r.diagnostics.len(), 0);
2026    }
2027
2028    #[test]
2029    fn external_entity() {
2030        let src = "external entity Customer { email: String }";
2031        let r = parse_ok(src);
2032        assert_eq!(r.diagnostics.len(), 0);
2033        match &r.module.declarations[0] {
2034            Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2035            other => panic!("expected Block, got {other:?}"),
2036        }
2037    }
2038
2039    #[test]
2040    fn where_expression() {
2041        let src = "entity E { active: items where status = active }";
2042        let r = parse_ok(src);
2043        assert_eq!(r.diagnostics.len(), 0);
2044    }
2045
2046    #[test]
2047    fn with_expression() {
2048        let src = "entity E { slots: InterviewSlot with candidacy = this }";
2049        let r = parse_ok(src);
2050        assert_eq!(r.diagnostics.len(), 0);
2051    }
2052
2053    #[test]
2054    fn lambda_expression() {
2055        let src = "entity E { v: items.any(i => i.active) }";
2056        let r = parse_ok(src);
2057        assert_eq!(r.diagnostics.len(), 0);
2058    }
2059
2060    #[test]
2061    fn deferred() {
2062        let src = "deferred InterviewerMatching.suggest";
2063        let r = parse_ok(src);
2064        assert_eq!(r.diagnostics.len(), 0);
2065    }
2066
2067    #[test]
2068    fn variant_declaration() {
2069        let src = "variant Email : Notification { subject: String }";
2070        let r = parse_ok(src);
2071        assert_eq!(r.diagnostics.len(), 0);
2072    }
2073
2074    // -- projection mapping -----------------------------------------------
2075
2076    #[test]
2077    fn projection_arrow() {
2078        let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2079        let r = parse_ok(src);
2080        assert_eq!(r.diagnostics.len(), 0);
2081    }
2082
2083    // -- transitions_to / becomes ------------------------------------------
2084
2085    #[test]
2086    fn transitions_to_trigger() {
2087        let src = "rule R { when: Interview.status transitions_to scheduled\n    ensures: Notification.created() }";
2088        let r = parse_ok(src);
2089        assert_eq!(r.diagnostics.len(), 0);
2090    }
2091
2092    #[test]
2093    fn becomes_trigger() {
2094        let src = "rule R { when: Interview.status becomes scheduled\n    ensures: Notification.created() }";
2095        let r = parse_ok(src);
2096        assert_eq!(r.diagnostics.len(), 0);
2097    }
2098
2099    // -- binding colon in clause values ------------------------------------
2100
2101    #[test]
2102    fn when_binding() {
2103        let src = "rule R {\n    when: interview: Interview.status transitions_to scheduled\n    ensures: Notification.created()\n}";
2104        let r = parse_ok(src);
2105        assert_eq!(r.diagnostics.len(), 0);
2106        // The when clause value should be a Binding wrapping a TransitionsTo
2107        let decl = &r.module.declarations[0];
2108        if let Decl::Block(b) = decl {
2109            if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2110                assert_eq!(keyword, "when");
2111                assert!(matches!(value, Expr::Binding { .. }));
2112            } else {
2113                panic!("expected clause");
2114            }
2115        } else {
2116            panic!("expected block decl");
2117        }
2118    }
2119
2120    #[test]
2121    fn when_binding_temporal() {
2122        let src = "rule R {\n    when: invitation: Invitation.expires_at <= now\n    ensures: Invitation.expired()\n}";
2123        let r = parse_ok(src);
2124        assert_eq!(r.diagnostics.len(), 0);
2125    }
2126
2127    #[test]
2128    fn when_binding_created() {
2129        let src = "rule R {\n    when: batch: DigestBatch.created\n    ensures: Email.created()\n}";
2130        let r = parse_ok(src);
2131        assert_eq!(r.diagnostics.len(), 0);
2132    }
2133
2134    #[test]
2135    fn facing_binding() {
2136        let src = "surface S {\n    facing viewer: Interviewer\n    exposes: InterviewList\n}";
2137        let r = parse_ok(src);
2138        assert_eq!(r.diagnostics.len(), 0);
2139    }
2140
2141    #[test]
2142    fn context_binding() {
2143        let src = "surface S {\n    facing viewer: Interviewer\n    context assignment: SlotConfirmation where interviewer = viewer\n}";
2144        let r = parse_ok(src);
2145        assert_eq!(r.diagnostics.len(), 0);
2146    }
2147
2148    // -- rule-level for block item -----------------------------------------
2149
2150    #[test]
2151    fn rule_level_for() {
2152        let src = r#"rule ProcessDigests {
2153    when: schedule: DigestSchedule.next_run_at <= now
2154    for user in Users where notification_setting.digest_enabled:
2155        ensures: DigestBatch.created(user: user)
2156}"#;
2157        let r = parse_ok(src);
2158        assert_eq!(r.diagnostics.len(), 0);
2159        if let Decl::Block(b) = &r.module.declarations[0] {
2160            // Should have when clause + for block item
2161            assert!(b.items.len() >= 2);
2162            assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2163        } else {
2164            panic!("expected block decl");
2165        }
2166    }
2167
2168    // -- let inside ensures blocks -----------------------------------------
2169
2170    #[test]
2171    fn let_in_ensures_block() {
2172        let src = r#"rule R {
2173    when: ScheduleInterview(candidacy, time, interviewers)
2174    ensures:
2175        let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2176        for interviewer in interviewers:
2177            SlotConfirmation.created(slot: slot, interviewer: interviewer)
2178}"#;
2179        let r = parse_ok(src);
2180        assert_eq!(r.diagnostics.len(), 0);
2181    }
2182
2183    // -- when guard on provides items --------------------------------------
2184
2185    #[test]
2186    fn provides_when_guard() {
2187        let src = "surface S {\n    facing viewer: Interviewer\n    provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2188        let r = parse_ok(src);
2189        assert_eq!(r.diagnostics.len(), 0);
2190    }
2191
2192    // -- optional type suffix ----------------------------------------------
2193
2194    #[test]
2195    fn optional_type_suffix() {
2196        let src = "entity E { locked_until: Timestamp? }";
2197        let r = parse_ok(src);
2198        assert_eq!(r.diagnostics.len(), 0);
2199    }
2200
2201    #[test]
2202    fn optional_trigger_param() {
2203        let src = "rule R { when: Report(interviewer, interview, reason, details?)\n    ensures: Done() }";
2204        let r = parse_ok(src);
2205        assert_eq!(r.diagnostics.len(), 0);
2206    }
2207
2208    // -- qualified name with config ----------------------------------------
2209
2210    #[test]
2211    fn qualified_config_access() {
2212        let src = "entity E { duration: oauth/config.session_duration }";
2213        let r = parse_ok(src);
2214        assert_eq!(r.diagnostics.len(), 0);
2215    }
2216
2217    // -- comprehensive integration test ------------------------------------
2218
2219    #[test]
2220    fn realistic_spec() {
2221        let src = r#"-- allium: 1
2222
2223enum OrderStatus { pending | shipped | delivered }
2224
2225external entity Customer {
2226    email: String
2227    name: String
2228}
2229
2230entity Order {
2231    customer: Customer
2232    status: OrderStatus
2233    total: Decimal
2234    items: OrderItem with order = this
2235    shipped_items: items where status = shipped
2236    confirmed_items: items where status = confirmed -> item
2237    is_complete: status = delivered
2238    locked_until: Timestamp?
2239}
2240
2241config {
2242    max_retries: Integer = 3
2243    timeout: Duration = 24.hours
2244}
2245
2246rule PlaceOrder {
2247    when: CustomerPlacesOrder(customer, items, total)
2248    requires: total > 0
2249    ensures: Order.created(customer: customer, status: pending, total: total)
2250}
2251
2252rule ShipOrder {
2253    when: order: Order.status transitions_to shipped
2254    ensures: Email.created(to: order.customer.email, template: order_shipped)
2255}
2256
2257open question "How do we handle partial shipments?"
2258"#;
2259        let r = parse_ok(src);
2260        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2261        assert_eq!(r.module.version, Some(1));
2262        assert_eq!(r.module.declarations.len(), 7);
2263    }
2264
2265    #[test]
2266    fn extension_behaviour_excerpt() {
2267        // Exercises: inline enums, generic types, or-triggers, named call
2268        // args, config with typed defaults, module declaration.
2269        let src = r#"value Document {
2270    uri: String
2271    text: String
2272}
2273
2274entity Finding {
2275    code: String
2276    severity: error | warning | info
2277    range: FindingRange
2278}
2279
2280entity DiagnosticsMode {
2281    value: strict | relaxed
2282}
2283
2284config {
2285    duplicateKey: String = "allium.config.duplicateKey"
2286}
2287
2288rule RefreshDiagnostics {
2289    when: DocumentOpened(document) or DocumentChanged(document)
2290    requires: document.language_id = "allium"
2291    ensures: FindingsComputed(document)
2292}
2293
2294surface DiagnosticsDashboard {
2295    facing viewer: Developer
2296    context doc: Document where viewer.active_document = doc
2297    provides: RunChecks(viewer) when doc.language_id = "allium"
2298    exposes: FindingList
2299}
2300
2301rule ProcessDigests {
2302    when: schedule: DigestSchedule.next_run_at <= now
2303    for user in Users where notification_setting.digest_enabled:
2304        let settings = user.notification_setting
2305        ensures: DigestBatch.created(user: user)
2306}
2307"#;
2308        let r = parse_ok(src);
2309        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2310        // value + entity + entity + config + rule + surface + rule = 7
2311        assert_eq!(r.module.declarations.len(), 7);
2312    }
2313
2314    #[test]
2315    fn exists_as_identifier() {
2316        let src = r#"rule R {
2317    when: X()
2318    ensures: CompletionItemAvailable(label: exists)
2319}"#;
2320        let r = parse_ok(src);
2321        assert_eq!(r.diagnostics.len(), 0);
2322    }
2323
2324    // -- pipe precedence: tighter than boolean ops ----------------------------
2325
2326    #[test]
2327    fn pipe_binds_tighter_than_or() {
2328        // `a or b | c` should parse as `a or (b | c)`, not `(a or b) | c`
2329        let src = "entity E { v: a or b | c }";
2330        let r = parse_ok(src);
2331        assert_eq!(r.diagnostics.len(), 0);
2332        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2333        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2334        // Top-level should be LogicalOp(Or)
2335        let Expr::LogicalOp { op, right, .. } = value else {
2336            panic!("expected LogicalOp, got {value:?}");
2337        };
2338        assert_eq!(*op, LogicalOp::Or);
2339        // Right side should be Pipe(b, c)
2340        assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2341    }
2342
2343    // -- variant with expression base -----------------------------------------
2344
2345    #[test]
2346    fn variant_with_pipe_base() {
2347        let src = "variant Mixed : TypeA | TypeB";
2348        let r = parse_ok(src);
2349        assert_eq!(r.diagnostics.len(), 0);
2350        let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2351        assert!(matches!(v.base, Expr::Pipe { .. }));
2352    }
2353
2354    // -- for-block with comparison in where filter ----------------------------
2355
2356    #[test]
2357    fn for_block_where_comparison() {
2358        let src = r#"rule R {
2359    when: X()
2360    for item in Items where item.status = active:
2361        ensures: Processed(item: item)
2362}"#;
2363        let r = parse_ok(src);
2364        assert_eq!(r.diagnostics.len(), 0);
2365        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2366        let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2367        assert!(filter.is_some());
2368        assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2369    }
2370
2371    // -- for-expression with comparison in where filter -----------------------
2372
2373    #[test]
2374    fn for_expr_where_comparison() {
2375        let src = r#"rule R {
2376    when: X()
2377    ensures:
2378        for item in Items where item.active = true:
2379            Processed(item: item)
2380}"#;
2381        let r = parse_ok(src);
2382        assert_eq!(r.diagnostics.len(), 0);
2383    }
2384
2385    // -- if/else if/else chain ------------------------------------------------
2386
2387    #[test]
2388    fn if_else_if_else() {
2389        let src = r#"rule R {
2390    when: X(v)
2391    ensures:
2392        if v < 10: Small()
2393        else if v < 100: Medium()
2394        else: Large()
2395}"#;
2396        let r = parse_ok(src);
2397        assert_eq!(r.diagnostics.len(), 0);
2398    }
2399
2400    // -- null coalescing and optional chaining --------------------------------
2401
2402    #[test]
2403    fn null_coalesce_and_optional_chain() {
2404        let src = "entity E { v: a?.b ?? fallback }";
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::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2409        // Top-level should be NullCoalesce
2410        assert!(matches!(value, Expr::NullCoalesce { .. }));
2411    }
2412
2413    // -- generic types --------------------------------------------------------
2414
2415    #[test]
2416    fn generic_type_nested() {
2417        let src = "entity E { v: List<Set<String>> }";
2418        let r = parse_ok(src);
2419        assert_eq!(r.diagnostics.len(), 0);
2420    }
2421
2422    // -- set literal, list literal, object literal ----------------------------
2423
2424    #[test]
2425    fn collection_literals() {
2426        let src = r#"rule R {
2427    when: X()
2428    ensures:
2429        let s = {a, b, c}
2430        let o = {name: "test", count: 42}
2431        Done()
2432}"#;
2433        let r = parse_ok(src);
2434        assert_eq!(r.diagnostics.len(), 0);
2435    }
2436
2437    #[test]
2438    fn spec_reject_list_literal() {
2439        // The spec does not define `[...]` list literal syntax.
2440        let src = r#"rule R {
2441    when: X()
2442    ensures:
2443        let l = [1, 2, 3]
2444        Done()
2445}"#;
2446        let r = parse_ok(src);
2447        assert!(
2448            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2449            "expected error for `[...]` list literal (not in spec), but parsed without errors"
2450        );
2451    }
2452
2453    // -- given block ----------------------------------------------------------
2454
2455    #[test]
2456    fn given_block() {
2457        let src = "given { viewer: User\n    time: Timestamp }";
2458        let r = parse_ok(src);
2459        assert_eq!(r.diagnostics.len(), 0);
2460        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2461        assert_eq!(b.kind, BlockKind::Given);
2462        assert!(b.name.is_none());
2463    }
2464
2465    // -- actor block ----------------------------------------------------------
2466
2467    #[test]
2468    fn actor_block() {
2469        let src = "actor Admin { identified_by: User where role = admin }";
2470        let r = parse_ok(src);
2471        assert_eq!(r.diagnostics.len(), 0);
2472        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2473        assert_eq!(b.kind, BlockKind::Actor);
2474    }
2475
2476    // -- join lookup ----------------------------------------------------------
2477
2478    #[test]
2479    fn join_lookup() {
2480        let src = "entity E { match: Other{field_a, field_b: value} }";
2481        let r = parse_ok(src);
2482        assert_eq!(r.diagnostics.len(), 0);
2483        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2484        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2485        assert!(matches!(value, Expr::JoinLookup { .. }));
2486    }
2487
2488    // -- in / not in with set literal -----------------------------------------
2489
2490    #[test]
2491    fn in_not_in_set() {
2492        let src = r#"rule R {
2493    when: X(s)
2494    requires: s in {a, b, c}
2495    requires: s not in {d, e}
2496    ensures: Done()
2497}"#;
2498        let r = parse_ok(src);
2499        assert_eq!(r.diagnostics.len(), 0);
2500    }
2501
2502    // -- comprehensive fixture file -------------------------------------------
2503
2504    #[test]
2505    fn comprehensive_fixture() {
2506        let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
2507        let r = parse(src);
2508        assert_eq!(
2509            r.diagnostics.len(),
2510            0,
2511            "expected no errors in comprehensive fixture, got: {:?}",
2512            r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
2513        );
2514        assert!(r.module.declarations.len() > 30, "expected many declarations");
2515    }
2516
2517    // -- error message quality ------------------------------------------------
2518
2519    #[test]
2520    fn error_expected_declaration() {
2521        let r = parse("-- allium: 1\n+ invalid");
2522        assert!(r.diagnostics.len() >= 1);
2523        let msg = &r.diagnostics[0].message;
2524        assert!(msg.contains("expected declaration"), "got: {msg}");
2525        assert!(msg.contains("entity"), "should list valid options, got: {msg}");
2526        assert!(msg.contains("rule"), "should list valid options, got: {msg}");
2527    }
2528
2529    #[test]
2530    fn error_expected_expression() {
2531        let r = parse("-- allium: 1\nentity E { v: }");
2532        assert!(r.diagnostics.len() >= 1);
2533        let msg = &r.diagnostics[0].message;
2534        assert!(msg.contains("expected expression"), "got: {msg}");
2535        assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
2536    }
2537
2538    #[test]
2539    fn error_expected_block_item() {
2540        let r = parse("-- allium: 1\nentity E { + }");
2541        assert!(r.diagnostics.len() >= 1);
2542        let msg = &r.diagnostics[0].message;
2543        assert!(msg.contains("expected block item"), "got: {msg}");
2544    }
2545
2546    #[test]
2547    fn error_expected_identifier() {
2548        let r = parse("-- allium: 1\nentity 123 {}");
2549        assert!(r.diagnostics.len() >= 1);
2550        let msg = &r.diagnostics[0].message;
2551        // Context-aware: says "entity name" not generic "identifier"
2552        assert!(msg.contains("expected entity name"), "got: {msg}");
2553        // Human-friendly: says "number" not "Number" or "TokenKind::Number"
2554        assert!(msg.contains("number"), "should say what was found, got: {msg}");
2555    }
2556
2557    #[test]
2558    fn error_missing_brace() {
2559        let r = parse("entity E {");
2560        assert!(r.diagnostics.len() >= 1);
2561        let msg = &r.diagnostics[0].message;
2562        assert!(msg.contains("expected"), "got: {msg}");
2563    }
2564
2565    #[test]
2566    fn error_recovery_multiple() {
2567        // Parser should recover and report multiple errors (on separate lines)
2568        let r = parse("entity E { + }\nentity F { - }");
2569        assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
2570    }
2571
2572    #[test]
2573    fn error_dedup_same_line() {
2574        // Multiple bad tokens on a single line should produce only one error
2575        let r = parse("-- allium: 1\n+ - * /");
2576        let errors: Vec<_> = r.diagnostics.iter()
2577            .filter(|d| d.severity == crate::diagnostic::Severity::Error)
2578            .collect();
2579        assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
2580    }
2581
2582    #[test]
2583    fn for_block() {
2584        let src = r#"rule R {
2585    when: X()
2586    for user in Users where user.active:
2587        ensures: Notified(user: user)
2588}"#;
2589        let r = parse_ok(src);
2590        assert_eq!(r.diagnostics.len(), 0);
2591        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2592        assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2593    }
2594
2595    #[test]
2596    fn for_expr() {
2597        let src = r#"rule R {
2598    when: X(project)
2599    ensures:
2600        let total = for task in project.tasks: task.effort
2601        Done(total: total)
2602}"#;
2603        let r = parse_ok(src);
2604        assert_eq!(r.diagnostics.len(), 0);
2605    }
2606
2607    #[test]
2608    fn for_where() {
2609        let src = r#"rule R {
2610    when: X()
2611    for item in Items where item.active:
2612        ensures: Processed(item: item)
2613}"#;
2614        let r = parse_ok(src);
2615        assert_eq!(r.diagnostics.len(), 0);
2616    }
2617
2618    #[test]
2619    fn spec_reject_for_with_filter() {
2620        // The spec uses `where` for iteration filtering; `with` is for
2621        // relationship declarations only.
2622        let src = r#"rule R {
2623    when: X()
2624    for slot in Slot with slot.role = reviewer:
2625        ensures: Reviewed(slot: slot)
2626}"#;
2627        let r = parse_ok(src);
2628        assert!(
2629            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2630            "expected error for `for ... with` (spec uses `where`), but parsed without errors"
2631        );
2632    }
2633
2634    #[test]
2635    fn block_level_if() {
2636        let src = r#"rule R {
2637    when: X(task)
2638    if task.priority = high:
2639        ensures: Escalated(task: task)
2640}"#;
2641        let r = parse_ok(src);
2642        assert_eq!(r.diagnostics.len(), 0);
2643        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2644        let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2645            panic!("expected IfBlock, got {:?}", b.items[1].kind);
2646        };
2647        assert_eq!(branches.len(), 1);
2648        assert!(else_items.is_none());
2649    }
2650
2651    #[test]
2652    fn block_level_if_else() {
2653        let src = r#"rule R {
2654    when: X(score)
2655    if score > 80:
2656        ensures: High()
2657    else if score > 40:
2658        ensures: Medium()
2659    else:
2660        ensures: Low()
2661}"#;
2662        let r = parse_ok(src);
2663        assert_eq!(r.diagnostics.len(), 0);
2664        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2665        let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2666            panic!("expected IfBlock, got {:?}", b.items[1].kind);
2667        };
2668        assert_eq!(branches.len(), 2);
2669        assert!(else_items.is_some());
2670    }
2671
2672    #[test]
2673    fn wildcard_type_parameter() {
2674        let src = "entity E { codec: Codec<*> }";
2675        let r = parse_ok(src);
2676        assert_eq!(r.diagnostics.len(), 0);
2677        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2678        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2679        if let Expr::GenericType { args, .. } = value {
2680            assert_eq!(args.len(), 1);
2681            if let Expr::Ident(id) = &args[0] {
2682                assert_eq!(id.name, "*");
2683            } else {
2684                panic!("expected wildcard ident, got {:?}", args[0]);
2685            }
2686        } else {
2687            panic!("expected GenericType, got {:?}", value);
2688        }
2689    }
2690
2691    #[test]
2692    fn guidance_clause_comment_only_value() {
2693        let src = r#"rule R {
2694    guidance: -- just a comment
2695    ensures: Done()
2696}"#;
2697        let r = parse_ok(src);
2698        assert_eq!(r.diagnostics.len(), 0);
2699        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2700        assert_eq!(b.items.len(), 2);
2701        // guidance clause should have an empty block value
2702        let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
2703        assert_eq!(keyword, "guidance");
2704        assert!(matches!(value, Expr::Block { items, .. } if items.is_empty()));
2705    }
2706
2707    #[test]
2708    fn spec_reject_for_expr_with_filter() {
2709        // Expression-level `for` also only accepts `where`, not `with`.
2710        let src = r#"rule R {
2711    when: X(project)
2712    ensures:
2713        let total = for task in project.tasks with task.active: task.effort
2714        Done(total: total)
2715}"#;
2716        let r = parse_ok(src);
2717        assert!(
2718            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2719            "expected error for `for ... with` in expression (spec uses `where`), but parsed without errors"
2720        );
2721    }
2722
2723    #[test]
2724    fn for_destructured_binding() {
2725        let src = r#"rule R {
2726    when: X()
2727    for (key, value) in Pairs where key != null:
2728        ensures: Processed(key: key, value: value)
2729}"#;
2730        let r = parse_ok(src);
2731        assert_eq!(r.diagnostics.len(), 0);
2732        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2733        let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
2734        assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
2735    }
2736
2737    #[test]
2738    fn dot_path_assignment() {
2739        let src = r#"entity Shard {
2740    ShardGroup.shard_cache: Shard with group = this
2741}"#;
2742        let r = parse_ok(src);
2743        assert_eq!(r.diagnostics.len(), 0);
2744        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2745        let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
2746            panic!("expected PathAssignment, got {:?}", b.items[0].kind);
2747        };
2748        assert!(matches!(path, Expr::MemberAccess { .. }));
2749    }
2750
2751    #[test]
2752    fn language_reference_fixture() {
2753        let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
2754        let r = parse(src);
2755        let errors: Vec<_> = r.diagnostics.iter()
2756            .filter(|d| d.severity == Severity::Error)
2757            .collect();
2758        assert_eq!(
2759            errors.len(),
2760            0,
2761            "expected no errors in language-reference fixture, got: {:?}",
2762            errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
2763        );
2764    }
2765
2766    // =====================================================================
2767    // V1 SPEC CONFORMANCE TESTS
2768    //
2769    // These tests verify that the parser conforms to the Allium V1 language
2770    // reference (docs/allium-v1-language-reference.md). Each test is tagged
2771    // with the finding number from the audit.
2772    //
2773    // Tests marked "should reject" are expected to FAIL until the parser
2774    // is updated to reject non-spec constructs.
2775    //
2776    // Tests marked "should parse" are expected to FAIL until the parser
2777    // is updated to handle spec-defined constructs.
2778    // =====================================================================
2779
2780    // -- Finding 1: spec uses `for`, not `for each` ---------------------------
2781
2782    #[test]
2783    fn spec_for_bare_form() {
2784        // The spec uses bare `for` exclusively. This must parse cleanly.
2785        let src = r#"rule ProcessDigests {
2786    when: schedule: DigestSchedule.next_run_at <= now
2787    for user in Users where notification_setting.digest_enabled:
2788        let settings = user.notification_setting
2789        ensures: DigestBatch.created(user: user)
2790}"#;
2791        let r = parse_ok(src);
2792        assert_eq!(r.diagnostics.len(), 0);
2793    }
2794
2795    #[test]
2796    fn spec_reject_for_each() {
2797        // `for each` is not in the spec. The parser should reject it.
2798        let src = r#"rule R {
2799    when: X()
2800    for each user in Users where user.active:
2801        ensures: Notified(user: user)
2802}"#;
2803        let r = parse_ok(src);
2804        assert!(
2805            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2806            "expected error for `for each` (not in spec), but parsed without errors"
2807        );
2808    }
2809
2810    // -- Finding 2: spec uses `=`, not `==` -----------------------------------
2811
2812    #[test]
2813    fn spec_reject_double_equals() {
2814        // The spec uses `=` for equality. `==` should not be accepted.
2815        let src = "rule R { when: X(a)\n    requires: a.status == active\n    ensures: Done() }";
2816        let r = parse_ok(src);
2817        assert!(
2818            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2819            "expected error for `==` (not in spec), but parsed without errors"
2820        );
2821    }
2822
2823    // -- Finding 3: `system` blocks are not in the spec -----------------------
2824
2825    #[test]
2826    fn spec_reject_system_block() {
2827        // `system` is not a declaration type in the V1 spec.
2828        let src = "system PaymentGateway {\n    timeout: 30.seconds\n}";
2829        let r = parse_ok(src);
2830        assert!(
2831            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2832            "expected error for `system` block (not in spec), but parsed without errors"
2833        );
2834    }
2835
2836    // -- Finding 6: `tags` clause is not in the spec --------------------------
2837
2838    #[test]
2839    fn spec_reject_tags_clause() {
2840        // `tags:` is not a clause keyword in the V1 spec.
2841        let src = r#"rule R {
2842    when: MigrationTriggered()
2843    tags: infrastructure, migration
2844    ensures: MigrationComplete()
2845}"#;
2846        let r = parse_ok(src);
2847        assert!(
2848            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2849            "expected error for `tags:` clause (not in spec), but parsed without errors"
2850        );
2851    }
2852
2853    // -- Finding 7: `includes`/`excludes` are not in the spec -----------------
2854
2855    #[test]
2856    fn spec_reject_includes_operator() {
2857        // The spec uses `x in collection`, not `collection includes x`.
2858        let src = r#"rule R {
2859    when: X(a, b)
2860    requires: a.items includes b
2861    ensures: Done()
2862}"#;
2863        let r = parse_ok(src);
2864        assert!(
2865            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2866            "expected error for `includes` operator (not in spec), but parsed without errors"
2867        );
2868    }
2869
2870    #[test]
2871    fn spec_reject_excludes_operator() {
2872        // The spec uses `x not in collection`, not `collection excludes x`.
2873        let src = r#"rule R {
2874    when: X(a, b)
2875    requires: a.items excludes b
2876    ensures: Done()
2877}"#;
2878        let r = parse_ok(src);
2879        assert!(
2880            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2881            "expected error for `excludes` operator (not in spec), but parsed without errors"
2882        );
2883    }
2884
2885    // -- Finding 8: range literals (`..`) are not in the spec -----------------
2886
2887    #[test]
2888    fn spec_reject_range_literal() {
2889        // The `..` range operator is not defined in the V1 spec.
2890        let src = r#"rule R {
2891    when: X(v)
2892    requires: v in [1..100]
2893    ensures: Done()
2894}"#;
2895        let r = parse_ok(src);
2896        assert!(
2897            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2898            "expected error for `..` range (not in spec), but parsed without errors"
2899        );
2900    }
2901
2902    // -- Finding 9: `within` is only for actors, not rules --------------------
2903
2904    #[test]
2905    fn spec_within_in_actor() {
2906        // The spec defines `within:` as an actor clause.
2907        let src = r#"actor WorkspaceAdmin {
2908    within: Workspace
2909    identified_by: User where role = admin
2910}"#;
2911        let r = parse_ok(src);
2912        assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
2913    }
2914
2915    // -- Finding 10: `module` declaration is not in the spec ------------------
2916
2917    #[test]
2918    fn spec_reject_module_declaration() {
2919        // `module Name` is not a declaration in the V1 spec.
2920        let src = "module my_spec";
2921        let r = parse_ok(src);
2922        assert!(
2923            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2924            "expected error for `module` declaration (not in spec), but parsed without errors"
2925        );
2926    }
2927
2928    // -- Finding 11: `guidance` at module level is not in the spec ------------
2929
2930    #[test]
2931    fn spec_reject_module_level_guidance() {
2932        // The spec shows `guidance:` only as a surface clause.
2933        let src = r#"guidance: "All rules must be idempotent""#;
2934        let r = parse_ok(src);
2935        assert!(
2936            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2937            "expected error for module-level `guidance:` (not in spec), but parsed without errors"
2938        );
2939    }
2940
2941    // -- Finding 12: `guarantee`/`timeout` in rules is not in the spec --------
2942
2943    #[test]
2944    fn spec_guarantee_in_surface() {
2945        // The spec defines `guarantee:` as a surface clause.
2946        let src = r#"surface S {
2947    facing viewer: User
2948    guarantee: DataIntegrity
2949}"#;
2950        let r = parse_ok(src);
2951        assert_eq!(r.diagnostics.len(), 0, "guarantee: in surface should parse cleanly");
2952    }
2953
2954    #[test]
2955    fn spec_timeout_in_surface() {
2956        // The spec defines `timeout:` as a surface clause with rule name syntax.
2957        let src = r#"surface InvitationView {
2958    facing recipient: Candidate
2959    context invitation: ResourceInvitation where email = recipient.email
2960    timeout: InvitationExpires
2961}"#;
2962        let r = parse_ok(src);
2963        assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
2964    }
2965
2966    #[test]
2967    fn spec_timeout_in_surface_with_when() {
2968        // The spec shows `timeout: RuleName when condition`.
2969        let src = r#"surface InvitationView {
2970    facing recipient: Candidate
2971    context invitation: ResourceInvitation where email = recipient.email
2972    timeout: InvitationExpires when invitation.expires_at <= now
2973}"#;
2974        let r = parse_ok(src);
2975        assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
2976    }
2977
2978    // -- Finding 15: suffix predicates are not in the spec --------------------
2979
2980    #[test]
2981    fn spec_reject_suffix_predicate() {
2982        // The spec does not define suffix predicate syntax like `starts_with`.
2983        let src = r#"rule R {
2984    when: X()
2985    requires: finding.code starts_with "allium."
2986    ensures: Done()
2987}"#;
2988        let r = parse_ok(src);
2989        assert!(
2990            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2991            "expected error for suffix predicate (not in spec), but parsed without errors"
2992        );
2993    }
2994
2995    // -- Finding 17: `.add()`/`.remove()` are in the spec ---------------------
2996
2997    #[test]
2998    fn spec_add_remove_in_ensures() {
2999        // The spec documents `.add()` and `.remove()` as ensures-only mutations.
3000        // These parse as regular method calls, which is correct.
3001        let src = r#"rule R {
3002    when: AssignInterviewer(interview, new_interviewer)
3003    ensures:
3004        interview.interviewers.add(new_interviewer)
3005}"#;
3006        let r = parse_ok(src);
3007        assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
3008    }
3009
3010    #[test]
3011    fn spec_remove_in_ensures() {
3012        let src = r#"rule R {
3013    when: RemoveInterviewer(interview, leaving)
3014    ensures:
3015        interview.interviewers.remove(leaving)
3016}"#;
3017        let r = parse_ok(src);
3018        assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3019    }
3020
3021    // -- Finding 18: `.first`/`.last` are in the spec -------------------------
3022
3023    #[test]
3024    fn spec_first_last_access() {
3025        // The spec documents `.first` and `.last` for ordered collections.
3026        let src = "entity E { latest: attempts.last\n    earliest: attempts.first }";
3027        let r = parse_ok(src);
3028        assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3029    }
3030
3031    // -- Finding 19: set arithmetic is in the spec ----------------------------
3032
3033    #[test]
3034    fn spec_set_arithmetic() {
3035        // The spec documents `+` and `-` on collections as set arithmetic.
3036        let src = r#"entity Role {
3037    permissions: Set<String>
3038    inherited: Set<String>
3039    all_permissions: permissions + inherited
3040    removed: old_mentions - new_mentions
3041}"#;
3042        let r = parse_ok(src);
3043        assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3044    }
3045
3046    // -- Finding 20: discard binding `_` is in the spec -----------------------
3047
3048    #[test]
3049    fn spec_discard_binding_in_trigger() {
3050        // The spec shows `when: _: LogProcessor.last_flush_check + ...`
3051        let src = r#"rule R {
3052    when: _: LogProcessor.last_flush_check <= now
3053    ensures: Flushed()
3054}"#;
3055        let r = parse_ok(src);
3056        assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3057    }
3058
3059    #[test]
3060    fn spec_discard_in_trigger_params() {
3061        // The spec shows `when: SomeEvent(_, slot)`
3062        let src = r#"rule R {
3063    when: SomeEvent(_, slot)
3064    ensures: Processed(slot: slot)
3065}"#;
3066        let r = parse_ok(src);
3067        assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3068    }
3069
3070    #[test]
3071    fn spec_discard_in_for() {
3072        // The spec shows `for _ in items: Counted(batch)`
3073        let src = r#"rule R {
3074    when: X(items)
3075    ensures:
3076        for _ in items: Counted()
3077}"#;
3078        let r = parse_ok(src);
3079        assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3080    }
3081
3082    // -- Finding 21: default with object literal is in the spec ---------------
3083
3084    #[test]
3085    fn spec_default_with_object_literal() {
3086        // The spec shows: default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }
3087        let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3088        let r = parse_ok(src);
3089        assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3090    }
3091
3092    #[test]
3093    fn spec_default_multiline_object() {
3094        // The spec shows multi-line defaults with object literals.
3095        let src = r#"default Role viewer = {
3096    name: "viewer",
3097    permissions: { "documents.read" }
3098}"#;
3099        let r = parse_ok(src);
3100        assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3101    }
3102
3103    // -- Spec surface features: related, let, guarantee, timeout --------------
3104
3105    #[test]
3106    fn spec_surface_related_clause() {
3107        // The spec shows `related:` with surface references.
3108        let src = r#"surface InterviewerDashboard {
3109    facing viewer: Interviewer
3110    context assignment: SlotConfirmation where interviewer = viewer
3111    related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3112}"#;
3113        let r = parse_ok(src);
3114        assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3115    }
3116
3117    #[test]
3118    fn spec_surface_let_binding() {
3119        // The spec shows `let` bindings inside surfaces.
3120        let src = r#"surface S {
3121    facing viewer: User
3122    let comments = Comments where parent = viewer
3123    exposes: CommentList
3124}"#;
3125        let r = parse_ok(src);
3126        assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3127    }
3128
3129    #[test]
3130    fn spec_surface_multiline_context_where() {
3131        // The spec shows context with where on a continuation line.
3132        let src = r#"surface InterviewerPendingAssignments {
3133    facing viewer: Interviewer
3134    context assignment: InterviewAssignment
3135        where interviewer = viewer and status = pending
3136    exposes: AssignmentList
3137}"#;
3138        let r = parse_ok(src);
3139        assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3140    }
3141
3142    // -- Spec: `for` inside surfaces ------------------------------------------
3143
3144    #[test]
3145    fn spec_for_in_surface_provides() {
3146        // The spec shows for iteration inside surface provides.
3147        let src = r#"surface TaskBoard {
3148    facing viewer: User
3149    for task in Task where task.assignee = viewer:
3150        provides: CompleteTask(viewer, task) when task.status = in_progress
3151    exposes: KanbanBoard
3152}"#;
3153        let r = parse_ok(src);
3154        assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3155    }
3156
3157    // -- Spec: `use` without alias --------------------------------------------
3158
3159    #[test]
3160    fn spec_use_without_alias() {
3161        // The spec shows `use` both with and without `as alias`.
3162        let src = r#"use "github.com/specs/notifications/def456""#;
3163        let r = parse_ok(src);
3164        assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3165    }
3166
3167    // -- Spec: empty external entity ------------------------------------------
3168
3169    #[test]
3170    fn spec_empty_external_entity() {
3171        // The spec shows external entities with empty bodies as type placeholders.
3172        let src = "external entity Commentable {}";
3173        let r = parse_ok(src);
3174        assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3175    }
3176
3177    // -- Spec: multi-line provides block in surface ---------------------------
3178
3179    #[test]
3180    fn spec_surface_multiline_provides() {
3181        // The spec shows provides as a multi-line block.
3182        let src = r#"surface ProjectDashboard {
3183    facing viewer: ProjectManager
3184    context project: Project where owner = viewer
3185    provides:
3186        CreateTask(viewer, project) when project.status = active
3187        ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3188    exposes: TaskList
3189}"#;
3190        let r = parse_ok(src);
3191        assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3192    }
3193
3194    // -- Spec: multi-line exposes block in surface ----------------------------
3195
3196    #[test]
3197    fn spec_surface_multiline_exposes() {
3198        // The spec shows exposes as a multi-line block.
3199        let src = r#"surface InterviewerDashboard {
3200    facing viewer: Interviewer
3201    context assignment: SlotConfirmation where interviewer = viewer
3202    exposes:
3203        assignment.slot.time
3204        assignment.status
3205}"#;
3206        let r = parse_ok(src);
3207        assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3208    }
3209
3210    // =====================================================================
3211    // COVERAGE GAP TESTS
3212    //
3213    // Dedicated unit tests for spec constructs that previously only had
3214    // fixture-file coverage.
3215    // =====================================================================
3216
3217    // -- Composite or-triggers ------------------------------------------------
3218
3219    #[test]
3220    fn composite_or_trigger() {
3221        let src = r#"rule R {
3222    when: EventA(x) or EventB(x) or EventC(x)
3223    ensures: Done()
3224}"#;
3225        let r = parse_ok(src);
3226        assert_eq!(r.diagnostics.len(), 0);
3227        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3228        let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
3229        assert_eq!(keyword, "when");
3230        // Top-level should be LogicalOp(Or) wrapping another Or
3231        let Expr::LogicalOp { op, left, .. } = value else {
3232            panic!("expected LogicalOp, got {value:?}");
3233        };
3234        assert_eq!(*op, LogicalOp::Or);
3235        assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
3236    }
3237
3238    // -- Value type declaration -----------------------------------------------
3239
3240    #[test]
3241    fn value_type_declaration() {
3242        let src = r#"value TimeRange {
3243    start: Timestamp
3244    end: Timestamp
3245    duration: end - start
3246}"#;
3247        let r = parse_ok(src);
3248        assert_eq!(r.diagnostics.len(), 0);
3249        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3250        assert_eq!(b.kind, BlockKind::Value);
3251        assert_eq!(b.name.as_ref().unwrap().name, "TimeRange");
3252        assert_eq!(b.items.len(), 3);
3253    }
3254
3255    // -- Qualified config block -----------------------------------------------
3256
3257    #[test]
3258    fn qualified_config_block() {
3259        let src = r#"use "github.com/specs/oauth/abc123" as oauth
3260oauth/config {
3261    session_duration: Duration = 24.hours
3262}"#;
3263        let r = parse_ok(src);
3264        assert_eq!(r.diagnostics.len(), 0);
3265        assert_eq!(r.module.declarations.len(), 2);
3266    }
3267
3268    // -- String interpolation -------------------------------------------------
3269
3270    #[test]
3271    fn string_interpolation_parts() {
3272        let src = r#"rule R {
3273    when: X(name, action)
3274    ensures: Log.created(message: "User {name} did {action}")
3275}"#;
3276        let r = parse_ok(src);
3277        assert_eq!(r.diagnostics.len(), 0);
3278        // Dig into the message arg to verify interpolation parts
3279        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3280        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3281        let Expr::Call { args, .. } = value else { panic!() };
3282        let CallArg::Named(arg) = &args[0] else { panic!() };
3283        let Expr::StringLiteral(s) = &arg.value else { panic!() };
3284        assert_eq!(s.parts.len(), 4, "expected 4 string parts: text, interp, text, interp");
3285        assert!(matches!(&s.parts[0], StringPart::Text(t) if t == "User "));
3286        assert!(matches!(&s.parts[1], StringPart::Interpolation(id) if id.name == "name"));
3287        assert!(matches!(&s.parts[2], StringPart::Text(t) if t == " did "));
3288        assert!(matches!(&s.parts[3], StringPart::Interpolation(id) if id.name == "action"));
3289    }
3290
3291    // -- `this` keyword as expression -----------------------------------------
3292
3293    #[test]
3294    fn this_keyword_expression() {
3295        // `Item with parent = this` parses as With(Item, Eq(parent, this))
3296        // because `with` binds looser than `=`, capturing the full predicate.
3297        let src = "entity E { items: Item with parent = this }";
3298        let r = parse_ok(src);
3299        assert_eq!(r.diagnostics.len(), 0);
3300        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3301        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3302        let Expr::With { predicate, .. } = value else {
3303            panic!("expected With, got {value:?}");
3304        };
3305        let Expr::Comparison { op, right, .. } = predicate.as_ref() else {
3306            panic!("expected Comparison in with predicate, got {predicate:?}");
3307        };
3308        assert_eq!(*op, ComparisonOp::Eq);
3309        assert!(matches!(right.as_ref(), Expr::This { .. }));
3310    }
3311
3312    // -- `not` prefix operator (standalone) -----------------------------------
3313
3314    #[test]
3315    fn not_prefix_standalone() {
3316        let src = r#"rule R {
3317    when: X(user)
3318    requires: not user.is_locked
3319    ensures: Done()
3320}"#;
3321        let r = parse_ok(src);
3322        assert_eq!(r.diagnostics.len(), 0);
3323        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3324        let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3325        assert_eq!(keyword, "requires");
3326        assert!(matches!(value, Expr::Not { .. }));
3327    }
3328
3329    // -- Unary minus ----------------------------------------------------------
3330
3331    #[test]
3332    fn unary_minus() {
3333        let src = "entity E { offset: -1 }";
3334        let r = parse_ok(src);
3335        assert_eq!(r.diagnostics.len(), 0);
3336        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3337        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3338        assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }
3339                        | Expr::NumberLiteral { .. }), "expected negation, got {value:?}");
3340    }
3341
3342    // -- Parenthesised expression grouping ------------------------------------
3343
3344    #[test]
3345    fn parenthesised_expression() {
3346        let src = "entity E { v: (a + b) * c }";
3347        let r = parse_ok(src);
3348        assert_eq!(r.diagnostics.len(), 0);
3349        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3350        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3351        // Top level should be Mul, with left being Add (grouped by parens)
3352        let Expr::BinaryOp { op, left, .. } = value else {
3353            panic!("expected BinaryOp, got {value:?}");
3354        };
3355        assert_eq!(*op, BinaryOp::Mul);
3356        assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3357    }
3358
3359    // -- Boolean literals -----------------------------------------------------
3360
3361    #[test]
3362    fn boolean_literals() {
3363        let src = r#"rule R {
3364    when: X(item)
3365    ensures:
3366        item.active = true
3367        item.deleted = false
3368}"#;
3369        let r = parse_ok(src);
3370        assert_eq!(r.diagnostics.len(), 0);
3371    }
3372
3373    // -- `null` literal -------------------------------------------------------
3374
3375    #[test]
3376    fn null_literal() {
3377        let src = "entity E { v: parent ?? null }";
3378        let r = parse_ok(src);
3379        assert_eq!(r.diagnostics.len(), 0);
3380        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3381        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3382        let Expr::NullCoalesce { right, .. } = value else { panic!() };
3383        assert!(matches!(right.as_ref(), Expr::Null { .. }));
3384    }
3385
3386    // -- Empty set literal ----------------------------------------------------
3387
3388    #[test]
3389    fn empty_set_literal() {
3390        let src = "entity E { tags: Set<String>\n    default_tags: {} }";
3391        let r = parse_ok(src);
3392        assert_eq!(r.diagnostics.len(), 0);
3393        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3394        let BlockItemKind::Assignment { value, .. } = &b.items[1].kind else { panic!() };
3395        let Expr::SetLiteral { elements, .. } = value else { panic!("expected SetLiteral, got {value:?}") };
3396        assert!(elements.is_empty());
3397    }
3398
3399    // =====================================================================
3400    // PRE-1.0 COVERAGE TESTS
3401    //
3402    // Additional tests addressing gaps identified during the pre-release
3403    // review: parameterised derived values, operator precedence matrix,
3404    // indentation-based multi-line detection, and set/object literal
3405    // disambiguation.
3406    // =====================================================================
3407
3408    // -- Parameterised derived values (ParamAssignment) --------------------
3409
3410    #[test]
3411    fn param_assignment_single() {
3412        let src = "entity Plan { can_use(feature): feature in features }";
3413        let r = parse_ok(src);
3414        assert_eq!(r.diagnostics.len(), 0);
3415        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3416        let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
3417            panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3418        };
3419        assert_eq!(name.name, "can_use");
3420        assert_eq!(params.len(), 1);
3421        assert_eq!(params[0].name, "feature");
3422        assert!(matches!(value, Expr::In { .. }));
3423    }
3424
3425    #[test]
3426    fn param_assignment_multiple() {
3427        let src = "entity E { distance(x, y): (x * x + y * y) }";
3428        let r = parse_ok(src);
3429        assert_eq!(r.diagnostics.len(), 0);
3430        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3431        let BlockItemKind::ParamAssignment { name, params, .. } = &b.items[0].kind else {
3432            panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3433        };
3434        assert_eq!(name.name, "distance");
3435        assert_eq!(params.len(), 2);
3436        assert_eq!(params[0].name, "x");
3437        assert_eq!(params[1].name, "y");
3438    }
3439
3440    #[test]
3441    fn param_assignment_simple_expression() {
3442        let src = "entity Task { remaining_effort(total): total - effort }";
3443        let r = parse_ok(src);
3444        assert_eq!(r.diagnostics.len(), 0);
3445        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3446        let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
3447            panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3448        };
3449        assert_eq!(name.name, "remaining_effort");
3450        assert_eq!(params.len(), 1);
3451        assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }));
3452    }
3453
3454    // -- Operator precedence matrix ----------------------------------------
3455
3456    #[test]
3457    fn precedence_logical_and_binds_tighter_than_or() {
3458        // `a or b and c` => Or(a, And(b, c))
3459        let src = "entity E { v: a or b and c }";
3460        let r = parse_ok(src);
3461        assert_eq!(r.diagnostics.len(), 0);
3462        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3463        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3464        let Expr::LogicalOp { op, right, .. } = value else {
3465            panic!("expected LogicalOp, got {value:?}");
3466        };
3467        assert_eq!(*op, LogicalOp::Or);
3468        assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3469    }
3470
3471    #[test]
3472    fn precedence_comparison_binds_tighter_than_and() {
3473        // `a = b and c != d` => And(Eq(a, b), NotEq(c, d))
3474        let src = "entity E { v: a = b and c != d }";
3475        let r = parse_ok(src);
3476        assert_eq!(r.diagnostics.len(), 0);
3477        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3478        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3479        let Expr::LogicalOp { op, left, right, .. } = value else {
3480            panic!("expected LogicalOp, got {value:?}");
3481        };
3482        assert_eq!(*op, LogicalOp::And);
3483        assert!(matches!(left.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
3484        assert!(matches!(right.as_ref(), Expr::Comparison { op: ComparisonOp::NotEq, .. }));
3485    }
3486
3487    #[test]
3488    fn precedence_arithmetic_binds_tighter_than_comparison() {
3489        // `a + b > c * d` => Gt(Add(a, b), Mul(c, d))
3490        let src = "entity E { v: a + b > c * d }";
3491        let r = parse_ok(src);
3492        assert_eq!(r.diagnostics.len(), 0);
3493        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3494        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3495        let Expr::Comparison { op, left, right, .. } = value else {
3496            panic!("expected Comparison, got {value:?}");
3497        };
3498        assert_eq!(*op, ComparisonOp::Gt);
3499        assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3500        assert!(matches!(right.as_ref(), Expr::BinaryOp { op: BinaryOp::Mul, .. }));
3501    }
3502
3503    #[test]
3504    fn precedence_null_coalesce_binds_tighter_than_comparison() {
3505        // `a ?? b = c` => Eq(NullCoalesce(a, b), c)
3506        let src = "entity E { v: a ?? b = c }";
3507        let r = parse_ok(src);
3508        assert_eq!(r.diagnostics.len(), 0);
3509        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3510        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3511        let Expr::Comparison { op, left, .. } = value else {
3512            panic!("expected Comparison, got {value:?}");
3513        };
3514        assert_eq!(*op, ComparisonOp::Eq);
3515        assert!(matches!(left.as_ref(), Expr::NullCoalesce { .. }));
3516    }
3517
3518    #[test]
3519    fn precedence_not_binds_tighter_than_and() {
3520        // `not a and b` => And(Not(a), b)
3521        let src = r#"rule R {
3522    when: X(a, b)
3523    requires: not a and b
3524    ensures: Done()
3525}"#;
3526        let r = parse_ok(src);
3527        assert_eq!(r.diagnostics.len(), 0);
3528        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3529        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3530        let Expr::LogicalOp { op, left, .. } = value else {
3531            panic!("expected LogicalOp, got {value:?}");
3532        };
3533        assert_eq!(*op, LogicalOp::And);
3534        assert!(matches!(left.as_ref(), Expr::Not { .. }));
3535    }
3536
3537    #[test]
3538    fn precedence_where_captures_full_condition() {
3539        // `items where status = active` => Where(items, Eq(status, active))
3540        // where (BP 7) binds looser than comparison (BP 30), so the full
3541        // condition `status = active` is captured as the where predicate.
3542        let src = "entity E { v: items where status = active }";
3543        let r = parse_ok(src);
3544        assert_eq!(r.diagnostics.len(), 0);
3545        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3546        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3547        let Expr::Where { condition, .. } = value else {
3548            panic!("expected Where, got {value:?}");
3549        };
3550        assert!(matches!(condition.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
3551    }
3552
3553    #[test]
3554    fn precedence_where_captures_and_or_conditions() {
3555        // `items where status = active and count > 0` =>
3556        //   Where(items, And(Eq(status, active), Gt(count, 0)))
3557        let src = "entity E { v: items where status = active and count > 0 }";
3558        let r = parse_ok(src);
3559        assert_eq!(r.diagnostics.len(), 0);
3560        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3561        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3562        let Expr::Where { condition, .. } = value else {
3563            panic!("expected Where, got {value:?}");
3564        };
3565        assert!(matches!(condition.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3566    }
3567
3568    #[test]
3569    fn precedence_projection_applies_to_where_result() {
3570        // `items where status = confirmed -> interviewer` =>
3571        //   ProjectionMap(Where(items, Eq(status, confirmed)), interviewer)
3572        let src = "entity E { v: items where status = confirmed -> interviewer }";
3573        let r = parse_ok(src);
3574        assert_eq!(r.diagnostics.len(), 0);
3575        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3576        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3577        let Expr::ProjectionMap { source, field, .. } = value else {
3578            panic!("expected ProjectionMap, got {value:?}");
3579        };
3580        assert_eq!(field.name, "interviewer");
3581        assert!(matches!(source.as_ref(), Expr::Where { .. }));
3582    }
3583
3584    #[test]
3585    fn precedence_lambda_binds_loosest() {
3586        // `items.any(i => i.active and i.valid)` => Lambda(i, And(active, valid))
3587        let src = "entity E { v: items.any(i => i.active and i.valid) }";
3588        let r = parse_ok(src);
3589        assert_eq!(r.diagnostics.len(), 0);
3590        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3591        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3592        let Expr::Call { args, .. } = value else { panic!() };
3593        let CallArg::Positional(Expr::Lambda { body, .. }) = &args[0] else { panic!() };
3594        assert!(matches!(body.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3595    }
3596
3597    #[test]
3598    fn precedence_in_binds_at_comparison_level() {
3599        // `x in {a, b} and y not in {c}` => And(In(x, {a,b}), NotIn(y, {c}))
3600        let src = r#"rule R {
3601    when: X(x, y)
3602    requires: x in {a, b} and y not in {c}
3603    ensures: Done()
3604}"#;
3605        let r = parse_ok(src);
3606        assert_eq!(r.diagnostics.len(), 0);
3607        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3608        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3609        let Expr::LogicalOp { op, left, right, .. } = value else {
3610            panic!("expected LogicalOp, got {value:?}");
3611        };
3612        assert_eq!(*op, LogicalOp::And);
3613        assert!(matches!(left.as_ref(), Expr::In { .. }));
3614        assert!(matches!(right.as_ref(), Expr::NotIn { .. }));
3615    }
3616
3617    // -- Multi-line clause value detection ----------------------------------
3618
3619    #[test]
3620    fn multiline_ensures_block() {
3621        let src = r#"rule R {
3622    when: X(doc)
3623    ensures:
3624        doc.status = published
3625        Notification.created(to: doc.author)
3626}"#;
3627        let r = parse_ok(src);
3628        assert_eq!(r.diagnostics.len(), 0);
3629        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3630        let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3631        assert_eq!(keyword, "ensures");
3632        let Expr::Block { items, .. } = value else {
3633            panic!("expected Block for multi-line ensures, got {value:?}");
3634        };
3635        assert_eq!(items.len(), 2);
3636    }
3637
3638    #[test]
3639    fn singleline_ensures_value() {
3640        let src = r#"rule R {
3641    when: X(doc)
3642    ensures: doc.status = published
3643}"#;
3644        let r = parse_ok(src);
3645        assert_eq!(r.diagnostics.len(), 0);
3646        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3647        let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3648        assert_eq!(keyword, "ensures");
3649        // Single-line value should NOT be wrapped in a Block
3650        assert!(!matches!(value, Expr::Block { .. }), "single-line ensures should not be Block");
3651    }
3652
3653    #[test]
3654    fn multiline_requires_with_continuation() {
3655        let src = r#"rule R {
3656    when: X(a)
3657    requires:
3658        a.count >= 2
3659        or a.items.any(i => i.can_solo)
3660    ensures: Done()
3661}"#;
3662        let r = parse_ok(src);
3663        assert_eq!(r.diagnostics.len(), 0);
3664    }
3665
3666    // -- Set vs object literal disambiguation ------------------------------
3667
3668    #[test]
3669    fn object_literal_single_field() {
3670        let src = r#"rule R {
3671    when: X()
3672    ensures:
3673        let o = {name: "test"}
3674        Done()
3675}"#;
3676        let r = parse_ok(src);
3677        assert_eq!(r.diagnostics.len(), 0);
3678        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3679        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3680        let Expr::Block { items, .. } = value else { panic!() };
3681        let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
3682        assert!(matches!(let_val.as_ref(), Expr::ObjectLiteral { .. }));
3683    }
3684
3685    #[test]
3686    fn set_literal_single_element() {
3687        let src = r#"rule R {
3688    when: X()
3689    ensures:
3690        let s = {active}
3691        Done()
3692}"#;
3693        let r = parse_ok(src);
3694        assert_eq!(r.diagnostics.len(), 0);
3695        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3696        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3697        let Expr::Block { items, .. } = value else { panic!() };
3698        let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
3699        assert!(matches!(let_val.as_ref(), Expr::SetLiteral { .. }),
3700            "bare {{ident}} should parse as set literal, got {:?}", let_val);
3701    }
3702
3703    // -- Lambda variations -------------------------------------------------
3704
3705    #[test]
3706    fn lambda_with_chained_access() {
3707        let src = "entity E { v: items.all(t => t.item.status = active) }";
3708        let r = parse_ok(src);
3709        assert_eq!(r.diagnostics.len(), 0);
3710    }
3711
3712    #[test]
3713    fn nested_lambda() {
3714        let src = "entity E { v: groups.any(g => g.items.all(i => i.valid)) }";
3715        let r = parse_ok(src);
3716        assert_eq!(r.diagnostics.len(), 0);
3717    }
3718
3719    // -- Qualified name variations -----------------------------------------
3720
3721    #[test]
3722    fn qualified_name_with_member_access() {
3723        let src = "entity E { v: shared/Validator.check }";
3724        let r = parse_ok(src);
3725        assert_eq!(r.diagnostics.len(), 0);
3726        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3727        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3728        let Expr::MemberAccess { object, field, .. } = value else {
3729            panic!("expected MemberAccess, got {value:?}");
3730        };
3731        assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
3732        assert_eq!(field.name, "check");
3733    }
3734
3735    #[test]
3736    fn qualified_name_in_call() {
3737        let src = r#"rule R {
3738    when: X(item)
3739    requires: shared/Validator.check(item: item)
3740    ensures: Done()
3741}"#;
3742        let r = parse_ok(src);
3743        assert_eq!(r.diagnostics.len(), 0);
3744    }
3745
3746    // -- Nested control flow -----------------------------------------------
3747
3748    #[test]
3749    fn nested_if_inside_for() {
3750        let src = r#"rule R {
3751    when: X()
3752    for user in Users where user.active:
3753        if user.role = admin:
3754            ensures: AdminNotified(user: user)
3755        else:
3756            ensures: UserNotified(user: user)
3757}"#;
3758        let r = parse_ok(src);
3759        assert_eq!(r.diagnostics.len(), 0);
3760        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3761        let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
3762        assert!(matches!(items[0].kind, BlockItemKind::IfBlock { .. }));
3763    }
3764
3765    #[test]
3766    fn for_with_let_before_ensures() {
3767        let src = r#"rule R {
3768    when: schedule: DigestSchedule.next_run_at <= now
3769    for user in Users where user.active:
3770        let pending = user.tasks where status = pending
3771        ensures: DigestEmail.created(to: user.email, tasks: pending)
3772}"#;
3773        let r = parse_ok(src);
3774        assert_eq!(r.diagnostics.len(), 0);
3775        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3776        let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
3777        assert_eq!(items.len(), 2, "for body should have let + ensures");
3778        assert!(matches!(items[0].kind, BlockItemKind::Let { .. }));
3779        assert!(matches!(items[1].kind, BlockItemKind::Clause { .. }));
3780    }
3781
3782    // -- Join lookup variations --------------------------------------------
3783
3784    #[test]
3785    fn join_lookup_all_unnamed() {
3786        let src = "entity E { match: Other{a, b, c} }";
3787        let r = parse_ok(src);
3788        assert_eq!(r.diagnostics.len(), 0);
3789        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3790        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3791        let Expr::JoinLookup { fields, .. } = value else { panic!() };
3792        assert_eq!(fields.len(), 3);
3793        assert!(fields.iter().all(|f| f.value.is_none()));
3794    }
3795
3796    #[test]
3797    fn join_lookup_all_named() {
3798        let src = "entity E { match: Membership{user: actor, workspace: ws} }";
3799        let r = parse_ok(src);
3800        assert_eq!(r.diagnostics.len(), 0);
3801        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3802        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3803        let Expr::JoinLookup { fields, .. } = value else { panic!() };
3804        assert_eq!(fields.len(), 2);
3805        assert!(fields.iter().all(|f| f.value.is_some()));
3806    }
3807
3808    #[test]
3809    fn join_lookup_in_requires() {
3810        let src = r#"rule R {
3811    when: X(user, workspace)
3812    requires: exists WorkspaceMembership{user: user, workspace: workspace}
3813    ensures: Done()
3814}"#;
3815        let r = parse_ok(src);
3816        assert_eq!(r.diagnostics.len(), 0);
3817    }
3818
3819    #[test]
3820    fn join_lookup_negated_in_requires() {
3821        let src = r#"rule R {
3822    when: X(email)
3823    requires: not exists User{email: email}
3824    ensures: Done()
3825}"#;
3826        let r = parse_ok(src);
3827        assert_eq!(r.diagnostics.len(), 0);
3828    }
3829}