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() || self.eat(TokenKind::With).is_some()
700        {
701            // Parse filter at min_bp 0 — colon terminates naturally since
702            // it's not an expression operator.
703            Some(self.parse_expr(0)?)
704        } else {
705            None
706        };
707
708        self.expect(TokenKind::Colon)?;
709
710        // The body contains nested block items at higher indentation.
711        let for_line = self.line_of(start);
712        let next_line = self.line_of(self.peek().span);
713
714        let items = if next_line > for_line {
715            let base_col = self.col_of(self.peek().span);
716            self.parse_indented_block_items(base_col)
717        } else {
718            // Single-line for: parse one block item
719            let mut items = Vec::new();
720            if let Some(item) = self.parse_block_item() {
721                items.push(item);
722            }
723            items
724        };
725
726        let end = items
727            .last()
728            .map(|i| i.span)
729            .unwrap_or(start);
730
731        Some(BlockItem {
732            span: start.merge(end),
733            kind: BlockItemKind::ForBlock {
734                binding,
735                collection,
736                filter,
737                items,
738            },
739        })
740    }
741
742    /// Collect block items at column >= `base_col` (for indented for-block bodies).
743    fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
744        let mut items = Vec::new();
745        while !self.at_eof()
746            && !self.at(TokenKind::RBrace)
747            && self.col_of(self.peek().span) >= base_col
748        {
749            if let Some(item) = self.parse_block_item() {
750                items.push(item);
751            } else {
752                self.advance();
753                break;
754            }
755        }
756        items
757    }
758
759    /// Parse `if condition: ... [else if ...: ...] [else: ...]` at block level.
760    fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
761        self.advance(); // consume `if`
762        let mut branches = Vec::new();
763
764        // First branch
765        let condition = self.parse_expr(0)?;
766        self.expect(TokenKind::Colon)?;
767        let if_line = self.line_of(start);
768        let items = self.parse_if_block_body(if_line);
769        branches.push(CondBlockBranch {
770            span: start.merge(items.last().map(|i| i.span).unwrap_or(start)),
771            condition,
772            items,
773        });
774
775        // else if / else
776        let mut else_items = None;
777        while self.at(TokenKind::Else) {
778            let else_tok = self.advance();
779            if self.at(TokenKind::If) {
780                let if_start = self.advance().span;
781                let cond = self.parse_expr(0)?;
782                self.expect(TokenKind::Colon)?;
783                let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
784                branches.push(CondBlockBranch {
785                    span: if_start.merge(body_items.last().map(|i| i.span).unwrap_or(if_start)),
786                    condition: cond,
787                    items: body_items,
788                });
789            } else {
790                self.expect(TokenKind::Colon)?;
791                let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
792                else_items = Some(body_items);
793                break;
794            }
795        }
796
797        let end = else_items
798            .as_ref()
799            .and_then(|items| items.last().map(|i| i.span))
800            .or_else(|| branches.last().and_then(|b| b.items.last().map(|i| i.span)))
801            .unwrap_or(start);
802
803        Some(BlockItem {
804            span: start.merge(end),
805            kind: BlockItemKind::IfBlock {
806                branches,
807                else_items,
808            },
809        })
810    }
811
812    /// Parse the body of an if/else if/else block branch.
813    fn parse_if_block_body(&mut self, keyword_line: u32) -> Vec<BlockItem> {
814        let next_line = self.line_of(self.peek().span);
815        if next_line > keyword_line {
816            let base_col = self.col_of(self.peek().span);
817            self.parse_indented_block_items(base_col)
818        } else {
819            // Single-line: parse one block item
820            let mut items = Vec::new();
821            if let Some(item) = self.parse_block_item() {
822                items.push(item);
823            }
824            items
825        }
826    }
827
828    fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
829        let name_tok = self.advance(); // consume name/keyword
830        let name_text = self.text(name_tok.span).to_string();
831        self.advance(); // consume ':'
832
833        let allows_binding = clause_allows_binding(&name_text);
834        let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
835        let value_span = value.span();
836
837        let kind = if is_clause_keyword(&name_text) {
838            BlockItemKind::Clause {
839                keyword: name_text,
840                value,
841            }
842        } else {
843            BlockItemKind::Assignment {
844                name: Ident {
845                    span: name_tok.span,
846                    name: name_text,
847                },
848                value,
849            }
850        };
851
852        Some(BlockItem {
853            span: start.merge(value_span),
854            kind,
855        })
856    }
857
858    /// Parse `Entity.field: value` — a dot-path reverse relationship declaration.
859    fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
860        let obj_tok = self.advance(); // consume first ident
861        self.advance(); // consume '.'
862        let field = self.parse_ident_in("field name")?;
863        self.advance(); // consume ':'
864
865        let path = Expr::MemberAccess {
866            span: obj_tok.span.merge(field.span),
867            object: Box::new(Expr::Ident(Ident {
868                span: obj_tok.span,
869                name: self.text(obj_tok.span).to_string(),
870            })),
871            field,
872        };
873
874        let value = self.parse_clause_value(start)?;
875        let value_span = value.span();
876        Some(BlockItem {
877            span: start.merge(value_span),
878            kind: BlockItemKind::PathAssignment { path, value },
879        })
880    }
881
882    fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
883        // Could be `name(params): value` (param assignment) or
884        // `name(args)` which is an expression that happens to start a clause
885        // value. Peek far enough to see if `)` is followed by `:`.
886        let saved_pos = self.pos;
887        let _name_tok = self.advance();
888        self.advance(); // (
889
890        // Try to scan past balanced parens
891        let mut depth = 1u32;
892        while !self.at_eof() && depth > 0 {
893            match self.peek_kind() {
894                TokenKind::LParen => {
895                    depth += 1;
896                    self.advance();
897                }
898                TokenKind::RParen => {
899                    depth -= 1;
900                    self.advance();
901                }
902                _ => {
903                    self.advance();
904                }
905            }
906        }
907
908        if self.at(TokenKind::Colon) {
909            // It's a parameterised assignment: restore and parse properly
910            self.pos = saved_pos;
911            let name = self.parse_ident_in("derived value name")?;
912            self.expect(TokenKind::LParen)?;
913            let params = self.parse_ident_list()?;
914            self.expect(TokenKind::RParen)?;
915            self.expect(TokenKind::Colon)?;
916            let value = self.parse_clause_value(start)?;
917            Some(BlockItem {
918                span: start.merge(value.span()),
919                kind: BlockItemKind::ParamAssignment {
920                    name,
921                    params,
922                    value,
923                },
924            })
925        } else {
926            // Not a param assignment — restore and fall through to assignment
927            self.pos = saved_pos;
928            // Check for regular `name: value`
929            if self.peek_at(1).kind == TokenKind::Colon {
930                // Nope, the (1) is LParen not Colon. Re-examine.
931            }
932            // Fall back: treat as `name: value` where value starts with a call
933            self.parse_assign_or_clause_item(start)
934        }
935    }
936
937    fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
938        let mut params = Vec::new();
939        if !self.at(TokenKind::RParen) {
940            params.push(self.parse_ident_in("parameter name")?);
941            while self.eat(TokenKind::Comma).is_some() {
942                params.push(self.parse_ident_in("parameter name")?);
943            }
944        }
945        Some(params)
946    }
947
948    /// Parse a for-loop binding: either a single ident or `(a, b)` destructuring.
949    fn parse_for_binding(&mut self) -> Option<ForBinding> {
950        if self.at(TokenKind::LParen) {
951            let start = self.advance().span; // consume '('
952            let mut idents = Vec::new();
953            idents.push(self.parse_ident_in("loop variable")?);
954            while self.eat(TokenKind::Comma).is_some() {
955                idents.push(self.parse_ident_in("loop variable")?);
956            }
957            let end = self.expect(TokenKind::RParen)?.span;
958            Some(ForBinding::Destructured(idents, start.merge(end)))
959        } else {
960            let ident = self.parse_ident_in("loop variable")?;
961            Some(ForBinding::Single(ident))
962        }
963    }
964
965    /// Parse a clause value, optionally checking for a `name: expr` binding
966    /// pattern at the start. Used for when, facing and context clauses where
967    /// the first `ident:` is a binding rather than a nested assignment.
968    fn parse_clause_value_maybe_binding(
969        &mut self,
970        clause_start: Span,
971        allow_binding: bool,
972    ) -> Option<Expr> {
973        if allow_binding
974            && self.peek_kind().is_word()
975            && self.peek_at(1).kind == TokenKind::Colon
976        {
977            // Check this isn't at the start of a new block item on the next line.
978            // Bindings only apply on the same line or the immediate indented value.
979            let clause_line = self.line_of(clause_start);
980            let next_line = self.line_of(self.peek().span);
981            let colon_is_block_item = next_line > clause_line
982                && self.peek_at(2).kind != TokenKind::Eof
983                && self.line_of(self.peek_at(2).span) == next_line;
984
985            if next_line == clause_line || colon_is_block_item {
986                let name = self.parse_ident_in("binding name")?;
987                self.advance(); // consume ':'
988                let inner = self.parse_clause_value(clause_start)?;
989                return Some(Expr::Binding {
990                    span: name.span.merge(inner.span()),
991                    name,
992                    value: Box::new(inner),
993                });
994            }
995        }
996        self.parse_clause_value(clause_start)
997    }
998
999    /// Parse a clause value. If the next token is on a new line (indented),
1000    /// collect a multi-line block. Otherwise parse a single expression.
1001    fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1002        let clause_line = self.line_of(clause_start);
1003        let next = self.peek();
1004        let next_line = self.line_of(next.span);
1005
1006        if next_line > clause_line {
1007            // Multi-line block — but only if the next token is actually
1008            // indented past the clause keyword. When a clause has only a
1009            // comment as its value (stripped by the lexer), the next visible
1010            // token is a sibling at the same indentation.
1011            let base_col = self.col_of(next.span);
1012            let clause_col = self.col_of(clause_start);
1013            if base_col <= clause_col {
1014                return Some(Expr::Block {
1015                    span: clause_start,
1016                    items: Vec::new(),
1017                });
1018            }
1019            self.parse_indented_block(base_col)
1020        } else {
1021            // Single-line clause value
1022            self.parse_expr(0)
1023        }
1024    }
1025
1026    /// Collect expressions that start at column >= `base_col` into a block.
1027    /// Also handles `let name = value` bindings inside clause value blocks.
1028    fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1029        let start = self.peek().span;
1030        let mut items = Vec::new();
1031
1032        while !self.at_eof()
1033            && !self.at(TokenKind::RBrace)
1034            && self.col_of(self.peek().span) >= base_col
1035        {
1036            // Handle `let name = value` inside expression blocks
1037            if self.at(TokenKind::Let) {
1038                let let_start = self.advance().span;
1039                if let Some(name) = self.parse_ident_in("binding name") {
1040                    if self.expect(TokenKind::Eq).is_some() {
1041                        if let Some(value) = self.parse_expr(0) {
1042                            items.push(Expr::LetExpr {
1043                                span: let_start.merge(value.span()),
1044                                name,
1045                                value: Box::new(value),
1046                            });
1047                            continue;
1048                        }
1049                    }
1050                }
1051                break;
1052            }
1053
1054            if let Some(expr) = self.parse_expr(0) {
1055                items.push(expr);
1056            } else {
1057                self.advance();
1058                break;
1059            }
1060        }
1061
1062        if items.len() == 1 {
1063            Some(items.pop().unwrap())
1064        } else {
1065            let end = items.last().map(|e| e.span()).unwrap_or(start);
1066            Some(Expr::Block {
1067                span: start.merge(end),
1068                items,
1069            })
1070        }
1071    }
1072}
1073
1074// ---------------------------------------------------------------------------
1075// Expression parsing — Pratt parser
1076// ---------------------------------------------------------------------------
1077
1078// Binding powers (even = left, odd = right for right-associative)
1079const BP_LAMBDA: u8 = 4;
1080const BP_WHEN_GUARD: u8 = 5;
1081const BP_OR: u8 = 10;
1082const BP_AND: u8 = 20;
1083const BP_COMPARE: u8 = 30;
1084const BP_TRANSITION: u8 = 32;
1085const BP_WITH_WHERE: u8 = 35;
1086const BP_PROJECTION: u8 = 37;
1087const BP_NULL_COALESCE: u8 = 40;
1088const BP_ADD: u8 = 50;
1089const BP_MUL: u8 = 60;
1090const BP_PIPE: u8 = 65;
1091const BP_PREFIX: u8 = 70;
1092const BP_POSTFIX: u8 = 80;
1093
1094impl<'s> Parser<'s> {
1095    pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1096        let mut lhs = self.parse_prefix()?;
1097
1098        loop {
1099            if let Some((l_bp, r_bp)) = self.infix_bp() {
1100                if l_bp < min_bp {
1101                    break;
1102                }
1103                lhs = self.parse_infix(lhs, r_bp)?;
1104            } else if let Some(l_bp) = self.postfix_bp() {
1105                if l_bp < min_bp {
1106                    break;
1107                }
1108                lhs = self.parse_postfix(lhs)?;
1109            } else {
1110                break;
1111            }
1112        }
1113
1114        Some(lhs)
1115    }
1116
1117    // -- prefix ---------------------------------------------------------
1118
1119    fn parse_prefix(&mut self) -> Option<Expr> {
1120        match self.peek_kind() {
1121            TokenKind::Not => {
1122                let start = self.advance().span;
1123                if self.at(TokenKind::Exists) {
1124                    self.advance();
1125                    let operand = self.parse_expr(BP_PREFIX)?;
1126                    Some(Expr::NotExists {
1127                        span: start.merge(operand.span()),
1128                        operand: Box::new(operand),
1129                    })
1130                } else {
1131                    let operand = self.parse_expr(BP_PREFIX)?;
1132                    Some(Expr::Not {
1133                        span: start.merge(operand.span()),
1134                        operand: Box::new(operand),
1135                    })
1136                }
1137            }
1138            TokenKind::Exists => {
1139                // When `exists` is not followed by an expression-start token,
1140                // treat it as a plain identifier (e.g. `label: exists`).
1141                let next = self.peek_at(1).kind;
1142                if matches!(
1143                    next,
1144                    TokenKind::RParen
1145                        | TokenKind::RBrace
1146                        | TokenKind::RBracket
1147                        | TokenKind::Comma
1148                        | TokenKind::Eof
1149                ) {
1150                    let id = self.parse_ident()?;
1151                    return Some(Expr::Ident(id));
1152                }
1153                let start = self.advance().span;
1154                let operand = self.parse_expr(BP_PREFIX)?;
1155                Some(Expr::Exists {
1156                    span: start.merge(operand.span()),
1157                    operand: Box::new(operand),
1158                })
1159            }
1160            TokenKind::If => self.parse_if_expr(),
1161            TokenKind::For => self.parse_for_expr(),
1162            TokenKind::LBrace => self.parse_brace_expr(),
1163            TokenKind::LBracket => self.parse_list_literal(),
1164            TokenKind::LParen => self.parse_paren_expr(),
1165            TokenKind::Number => {
1166                let t = self.advance();
1167                Some(Expr::NumberLiteral {
1168                    span: t.span,
1169                    value: self.text(t.span).to_string(),
1170                })
1171            }
1172            TokenKind::Duration => {
1173                let t = self.advance();
1174                Some(Expr::DurationLiteral {
1175                    span: t.span,
1176                    value: self.text(t.span).to_string(),
1177                })
1178            }
1179            TokenKind::String => {
1180                let sl = self.parse_string()?;
1181                Some(Expr::StringLiteral(sl))
1182            }
1183            TokenKind::True => {
1184                let t = self.advance();
1185                Some(Expr::BoolLiteral {
1186                    span: t.span,
1187                    value: true,
1188                })
1189            }
1190            TokenKind::False => {
1191                let t = self.advance();
1192                Some(Expr::BoolLiteral {
1193                    span: t.span,
1194                    value: false,
1195                })
1196            }
1197            TokenKind::Null => {
1198                let t = self.advance();
1199                Some(Expr::Null { span: t.span })
1200            }
1201            TokenKind::Now => {
1202                let t = self.advance();
1203                Some(Expr::Now { span: t.span })
1204            }
1205            TokenKind::This => {
1206                let t = self.advance();
1207                Some(Expr::This { span: t.span })
1208            }
1209            TokenKind::Within => {
1210                let t = self.advance();
1211                Some(Expr::Within { span: t.span })
1212            }
1213            k if k.is_word() => {
1214                let id = self.parse_ident()?;
1215                Some(Expr::Ident(id))
1216            }
1217            TokenKind::Star => {
1218                // Wildcard `*` in type position (e.g. `Codec<*>`)
1219                let t = self.advance();
1220                Some(Expr::Ident(Ident {
1221                    span: t.span,
1222                    name: "*".into(),
1223                }))
1224            }
1225            TokenKind::Minus => {
1226                // Unary minus: -expr → BinaryOp(0, Sub, expr)
1227                let start = self.advance().span;
1228                let operand = self.parse_expr(BP_PREFIX)?;
1229                Some(Expr::BinaryOp {
1230                    span: start.merge(operand.span()),
1231                    left: Box::new(Expr::NumberLiteral {
1232                        span: start,
1233                        value: "0".into(),
1234                    }),
1235                    op: BinaryOp::Sub,
1236                    right: Box::new(operand),
1237                })
1238            }
1239            _ => {
1240                self.error(
1241                    self.peek().span,
1242                    format!(
1243                        "expected expression (identifier, number, string, true/false, null, \
1244                         if/for/not/exists, '(', '{{', '['), found {}",
1245                        self.peek_kind(),
1246                    ),
1247                );
1248                None
1249            }
1250        }
1251    }
1252
1253    // -- infix binding powers -------------------------------------------
1254
1255    fn infix_bp(&self) -> Option<(u8, u8)> {
1256        match self.peek_kind() {
1257            TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), // right-assoc
1258            // `when` as an inline guard on provides/related items
1259            TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1260            TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1261            TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1262            TokenKind::And => Some((BP_AND, BP_AND + 1)),
1263            TokenKind::Eq | TokenKind::BangEq => {
1264                Some((BP_COMPARE, BP_COMPARE + 1))
1265            }
1266            TokenKind::Lt => {
1267                // If `<` is immediately adjacent to a word token (no space),
1268                // treat as generic type postfix, not comparison infix.
1269                if self.pos > 0 {
1270                    let prev = self.tokens[self.pos - 1];
1271                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1272                        return None;
1273                    }
1274                }
1275                Some((BP_COMPARE, BP_COMPARE + 1))
1276            }
1277            TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1278                Some((BP_COMPARE, BP_COMPARE + 1))
1279            }
1280            TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1281            // `not in` — only when followed by `in`
1282            TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1283                Some((BP_COMPARE, BP_COMPARE + 1))
1284            }
1285            TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1286            TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1287            TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1288            TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1289            TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1290            TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1291            TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1292            TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1293            _ => None,
1294        }
1295    }
1296
1297    fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1298        let op_tok = self.advance();
1299        match op_tok.kind {
1300            TokenKind::FatArrow => {
1301                let body = self.parse_expr(r_bp)?;
1302                Some(Expr::Lambda {
1303                    span: lhs.span().merge(body.span()),
1304                    param: Box::new(lhs),
1305                    body: Box::new(body),
1306                })
1307            }
1308            TokenKind::Pipe => {
1309                let rhs = self.parse_expr(r_bp)?;
1310                Some(Expr::Pipe {
1311                    span: lhs.span().merge(rhs.span()),
1312                    left: Box::new(lhs),
1313                    right: Box::new(rhs),
1314                })
1315            }
1316            TokenKind::Or => {
1317                let rhs = self.parse_expr(r_bp)?;
1318                Some(Expr::LogicalOp {
1319                    span: lhs.span().merge(rhs.span()),
1320                    left: Box::new(lhs),
1321                    op: LogicalOp::Or,
1322                    right: Box::new(rhs),
1323                })
1324            }
1325            TokenKind::And => {
1326                let rhs = self.parse_expr(r_bp)?;
1327                Some(Expr::LogicalOp {
1328                    span: lhs.span().merge(rhs.span()),
1329                    left: Box::new(lhs),
1330                    op: LogicalOp::And,
1331                    right: Box::new(rhs),
1332                })
1333            }
1334            TokenKind::Eq => {
1335                let rhs = self.parse_expr(r_bp)?;
1336                Some(Expr::Comparison {
1337                    span: lhs.span().merge(rhs.span()),
1338                    left: Box::new(lhs),
1339                    op: ComparisonOp::Eq,
1340                    right: Box::new(rhs),
1341                })
1342            }
1343            TokenKind::BangEq => {
1344                let rhs = self.parse_expr(r_bp)?;
1345                Some(Expr::Comparison {
1346                    span: lhs.span().merge(rhs.span()),
1347                    left: Box::new(lhs),
1348                    op: ComparisonOp::NotEq,
1349                    right: Box::new(rhs),
1350                })
1351            }
1352            TokenKind::Lt => {
1353                let rhs = self.parse_expr(r_bp)?;
1354                Some(Expr::Comparison {
1355                    span: lhs.span().merge(rhs.span()),
1356                    left: Box::new(lhs),
1357                    op: ComparisonOp::Lt,
1358                    right: Box::new(rhs),
1359                })
1360            }
1361            TokenKind::LtEq => {
1362                let rhs = self.parse_expr(r_bp)?;
1363                Some(Expr::Comparison {
1364                    span: lhs.span().merge(rhs.span()),
1365                    left: Box::new(lhs),
1366                    op: ComparisonOp::LtEq,
1367                    right: Box::new(rhs),
1368                })
1369            }
1370            TokenKind::Gt => {
1371                let rhs = self.parse_expr(r_bp)?;
1372                Some(Expr::Comparison {
1373                    span: lhs.span().merge(rhs.span()),
1374                    left: Box::new(lhs),
1375                    op: ComparisonOp::Gt,
1376                    right: Box::new(rhs),
1377                })
1378            }
1379            TokenKind::GtEq => {
1380                let rhs = self.parse_expr(r_bp)?;
1381                Some(Expr::Comparison {
1382                    span: lhs.span().merge(rhs.span()),
1383                    left: Box::new(lhs),
1384                    op: ComparisonOp::GtEq,
1385                    right: Box::new(rhs),
1386                })
1387            }
1388            TokenKind::In => {
1389                let rhs = self.parse_expr(r_bp)?;
1390                Some(Expr::In {
1391                    span: lhs.span().merge(rhs.span()),
1392                    element: Box::new(lhs),
1393                    collection: Box::new(rhs),
1394                })
1395            }
1396            TokenKind::Not => {
1397                // `not in`
1398                self.expect(TokenKind::In)?;
1399                let rhs = self.parse_expr(r_bp)?;
1400                Some(Expr::NotIn {
1401                    span: lhs.span().merge(rhs.span()),
1402                    element: Box::new(lhs),
1403                    collection: Box::new(rhs),
1404                })
1405            }
1406            TokenKind::Where => {
1407                let rhs = self.parse_expr(r_bp)?;
1408                Some(Expr::Where {
1409                    span: lhs.span().merge(rhs.span()),
1410                    source: Box::new(lhs),
1411                    condition: Box::new(rhs),
1412                })
1413            }
1414            TokenKind::With => {
1415                let rhs = self.parse_expr(r_bp)?;
1416                Some(Expr::With {
1417                    span: lhs.span().merge(rhs.span()),
1418                    source: Box::new(lhs),
1419                    predicate: Box::new(rhs),
1420                })
1421            }
1422            TokenKind::QuestionQuestion => {
1423                let rhs = self.parse_expr(r_bp)?;
1424                Some(Expr::NullCoalesce {
1425                    span: lhs.span().merge(rhs.span()),
1426                    left: Box::new(lhs),
1427                    right: Box::new(rhs),
1428                })
1429            }
1430            TokenKind::Plus => {
1431                let rhs = self.parse_expr(r_bp)?;
1432                Some(Expr::BinaryOp {
1433                    span: lhs.span().merge(rhs.span()),
1434                    left: Box::new(lhs),
1435                    op: BinaryOp::Add,
1436                    right: Box::new(rhs),
1437                })
1438            }
1439            TokenKind::Minus => {
1440                let rhs = self.parse_expr(r_bp)?;
1441                Some(Expr::BinaryOp {
1442                    span: lhs.span().merge(rhs.span()),
1443                    left: Box::new(lhs),
1444                    op: BinaryOp::Sub,
1445                    right: Box::new(rhs),
1446                })
1447            }
1448            TokenKind::Star => {
1449                let rhs = self.parse_expr(r_bp)?;
1450                Some(Expr::BinaryOp {
1451                    span: lhs.span().merge(rhs.span()),
1452                    left: Box::new(lhs),
1453                    op: BinaryOp::Mul,
1454                    right: Box::new(rhs),
1455                })
1456            }
1457            TokenKind::Slash => {
1458                // Check for qualified name: `alias/Name` or `alias/config`
1459                // Qualified if the LHS is a bare identifier and the RHS is a
1460                // word that either starts with uppercase or is a block keyword
1461                // (like `config`).
1462                if let Expr::Ident(ref id) = lhs {
1463                    if self.peek_kind().is_word() {
1464                        let next_text = self.text(self.peek().span);
1465                        let is_qualified = next_text
1466                            .chars()
1467                            .next()
1468                            .is_some_and(|c| c.is_uppercase())
1469                            || matches!(
1470                                self.peek_kind(),
1471                                TokenKind::Config | TokenKind::Entity | TokenKind::Value
1472                            );
1473                        if is_qualified {
1474                            let name_tok = self.advance();
1475                            return Some(Expr::QualifiedName(QualifiedName {
1476                                span: lhs.span().merge(name_tok.span),
1477                                qualifier: Some(id.name.clone()),
1478                                name: self.text(name_tok.span).to_string(),
1479                            }));
1480                        }
1481                    }
1482                }
1483                let rhs = self.parse_expr(r_bp)?;
1484                Some(Expr::BinaryOp {
1485                    span: lhs.span().merge(rhs.span()),
1486                    left: Box::new(lhs),
1487                    op: BinaryOp::Div,
1488                    right: Box::new(rhs),
1489                })
1490            }
1491            TokenKind::ThinArrow => {
1492                let field = self.parse_ident_in("projection field")?;
1493                Some(Expr::ProjectionMap {
1494                    span: lhs.span().merge(field.span),
1495                    source: Box::new(lhs),
1496                    field,
1497                })
1498            }
1499            TokenKind::TransitionsTo => {
1500                let rhs = self.parse_expr(r_bp)?;
1501                Some(Expr::TransitionsTo {
1502                    span: lhs.span().merge(rhs.span()),
1503                    subject: Box::new(lhs),
1504                    new_state: Box::new(rhs),
1505                })
1506            }
1507            TokenKind::Becomes => {
1508                let rhs = self.parse_expr(r_bp)?;
1509                Some(Expr::Becomes {
1510                    span: lhs.span().merge(rhs.span()),
1511                    subject: Box::new(lhs),
1512                    new_state: Box::new(rhs),
1513                })
1514            }
1515            TokenKind::When => {
1516                // Inline guard: `action when condition`
1517                let rhs = self.parse_expr(r_bp)?;
1518                Some(Expr::WhenGuard {
1519                    span: lhs.span().merge(rhs.span()),
1520                    action: Box::new(lhs),
1521                    condition: Box::new(rhs),
1522                })
1523            }
1524            _ => {
1525                self.error(
1526                    op_tok.span,
1527                    format!("unexpected infix operator {}", op_tok.kind),
1528                );
1529                None
1530            }
1531        }
1532    }
1533
1534    // -- postfix --------------------------------------------------------
1535
1536    fn postfix_bp(&self) -> Option<u8> {
1537        match self.peek_kind() {
1538            TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
1539            TokenKind::QuestionMark => Some(BP_POSTFIX),
1540            // `<` for generic types like `Set<T>`, `List<T>` — only treated
1541            // as postfix when it immediately follows a word with no space.
1542            TokenKind::Lt => {
1543                if self.pos > 0 {
1544                    let prev = self.tokens[self.pos - 1];
1545                    // Only if `<` starts immediately after the previous token
1546                    // (no whitespace gap) to distinguish from comparisons.
1547                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1548                        return Some(BP_POSTFIX);
1549                    }
1550                }
1551                None
1552            }
1553            TokenKind::LParen => Some(BP_POSTFIX),
1554            TokenKind::LBrace => {
1555                // Join lookup: only when preceded by something that looks
1556                // like an entity name (handled generically — any expr can
1557                // be followed by { for join lookup in expression position).
1558                // But only if the { is on the same line to avoid consuming
1559                // a block body.
1560                let next = self.peek();
1561                let prev_end = if self.pos > 0 {
1562                    self.tokens[self.pos - 1].span.end
1563                } else {
1564                    0
1565                };
1566                // Same line check
1567                if self.line_of(Span::new(prev_end, prev_end))
1568                    == self.line_of(next.span)
1569                {
1570                    Some(BP_POSTFIX)
1571                } else {
1572                    None
1573                }
1574            }
1575            _ => None,
1576        }
1577    }
1578
1579    fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
1580        match self.peek_kind() {
1581            TokenKind::QuestionMark => {
1582                let end = self.advance().span;
1583                Some(Expr::TypeOptional {
1584                    span: lhs.span().merge(end),
1585                    inner: Box::new(lhs),
1586                })
1587            }
1588            TokenKind::Lt => {
1589                // Generic type: `Set<T>`, `List<Node?>`
1590                self.advance(); // consume <
1591                let mut args = Vec::new();
1592                // Parse args above comparison BP so `>` isn't consumed as infix
1593                while !self.at(TokenKind::Gt) && !self.at_eof() {
1594                    args.push(self.parse_expr(BP_COMPARE + 1)?);
1595                    self.eat(TokenKind::Comma);
1596                }
1597                let end = self.expect(TokenKind::Gt)?.span;
1598                Some(Expr::GenericType {
1599                    span: lhs.span().merge(end),
1600                    name: Box::new(lhs),
1601                    args,
1602                })
1603            }
1604            TokenKind::Dot => {
1605                self.advance();
1606                let field = self.parse_ident_in("field name")?;
1607                Some(Expr::MemberAccess {
1608                    span: lhs.span().merge(field.span),
1609                    object: Box::new(lhs),
1610                    field,
1611                })
1612            }
1613            TokenKind::QuestionDot => {
1614                self.advance();
1615                let field = self.parse_ident_in("field name")?;
1616                Some(Expr::OptionalAccess {
1617                    span: lhs.span().merge(field.span),
1618                    object: Box::new(lhs),
1619                    field,
1620                })
1621            }
1622            TokenKind::LParen => {
1623                self.advance();
1624                let args = self.parse_call_args()?;
1625                let end = self.expect(TokenKind::RParen)?.span;
1626                Some(Expr::Call {
1627                    span: lhs.span().merge(end),
1628                    function: Box::new(lhs),
1629                    args,
1630                })
1631            }
1632            TokenKind::LBrace => {
1633                self.advance();
1634                let fields = self.parse_join_fields()?;
1635                let end = self.expect(TokenKind::RBrace)?.span;
1636                Some(Expr::JoinLookup {
1637                    span: lhs.span().merge(end),
1638                    entity: Box::new(lhs),
1639                    fields,
1640                })
1641            }
1642            _ => None,
1643        }
1644    }
1645
1646    // -- call arguments -------------------------------------------------
1647
1648    fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
1649        let mut args = Vec::new();
1650        while !self.at(TokenKind::RParen) && !self.at_eof() {
1651            // Check for named argument: `name: value`
1652            if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1653                let name = self.parse_ident_in("argument name")?;
1654                self.advance(); // :
1655                let value = self.parse_expr(0)?;
1656                args.push(CallArg::Named(NamedArg {
1657                    span: name.span.merge(value.span()),
1658                    name,
1659                    value,
1660                }));
1661            } else {
1662                let expr = self.parse_expr(0)?;
1663                args.push(CallArg::Positional(expr));
1664            }
1665            self.eat(TokenKind::Comma);
1666        }
1667        Some(args)
1668    }
1669
1670    // -- join fields ----------------------------------------------------
1671
1672    fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
1673        let mut fields = Vec::new();
1674        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1675            let field = self.parse_ident_in("join field name")?;
1676            let value = if self.eat(TokenKind::Colon).is_some() {
1677                Some(self.parse_expr(0)?)
1678            } else {
1679                None
1680            };
1681            fields.push(JoinField {
1682                span: field.span.merge(
1683                    value
1684                        .as_ref()
1685                        .map(|v| v.span())
1686                        .unwrap_or(field.span),
1687                ),
1688                field,
1689                value,
1690            });
1691            self.eat(TokenKind::Comma);
1692        }
1693        Some(fields)
1694    }
1695
1696    // -- if expression --------------------------------------------------
1697
1698    fn parse_if_expr(&mut self) -> Option<Expr> {
1699        let start = self.advance().span; // consume `if`
1700        let mut branches = Vec::new();
1701
1702        // First branch
1703        let condition = self.parse_expr(0)?;
1704        self.expect(TokenKind::Colon)?;
1705        let body = self.parse_branch_body(start)?;
1706        branches.push(CondBranch {
1707            span: start.merge(body.span()),
1708            condition,
1709            body,
1710        });
1711
1712        // else if / else
1713        let mut else_body = None;
1714        while self.at(TokenKind::Else) {
1715            let else_tok = self.advance();
1716            if self.at(TokenKind::If) {
1717                let if_start = self.advance().span;
1718                let cond = self.parse_expr(0)?;
1719                self.expect(TokenKind::Colon)?;
1720                let body = self.parse_branch_body(else_tok.span)?;
1721                branches.push(CondBranch {
1722                    span: if_start.merge(body.span()),
1723                    condition: cond,
1724                    body,
1725                });
1726            } else {
1727                self.expect(TokenKind::Colon)?;
1728                let body = self.parse_branch_body(else_tok.span)?;
1729                else_body = Some(Box::new(body));
1730                break;
1731            }
1732        }
1733
1734        let end = else_body
1735            .as_ref()
1736            .map(|b| b.span())
1737            .or_else(|| branches.last().map(|b| b.body.span()))
1738            .unwrap_or(start);
1739
1740        Some(Expr::Conditional {
1741            span: start.merge(end),
1742            branches,
1743            else_body,
1744        })
1745    }
1746
1747    fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
1748        let keyword_line = self.line_of(keyword_span);
1749        let next_line = self.line_of(self.peek().span);
1750
1751        if next_line > keyword_line {
1752            let base_col = self.col_of(self.peek().span);
1753            self.parse_indented_block(base_col)
1754        } else {
1755            self.parse_expr(0)
1756        }
1757    }
1758
1759    // -- for expression -------------------------------------------------
1760
1761    fn parse_for_expr(&mut self) -> Option<Expr> {
1762        let start = self.advance().span; // consume `for`
1763        let binding = self.parse_for_binding()?;
1764        self.expect(TokenKind::In)?;
1765
1766        // Parse collection, stopping before `where` and `:`
1767        let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1768
1769        let filter =
1770            if self.eat(TokenKind::Where).is_some() || self.eat(TokenKind::With).is_some() {
1771                // Parse filter at min_bp 0 — colon terminates naturally.
1772                Some(Box::new(self.parse_expr(0)?))
1773            } else {
1774                None
1775            };
1776
1777        self.expect(TokenKind::Colon)?;
1778        let body = self.parse_branch_body(start)?;
1779
1780        Some(Expr::For {
1781            span: start.merge(body.span()),
1782            binding,
1783            collection: Box::new(collection),
1784            filter,
1785            body: Box::new(body),
1786        })
1787    }
1788
1789    // -- brace expressions: set literal or object literal ---------------
1790
1791    fn parse_brace_expr(&mut self) -> Option<Expr> {
1792        let start = self.advance().span; // consume {
1793
1794        if self.at(TokenKind::RBrace) {
1795            let end = self.advance().span;
1796            return Some(Expr::SetLiteral {
1797                span: start.merge(end),
1798                elements: Vec::new(),
1799            });
1800        }
1801
1802        // Peek: if first item is `ident:`, it's an object literal
1803        if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1804            return self.parse_object_literal(start);
1805        }
1806
1807        // Otherwise set literal
1808        self.parse_set_literal(start)
1809    }
1810
1811    fn parse_list_literal(&mut self) -> Option<Expr> {
1812        let start = self.advance().span; // consume [
1813        let mut elements = Vec::new();
1814        while !self.at(TokenKind::RBracket) && !self.at_eof() {
1815            elements.push(self.parse_expr(0)?);
1816            self.eat(TokenKind::Comma);
1817        }
1818        let end = self.expect(TokenKind::RBracket)?.span;
1819        Some(Expr::ListLiteral {
1820            span: start.merge(end),
1821            elements,
1822        })
1823    }
1824
1825    fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
1826        let mut fields = Vec::new();
1827        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1828            let name = self.parse_ident_in("field name")?;
1829            self.expect(TokenKind::Colon)?;
1830            let value = self.parse_expr(0)?;
1831            fields.push(NamedArg {
1832                span: name.span.merge(value.span()),
1833                name,
1834                value,
1835            });
1836            self.eat(TokenKind::Comma);
1837        }
1838        let end = self.expect(TokenKind::RBrace)?.span;
1839        Some(Expr::ObjectLiteral {
1840            span: start.merge(end),
1841            fields,
1842        })
1843    }
1844
1845    fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
1846        let mut elements = Vec::new();
1847        while !self.at(TokenKind::RBrace) && !self.at_eof() {
1848            elements.push(self.parse_expr(0)?);
1849            self.eat(TokenKind::Comma);
1850        }
1851        let end = self.expect(TokenKind::RBrace)?.span;
1852        Some(Expr::SetLiteral {
1853            span: start.merge(end),
1854            elements,
1855        })
1856    }
1857
1858    // -- parenthesised expression ---------------------------------------
1859
1860    fn parse_paren_expr(&mut self) -> Option<Expr> {
1861        self.advance(); // (
1862        let expr = self.parse_expr(0)?;
1863        self.expect(TokenKind::RParen)?;
1864        Some(expr)
1865    }
1866}
1867
1868// ---------------------------------------------------------------------------
1869// Tests
1870// ---------------------------------------------------------------------------
1871
1872#[cfg(test)]
1873mod tests {
1874    use super::*;
1875    use crate::diagnostic::Severity;
1876
1877    fn parse_ok(src: &str) -> ParseResult {
1878        // Prefix with version marker if not already present, to avoid
1879        // spurious "missing version marker" warnings in every test.
1880        let owned;
1881        let input = if src.starts_with("-- allium:") {
1882            src
1883        } else {
1884            owned = format!("-- allium: 1\n{src}");
1885            &owned
1886        };
1887        let result = parse(input);
1888        if !result.diagnostics.is_empty() {
1889            for d in &result.diagnostics {
1890                eprintln!(
1891                    "  [{:?}] {} ({}..{})",
1892                    d.severity, d.message, d.span.start, d.span.end
1893                );
1894            }
1895        }
1896        result
1897    }
1898
1899    #[test]
1900    fn version_marker() {
1901        let r = parse_ok("-- allium: 1\n");
1902        assert_eq!(r.module.version, Some(1));
1903        assert_eq!(r.diagnostics.len(), 0);
1904    }
1905
1906    #[test]
1907    fn version_missing_warns() {
1908        let r = parse("entity User {}");
1909        assert_eq!(r.module.version, None);
1910        assert_eq!(r.diagnostics.len(), 1);
1911        assert_eq!(r.diagnostics[0].severity, Severity::Warning);
1912        assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
1913    }
1914
1915    #[test]
1916    fn version_unsupported_errors() {
1917        let r = parse("-- allium: 99\nentity User {}");
1918        assert_eq!(r.module.version, Some(99));
1919        assert!(r.diagnostics.iter().any(|d|
1920            d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
1921        ), "expected unsupported version error, got: {:?}", r.diagnostics);
1922    }
1923
1924    #[test]
1925    fn empty_entity() {
1926        let r = parse_ok("entity User {}");
1927        assert_eq!(r.diagnostics.len(), 0);
1928        assert_eq!(r.module.declarations.len(), 1);
1929        match &r.module.declarations[0] {
1930            Decl::Block(b) => {
1931                assert_eq!(b.kind, BlockKind::Entity);
1932                assert_eq!(b.name.as_ref().unwrap().name, "User");
1933            }
1934            other => panic!("expected Block, got {other:?}"),
1935        }
1936    }
1937
1938    #[test]
1939    fn entity_with_fields() {
1940        let src = r#"entity Order {
1941    customer: Customer
1942    status: pending | active | completed
1943    total: Decimal
1944}"#;
1945        let r = parse_ok(src);
1946        assert_eq!(r.diagnostics.len(), 0);
1947        match &r.module.declarations[0] {
1948            Decl::Block(b) => {
1949                assert_eq!(b.items.len(), 3);
1950            }
1951            other => panic!("expected Block, got {other:?}"),
1952        }
1953    }
1954
1955    #[test]
1956    fn use_declaration() {
1957        let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
1958        assert_eq!(r.diagnostics.len(), 0);
1959        match &r.module.declarations[0] {
1960            Decl::Use(u) => {
1961                assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
1962            }
1963            other => panic!("expected Use, got {other:?}"),
1964        }
1965    }
1966
1967    #[test]
1968    fn enum_declaration() {
1969        let src = "enum OrderStatus { pending | shipped | delivered }";
1970        let r = parse_ok(src);
1971        assert_eq!(r.diagnostics.len(), 0);
1972    }
1973
1974    #[test]
1975    fn config_block() {
1976        let src = r#"config {
1977    max_retries: Integer = 3
1978    timeout: Duration = 24.hours
1979}"#;
1980        // Config entries are `name: Type = default`. The parser sees
1981        // `name: Type = default` as an assignment where the value is
1982        // `Type = default` (comparison with Eq). That's fine for the
1983        // parse tree — semantic pass separates type from default.
1984        let r = parse_ok(src);
1985        assert_eq!(r.diagnostics.len(), 0);
1986    }
1987
1988    #[test]
1989    fn rule_declaration() {
1990        let src = r#"rule PlaceOrder {
1991    when: CustomerPlacesOrder(customer, items, total)
1992    requires: total > 0
1993    ensures: Order.created(customer: customer, status: pending, total: total)
1994}"#;
1995        let r = parse_ok(src);
1996        assert_eq!(r.diagnostics.len(), 0);
1997        match &r.module.declarations[0] {
1998            Decl::Block(b) => {
1999                assert_eq!(b.kind, BlockKind::Rule);
2000                assert_eq!(b.items.len(), 3);
2001            }
2002            other => panic!("expected Block, got {other:?}"),
2003        }
2004    }
2005
2006    #[test]
2007    fn expression_precedence() {
2008        let r = parse_ok("rule T { v: a + b * c }");
2009        // The value should be Add(a, Mul(b, c))
2010        match &r.module.declarations[0] {
2011            Decl::Block(b) => match &b.items[0].kind {
2012                BlockItemKind::Assignment { value, .. } => match value {
2013                    Expr::BinaryOp { op, right, .. } => {
2014                        assert_eq!(*op, BinaryOp::Add);
2015                        assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2016                    }
2017                    other => panic!("expected BinaryOp, got {other:?}"),
2018                },
2019                other => panic!("expected Assignment, got {other:?}"),
2020            },
2021            other => panic!("expected Block, got {other:?}"),
2022        }
2023    }
2024
2025    #[test]
2026    fn default_declaration() {
2027        let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2028        let r = parse_ok(src);
2029        assert_eq!(r.diagnostics.len(), 0);
2030    }
2031
2032    #[test]
2033    fn open_question() {
2034        let src = r#"open question "Should admins be role-specific?""#;
2035        let r = parse_ok(src);
2036        assert_eq!(r.diagnostics.len(), 0);
2037    }
2038
2039    #[test]
2040    fn external_entity() {
2041        let src = "external entity Customer { email: String }";
2042        let r = parse_ok(src);
2043        assert_eq!(r.diagnostics.len(), 0);
2044        match &r.module.declarations[0] {
2045            Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2046            other => panic!("expected Block, got {other:?}"),
2047        }
2048    }
2049
2050    #[test]
2051    fn where_expression() {
2052        let src = "entity E { active: items where status = active }";
2053        let r = parse_ok(src);
2054        assert_eq!(r.diagnostics.len(), 0);
2055    }
2056
2057    #[test]
2058    fn with_expression() {
2059        let src = "entity E { slots: InterviewSlot with candidacy = this }";
2060        let r = parse_ok(src);
2061        assert_eq!(r.diagnostics.len(), 0);
2062    }
2063
2064    #[test]
2065    fn lambda_expression() {
2066        let src = "entity E { v: items.any(i => i.active) }";
2067        let r = parse_ok(src);
2068        assert_eq!(r.diagnostics.len(), 0);
2069    }
2070
2071    #[test]
2072    fn deferred() {
2073        let src = "deferred InterviewerMatching.suggest";
2074        let r = parse_ok(src);
2075        assert_eq!(r.diagnostics.len(), 0);
2076    }
2077
2078    #[test]
2079    fn variant_declaration() {
2080        let src = "variant Email : Notification { subject: String }";
2081        let r = parse_ok(src);
2082        assert_eq!(r.diagnostics.len(), 0);
2083    }
2084
2085    // -- projection mapping -----------------------------------------------
2086
2087    #[test]
2088    fn projection_arrow() {
2089        let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2090        let r = parse_ok(src);
2091        assert_eq!(r.diagnostics.len(), 0);
2092    }
2093
2094    // -- transitions_to / becomes ------------------------------------------
2095
2096    #[test]
2097    fn transitions_to_trigger() {
2098        let src = "rule R { when: Interview.status transitions_to scheduled\n    ensures: Notification.created() }";
2099        let r = parse_ok(src);
2100        assert_eq!(r.diagnostics.len(), 0);
2101    }
2102
2103    #[test]
2104    fn becomes_trigger() {
2105        let src = "rule R { when: Interview.status becomes scheduled\n    ensures: Notification.created() }";
2106        let r = parse_ok(src);
2107        assert_eq!(r.diagnostics.len(), 0);
2108    }
2109
2110    // -- binding colon in clause values ------------------------------------
2111
2112    #[test]
2113    fn when_binding() {
2114        let src = "rule R {\n    when: interview: Interview.status transitions_to scheduled\n    ensures: Notification.created()\n}";
2115        let r = parse_ok(src);
2116        assert_eq!(r.diagnostics.len(), 0);
2117        // The when clause value should be a Binding wrapping a TransitionsTo
2118        let decl = &r.module.declarations[0];
2119        if let Decl::Block(b) = decl {
2120            if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2121                assert_eq!(keyword, "when");
2122                assert!(matches!(value, Expr::Binding { .. }));
2123            } else {
2124                panic!("expected clause");
2125            }
2126        } else {
2127            panic!("expected block decl");
2128        }
2129    }
2130
2131    #[test]
2132    fn when_binding_temporal() {
2133        let src = "rule R {\n    when: invitation: Invitation.expires_at <= now\n    ensures: Invitation.expired()\n}";
2134        let r = parse_ok(src);
2135        assert_eq!(r.diagnostics.len(), 0);
2136    }
2137
2138    #[test]
2139    fn when_binding_created() {
2140        let src = "rule R {\n    when: batch: DigestBatch.created\n    ensures: Email.created()\n}";
2141        let r = parse_ok(src);
2142        assert_eq!(r.diagnostics.len(), 0);
2143    }
2144
2145    #[test]
2146    fn facing_binding() {
2147        let src = "surface S {\n    facing viewer: Interviewer\n    exposes: InterviewList\n}";
2148        let r = parse_ok(src);
2149        assert_eq!(r.diagnostics.len(), 0);
2150    }
2151
2152    #[test]
2153    fn context_binding() {
2154        let src = "surface S {\n    facing viewer: Interviewer\n    context assignment: SlotConfirmation where interviewer = viewer\n}";
2155        let r = parse_ok(src);
2156        assert_eq!(r.diagnostics.len(), 0);
2157    }
2158
2159    // -- rule-level for block item -----------------------------------------
2160
2161    #[test]
2162    fn rule_level_for() {
2163        let src = r#"rule ProcessDigests {
2164    when: schedule: DigestSchedule.next_run_at <= now
2165    for user in Users where notification_setting.digest_enabled:
2166        ensures: DigestBatch.created(user: user)
2167}"#;
2168        let r = parse_ok(src);
2169        assert_eq!(r.diagnostics.len(), 0);
2170        if let Decl::Block(b) = &r.module.declarations[0] {
2171            // Should have when clause + for block item
2172            assert!(b.items.len() >= 2);
2173            assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2174        } else {
2175            panic!("expected block decl");
2176        }
2177    }
2178
2179    // -- let inside ensures blocks -----------------------------------------
2180
2181    #[test]
2182    fn let_in_ensures_block() {
2183        let src = r#"rule R {
2184    when: ScheduleInterview(candidacy, time, interviewers)
2185    ensures:
2186        let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2187        for interviewer in interviewers:
2188            SlotConfirmation.created(slot: slot, interviewer: interviewer)
2189}"#;
2190        let r = parse_ok(src);
2191        assert_eq!(r.diagnostics.len(), 0);
2192    }
2193
2194    // -- when guard on provides items --------------------------------------
2195
2196    #[test]
2197    fn provides_when_guard() {
2198        let src = "surface S {\n    facing viewer: Interviewer\n    provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2199        let r = parse_ok(src);
2200        assert_eq!(r.diagnostics.len(), 0);
2201    }
2202
2203    // -- optional type suffix ----------------------------------------------
2204
2205    #[test]
2206    fn optional_type_suffix() {
2207        let src = "entity E { locked_until: Timestamp? }";
2208        let r = parse_ok(src);
2209        assert_eq!(r.diagnostics.len(), 0);
2210    }
2211
2212    #[test]
2213    fn optional_trigger_param() {
2214        let src = "rule R { when: Report(interviewer, interview, reason, details?)\n    ensures: Done() }";
2215        let r = parse_ok(src);
2216        assert_eq!(r.diagnostics.len(), 0);
2217    }
2218
2219    // -- qualified name with config ----------------------------------------
2220
2221    #[test]
2222    fn qualified_config_access() {
2223        let src = "entity E { duration: oauth/config.session_duration }";
2224        let r = parse_ok(src);
2225        assert_eq!(r.diagnostics.len(), 0);
2226    }
2227
2228    // -- comprehensive integration test ------------------------------------
2229
2230    #[test]
2231    fn realistic_spec() {
2232        let src = r#"-- allium: 1
2233
2234enum OrderStatus { pending | shipped | delivered }
2235
2236external entity Customer {
2237    email: String
2238    name: String
2239}
2240
2241entity Order {
2242    customer: Customer
2243    status: OrderStatus
2244    total: Decimal
2245    items: OrderItem with order = this
2246    shipped_items: items where status = shipped
2247    confirmed_items: items where status = confirmed -> item
2248    is_complete: status = delivered
2249    locked_until: Timestamp?
2250}
2251
2252config {
2253    max_retries: Integer = 3
2254    timeout: Duration = 24.hours
2255}
2256
2257rule PlaceOrder {
2258    when: CustomerPlacesOrder(customer, items, total)
2259    requires: total > 0
2260    ensures: Order.created(customer: customer, status: pending, total: total)
2261}
2262
2263rule ShipOrder {
2264    when: order: Order.status transitions_to shipped
2265    ensures: Email.created(to: order.customer.email, template: order_shipped)
2266}
2267
2268open question "How do we handle partial shipments?"
2269"#;
2270        let r = parse_ok(src);
2271        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2272        assert_eq!(r.module.version, Some(1));
2273        assert_eq!(r.module.declarations.len(), 7);
2274    }
2275
2276    #[test]
2277    fn extension_behaviour_excerpt() {
2278        // Exercises: inline enums, generic types, or-triggers, named call
2279        // args, config with typed defaults, module declaration.
2280        let src = r#"value Document {
2281    uri: String
2282    text: String
2283}
2284
2285entity Finding {
2286    code: String
2287    severity: error | warning | info
2288    range: FindingRange
2289}
2290
2291entity DiagnosticsMode {
2292    value: strict | relaxed
2293}
2294
2295config {
2296    duplicateKey: String = "allium.config.duplicateKey"
2297}
2298
2299rule RefreshDiagnostics {
2300    when: DocumentOpened(document) or DocumentChanged(document)
2301    requires: document.language_id = "allium"
2302    ensures: FindingsComputed(document)
2303}
2304
2305surface DiagnosticsDashboard {
2306    facing viewer: Developer
2307    context doc: Document where viewer.active_document = doc
2308    provides: RunChecks(viewer) when doc.language_id = "allium"
2309    exposes: FindingList
2310}
2311
2312rule ProcessDigests {
2313    when: schedule: DigestSchedule.next_run_at <= now
2314    for user in Users where notification_setting.digest_enabled:
2315        let settings = user.notification_setting
2316        ensures: DigestBatch.created(user: user)
2317}
2318"#;
2319        let r = parse_ok(src);
2320        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2321        // value + entity + entity + config + rule + surface + rule = 7
2322        assert_eq!(r.module.declarations.len(), 7);
2323    }
2324
2325    #[test]
2326    fn exists_as_identifier() {
2327        let src = r#"rule R {
2328    when: X()
2329    ensures: CompletionItemAvailable(label: exists)
2330}"#;
2331        let r = parse_ok(src);
2332        assert_eq!(r.diagnostics.len(), 0);
2333    }
2334
2335    // -- pipe precedence: tighter than boolean ops ----------------------------
2336
2337    #[test]
2338    fn pipe_binds_tighter_than_or() {
2339        // `a or b | c` should parse as `a or (b | c)`, not `(a or b) | c`
2340        let src = "entity E { v: a or b | c }";
2341        let r = parse_ok(src);
2342        assert_eq!(r.diagnostics.len(), 0);
2343        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2344        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2345        // Top-level should be LogicalOp(Or)
2346        let Expr::LogicalOp { op, right, .. } = value else {
2347            panic!("expected LogicalOp, got {value:?}");
2348        };
2349        assert_eq!(*op, LogicalOp::Or);
2350        // Right side should be Pipe(b, c)
2351        assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2352    }
2353
2354    // -- variant with expression base -----------------------------------------
2355
2356    #[test]
2357    fn variant_with_pipe_base() {
2358        let src = "variant Mixed : TypeA | TypeB";
2359        let r = parse_ok(src);
2360        assert_eq!(r.diagnostics.len(), 0);
2361        let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2362        assert!(matches!(v.base, Expr::Pipe { .. }));
2363    }
2364
2365    // -- for-block with comparison in where filter ----------------------------
2366
2367    #[test]
2368    fn for_block_where_comparison() {
2369        let src = r#"rule R {
2370    when: X()
2371    for item in Items where item.status = active:
2372        ensures: Processed(item: item)
2373}"#;
2374        let r = parse_ok(src);
2375        assert_eq!(r.diagnostics.len(), 0);
2376        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2377        let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2378        assert!(filter.is_some());
2379        assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2380    }
2381
2382    // -- for-expression with comparison in where filter -----------------------
2383
2384    #[test]
2385    fn for_expr_where_comparison() {
2386        let src = r#"rule R {
2387    when: X()
2388    ensures:
2389        for item in Items where item.active = true:
2390            Processed(item: item)
2391}"#;
2392        let r = parse_ok(src);
2393        assert_eq!(r.diagnostics.len(), 0);
2394    }
2395
2396    // -- if/else if/else chain ------------------------------------------------
2397
2398    #[test]
2399    fn if_else_if_else() {
2400        let src = r#"rule R {
2401    when: X(v)
2402    ensures:
2403        if v < 10: Small()
2404        else if v < 100: Medium()
2405        else: Large()
2406}"#;
2407        let r = parse_ok(src);
2408        assert_eq!(r.diagnostics.len(), 0);
2409    }
2410
2411    // -- null coalescing and optional chaining --------------------------------
2412
2413    #[test]
2414    fn null_coalesce_and_optional_chain() {
2415        let src = "entity E { v: a?.b ?? fallback }";
2416        let r = parse_ok(src);
2417        assert_eq!(r.diagnostics.len(), 0);
2418        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2419        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2420        // Top-level should be NullCoalesce
2421        assert!(matches!(value, Expr::NullCoalesce { .. }));
2422    }
2423
2424    // -- generic types --------------------------------------------------------
2425
2426    #[test]
2427    fn generic_type_nested() {
2428        let src = "entity E { v: List<Set<String>> }";
2429        let r = parse_ok(src);
2430        assert_eq!(r.diagnostics.len(), 0);
2431    }
2432
2433    // -- set literal, list literal, object literal ----------------------------
2434
2435    #[test]
2436    fn collection_literals() {
2437        let src = r#"rule R {
2438    when: X()
2439    ensures:
2440        let s = {a, b, c}
2441        let l = [1, 2, 3]
2442        let o = {name: "test", count: 42}
2443        Done()
2444}"#;
2445        let r = parse_ok(src);
2446        assert_eq!(r.diagnostics.len(), 0);
2447    }
2448
2449    // -- given block ----------------------------------------------------------
2450
2451    #[test]
2452    fn given_block() {
2453        let src = "given { viewer: User\n    time: Timestamp }";
2454        let r = parse_ok(src);
2455        assert_eq!(r.diagnostics.len(), 0);
2456        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2457        assert_eq!(b.kind, BlockKind::Given);
2458        assert!(b.name.is_none());
2459    }
2460
2461    // -- actor block ----------------------------------------------------------
2462
2463    #[test]
2464    fn actor_block() {
2465        let src = "actor Admin { identified_by: User where role = admin }";
2466        let r = parse_ok(src);
2467        assert_eq!(r.diagnostics.len(), 0);
2468        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2469        assert_eq!(b.kind, BlockKind::Actor);
2470    }
2471
2472    // -- join lookup ----------------------------------------------------------
2473
2474    #[test]
2475    fn join_lookup() {
2476        let src = "entity E { match: Other{field_a, field_b: value} }";
2477        let r = parse_ok(src);
2478        assert_eq!(r.diagnostics.len(), 0);
2479        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2480        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2481        assert!(matches!(value, Expr::JoinLookup { .. }));
2482    }
2483
2484    // -- in / not in with set literal -----------------------------------------
2485
2486    #[test]
2487    fn in_not_in_set() {
2488        let src = r#"rule R {
2489    when: X(s)
2490    requires: s in {a, b, c}
2491    requires: s not in {d, e}
2492    ensures: Done()
2493}"#;
2494        let r = parse_ok(src);
2495        assert_eq!(r.diagnostics.len(), 0);
2496    }
2497
2498    // -- comprehensive fixture file -------------------------------------------
2499
2500    #[test]
2501    fn comprehensive_fixture() {
2502        let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
2503        let r = parse(src);
2504        assert_eq!(
2505            r.diagnostics.len(),
2506            0,
2507            "expected no errors in comprehensive fixture, got: {:?}",
2508            r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
2509        );
2510        assert!(r.module.declarations.len() > 30, "expected many declarations");
2511    }
2512
2513    // -- error message quality ------------------------------------------------
2514
2515    #[test]
2516    fn error_expected_declaration() {
2517        let r = parse("-- allium: 1\n+ invalid");
2518        assert!(r.diagnostics.len() >= 1);
2519        let msg = &r.diagnostics[0].message;
2520        assert!(msg.contains("expected declaration"), "got: {msg}");
2521        assert!(msg.contains("entity"), "should list valid options, got: {msg}");
2522        assert!(msg.contains("rule"), "should list valid options, got: {msg}");
2523    }
2524
2525    #[test]
2526    fn error_expected_expression() {
2527        let r = parse("-- allium: 1\nentity E { v: }");
2528        assert!(r.diagnostics.len() >= 1);
2529        let msg = &r.diagnostics[0].message;
2530        assert!(msg.contains("expected expression"), "got: {msg}");
2531        assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
2532    }
2533
2534    #[test]
2535    fn error_expected_block_item() {
2536        let r = parse("-- allium: 1\nentity E { + }");
2537        assert!(r.diagnostics.len() >= 1);
2538        let msg = &r.diagnostics[0].message;
2539        assert!(msg.contains("expected block item"), "got: {msg}");
2540    }
2541
2542    #[test]
2543    fn error_expected_identifier() {
2544        let r = parse("-- allium: 1\nentity 123 {}");
2545        assert!(r.diagnostics.len() >= 1);
2546        let msg = &r.diagnostics[0].message;
2547        // Context-aware: says "entity name" not generic "identifier"
2548        assert!(msg.contains("expected entity name"), "got: {msg}");
2549        // Human-friendly: says "number" not "Number" or "TokenKind::Number"
2550        assert!(msg.contains("number"), "should say what was found, got: {msg}");
2551    }
2552
2553    #[test]
2554    fn error_missing_brace() {
2555        let r = parse("entity E {");
2556        assert!(r.diagnostics.len() >= 1);
2557        let msg = &r.diagnostics[0].message;
2558        assert!(msg.contains("expected"), "got: {msg}");
2559    }
2560
2561    #[test]
2562    fn error_recovery_multiple() {
2563        // Parser should recover and report multiple errors (on separate lines)
2564        let r = parse("entity E { + }\nentity F { - }");
2565        assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
2566    }
2567
2568    #[test]
2569    fn error_dedup_same_line() {
2570        // Multiple bad tokens on a single line should produce only one error
2571        let r = parse("-- allium: 1\n+ - * /");
2572        let errors: Vec<_> = r.diagnostics.iter()
2573            .filter(|d| d.severity == crate::diagnostic::Severity::Error)
2574            .collect();
2575        assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
2576    }
2577
2578    #[test]
2579    fn for_block() {
2580        let src = r#"rule R {
2581    when: X()
2582    for user in Users where user.active:
2583        ensures: Notified(user: user)
2584}"#;
2585        let r = parse_ok(src);
2586        assert_eq!(r.diagnostics.len(), 0);
2587        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2588        assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2589    }
2590
2591    #[test]
2592    fn for_expr() {
2593        let src = r#"rule R {
2594    when: X(project)
2595    ensures:
2596        let total = for task in project.tasks: task.effort
2597        Done(total: total)
2598}"#;
2599        let r = parse_ok(src);
2600        assert_eq!(r.diagnostics.len(), 0);
2601    }
2602
2603    #[test]
2604    fn for_where() {
2605        let src = r#"rule R {
2606    when: X()
2607    for item in Items where item.active:
2608        ensures: Processed(item: item)
2609}"#;
2610        let r = parse_ok(src);
2611        assert_eq!(r.diagnostics.len(), 0);
2612    }
2613
2614    #[test]
2615    fn for_with() {
2616        let src = r#"rule R {
2617    when: X()
2618    for slot in Slot with slot.role = reviewer:
2619        ensures: Reviewed(slot: slot)
2620}"#;
2621        let r = parse_ok(src);
2622        assert_eq!(r.diagnostics.len(), 0);
2623        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2624        let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2625        assert!(filter.is_some());
2626    }
2627
2628    #[test]
2629    fn block_level_if() {
2630        let src = r#"rule R {
2631    when: X(task)
2632    if task.priority = high:
2633        ensures: Escalated(task: task)
2634}"#;
2635        let r = parse_ok(src);
2636        assert_eq!(r.diagnostics.len(), 0);
2637        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2638        let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2639            panic!("expected IfBlock, got {:?}", b.items[1].kind);
2640        };
2641        assert_eq!(branches.len(), 1);
2642        assert!(else_items.is_none());
2643    }
2644
2645    #[test]
2646    fn block_level_if_else() {
2647        let src = r#"rule R {
2648    when: X(score)
2649    if score > 80:
2650        ensures: High()
2651    else if score > 40:
2652        ensures: Medium()
2653    else:
2654        ensures: Low()
2655}"#;
2656        let r = parse_ok(src);
2657        assert_eq!(r.diagnostics.len(), 0);
2658        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2659        let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2660            panic!("expected IfBlock, got {:?}", b.items[1].kind);
2661        };
2662        assert_eq!(branches.len(), 2);
2663        assert!(else_items.is_some());
2664    }
2665
2666    #[test]
2667    fn wildcard_type_parameter() {
2668        let src = "entity E { codec: Codec<*> }";
2669        let r = parse_ok(src);
2670        assert_eq!(r.diagnostics.len(), 0);
2671        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2672        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2673        if let Expr::GenericType { args, .. } = value {
2674            assert_eq!(args.len(), 1);
2675            if let Expr::Ident(id) = &args[0] {
2676                assert_eq!(id.name, "*");
2677            } else {
2678                panic!("expected wildcard ident, got {:?}", args[0]);
2679            }
2680        } else {
2681            panic!("expected GenericType, got {:?}", value);
2682        }
2683    }
2684
2685    #[test]
2686    fn guidance_clause_comment_only_value() {
2687        let src = r#"rule R {
2688    guidance: -- just a comment
2689    ensures: Done()
2690}"#;
2691        let r = parse_ok(src);
2692        assert_eq!(r.diagnostics.len(), 0);
2693        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2694        assert_eq!(b.items.len(), 2);
2695        // guidance clause should have an empty block value
2696        let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
2697        assert_eq!(keyword, "guidance");
2698        assert!(matches!(value, Expr::Block { items, .. } if items.is_empty()));
2699    }
2700
2701    #[test]
2702    fn for_expr_with_filter() {
2703        let src = r#"rule R {
2704    when: X(project)
2705    ensures:
2706        let total = for task in project.tasks with task.active: task.effort
2707        Done(total: total)
2708}"#;
2709        let r = parse_ok(src);
2710        assert_eq!(r.diagnostics.len(), 0);
2711    }
2712
2713    #[test]
2714    fn for_destructured_binding() {
2715        let src = r#"rule R {
2716    when: X()
2717    for (key, value) in Pairs where key != null:
2718        ensures: Processed(key: key, value: value)
2719}"#;
2720        let r = parse_ok(src);
2721        assert_eq!(r.diagnostics.len(), 0);
2722        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2723        let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
2724        assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
2725    }
2726
2727    #[test]
2728    fn dot_path_assignment() {
2729        let src = r#"entity Shard {
2730    ShardGroup.shard_cache: Shard with group = this
2731}"#;
2732        let r = parse_ok(src);
2733        assert_eq!(r.diagnostics.len(), 0);
2734        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2735        let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
2736            panic!("expected PathAssignment, got {:?}", b.items[0].kind);
2737        };
2738        assert!(matches!(path, Expr::MemberAccess { .. }));
2739    }
2740
2741    #[test]
2742    fn language_reference_fixture() {
2743        let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
2744        let r = parse(src);
2745        let errors: Vec<_> = r.diagnostics.iter()
2746            .filter(|d| d.severity == Severity::Error)
2747            .collect();
2748        assert_eq!(
2749            errors.len(),
2750            0,
2751            "expected no errors in language-reference fixture, got: {:?}",
2752            errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
2753        );
2754    }
2755
2756    // =====================================================================
2757    // V1 SPEC CONFORMANCE TESTS
2758    //
2759    // These tests verify that the parser conforms to the Allium V1 language
2760    // reference (docs/allium-v1-language-reference.md). Each test is tagged
2761    // with the finding number from the audit.
2762    //
2763    // Tests marked "should reject" are expected to FAIL until the parser
2764    // is updated to reject non-spec constructs.
2765    //
2766    // Tests marked "should parse" are expected to FAIL until the parser
2767    // is updated to handle spec-defined constructs.
2768    // =====================================================================
2769
2770    // -- Finding 1: spec uses `for`, not `for each` ---------------------------
2771
2772    #[test]
2773    fn spec_for_bare_form() {
2774        // The spec uses bare `for` exclusively. This must parse cleanly.
2775        let src = r#"rule ProcessDigests {
2776    when: schedule: DigestSchedule.next_run_at <= now
2777    for user in Users where notification_setting.digest_enabled:
2778        let settings = user.notification_setting
2779        ensures: DigestBatch.created(user: user)
2780}"#;
2781        let r = parse_ok(src);
2782        assert_eq!(r.diagnostics.len(), 0);
2783    }
2784
2785    #[test]
2786    fn spec_reject_for_each() {
2787        // `for each` is not in the spec. The parser should reject it.
2788        let src = r#"rule R {
2789    when: X()
2790    for each user in Users where user.active:
2791        ensures: Notified(user: user)
2792}"#;
2793        let r = parse_ok(src);
2794        assert!(
2795            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2796            "expected error for `for each` (not in spec), but parsed without errors"
2797        );
2798    }
2799
2800    // -- Finding 2: spec uses `=`, not `==` -----------------------------------
2801
2802    #[test]
2803    fn spec_reject_double_equals() {
2804        // The spec uses `=` for equality. `==` should not be accepted.
2805        let src = "rule R { when: X(a)\n    requires: a.status == active\n    ensures: Done() }";
2806        let r = parse_ok(src);
2807        assert!(
2808            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2809            "expected error for `==` (not in spec), but parsed without errors"
2810        );
2811    }
2812
2813    // -- Finding 3: `system` blocks are not in the spec -----------------------
2814
2815    #[test]
2816    fn spec_reject_system_block() {
2817        // `system` is not a declaration type in the V1 spec.
2818        let src = "system PaymentGateway {\n    timeout: 30.seconds\n}";
2819        let r = parse_ok(src);
2820        assert!(
2821            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2822            "expected error for `system` block (not in spec), but parsed without errors"
2823        );
2824    }
2825
2826    // -- Finding 6: `tags` clause is not in the spec --------------------------
2827
2828    #[test]
2829    fn spec_reject_tags_clause() {
2830        // `tags:` is not a clause keyword in the V1 spec.
2831        let src = r#"rule R {
2832    when: MigrationTriggered()
2833    tags: infrastructure, migration
2834    ensures: MigrationComplete()
2835}"#;
2836        let r = parse_ok(src);
2837        assert!(
2838            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2839            "expected error for `tags:` clause (not in spec), but parsed without errors"
2840        );
2841    }
2842
2843    // -- Finding 7: `includes`/`excludes` are not in the spec -----------------
2844
2845    #[test]
2846    fn spec_reject_includes_operator() {
2847        // The spec uses `x in collection`, not `collection includes x`.
2848        let src = r#"rule R {
2849    when: X(a, b)
2850    requires: a.items includes b
2851    ensures: Done()
2852}"#;
2853        let r = parse_ok(src);
2854        assert!(
2855            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2856            "expected error for `includes` operator (not in spec), but parsed without errors"
2857        );
2858    }
2859
2860    #[test]
2861    fn spec_reject_excludes_operator() {
2862        // The spec uses `x not in collection`, not `collection excludes x`.
2863        let src = r#"rule R {
2864    when: X(a, b)
2865    requires: a.items excludes b
2866    ensures: Done()
2867}"#;
2868        let r = parse_ok(src);
2869        assert!(
2870            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2871            "expected error for `excludes` operator (not in spec), but parsed without errors"
2872        );
2873    }
2874
2875    // -- Finding 8: range literals (`..`) are not in the spec -----------------
2876
2877    #[test]
2878    fn spec_reject_range_literal() {
2879        // The `..` range operator is not defined in the V1 spec.
2880        let src = r#"rule R {
2881    when: X(v)
2882    requires: v in [1..100]
2883    ensures: Done()
2884}"#;
2885        let r = parse_ok(src);
2886        assert!(
2887            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2888            "expected error for `..` range (not in spec), but parsed without errors"
2889        );
2890    }
2891
2892    // -- Finding 9: `within` is only for actors, not rules --------------------
2893
2894    #[test]
2895    fn spec_within_in_actor() {
2896        // The spec defines `within:` as an actor clause.
2897        let src = r#"actor WorkspaceAdmin {
2898    within: Workspace
2899    identified_by: User where role = admin
2900}"#;
2901        let r = parse_ok(src);
2902        assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
2903    }
2904
2905    // -- Finding 10: `module` declaration is not in the spec ------------------
2906
2907    #[test]
2908    fn spec_reject_module_declaration() {
2909        // `module Name` is not a declaration in the V1 spec.
2910        let src = "module my_spec";
2911        let r = parse_ok(src);
2912        assert!(
2913            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2914            "expected error for `module` declaration (not in spec), but parsed without errors"
2915        );
2916    }
2917
2918    // -- Finding 11: `guidance` at module level is not in the spec ------------
2919
2920    #[test]
2921    fn spec_reject_module_level_guidance() {
2922        // The spec shows `guidance:` only as a surface clause.
2923        let src = r#"guidance: "All rules must be idempotent""#;
2924        let r = parse_ok(src);
2925        assert!(
2926            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2927            "expected error for module-level `guidance:` (not in spec), but parsed without errors"
2928        );
2929    }
2930
2931    // -- Finding 12: `guarantee`/`timeout` in rules is not in the spec --------
2932
2933    #[test]
2934    fn spec_guarantee_in_surface() {
2935        // The spec defines `guarantee:` as a surface clause.
2936        let src = r#"surface S {
2937    facing viewer: User
2938    guarantee: DataIntegrity
2939}"#;
2940        let r = parse_ok(src);
2941        assert_eq!(r.diagnostics.len(), 0, "guarantee: in surface should parse cleanly");
2942    }
2943
2944    #[test]
2945    fn spec_timeout_in_surface() {
2946        // The spec defines `timeout:` as a surface clause with rule name syntax.
2947        let src = r#"surface InvitationView {
2948    facing recipient: Candidate
2949    context invitation: ResourceInvitation where email = recipient.email
2950    timeout: InvitationExpires
2951}"#;
2952        let r = parse_ok(src);
2953        assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
2954    }
2955
2956    #[test]
2957    fn spec_timeout_in_surface_with_when() {
2958        // The spec shows `timeout: RuleName when condition`.
2959        let src = r#"surface InvitationView {
2960    facing recipient: Candidate
2961    context invitation: ResourceInvitation where email = recipient.email
2962    timeout: InvitationExpires when invitation.expires_at <= now
2963}"#;
2964        let r = parse_ok(src);
2965        assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
2966    }
2967
2968    // -- Finding 15: suffix predicates are not in the spec --------------------
2969
2970    #[test]
2971    fn spec_reject_suffix_predicate() {
2972        // The spec does not define suffix predicate syntax like `starts_with`.
2973        let src = r#"rule R {
2974    when: X()
2975    requires: finding.code starts_with "allium."
2976    ensures: Done()
2977}"#;
2978        let r = parse_ok(src);
2979        assert!(
2980            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2981            "expected error for suffix predicate (not in spec), but parsed without errors"
2982        );
2983    }
2984
2985    // -- Finding 17: `.add()`/`.remove()` are in the spec ---------------------
2986
2987    #[test]
2988    fn spec_add_remove_in_ensures() {
2989        // The spec documents `.add()` and `.remove()` as ensures-only mutations.
2990        // These parse as regular method calls, which is correct.
2991        let src = r#"rule R {
2992    when: AssignInterviewer(interview, new_interviewer)
2993    ensures:
2994        interview.interviewers.add(new_interviewer)
2995}"#;
2996        let r = parse_ok(src);
2997        assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
2998    }
2999
3000    #[test]
3001    fn spec_remove_in_ensures() {
3002        let src = r#"rule R {
3003    when: RemoveInterviewer(interview, leaving)
3004    ensures:
3005        interview.interviewers.remove(leaving)
3006}"#;
3007        let r = parse_ok(src);
3008        assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3009    }
3010
3011    // -- Finding 18: `.first`/`.last` are in the spec -------------------------
3012
3013    #[test]
3014    fn spec_first_last_access() {
3015        // The spec documents `.first` and `.last` for ordered collections.
3016        let src = "entity E { latest: attempts.last\n    earliest: attempts.first }";
3017        let r = parse_ok(src);
3018        assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3019    }
3020
3021    // -- Finding 19: set arithmetic is in the spec ----------------------------
3022
3023    #[test]
3024    fn spec_set_arithmetic() {
3025        // The spec documents `+` and `-` on collections as set arithmetic.
3026        let src = r#"entity Role {
3027    permissions: Set<String>
3028    inherited: Set<String>
3029    all_permissions: permissions + inherited
3030    removed: old_mentions - new_mentions
3031}"#;
3032        let r = parse_ok(src);
3033        assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3034    }
3035
3036    // -- Finding 20: discard binding `_` is in the spec -----------------------
3037
3038    #[test]
3039    fn spec_discard_binding_in_trigger() {
3040        // The spec shows `when: _: LogProcessor.last_flush_check + ...`
3041        let src = r#"rule R {
3042    when: _: LogProcessor.last_flush_check <= now
3043    ensures: Flushed()
3044}"#;
3045        let r = parse_ok(src);
3046        assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3047    }
3048
3049    #[test]
3050    fn spec_discard_in_trigger_params() {
3051        // The spec shows `when: SomeEvent(_, slot)`
3052        let src = r#"rule R {
3053    when: SomeEvent(_, slot)
3054    ensures: Processed(slot: slot)
3055}"#;
3056        let r = parse_ok(src);
3057        assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3058    }
3059
3060    #[test]
3061    fn spec_discard_in_for() {
3062        // The spec shows `for _ in items: Counted(batch)`
3063        let src = r#"rule R {
3064    when: X(items)
3065    ensures:
3066        for _ in items: Counted()
3067}"#;
3068        let r = parse_ok(src);
3069        assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3070    }
3071
3072    // -- Finding 21: default with object literal is in the spec ---------------
3073
3074    #[test]
3075    fn spec_default_with_object_literal() {
3076        // The spec shows: default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }
3077        let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3078        let r = parse_ok(src);
3079        assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3080    }
3081
3082    #[test]
3083    fn spec_default_multiline_object() {
3084        // The spec shows multi-line defaults with object literals.
3085        let src = r#"default Role viewer = {
3086    name: "viewer",
3087    permissions: { "documents.read" }
3088}"#;
3089        let r = parse_ok(src);
3090        assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3091    }
3092
3093    // -- Spec surface features: related, let, guarantee, timeout --------------
3094
3095    #[test]
3096    fn spec_surface_related_clause() {
3097        // The spec shows `related:` with surface references.
3098        let src = r#"surface InterviewerDashboard {
3099    facing viewer: Interviewer
3100    context assignment: SlotConfirmation where interviewer = viewer
3101    related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3102}"#;
3103        let r = parse_ok(src);
3104        assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3105    }
3106
3107    #[test]
3108    fn spec_surface_let_binding() {
3109        // The spec shows `let` bindings inside surfaces.
3110        let src = r#"surface S {
3111    facing viewer: User
3112    let comments = Comments where parent = viewer
3113    exposes: CommentList
3114}"#;
3115        let r = parse_ok(src);
3116        assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3117    }
3118
3119    #[test]
3120    fn spec_surface_multiline_context_where() {
3121        // The spec shows context with where on a continuation line.
3122        let src = r#"surface InterviewerPendingAssignments {
3123    facing viewer: Interviewer
3124    context assignment: InterviewAssignment
3125        where interviewer = viewer and status = pending
3126    exposes: AssignmentList
3127}"#;
3128        let r = parse_ok(src);
3129        assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3130    }
3131
3132    // -- Spec: `for` inside surfaces ------------------------------------------
3133
3134    #[test]
3135    fn spec_for_in_surface_provides() {
3136        // The spec shows for iteration inside surface provides.
3137        let src = r#"surface TaskBoard {
3138    facing viewer: User
3139    for task in Task where task.assignee = viewer:
3140        provides: CompleteTask(viewer, task) when task.status = in_progress
3141    exposes: KanbanBoard
3142}"#;
3143        let r = parse_ok(src);
3144        assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3145    }
3146
3147    // -- Spec: `use` without alias --------------------------------------------
3148
3149    #[test]
3150    fn spec_use_without_alias() {
3151        // The spec shows `use` both with and without `as alias`.
3152        let src = r#"use "github.com/specs/notifications/def456""#;
3153        let r = parse_ok(src);
3154        assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3155    }
3156
3157    // -- Spec: empty external entity ------------------------------------------
3158
3159    #[test]
3160    fn spec_empty_external_entity() {
3161        // The spec shows external entities with empty bodies as type placeholders.
3162        let src = "external entity Commentable {}";
3163        let r = parse_ok(src);
3164        assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3165    }
3166
3167    // -- Spec: multi-line provides block in surface ---------------------------
3168
3169    #[test]
3170    fn spec_surface_multiline_provides() {
3171        // The spec shows provides as a multi-line block.
3172        let src = r#"surface ProjectDashboard {
3173    facing viewer: ProjectManager
3174    context project: Project where owner = viewer
3175    provides:
3176        CreateTask(viewer, project) when project.status = active
3177        ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3178    exposes: TaskList
3179}"#;
3180        let r = parse_ok(src);
3181        assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3182    }
3183
3184    // -- Spec: multi-line exposes block in surface ----------------------------
3185
3186    #[test]
3187    fn spec_surface_multiline_exposes() {
3188        // The spec shows exposes as a multi-line block.
3189        let src = r#"surface InterviewerDashboard {
3190    facing viewer: Interviewer
3191    context assignment: SlotConfirmation where interviewer = viewer
3192    exposes:
3193        assignment.slot.time
3194        assignment.status
3195}"#;
3196        let r = parse_ok(src);
3197        assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3198    }
3199}