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 serde::Serialize;
8
9use crate::ast::*;
10use crate::diagnostic::Diagnostic;
11use crate::lexer::{lex, SourceMap, Token, TokenKind};
12use crate::Span;
13
14// ---------------------------------------------------------------------------
15// Public API
16// ---------------------------------------------------------------------------
17
18#[derive(Debug, Serialize)]
19pub struct ParseResult {
20    pub module: Module,
21    pub diagnostics: Vec<Diagnostic>,
22}
23
24/// Parse an Allium source file into a [`Module`].
25pub fn parse(source: &str) -> ParseResult {
26    let tokens = lex(source);
27    let source_map = SourceMap::new(source);
28    let mut p = Parser {
29        source,
30        tokens,
31        pos: 0,
32        source_map,
33        diagnostics: Vec::new(),
34    };
35    let module = p.parse_module();
36    ParseResult {
37        module,
38        diagnostics: p.diagnostics,
39    }
40}
41
42// ---------------------------------------------------------------------------
43// Parser state
44// ---------------------------------------------------------------------------
45
46struct Parser<'s> {
47    source: &'s str,
48    tokens: Vec<Token>,
49    pos: usize,
50    source_map: SourceMap,
51    diagnostics: Vec<Diagnostic>,
52}
53
54// ---------------------------------------------------------------------------
55// Navigation helpers
56// ---------------------------------------------------------------------------
57
58impl<'s> Parser<'s> {
59    fn peek(&self) -> Token {
60        self.tokens[self.pos]
61    }
62
63    fn peek_kind(&self) -> TokenKind {
64        self.tokens[self.pos].kind
65    }
66
67    fn peek_at(&self, offset: usize) -> Token {
68        let idx = (self.pos + offset).min(self.tokens.len() - 1);
69        self.tokens[idx]
70    }
71
72    fn advance(&mut self) -> Token {
73        let tok = self.tokens[self.pos];
74        if tok.kind != TokenKind::Eof {
75            self.pos += 1;
76        }
77        tok
78    }
79
80    fn at(&self, kind: TokenKind) -> bool {
81        self.peek_kind() == kind
82    }
83
84    fn at_eof(&self) -> bool {
85        self.at(TokenKind::Eof)
86    }
87
88    fn eat(&mut self, kind: TokenKind) -> Option<Token> {
89        if self.at(kind) {
90            Some(self.advance())
91        } else {
92            None
93        }
94    }
95
96    fn expect(&mut self, kind: TokenKind) -> Option<Token> {
97        if self.at(kind) {
98            Some(self.advance())
99        } else {
100            self.error(
101                self.peek().span,
102                format!("expected {kind}, found {}", self.peek_kind()),
103            );
104            None
105        }
106    }
107
108    fn text(&self, span: Span) -> &'s str {
109        &self.source[span.start..span.end]
110    }
111
112    fn line_of(&self, span: Span) -> u32 {
113        self.source_map.line_col(span.start).0
114    }
115
116    fn col_of(&self, span: Span) -> u32 {
117        self.source_map.line_col(span.start).1
118    }
119
120    fn error(&mut self, span: Span, msg: impl Into<String>) {
121        let line = self.source_map.line_col(span.start).0;
122        if let Some(last) = self.diagnostics.last() {
123            if last.severity == crate::diagnostic::Severity::Error
124                && self.source_map.line_col(last.span.start).0 == line
125            {
126                return;
127            }
128        }
129        self.diagnostics.push(Diagnostic::error(span, msg));
130    }
131
132    /// Consume and return an [`Ident`] from any word token.
133    fn parse_ident(&mut self) -> Option<Ident> {
134        self.parse_ident_in("identifier")
135    }
136
137    /// Consume and return an [`Ident`] with a context-specific label for errors.
138    fn parse_ident_in(&mut self, context: &str) -> Option<Ident> {
139        let tok = self.peek();
140        if tok.kind.is_word() {
141            self.advance();
142            Some(Ident {
143                span: tok.span,
144                name: self.text(tok.span).to_string(),
145            })
146        } else {
147            self.error(
148                tok.span,
149                format!("expected {context}, found {}", tok.kind),
150            );
151            None
152        }
153    }
154
155    /// Consume a string token and produce a [`StringLiteral`].
156    fn parse_string(&mut self) -> Option<StringLiteral> {
157        let tok = self.expect(TokenKind::String)?;
158        let raw = self.text(tok.span);
159        // Strip surrounding quotes
160        let inner = &raw[1..raw.len() - 1];
161        let parts = parse_string_parts(inner, tok.span.start + 1);
162        Some(StringLiteral {
163            span: tok.span,
164            parts,
165        })
166    }
167}
168
169/// Split the inner content of a string literal into text and interpolation
170/// parts. `base_offset` is the byte offset of the first character after the
171/// opening quote in the source file.
172fn parse_string_parts(inner: &str, base_offset: usize) -> Vec<StringPart> {
173    let mut parts = Vec::new();
174    let mut buf = String::new();
175    let bytes = inner.as_bytes();
176    let mut i = 0;
177    while i < bytes.len() {
178        if bytes[i] == b'\\' && i + 1 < bytes.len() {
179            buf.push(bytes[i + 1] as char);
180            i += 2;
181        } else if bytes[i] == b'{' {
182            if !buf.is_empty() {
183                parts.push(StringPart::Text(std::mem::take(&mut buf)));
184            }
185            i += 1; // skip {
186            let start = i;
187            while i < bytes.len() && bytes[i] != b'}' {
188                i += 1;
189            }
190            let name = std::str::from_utf8(&bytes[start..i]).unwrap_or("").to_string();
191            let span_start = base_offset + start;
192            let span_end = base_offset + i;
193            parts.push(StringPart::Interpolation(Ident {
194                span: Span::new(span_start, span_end),
195                name,
196            }));
197            if i < bytes.len() {
198                i += 1; // skip }
199            }
200        } else {
201            buf.push(bytes[i] as char);
202            i += 1;
203        }
204    }
205    if !buf.is_empty() {
206        parts.push(StringPart::Text(buf));
207    }
208    parts
209}
210
211// ---------------------------------------------------------------------------
212// Clause-keyword recognition
213// ---------------------------------------------------------------------------
214
215/// Returns true for identifiers that act as clause keywords inside blocks.
216/// These are parsed as `Clause` items rather than `Assignment` items.
217fn is_clause_keyword(text: &str) -> bool {
218    matches!(
219        text,
220        "when"
221            | "requires"
222            | "ensures"
223            | "facing"
224            | "context"
225            | "exposes"
226            | "provides"
227            | "related"
228            | "timeout"
229            | "contracts"
230            | "identified_by"
231            | "within"
232    )
233}
234
235/// True for clause keywords whose value can start with a `name: expr` binding.
236fn clause_allows_binding(keyword: &str) -> bool {
237    matches!(keyword, "when")
238}
239
240/// True for keywords that use `keyword name: value` syntax (no colon after the
241/// keyword). These directly embed a binding.
242fn is_binding_clause_keyword(text: &str) -> bool {
243    matches!(text, "facing" | "context")
244}
245
246/// True if the current token is a keyword that begins a clause.
247fn token_is_clause_keyword(kind: TokenKind) -> bool {
248    matches!(
249        kind,
250        TokenKind::When | TokenKind::Requires | TokenKind::Ensures | TokenKind::Within
251            | TokenKind::Invariant
252    )
253}
254
255// ---------------------------------------------------------------------------
256// Module parsing
257// ---------------------------------------------------------------------------
258
259impl<'s> Parser<'s> {
260    fn parse_module(&mut self) -> Module {
261        let start = self.peek().span;
262        // Version marker is a comment: `-- allium: N`. Detect it from the raw
263        // source before the lexer strips it.
264        let version = detect_version(self.source);
265
266        match version {
267            None => {
268                self.diagnostics.push(Diagnostic::warning(
269                    start,
270                    "missing version marker; expected '-- allium: 1' as the first line",
271                ));
272            }
273            Some(1) | Some(2) => {}
274            Some(v) => {
275                self.diagnostics.push(Diagnostic::error(
276                    start,
277                    format!("unsupported allium version {v}; this parser supports versions 1 and 2"),
278                ));
279            }
280        }
281
282        let mut decls = Vec::new();
283        while !self.at_eof() {
284            if let Some(d) = self.parse_decl() {
285                decls.push(d);
286            } else {
287                // Recovery: skip one token and try again
288                self.advance();
289            }
290        }
291        let end = self.peek().span;
292        Module {
293            span: start.merge(end),
294            version,
295            declarations: decls,
296        }
297    }
298}
299
300fn detect_version(source: &str) -> Option<u32> {
301    for line in source.lines() {
302        let trimmed = line.trim();
303        if trimmed.is_empty() {
304            continue;
305        }
306        if let Some(rest) = trimmed.strip_prefix("--") {
307            let rest = rest.trim();
308            if let Some(ver) = rest.strip_prefix("allium:") {
309                return ver.trim().parse().ok();
310            }
311        }
312        break; // only check leading lines
313    }
314    None
315}
316
317// ---------------------------------------------------------------------------
318// Declaration parsing
319// ---------------------------------------------------------------------------
320
321impl<'s> Parser<'s> {
322    fn parse_decl(&mut self) -> Option<Decl> {
323        match self.peek_kind() {
324            TokenKind::Use => self.parse_use_decl().map(Decl::Use),
325            TokenKind::Rule => self.parse_block(BlockKind::Rule).map(Decl::Block),
326            TokenKind::Entity => self.parse_block(BlockKind::Entity).map(Decl::Block),
327            TokenKind::External => {
328                let start = self.advance().span;
329                if self.at(TokenKind::Entity) {
330                    self.parse_block_from(start, BlockKind::ExternalEntity)
331                        .map(Decl::Block)
332                } else {
333                    self.error(self.peek().span, "expected 'entity' after 'external'");
334                    None
335                }
336            }
337            TokenKind::Value => self.parse_block(BlockKind::Value).map(Decl::Block),
338            TokenKind::Enum => self.parse_block(BlockKind::Enum).map(Decl::Block),
339            TokenKind::Given => self.parse_anonymous_block(BlockKind::Given).map(Decl::Block),
340            TokenKind::Config => self.parse_anonymous_block(BlockKind::Config).map(Decl::Block),
341            TokenKind::Surface => self.parse_block(BlockKind::Surface).map(Decl::Block),
342            TokenKind::Actor => self.parse_block(BlockKind::Actor).map(Decl::Block),
343            TokenKind::Contract => self.parse_contract_decl().map(Decl::Block),
344            TokenKind::Invariant => self.parse_invariant_decl().map(Decl::Invariant),
345            TokenKind::Default => self.parse_default_decl().map(Decl::Default),
346            TokenKind::Variant => self.parse_variant_decl().map(Decl::Variant),
347            TokenKind::Deferred => self.parse_deferred_decl().map(Decl::Deferred),
348            TokenKind::Open => self.parse_open_question_decl().map(Decl::OpenQuestion),
349            // Qualified config: `alias/config { ... }`
350            TokenKind::Ident
351                if self.peek_at(1).kind == TokenKind::Slash
352                    && self.text(self.peek_at(2).span) == "config" =>
353            {
354                self.parse_qualified_config().map(Decl::Block)
355            }
356            _ => {
357                self.error(
358                    self.peek().span,
359                    format!(
360                        "expected declaration (entity, rule, enum, value, config, surface, actor, \
361                         given, default, variant, deferred, use, open question, contract, invariant), found {}",
362                        self.peek_kind(),
363                    ),
364                );
365                None
366            }
367        }
368    }
369
370    // -- module declaration -----------------------------------------------
371
372    // -- use declaration ------------------------------------------------
373
374    fn parse_use_decl(&mut self) -> Option<UseDecl> {
375        let start = self.expect(TokenKind::Use)?.span;
376        let path = self.parse_string()?;
377        let alias = if self.eat(TokenKind::As).is_some() {
378            Some(self.parse_ident_in("import alias")?)
379        } else {
380            None
381        };
382        let end = alias
383            .as_ref()
384            .map(|a| a.span)
385            .unwrap_or(path.span);
386        Some(UseDecl {
387            span: start.merge(end),
388            path,
389            alias,
390        })
391    }
392
393    // -- named block: `keyword Name { ... }` ----------------------------
394
395    fn parse_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
396        let start = self.advance().span; // consume keyword
397        self.parse_block_from(start, kind)
398    }
399
400    fn parse_block_from(&mut self, start: Span, kind: BlockKind) -> Option<BlockDecl> {
401        // For ExternalEntity the keyword was already consumed by the caller;
402        // here we consume Entity.
403        if kind == BlockKind::ExternalEntity {
404            self.expect(TokenKind::Entity)?;
405        }
406        let context = match kind {
407            BlockKind::Entity | BlockKind::ExternalEntity => "entity name",
408            BlockKind::Rule => "rule name",
409            BlockKind::Surface => "surface name",
410            BlockKind::Actor => "actor name",
411            BlockKind::Value => "value type name",
412            BlockKind::Enum => "enum name",
413            _ => "block name",
414        };
415        let name = Some(self.parse_ident_in(context)?);
416        self.expect(TokenKind::LBrace)?;
417        let items = if kind == BlockKind::Enum {
418            self.parse_enum_body()
419        } else {
420            self.parse_block_items()
421        };
422        let end = self.expect(TokenKind::RBrace)?.span;
423        Some(BlockDecl {
424            span: start.merge(end),
425            kind,
426            name,
427            items,
428        })
429    }
430
431    // -- anonymous block: `keyword { ... }` -----------------------------
432
433    /// Parse enum body: pipe-separated variant names.
434    /// `{ pending | shipped | delivered }`
435    fn parse_enum_body(&mut self) -> Vec<BlockItem> {
436        let mut items = Vec::new();
437        while !self.at(TokenKind::RBrace) && !self.at_eof() {
438            if self.eat(TokenKind::Pipe).is_some() {
439                continue;
440            }
441            if let Some(ident) = self.parse_ident_in("enum variant") {
442                items.push(BlockItem {
443                    span: ident.span,
444                    kind: BlockItemKind::EnumVariant { name: ident },
445                });
446            } else {
447                self.advance(); // skip unrecognised token
448            }
449        }
450        items
451    }
452
453    fn parse_anonymous_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
454        let start = self.advance().span;
455        self.expect(TokenKind::LBrace)?;
456        let items = self.parse_block_items();
457        let end = self.expect(TokenKind::RBrace)?.span;
458        Some(BlockDecl {
459            span: start.merge(end),
460            kind,
461            name: None,
462            items,
463        })
464    }
465
466    // -- qualified config: `alias/config { ... }` -----------------------
467
468    fn parse_qualified_config(&mut self) -> Option<BlockDecl> {
469        let alias = self.parse_ident_in("config qualifier")?;
470        let start = alias.span;
471        self.expect(TokenKind::Slash)?;
472        self.advance(); // consume "config" ident
473        self.expect(TokenKind::LBrace)?;
474        let items = self.parse_block_items();
475        let end = self.expect(TokenKind::RBrace)?.span;
476        Some(BlockDecl {
477            span: start.merge(end),
478            kind: BlockKind::Config,
479            name: Some(alias),
480            items,
481        })
482    }
483
484    // -- default declaration -------------------------------------------
485
486    fn parse_default_decl(&mut self) -> Option<DefaultDecl> {
487        let start = self.expect(TokenKind::Default)?.span;
488
489        // `default [TypeName] instanceName = value`
490        // The type name is optional. If the next two tokens are both words
491        // and the second is followed by `=`, the first is the type.
492        let (type_name, name) = if self.peek_kind().is_word()
493            && self.peek_at(1).kind.is_word()
494            && self.peek_at(2).kind == TokenKind::Eq
495        {
496            let t = self.parse_ident_in("type name")?;
497            let n = self.parse_ident_in("default name")?;
498            (Some(t), n)
499        } else {
500            (None, self.parse_ident_in("default name")?)
501        };
502
503        self.expect(TokenKind::Eq)?;
504        let value = self.parse_expr(0)?;
505        Some(DefaultDecl {
506            span: start.merge(value.span()),
507            type_name,
508            name,
509            value,
510        })
511    }
512
513    // -- variant declaration -------------------------------------------
514
515    fn parse_variant_decl(&mut self) -> Option<VariantDecl> {
516        let start = self.expect(TokenKind::Variant)?.span;
517        let name = self.parse_ident_in("variant name")?;
518        self.expect(TokenKind::Colon)?;
519        let base = self.parse_expr(0)?;
520
521        let items = if self.eat(TokenKind::LBrace).is_some() {
522            let items = self.parse_block_items();
523            self.expect(TokenKind::RBrace)?;
524            items
525        } else {
526            Vec::new()
527        };
528
529        let end = if let Some(last) = items.last() {
530            last.span
531        } else {
532            base.span()
533        };
534        Some(VariantDecl {
535            span: start.merge(end),
536            name,
537            base,
538            items,
539        })
540    }
541
542    // -- deferred declaration ------------------------------------------
543
544    fn parse_deferred_decl(&mut self) -> Option<DeferredDecl> {
545        let start = self.expect(TokenKind::Deferred)?.span;
546        let path = self.parse_expr(0)?;
547        Some(DeferredDecl {
548            span: start.merge(path.span()),
549            path,
550        })
551    }
552
553    // -- open question --------------------------------------------------
554
555    fn parse_open_question_decl(&mut self) -> Option<OpenQuestionDecl> {
556        let start = self.expect(TokenKind::Open)?.span;
557        self.expect(TokenKind::Question)?;
558        let text = self.parse_string()?;
559        Some(OpenQuestionDecl {
560            span: start.merge(text.span),
561            text,
562        })
563    }
564
565    // -- contract declaration -------------------------------------------
566
567    fn parse_contract_decl(&mut self) -> Option<BlockDecl> {
568        let start = self.advance().span; // consume `contract`
569        let name = self.parse_ident_in("contract name")?;
570
571        // Reject lowercase contract names
572        if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
573            self.diagnostics.push(Diagnostic::error(
574                name.span,
575                "contract name must start with an uppercase letter",
576            ));
577        }
578
579        // Reject colon-delimited body
580        if self.at(TokenKind::Colon) {
581            self.error(
582                self.peek().span,
583                "contract body must use braces { }, not a colon",
584            );
585            return None;
586        }
587
588        self.expect(TokenKind::LBrace)?;
589        let items = self.parse_block_items();
590        let end = self.expect(TokenKind::RBrace)?.span;
591        Some(BlockDecl {
592            span: start.merge(end),
593            kind: BlockKind::Contract,
594            name: Some(name),
595            items,
596        })
597    }
598
599    // -- invariant declaration ------------------------------------------
600
601    fn parse_invariant_decl(&mut self) -> Option<InvariantDecl> {
602        let start = self.advance().span; // consume `invariant`
603        let name = self.parse_ident_in("invariant name")?;
604
605        // Reject lowercase invariant names
606        if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
607            self.diagnostics.push(Diagnostic::error(
608                name.span,
609                "invariant name must start with an uppercase letter",
610            ));
611        }
612
613        self.expect(TokenKind::LBrace)?;
614        let body = self.parse_invariant_body()?;
615        let end = self.expect(TokenKind::RBrace)?.span;
616        Some(InvariantDecl {
617            span: start.merge(end),
618            name,
619            body,
620        })
621    }
622
623    /// Parse the body of an invariant block — a sequence of expressions and
624    /// let bindings, similar to a clause value block.
625    fn parse_invariant_body(&mut self) -> Option<Expr> {
626        let start = self.peek().span;
627        let mut items = Vec::new();
628
629        while !self.at(TokenKind::RBrace) && !self.at_eof() {
630            if self.at(TokenKind::Let) {
631                let let_start = self.advance().span;
632                let name = self.parse_ident_in("binding name")?;
633                self.expect(TokenKind::Eq)?;
634                let value = self.parse_expr(0)?;
635                items.push(Expr::LetExpr {
636                    span: let_start.merge(value.span()),
637                    name,
638                    value: Box::new(value),
639                });
640            } else if let Some(expr) = self.parse_expr(0) {
641                items.push(expr);
642            } else {
643                self.advance();
644                break;
645            }
646        }
647
648        if items.len() == 1 {
649            Some(items.pop().unwrap())
650        } else {
651            let end = items.last().map(|e| e.span()).unwrap_or(start);
652            Some(Expr::Block {
653                span: start.merge(end),
654                items,
655            })
656        }
657    }
658}
659
660// ---------------------------------------------------------------------------
661// Block item parsing
662// ---------------------------------------------------------------------------
663
664impl<'s> Parser<'s> {
665    fn parse_block_items(&mut self) -> Vec<BlockItem> {
666        let mut items = Vec::new();
667        while !self.at(TokenKind::RBrace) && !self.at_eof() {
668            if let Some(item) = self.parse_block_item() {
669                items.push(item);
670                self.eat(TokenKind::Comma);
671            } else {
672                // Recovery: skip one token
673                self.advance();
674            }
675        }
676        items
677    }
678
679    fn parse_block_item(&mut self) -> Option<BlockItem> {
680        let start = self.peek().span;
681
682        // `let name = value`
683        if self.at(TokenKind::Let) {
684            return self.parse_let_item(start);
685        }
686
687        // `for binding in collection [where filter]: ...` at block level
688        if self.at(TokenKind::For) {
689            return self.parse_for_block_item(start);
690        }
691
692        // `if condition: ... [else if ...: ...] [else: ...]` at block level
693        if self.at(TokenKind::If) {
694            return self.parse_if_block_item(start);
695        }
696
697        // `@invariant`, `@guidance`, `@guarantee` — prose annotations
698        if self.at(TokenKind::At) {
699            return self.parse_annotation(start);
700        }
701
702        // `invariant Name { expr }` — expression-bearing invariant inside a block
703        if self.at(TokenKind::Invariant) && self.peek_at(1).kind.is_word()
704            && self.peek_at(2).kind != TokenKind::Colon
705        {
706            return self.parse_invariant_block_item(start);
707        }
708
709        // `open question "text"` (inside a block)
710        if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
711            self.advance(); // open
712            self.advance(); // question
713            let text = self.parse_string()?;
714            return Some(BlockItem {
715                span: start.merge(text.span),
716                kind: BlockItemKind::OpenQuestion { text },
717            });
718        }
719
720        // Migration diagnostics: old colon-form prose constructs
721        if self.peek_kind() == TokenKind::Ident {
722            let word = self.text(self.peek().span);
723            if (word == "guidance" || word == "guarantee")
724                && self.peek_at(1).kind == TokenKind::Colon
725            {
726                let kw = word.to_string();
727                self.error(
728                    self.peek().span,
729                    format!(
730                        "`{kw}:` syntax was replaced by `@{kw}`. Use `@{kw}` followed by indented comment lines."
731                    ),
732                );
733                // Fall through to normal clause parsing so we don't lose the rest
734            }
735        }
736
737        // Migration diagnostic: old `invariant:` colon form
738        if self.at(TokenKind::Invariant) && self.peek_at(1).kind == TokenKind::Colon {
739            self.error(
740                self.peek().span,
741                "`invariant:` syntax was replaced by `@invariant`. Use `@invariant Name` followed by indented comment lines.",
742            );
743            // Fall through to normal clause parsing
744        }
745
746        // Everything else: `name: value` or `keyword: value` or
747        // `name(params): value`
748        if self.peek_kind().is_word() {
749            // `contracts:` clause — dispatch before generic clause/assignment
750            if self.text(self.peek().span) == "contracts"
751                && self.peek_at(1).kind == TokenKind::Colon
752            {
753                return self.parse_contracts_clause(start);
754            }
755
756            // `facing name: Type` / `context name: Type [where ...]` — binding
757            // clause keywords that don't use `:` after the keyword itself.
758            if is_binding_clause_keyword(self.text(self.peek().span))
759                && self.peek_at(1).kind.is_word()
760                && self.peek_at(2).kind == TokenKind::Colon
761            {
762                return self.parse_binding_clause_item(start);
763            }
764
765            // Check for `Name.field:` — dot-path reverse relationship
766            if self.peek_at(1).kind == TokenKind::Dot
767                && self.peek_at(2).kind.is_word()
768                && self.peek_at(3).kind == TokenKind::Colon
769            {
770                return self.parse_path_assignment_item(start);
771            }
772
773            // Check for `name(` — potential parameterised assignment
774            if self.peek_at(1).kind == TokenKind::LParen {
775                return self.parse_param_or_clause_item(start);
776            }
777
778            // Check for `name:` — assignment or clause
779            if self.peek_at(1).kind == TokenKind::Colon {
780                return self.parse_assign_or_clause_item(start);
781            }
782        }
783
784        // For clauses whose keyword is a separate TokenKind (when, requires, etc.)
785        if token_is_clause_keyword(self.peek_kind()) && self.peek_at(1).kind == TokenKind::Colon {
786            return self.parse_assign_or_clause_item(start);
787        }
788
789        self.error(
790            start,
791            format!(
792                "expected block item (name: value, let name = value, when:/requires:/ensures: clause, \
793                 for ... in ...:, or open question), found {}",
794                self.peek_kind(),
795            ),
796        );
797        None
798    }
799
800    fn parse_let_item(&mut self, start: Span) -> Option<BlockItem> {
801        self.advance(); // consume `let`
802        let name = self.parse_ident_in("binding name")?;
803        self.expect(TokenKind::Eq)?;
804        let value = self.parse_clause_value(start)?;
805        Some(BlockItem {
806            span: start.merge(value.span()),
807            kind: BlockItemKind::Let { name, value },
808        })
809    }
810
811    /// Parse `facing name: Type` or `context name: Type [where ...]`.
812    /// These keywords don't take `:` after the keyword — they embed a binding directly.
813    fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
814        let keyword_tok = self.advance(); // consume facing/context
815        let keyword = self.text(keyword_tok.span).to_string();
816        let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
817        self.advance(); // consume ':'
818        let type_expr = self.parse_clause_value(start)?;
819        let value_span = type_expr.span();
820        let value = Expr::Binding {
821            span: binding_name.span.merge(value_span),
822            name: binding_name,
823            value: Box::new(type_expr),
824        };
825        Some(BlockItem {
826            span: start.merge(value_span),
827            kind: BlockItemKind::Clause { keyword, value },
828        })
829    }
830
831    /// Parse `for binding in collection [where filter]:` at block level.
832    /// The body is a set of nested block items (let, requires, ensures, etc.).
833    fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
834        self.advance(); // consume `for`
835        let binding = self.parse_for_binding()?;
836        self.expect(TokenKind::In)?;
837
838        let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
839
840        let filter = if self.eat(TokenKind::Where).is_some() {
841            // Parse filter at min_bp 0 — colon terminates naturally since
842            // it's not an expression operator.
843            Some(self.parse_expr(0)?)
844        } else {
845            None
846        };
847
848        self.expect(TokenKind::Colon)?;
849
850        // The body contains nested block items at higher indentation.
851        let for_line = self.line_of(start);
852        let next_line = self.line_of(self.peek().span);
853
854        let items = if next_line > for_line {
855            let base_col = self.col_of(self.peek().span);
856            self.parse_indented_block_items(base_col)
857        } else {
858            // Single-line for: parse one block item
859            let mut items = Vec::new();
860            if let Some(item) = self.parse_block_item() {
861                items.push(item);
862            }
863            items
864        };
865
866        let end = items
867            .last()
868            .map(|i| i.span)
869            .unwrap_or(start);
870
871        Some(BlockItem {
872            span: start.merge(end),
873            kind: BlockItemKind::ForBlock {
874                binding,
875                collection,
876                filter,
877                items,
878            },
879        })
880    }
881
882    /// Collect block items at column >= `base_col` (for indented for-block bodies).
883    fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
884        let mut items = Vec::new();
885        while !self.at_eof()
886            && !self.at(TokenKind::RBrace)
887            && self.col_of(self.peek().span) >= base_col
888        {
889            if let Some(item) = self.parse_block_item() {
890                items.push(item);
891            } else {
892                self.advance();
893                break;
894            }
895        }
896        items
897    }
898
899    /// Parse `if condition: ... [else if ...: ...] [else: ...]` at block level.
900    fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
901        self.advance(); // consume `if`
902        let mut branches = Vec::new();
903
904        // First branch
905        let condition = self.parse_expr(0)?;
906        self.expect(TokenKind::Colon)?;
907        let if_line = self.line_of(start);
908        let items = self.parse_if_block_body(if_line);
909        branches.push(CondBlockBranch {
910            span: start.merge(items.last().map(|i| i.span).unwrap_or(start)),
911            condition,
912            items,
913        });
914
915        // else if / else
916        let mut else_items = None;
917        while self.at(TokenKind::Else) {
918            let else_tok = self.advance();
919            if self.at(TokenKind::If) {
920                let if_start = self.advance().span;
921                let cond = self.parse_expr(0)?;
922                self.expect(TokenKind::Colon)?;
923                let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
924                branches.push(CondBlockBranch {
925                    span: if_start.merge(body_items.last().map(|i| i.span).unwrap_or(if_start)),
926                    condition: cond,
927                    items: body_items,
928                });
929            } else {
930                self.expect(TokenKind::Colon)?;
931                let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
932                else_items = Some(body_items);
933                break;
934            }
935        }
936
937        let end = else_items
938            .as_ref()
939            .and_then(|items| items.last().map(|i| i.span))
940            .or_else(|| branches.last().and_then(|b| b.items.last().map(|i| i.span)))
941            .unwrap_or(start);
942
943        Some(BlockItem {
944            span: start.merge(end),
945            kind: BlockItemKind::IfBlock {
946                branches,
947                else_items,
948            },
949        })
950    }
951
952    /// Parse the body of an if/else if/else block branch.
953    fn parse_if_block_body(&mut self, keyword_line: u32) -> Vec<BlockItem> {
954        let next_line = self.line_of(self.peek().span);
955        if next_line > keyword_line {
956            let base_col = self.col_of(self.peek().span);
957            self.parse_indented_block_items(base_col)
958        } else {
959            // Single-line: parse one block item
960            let mut items = Vec::new();
961            if let Some(item) = self.parse_block_item() {
962                items.push(item);
963            }
964            items
965        }
966    }
967
968    /// Parse `contracts:` clause with indented `demands`/`fulfils` entries.
969    fn parse_contracts_clause(&mut self, start: Span) -> Option<BlockItem> {
970        self.advance(); // consume `contracts`
971        self.advance(); // consume `:`
972
973        let contracts_col = self.col_of(start);
974        let mut entries = Vec::new();
975
976        while !self.at_eof()
977            && !self.at(TokenKind::RBrace)
978            && self.col_of(self.peek().span) > contracts_col
979        {
980            if !self.peek_kind().is_word() {
981                break;
982            }
983
984            let entry_start = self.peek().span;
985            let direction_tok = self.advance();
986            let direction_text = self.text(direction_tok.span);
987
988            let direction = match direction_text {
989                "demands" => ContractDirection::Demands,
990                "fulfils" => ContractDirection::Fulfils,
991                other => {
992                    self.error(
993                        direction_tok.span,
994                        format!(
995                            "Unknown direction '{other}' in contracts clause. Use `demands` or `fulfils`."
996                        ),
997                    );
998                    // Skip the rest of this entry
999                    if self.peek_kind().is_word() {
1000                        self.advance();
1001                    }
1002                    continue;
1003                }
1004            };
1005
1006            let name = self.parse_ident_in("contract name")?;
1007
1008            // Reject inline braced blocks
1009            if self.at(TokenKind::LBrace) {
1010                self.error(
1011                    self.peek().span,
1012                    "Inline contract blocks are not allowed in `contracts:`. Declare the contract at module level.",
1013                );
1014                return None;
1015            }
1016
1017            let end = name.span;
1018            entries.push(ContractBinding {
1019                direction,
1020                name,
1021                span: entry_start.merge(end),
1022            });
1023        }
1024
1025        if entries.is_empty() {
1026            self.error(
1027                start,
1028                "Empty `contracts:` clause. Add at least one `demands` or `fulfils` entry.",
1029            );
1030            return None;
1031        }
1032
1033        let end = entries.last().unwrap().span;
1034        Some(BlockItem {
1035            span: start.merge(end),
1036            kind: BlockItemKind::ContractsClause { entries },
1037        })
1038    }
1039
1040    /// Parse `@invariant Name`, `@guidance`, or `@guarantee Name` with comment body.
1041    fn parse_annotation(&mut self, start: Span) -> Option<BlockItem> {
1042        let at_tok = self.advance(); // consume `@`
1043        let at_col = self.col_of(at_tok.span);
1044
1045        if !self.peek_kind().is_word() {
1046            self.error(
1047                self.peek().span,
1048                format!("expected annotation keyword after `@`, found {}", self.peek_kind()),
1049            );
1050            return None;
1051        }
1052
1053        let keyword_tok = self.advance();
1054        let keyword_text = self.text(keyword_tok.span);
1055
1056        let kind = match keyword_text {
1057            "invariant" => AnnotationKind::Invariant,
1058            "guidance" => AnnotationKind::Guidance,
1059            "guarantee" => AnnotationKind::Guarantee,
1060            other => {
1061                self.error(
1062                    keyword_tok.span,
1063                    format!(
1064                        "Unknown annotation `@{other}`. Use `@invariant`, `@guidance` or `@guarantee`."
1065                    ),
1066                );
1067                return None;
1068            }
1069        };
1070
1071        // Parse optional name
1072        let name = match &kind {
1073            AnnotationKind::Invariant | AnnotationKind::Guarantee => {
1074                let n = self.parse_ident_in("annotation name")?;
1075                if n.name.chars().next().is_some_and(|c| c.is_lowercase()) {
1076                    self.diagnostics.push(Diagnostic::error(
1077                        n.span,
1078                        "Annotation names must be PascalCase.",
1079                    ));
1080                }
1081                Some(n)
1082            }
1083            AnnotationKind::Guidance => {
1084                // Reject name after @guidance
1085                if self.peek_kind().is_word()
1086                    && self.line_of(self.peek().span) == self.line_of(keyword_tok.span)
1087                {
1088                    self.error(
1089                        self.peek().span,
1090                        "`@guidance` does not take a name. Remove the name after `@guidance`.",
1091                    );
1092                    return None;
1093                }
1094                None
1095            }
1096        };
1097
1098        // Parse comment body from source lines.
1099        // The last consumed token tells us which line the annotation header is on.
1100        let last_header_span = name.as_ref().map(|n| n.span).unwrap_or(keyword_tok.span);
1101        let header_line = self.line_of(last_header_span);
1102        let body = self.parse_annotation_body(at_col, header_line);
1103
1104        if body.is_empty() {
1105            self.error(
1106                last_header_span,
1107                "Annotations must be followed by at least one indented comment line.",
1108            );
1109            return None;
1110        }
1111
1112        Some(BlockItem {
1113            span: start.merge(last_header_span),
1114            kind: BlockItemKind::Annotation(Annotation {
1115                kind,
1116                name,
1117                body,
1118                span: start.merge(last_header_span),
1119            }),
1120        })
1121    }
1122
1123    /// Scan source lines for indented `-- ` comment lines forming an annotation body.
1124    /// Starts from the line after `header_line` and collects lines indented deeper
1125    /// than `at_col`.
1126    fn parse_annotation_body(&self, at_col: u32, header_line: u32) -> Vec<String> {
1127        let mut body = Vec::new();
1128        let lines: Vec<&str> = self.source.lines().collect();
1129        let mut line_idx = (header_line + 1) as usize;
1130
1131        while line_idx < lines.len() {
1132            let line = lines[line_idx];
1133            let trimmed = line.trim_start();
1134
1135            if trimmed.is_empty() {
1136                if !body.is_empty() {
1137                    body.push(String::new());
1138                }
1139                line_idx += 1;
1140                continue;
1141            }
1142
1143            let indent = (line.len() - trimmed.len()) as u32;
1144            if indent <= at_col {
1145                break;
1146            }
1147
1148            if let Some(comment) = trimmed.strip_prefix("-- ") {
1149                body.push(comment.to_string());
1150            } else if trimmed == "--" {
1151                body.push(String::new());
1152            } else {
1153                break;
1154            }
1155
1156            line_idx += 1;
1157        }
1158
1159        // Trim trailing blank lines
1160        while body.last().is_some_and(|l| l.is_empty()) {
1161            body.pop();
1162        }
1163
1164        body
1165    }
1166
1167    /// Parse `invariant Name { expr }` inside a block (entity-level).
1168    fn parse_invariant_block_item(&mut self, start: Span) -> Option<BlockItem> {
1169        self.advance(); // consume `invariant`
1170        let name = self.parse_ident_in("invariant name")?;
1171
1172        // Reject lowercase invariant names
1173        if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
1174            self.diagnostics.push(Diagnostic::error(
1175                name.span,
1176                "invariant name must start with an uppercase letter",
1177            ));
1178        }
1179
1180        self.expect(TokenKind::LBrace)?;
1181        let body = self.parse_invariant_body()?;
1182        let end = self.expect(TokenKind::RBrace)?.span;
1183        Some(BlockItem {
1184            span: start.merge(end),
1185            kind: BlockItemKind::InvariantBlock { name, body },
1186        })
1187    }
1188
1189    fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
1190        let name_tok = self.advance(); // consume name/keyword
1191        let name_text = self.text(name_tok.span).to_string();
1192        self.advance(); // consume ':'
1193
1194        let allows_binding = clause_allows_binding(&name_text);
1195        let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
1196        let value_span = value.span();
1197
1198        let kind = if is_clause_keyword(&name_text) {
1199            BlockItemKind::Clause {
1200                keyword: name_text,
1201                value,
1202            }
1203        } else {
1204            BlockItemKind::Assignment {
1205                name: Ident {
1206                    span: name_tok.span,
1207                    name: name_text,
1208                },
1209                value,
1210            }
1211        };
1212
1213        Some(BlockItem {
1214            span: start.merge(value_span),
1215            kind,
1216        })
1217    }
1218
1219    /// Parse `Entity.field: value` — a dot-path reverse relationship declaration.
1220    fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
1221        let obj_tok = self.advance(); // consume first ident
1222        self.advance(); // consume '.'
1223        let field = self.parse_ident_in("field name")?;
1224        self.advance(); // consume ':'
1225
1226        let path = Expr::MemberAccess {
1227            span: obj_tok.span.merge(field.span),
1228            object: Box::new(Expr::Ident(Ident {
1229                span: obj_tok.span,
1230                name: self.text(obj_tok.span).to_string(),
1231            })),
1232            field,
1233        };
1234
1235        let value = self.parse_clause_value(start)?;
1236        let value_span = value.span();
1237        Some(BlockItem {
1238            span: start.merge(value_span),
1239            kind: BlockItemKind::PathAssignment { path, value },
1240        })
1241    }
1242
1243    fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
1244        // Could be `name(params): value` (param assignment) or
1245        // `name(args)` which is an expression that happens to start a clause
1246        // value. Peek far enough to see if `)` is followed by `:`.
1247        let saved_pos = self.pos;
1248        let _name_tok = self.advance();
1249        self.advance(); // (
1250
1251        // Try to scan past balanced parens
1252        let mut depth = 1u32;
1253        while !self.at_eof() && depth > 0 {
1254            match self.peek_kind() {
1255                TokenKind::LParen => {
1256                    depth += 1;
1257                    self.advance();
1258                }
1259                TokenKind::RParen => {
1260                    depth -= 1;
1261                    self.advance();
1262                }
1263                _ => {
1264                    self.advance();
1265                }
1266            }
1267        }
1268
1269        if self.at(TokenKind::Colon) {
1270            // It's a parameterised assignment: restore and parse properly
1271            self.pos = saved_pos;
1272            let name = self.parse_ident_in("derived value name")?;
1273            self.expect(TokenKind::LParen)?;
1274            let params = self.parse_ident_list()?;
1275            self.expect(TokenKind::RParen)?;
1276            self.expect(TokenKind::Colon)?;
1277            let value = self.parse_clause_value(start)?;
1278            Some(BlockItem {
1279                span: start.merge(value.span()),
1280                kind: BlockItemKind::ParamAssignment {
1281                    name,
1282                    params,
1283                    value,
1284                },
1285            })
1286        } else {
1287            // Not a param assignment — restore and fall through to assignment
1288            self.pos = saved_pos;
1289            // Check for regular `name: value`
1290            if self.peek_at(1).kind == TokenKind::Colon {
1291                // Nope, the (1) is LParen not Colon. Re-examine.
1292            }
1293            // Fall back: treat as `name: value` where value starts with a call
1294            self.parse_assign_or_clause_item(start)
1295        }
1296    }
1297
1298    fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
1299        let mut params = Vec::new();
1300        if !self.at(TokenKind::RParen) {
1301            params.push(self.parse_ident_in("parameter name")?);
1302            while self.eat(TokenKind::Comma).is_some() {
1303                params.push(self.parse_ident_in("parameter name")?);
1304            }
1305        }
1306        Some(params)
1307    }
1308
1309    /// Parse a for-loop binding: either a single ident or `(a, b)` destructuring.
1310    fn parse_for_binding(&mut self) -> Option<ForBinding> {
1311        if self.at(TokenKind::LParen) {
1312            let start = self.advance().span; // consume '('
1313            let mut idents = Vec::new();
1314            idents.push(self.parse_ident_in("loop variable")?);
1315            while self.eat(TokenKind::Comma).is_some() {
1316                idents.push(self.parse_ident_in("loop variable")?);
1317            }
1318            let end = self.expect(TokenKind::RParen)?.span;
1319            Some(ForBinding::Destructured(idents, start.merge(end)))
1320        } else {
1321            let ident = self.parse_ident_in("loop variable")?;
1322            Some(ForBinding::Single(ident))
1323        }
1324    }
1325
1326    /// Parse a clause value, optionally checking for a `name: expr` binding
1327    /// pattern at the start. Used for when, facing and context clauses where
1328    /// the first `ident:` is a binding rather than a nested assignment.
1329    fn parse_clause_value_maybe_binding(
1330        &mut self,
1331        clause_start: Span,
1332        allow_binding: bool,
1333    ) -> Option<Expr> {
1334        if allow_binding
1335            && self.peek_kind().is_word()
1336            && self.peek_at(1).kind == TokenKind::Colon
1337        {
1338            // Check this isn't at the start of a new block item on the next line.
1339            // Bindings only apply on the same line or the immediate indented value.
1340            let clause_line = self.line_of(clause_start);
1341            let next_line = self.line_of(self.peek().span);
1342            let colon_is_block_item = next_line > clause_line
1343                && self.peek_at(2).kind != TokenKind::Eof
1344                && self.line_of(self.peek_at(2).span) == next_line;
1345
1346            if next_line == clause_line || colon_is_block_item {
1347                let name = self.parse_ident_in("binding name")?;
1348                self.advance(); // consume ':'
1349                let inner = self.parse_clause_value(clause_start)?;
1350                return Some(Expr::Binding {
1351                    span: name.span.merge(inner.span()),
1352                    name,
1353                    value: Box::new(inner),
1354                });
1355            }
1356        }
1357        self.parse_clause_value(clause_start)
1358    }
1359
1360    /// Parse a clause value. If the next token is on a new line (indented),
1361    /// collect a multi-line block. Otherwise parse a single expression.
1362    fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1363        let clause_line = self.line_of(clause_start);
1364        let next = self.peek();
1365        let next_line = self.line_of(next.span);
1366
1367        if next_line > clause_line {
1368            // Multi-line block — but only if the next token is actually
1369            // indented past the clause keyword. When a clause has only a
1370            // comment as its value (stripped by the lexer), the next visible
1371            // token is a sibling at the same indentation.
1372            let base_col = self.col_of(next.span);
1373            let clause_col = self.col_of(clause_start);
1374            if base_col <= clause_col {
1375                return Some(Expr::Block {
1376                    span: clause_start,
1377                    items: Vec::new(),
1378                });
1379            }
1380            self.parse_indented_block(base_col)
1381        } else {
1382            // Single-line clause value
1383            self.parse_expr(0)
1384        }
1385    }
1386
1387    /// Collect expressions that start at column >= `base_col` into a block.
1388    /// Also handles `let name = value` bindings inside clause value blocks.
1389    fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1390        let start = self.peek().span;
1391        let mut items = Vec::new();
1392
1393        while !self.at_eof()
1394            && !self.at(TokenKind::RBrace)
1395            && self.col_of(self.peek().span) >= base_col
1396        {
1397            // Handle `let name = value` inside expression blocks
1398            if self.at(TokenKind::Let) {
1399                let let_start = self.advance().span;
1400                if let Some(name) = self.parse_ident_in("binding name") {
1401                    if self.expect(TokenKind::Eq).is_some() {
1402                        if let Some(value) = self.parse_expr(0) {
1403                            items.push(Expr::LetExpr {
1404                                span: let_start.merge(value.span()),
1405                                name,
1406                                value: Box::new(value),
1407                            });
1408                            continue;
1409                        }
1410                    }
1411                }
1412                break;
1413            }
1414
1415            if let Some(expr) = self.parse_expr(0) {
1416                items.push(expr);
1417            } else {
1418                self.advance();
1419                break;
1420            }
1421        }
1422
1423        if items.len() == 1 {
1424            Some(items.pop().unwrap())
1425        } else {
1426            let end = items.last().map(|e| e.span()).unwrap_or(start);
1427            Some(Expr::Block {
1428                span: start.merge(end),
1429                items,
1430            })
1431        }
1432    }
1433}
1434
1435// ---------------------------------------------------------------------------
1436// Expression parsing — Pratt parser
1437// ---------------------------------------------------------------------------
1438
1439// Binding powers (even = left, odd = right for right-associative)
1440const BP_LAMBDA: u8 = 4;
1441const BP_WHEN_GUARD: u8 = 5;
1442const BP_PROJECTION: u8 = 6;
1443const BP_WITH_WHERE: u8 = 7;
1444const BP_IMPLIES: u8 = 8;
1445const BP_OR: u8 = 10;
1446const BP_AND: u8 = 20;
1447const BP_COMPARE: u8 = 30;
1448const BP_TRANSITION: u8 = 32;
1449const BP_NULL_COALESCE: u8 = 40;
1450const BP_ADD: u8 = 50;
1451const BP_MUL: u8 = 60;
1452const BP_PIPE: u8 = 65;
1453const BP_PREFIX: u8 = 70;
1454const BP_POSTFIX: u8 = 80;
1455
1456impl<'s> Parser<'s> {
1457    pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1458        let mut lhs = self.parse_prefix()?;
1459
1460        loop {
1461            if let Some((l_bp, r_bp)) = self.infix_bp() {
1462                if l_bp < min_bp {
1463                    break;
1464                }
1465                lhs = self.parse_infix(lhs, r_bp)?;
1466            } else if let Some(l_bp) = self.postfix_bp() {
1467                if l_bp < min_bp {
1468                    break;
1469                }
1470                lhs = self.parse_postfix(lhs)?;
1471            } else {
1472                break;
1473            }
1474        }
1475
1476        Some(lhs)
1477    }
1478
1479    // -- prefix ---------------------------------------------------------
1480
1481    fn parse_prefix(&mut self) -> Option<Expr> {
1482        match self.peek_kind() {
1483            TokenKind::Not => {
1484                let start = self.advance().span;
1485                if self.at(TokenKind::Exists) {
1486                    self.advance();
1487                    let operand = self.parse_expr(BP_PREFIX)?;
1488                    Some(Expr::NotExists {
1489                        span: start.merge(operand.span()),
1490                        operand: Box::new(operand),
1491                    })
1492                } else {
1493                    let operand = self.parse_expr(BP_PREFIX)?;
1494                    Some(Expr::Not {
1495                        span: start.merge(operand.span()),
1496                        operand: Box::new(operand),
1497                    })
1498                }
1499            }
1500            TokenKind::Exists => {
1501                // When `exists` is not followed by an expression-start token,
1502                // treat it as a plain identifier (e.g. `label: exists`).
1503                let next = self.peek_at(1).kind;
1504                if matches!(
1505                    next,
1506                    TokenKind::RParen
1507                        | TokenKind::RBrace
1508                        | TokenKind::RBracket
1509                        | TokenKind::Comma
1510                        | TokenKind::Eof
1511                ) {
1512                    let id = self.parse_ident()?;
1513                    return Some(Expr::Ident(id));
1514                }
1515                let start = self.advance().span;
1516                let operand = self.parse_expr(BP_PREFIX)?;
1517                Some(Expr::Exists {
1518                    span: start.merge(operand.span()),
1519                    operand: Box::new(operand),
1520                })
1521            }
1522            TokenKind::If => self.parse_if_expr(),
1523            TokenKind::For => self.parse_for_expr(),
1524            TokenKind::LBrace => self.parse_brace_expr(),
1525            TokenKind::LBracket => {
1526                let t = self.advance();
1527                self.error(t.span, "list literals `[...]` are not supported; use `Set<T>` type annotation or `{...}` set literal");
1528                None
1529            }
1530            TokenKind::LParen => self.parse_paren_expr(),
1531            TokenKind::Number => {
1532                let t = self.advance();
1533                Some(Expr::NumberLiteral {
1534                    span: t.span,
1535                    value: self.text(t.span).to_string(),
1536                })
1537            }
1538            TokenKind::Duration => {
1539                let t = self.advance();
1540                Some(Expr::DurationLiteral {
1541                    span: t.span,
1542                    value: self.text(t.span).to_string(),
1543                })
1544            }
1545            TokenKind::String => {
1546                let sl = self.parse_string()?;
1547                Some(Expr::StringLiteral(sl))
1548            }
1549            TokenKind::True => {
1550                let t = self.advance();
1551                Some(Expr::BoolLiteral {
1552                    span: t.span,
1553                    value: true,
1554                })
1555            }
1556            TokenKind::False => {
1557                let t = self.advance();
1558                Some(Expr::BoolLiteral {
1559                    span: t.span,
1560                    value: false,
1561                })
1562            }
1563            TokenKind::Null => {
1564                let t = self.advance();
1565                Some(Expr::Null { span: t.span })
1566            }
1567            TokenKind::Now => {
1568                let t = self.advance();
1569                Some(Expr::Now { span: t.span })
1570            }
1571            TokenKind::This => {
1572                let t = self.advance();
1573                Some(Expr::This { span: t.span })
1574            }
1575            TokenKind::Within => {
1576                let t = self.advance();
1577                Some(Expr::Within { span: t.span })
1578            }
1579            k if k.is_word() => {
1580                let id = self.parse_ident()?;
1581                Some(Expr::Ident(id))
1582            }
1583            TokenKind::Star => {
1584                // Wildcard `*` in type position (e.g. `Codec<*>`)
1585                let t = self.advance();
1586                Some(Expr::Ident(Ident {
1587                    span: t.span,
1588                    name: "*".into(),
1589                }))
1590            }
1591            TokenKind::Minus => {
1592                // Unary minus: -expr → BinaryOp(0, Sub, expr)
1593                let start = self.advance().span;
1594                let operand = self.parse_expr(BP_PREFIX)?;
1595                Some(Expr::BinaryOp {
1596                    span: start.merge(operand.span()),
1597                    left: Box::new(Expr::NumberLiteral {
1598                        span: start,
1599                        value: "0".into(),
1600                    }),
1601                    op: BinaryOp::Sub,
1602                    right: Box::new(operand),
1603                })
1604            }
1605            _ => {
1606                self.error(
1607                    self.peek().span,
1608                    format!(
1609                        "expected expression (identifier, number, string, true/false, null, \
1610                         if/for/not/exists, '(', '{{', '['), found {}",
1611                        self.peek_kind(),
1612                    ),
1613                );
1614                None
1615            }
1616        }
1617    }
1618
1619    // -- infix binding powers -------------------------------------------
1620
1621    fn infix_bp(&self) -> Option<(u8, u8)> {
1622        match self.peek_kind() {
1623            TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), // right-assoc
1624            // `when` as an inline guard on provides/related items
1625            TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1626            TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1627            TokenKind::Implies => Some((BP_IMPLIES, BP_IMPLIES - 1)), // right-assoc
1628            TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1629            TokenKind::And => Some((BP_AND, BP_AND + 1)),
1630            TokenKind::Eq | TokenKind::BangEq => {
1631                Some((BP_COMPARE, BP_COMPARE + 1))
1632            }
1633            TokenKind::Lt => {
1634                // If `<` is immediately adjacent to a word token (no space),
1635                // treat as generic type postfix, not comparison infix.
1636                if self.pos > 0 {
1637                    let prev = self.tokens[self.pos - 1];
1638                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1639                        return None;
1640                    }
1641                }
1642                Some((BP_COMPARE, BP_COMPARE + 1))
1643            }
1644            TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1645                Some((BP_COMPARE, BP_COMPARE + 1))
1646            }
1647            TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1648            // `not in` — only when followed by `in`
1649            TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1650                Some((BP_COMPARE, BP_COMPARE + 1))
1651            }
1652            TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1653            TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1654            TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1655            TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1656            TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1657            TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1658            TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1659            TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1660            _ => None,
1661        }
1662    }
1663
1664    fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1665        let op_tok = self.advance();
1666        match op_tok.kind {
1667            TokenKind::FatArrow => {
1668                let body = self.parse_expr(r_bp)?;
1669                Some(Expr::Lambda {
1670                    span: lhs.span().merge(body.span()),
1671                    param: Box::new(lhs),
1672                    body: Box::new(body),
1673                })
1674            }
1675            TokenKind::Pipe => {
1676                let rhs = self.parse_expr(r_bp)?;
1677                Some(Expr::Pipe {
1678                    span: lhs.span().merge(rhs.span()),
1679                    left: Box::new(lhs),
1680                    right: Box::new(rhs),
1681                })
1682            }
1683            TokenKind::Implies => {
1684                let rhs = self.parse_expr(r_bp)?;
1685                Some(Expr::LogicalOp {
1686                    span: lhs.span().merge(rhs.span()),
1687                    left: Box::new(lhs),
1688                    op: LogicalOp::Implies,
1689                    right: Box::new(rhs),
1690                })
1691            }
1692            TokenKind::Or => {
1693                let rhs = self.parse_expr(r_bp)?;
1694                Some(Expr::LogicalOp {
1695                    span: lhs.span().merge(rhs.span()),
1696                    left: Box::new(lhs),
1697                    op: LogicalOp::Or,
1698                    right: Box::new(rhs),
1699                })
1700            }
1701            TokenKind::And => {
1702                let rhs = self.parse_expr(r_bp)?;
1703                Some(Expr::LogicalOp {
1704                    span: lhs.span().merge(rhs.span()),
1705                    left: Box::new(lhs),
1706                    op: LogicalOp::And,
1707                    right: Box::new(rhs),
1708                })
1709            }
1710            TokenKind::Eq => {
1711                let rhs = self.parse_expr(r_bp)?;
1712                Some(Expr::Comparison {
1713                    span: lhs.span().merge(rhs.span()),
1714                    left: Box::new(lhs),
1715                    op: ComparisonOp::Eq,
1716                    right: Box::new(rhs),
1717                })
1718            }
1719            TokenKind::BangEq => {
1720                let rhs = self.parse_expr(r_bp)?;
1721                Some(Expr::Comparison {
1722                    span: lhs.span().merge(rhs.span()),
1723                    left: Box::new(lhs),
1724                    op: ComparisonOp::NotEq,
1725                    right: Box::new(rhs),
1726                })
1727            }
1728            TokenKind::Lt => {
1729                let rhs = self.parse_expr(r_bp)?;
1730                Some(Expr::Comparison {
1731                    span: lhs.span().merge(rhs.span()),
1732                    left: Box::new(lhs),
1733                    op: ComparisonOp::Lt,
1734                    right: Box::new(rhs),
1735                })
1736            }
1737            TokenKind::LtEq => {
1738                let rhs = self.parse_expr(r_bp)?;
1739                Some(Expr::Comparison {
1740                    span: lhs.span().merge(rhs.span()),
1741                    left: Box::new(lhs),
1742                    op: ComparisonOp::LtEq,
1743                    right: Box::new(rhs),
1744                })
1745            }
1746            TokenKind::Gt => {
1747                let rhs = self.parse_expr(r_bp)?;
1748                Some(Expr::Comparison {
1749                    span: lhs.span().merge(rhs.span()),
1750                    left: Box::new(lhs),
1751                    op: ComparisonOp::Gt,
1752                    right: Box::new(rhs),
1753                })
1754            }
1755            TokenKind::GtEq => {
1756                let rhs = self.parse_expr(r_bp)?;
1757                Some(Expr::Comparison {
1758                    span: lhs.span().merge(rhs.span()),
1759                    left: Box::new(lhs),
1760                    op: ComparisonOp::GtEq,
1761                    right: Box::new(rhs),
1762                })
1763            }
1764            TokenKind::In => {
1765                let rhs = self.parse_expr(r_bp)?;
1766                Some(Expr::In {
1767                    span: lhs.span().merge(rhs.span()),
1768                    element: Box::new(lhs),
1769                    collection: Box::new(rhs),
1770                })
1771            }
1772            TokenKind::Not => {
1773                // `not in`
1774                self.expect(TokenKind::In)?;
1775                let rhs = self.parse_expr(r_bp)?;
1776                Some(Expr::NotIn {
1777                    span: lhs.span().merge(rhs.span()),
1778                    element: Box::new(lhs),
1779                    collection: Box::new(rhs),
1780                })
1781            }
1782            TokenKind::Where => {
1783                let rhs = self.parse_expr(r_bp)?;
1784                Some(Expr::Where {
1785                    span: lhs.span().merge(rhs.span()),
1786                    source: Box::new(lhs),
1787                    condition: Box::new(rhs),
1788                })
1789            }
1790            TokenKind::With => {
1791                let rhs = self.parse_expr(r_bp)?;
1792                Some(Expr::With {
1793                    span: lhs.span().merge(rhs.span()),
1794                    source: Box::new(lhs),
1795                    predicate: Box::new(rhs),
1796                })
1797            }
1798            TokenKind::QuestionQuestion => {
1799                let rhs = self.parse_expr(r_bp)?;
1800                Some(Expr::NullCoalesce {
1801                    span: lhs.span().merge(rhs.span()),
1802                    left: Box::new(lhs),
1803                    right: Box::new(rhs),
1804                })
1805            }
1806            TokenKind::Plus => {
1807                let rhs = self.parse_expr(r_bp)?;
1808                Some(Expr::BinaryOp {
1809                    span: lhs.span().merge(rhs.span()),
1810                    left: Box::new(lhs),
1811                    op: BinaryOp::Add,
1812                    right: Box::new(rhs),
1813                })
1814            }
1815            TokenKind::Minus => {
1816                let rhs = self.parse_expr(r_bp)?;
1817                Some(Expr::BinaryOp {
1818                    span: lhs.span().merge(rhs.span()),
1819                    left: Box::new(lhs),
1820                    op: BinaryOp::Sub,
1821                    right: Box::new(rhs),
1822                })
1823            }
1824            TokenKind::Star => {
1825                let rhs = self.parse_expr(r_bp)?;
1826                Some(Expr::BinaryOp {
1827                    span: lhs.span().merge(rhs.span()),
1828                    left: Box::new(lhs),
1829                    op: BinaryOp::Mul,
1830                    right: Box::new(rhs),
1831                })
1832            }
1833            TokenKind::Slash => {
1834                // Check for qualified name: `alias/Name` or `alias/config`
1835                // Qualified if the LHS is a bare identifier and the RHS is a
1836                // word that either starts with uppercase or is a block keyword
1837                // (like `config`).
1838                if let Expr::Ident(ref id) = lhs {
1839                    if self.peek_kind().is_word() {
1840                        let next_text = self.text(self.peek().span);
1841                        let is_qualified = next_text
1842                            .chars()
1843                            .next()
1844                            .is_some_and(|c| c.is_uppercase())
1845                            || matches!(
1846                                self.peek_kind(),
1847                                TokenKind::Config | TokenKind::Entity | TokenKind::Value
1848                            );
1849                        if is_qualified {
1850                            let name_tok = self.advance();
1851                            return Some(Expr::QualifiedName(QualifiedName {
1852                                span: lhs.span().merge(name_tok.span),
1853                                qualifier: Some(id.name.clone()),
1854                                name: self.text(name_tok.span).to_string(),
1855                            }));
1856                        }
1857                    }
1858                }
1859                let rhs = self.parse_expr(r_bp)?;
1860                Some(Expr::BinaryOp {
1861                    span: lhs.span().merge(rhs.span()),
1862                    left: Box::new(lhs),
1863                    op: BinaryOp::Div,
1864                    right: Box::new(rhs),
1865                })
1866            }
1867            TokenKind::ThinArrow => {
1868                let field = self.parse_ident_in("projection field")?;
1869                Some(Expr::ProjectionMap {
1870                    span: lhs.span().merge(field.span),
1871                    source: Box::new(lhs),
1872                    field,
1873                })
1874            }
1875            TokenKind::TransitionsTo => {
1876                let rhs = self.parse_expr(r_bp)?;
1877                Some(Expr::TransitionsTo {
1878                    span: lhs.span().merge(rhs.span()),
1879                    subject: Box::new(lhs),
1880                    new_state: Box::new(rhs),
1881                })
1882            }
1883            TokenKind::Becomes => {
1884                let rhs = self.parse_expr(r_bp)?;
1885                Some(Expr::Becomes {
1886                    span: lhs.span().merge(rhs.span()),
1887                    subject: Box::new(lhs),
1888                    new_state: Box::new(rhs),
1889                })
1890            }
1891            TokenKind::When => {
1892                // Inline guard: `action when condition`
1893                let rhs = self.parse_expr(r_bp)?;
1894                Some(Expr::WhenGuard {
1895                    span: lhs.span().merge(rhs.span()),
1896                    action: Box::new(lhs),
1897                    condition: Box::new(rhs),
1898                })
1899            }
1900            _ => {
1901                self.error(
1902                    op_tok.span,
1903                    format!("unexpected infix operator {}", op_tok.kind),
1904                );
1905                None
1906            }
1907        }
1908    }
1909
1910    // -- postfix --------------------------------------------------------
1911
1912    fn postfix_bp(&self) -> Option<u8> {
1913        match self.peek_kind() {
1914            TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
1915            TokenKind::QuestionMark => Some(BP_POSTFIX),
1916            // `<` for generic types like `Set<T>`, `List<T>` — only treated
1917            // as postfix when it immediately follows a word with no space.
1918            TokenKind::Lt => {
1919                if self.pos > 0 {
1920                    let prev = self.tokens[self.pos - 1];
1921                    // Only if `<` starts immediately after the previous token
1922                    // (no whitespace gap) to distinguish from comparisons.
1923                    if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1924                        return Some(BP_POSTFIX);
1925                    }
1926                }
1927                None
1928            }
1929            TokenKind::LParen => Some(BP_POSTFIX),
1930            TokenKind::LBrace => {
1931                // Join lookup: only when preceded by something that looks
1932                // like an entity name (handled generically — any expr can
1933                // be followed by { for join lookup in expression position).
1934                // But only if the { is on the same line to avoid consuming
1935                // a block body.
1936                let next = self.peek();
1937                let prev_end = if self.pos > 0 {
1938                    self.tokens[self.pos - 1].span.end
1939                } else {
1940                    0
1941                };
1942                // Same line check
1943                if self.line_of(Span::new(prev_end, prev_end))
1944                    == self.line_of(next.span)
1945                {
1946                    Some(BP_POSTFIX)
1947                } else {
1948                    None
1949                }
1950            }
1951            _ => None,
1952        }
1953    }
1954
1955    fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
1956        match self.peek_kind() {
1957            TokenKind::QuestionMark => {
1958                let end = self.advance().span;
1959                Some(Expr::TypeOptional {
1960                    span: lhs.span().merge(end),
1961                    inner: Box::new(lhs),
1962                })
1963            }
1964            TokenKind::Lt => {
1965                // Generic type: `Set<T>`, `List<Node?>`
1966                self.advance(); // consume <
1967                let mut args = Vec::new();
1968                // Parse args above comparison BP so `>` isn't consumed as infix
1969                while !self.at(TokenKind::Gt) && !self.at_eof() {
1970                    args.push(self.parse_expr(BP_COMPARE + 1)?);
1971                    self.eat(TokenKind::Comma);
1972                }
1973                let end = self.expect(TokenKind::Gt)?.span;
1974                Some(Expr::GenericType {
1975                    span: lhs.span().merge(end),
1976                    name: Box::new(lhs),
1977                    args,
1978                })
1979            }
1980            TokenKind::Dot => {
1981                self.advance();
1982                let field = self.parse_ident_in("field name")?;
1983                Some(Expr::MemberAccess {
1984                    span: lhs.span().merge(field.span),
1985                    object: Box::new(lhs),
1986                    field,
1987                })
1988            }
1989            TokenKind::QuestionDot => {
1990                self.advance();
1991                let field = self.parse_ident_in("field name")?;
1992                Some(Expr::OptionalAccess {
1993                    span: lhs.span().merge(field.span),
1994                    object: Box::new(lhs),
1995                    field,
1996                })
1997            }
1998            TokenKind::LParen => {
1999                self.advance();
2000                let args = self.parse_call_args()?;
2001                let end = self.expect(TokenKind::RParen)?.span;
2002                Some(Expr::Call {
2003                    span: lhs.span().merge(end),
2004                    function: Box::new(lhs),
2005                    args,
2006                })
2007            }
2008            TokenKind::LBrace => {
2009                self.advance();
2010                let fields = self.parse_join_fields()?;
2011                let end = self.expect(TokenKind::RBrace)?.span;
2012                Some(Expr::JoinLookup {
2013                    span: lhs.span().merge(end),
2014                    entity: Box::new(lhs),
2015                    fields,
2016                })
2017            }
2018            _ => None,
2019        }
2020    }
2021
2022    // -- call arguments -------------------------------------------------
2023
2024    fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
2025        let mut args = Vec::new();
2026        while !self.at(TokenKind::RParen) && !self.at_eof() {
2027            // Check for named argument: `name: value`
2028            if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2029                let name = self.parse_ident_in("argument name")?;
2030                self.advance(); // :
2031                let value = self.parse_expr(0)?;
2032                args.push(CallArg::Named(NamedArg {
2033                    span: name.span.merge(value.span()),
2034                    name,
2035                    value,
2036                }));
2037            } else {
2038                let expr = self.parse_expr(0)?;
2039                args.push(CallArg::Positional(expr));
2040            }
2041            self.eat(TokenKind::Comma);
2042        }
2043        Some(args)
2044    }
2045
2046    // -- join fields ----------------------------------------------------
2047
2048    fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
2049        let mut fields = Vec::new();
2050        while !self.at(TokenKind::RBrace) && !self.at_eof() {
2051            let field = self.parse_ident_in("join field name")?;
2052            let value = if self.eat(TokenKind::Colon).is_some() {
2053                Some(self.parse_expr(0)?)
2054            } else {
2055                None
2056            };
2057            fields.push(JoinField {
2058                span: field.span.merge(
2059                    value
2060                        .as_ref()
2061                        .map(|v| v.span())
2062                        .unwrap_or(field.span),
2063                ),
2064                field,
2065                value,
2066            });
2067            self.eat(TokenKind::Comma);
2068        }
2069        Some(fields)
2070    }
2071
2072    // -- if expression --------------------------------------------------
2073
2074    fn parse_if_expr(&mut self) -> Option<Expr> {
2075        let start = self.advance().span; // consume `if`
2076        let mut branches = Vec::new();
2077
2078        // First branch
2079        let condition = self.parse_expr(0)?;
2080        self.expect(TokenKind::Colon)?;
2081        let body = self.parse_branch_body(start)?;
2082        branches.push(CondBranch {
2083            span: start.merge(body.span()),
2084            condition,
2085            body,
2086        });
2087
2088        // else if / else
2089        let mut else_body = None;
2090        while self.at(TokenKind::Else) {
2091            let else_tok = self.advance();
2092            if self.at(TokenKind::If) {
2093                let if_start = self.advance().span;
2094                let cond = self.parse_expr(0)?;
2095                self.expect(TokenKind::Colon)?;
2096                let body = self.parse_branch_body(else_tok.span)?;
2097                branches.push(CondBranch {
2098                    span: if_start.merge(body.span()),
2099                    condition: cond,
2100                    body,
2101                });
2102            } else {
2103                self.expect(TokenKind::Colon)?;
2104                let body = self.parse_branch_body(else_tok.span)?;
2105                else_body = Some(Box::new(body));
2106                break;
2107            }
2108        }
2109
2110        let end = else_body
2111            .as_ref()
2112            .map(|b| b.span())
2113            .or_else(|| branches.last().map(|b| b.body.span()))
2114            .unwrap_or(start);
2115
2116        Some(Expr::Conditional {
2117            span: start.merge(end),
2118            branches,
2119            else_body,
2120        })
2121    }
2122
2123    fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
2124        let keyword_line = self.line_of(keyword_span);
2125        let next_line = self.line_of(self.peek().span);
2126
2127        if next_line > keyword_line {
2128            let base_col = self.col_of(self.peek().span);
2129            self.parse_indented_block(base_col)
2130        } else {
2131            self.parse_expr(0)
2132        }
2133    }
2134
2135    // -- for expression -------------------------------------------------
2136
2137    fn parse_for_expr(&mut self) -> Option<Expr> {
2138        let start = self.advance().span; // consume `for`
2139        let binding = self.parse_for_binding()?;
2140        self.expect(TokenKind::In)?;
2141
2142        // Parse collection, stopping before `where` and `:`
2143        let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
2144
2145        let filter = if self.eat(TokenKind::Where).is_some() {
2146            // Parse filter at min_bp 0 — colon terminates naturally.
2147            Some(Box::new(self.parse_expr(0)?))
2148        } else {
2149            None
2150        };
2151
2152        self.expect(TokenKind::Colon)?;
2153        let body = self.parse_branch_body(start)?;
2154
2155        Some(Expr::For {
2156            span: start.merge(body.span()),
2157            binding,
2158            collection: Box::new(collection),
2159            filter,
2160            body: Box::new(body),
2161        })
2162    }
2163
2164    // -- brace expressions: set literal or object literal ---------------
2165
2166    fn parse_brace_expr(&mut self) -> Option<Expr> {
2167        let start = self.advance().span; // consume {
2168
2169        if self.at(TokenKind::RBrace) {
2170            let end = self.advance().span;
2171            return Some(Expr::SetLiteral {
2172                span: start.merge(end),
2173                elements: Vec::new(),
2174            });
2175        }
2176
2177        // Peek: if first item is `ident:`, it's an object literal
2178        if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2179            return self.parse_object_literal(start);
2180        }
2181
2182        // Otherwise set literal
2183        self.parse_set_literal(start)
2184    }
2185
2186
2187    fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
2188        let mut fields = Vec::new();
2189        while !self.at(TokenKind::RBrace) && !self.at_eof() {
2190            let name = self.parse_ident_in("field name")?;
2191            self.expect(TokenKind::Colon)?;
2192            let value = self.parse_expr(0)?;
2193            fields.push(NamedArg {
2194                span: name.span.merge(value.span()),
2195                name,
2196                value,
2197            });
2198            self.eat(TokenKind::Comma);
2199        }
2200        let end = self.expect(TokenKind::RBrace)?.span;
2201        Some(Expr::ObjectLiteral {
2202            span: start.merge(end),
2203            fields,
2204        })
2205    }
2206
2207    fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
2208        let mut elements = Vec::new();
2209        while !self.at(TokenKind::RBrace) && !self.at_eof() {
2210            elements.push(self.parse_expr(0)?);
2211            self.eat(TokenKind::Comma);
2212        }
2213        let end = self.expect(TokenKind::RBrace)?.span;
2214        Some(Expr::SetLiteral {
2215            span: start.merge(end),
2216            elements,
2217        })
2218    }
2219
2220    // -- parenthesised expression ---------------------------------------
2221
2222    fn parse_paren_expr(&mut self) -> Option<Expr> {
2223        let start = self.advance().span; // (
2224
2225        // Detect `(name: expr, ...)` — typed signature parameters.
2226        if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2227            let mut bindings = Vec::new();
2228            while !self.at(TokenKind::RParen) && !self.at_eof() {
2229                let name = self.parse_ident_in("parameter name")?;
2230                self.expect(TokenKind::Colon)?;
2231                let value = self.parse_expr(0)?;
2232                bindings.push(Expr::Binding {
2233                    span: name.span.merge(value.span()),
2234                    name,
2235                    value: Box::new(value),
2236                });
2237                self.eat(TokenKind::Comma);
2238            }
2239            self.expect(TokenKind::RParen)?;
2240            if bindings.len() == 1 {
2241                return Some(bindings.into_iter().next().unwrap());
2242            }
2243            let span = start.merge(bindings.last().unwrap().span());
2244            return Some(Expr::Block {
2245                span,
2246                items: bindings,
2247            });
2248        }
2249
2250        let expr = self.parse_expr(0)?;
2251        self.expect(TokenKind::RParen)?;
2252        Some(expr)
2253    }
2254}
2255
2256// ---------------------------------------------------------------------------
2257// Tests
2258// ---------------------------------------------------------------------------
2259
2260#[cfg(test)]
2261mod tests {
2262    use super::*;
2263    use crate::diagnostic::Severity;
2264
2265    fn parse_ok(src: &str) -> ParseResult {
2266        // Prefix with version marker if not already present, to avoid
2267        // spurious "missing version marker" warnings in every test.
2268        let owned;
2269        let input = if src.starts_with("-- allium:") {
2270            src
2271        } else {
2272            owned = format!("-- allium: 1\n{src}");
2273            &owned
2274        };
2275        let result = parse(input);
2276        if !result.diagnostics.is_empty() {
2277            for d in &result.diagnostics {
2278                eprintln!(
2279                    "  [{:?}] {} ({}..{})",
2280                    d.severity, d.message, d.span.start, d.span.end
2281                );
2282            }
2283        }
2284        result
2285    }
2286
2287    #[test]
2288    fn version_marker() {
2289        let r = parse_ok("-- allium: 1\n");
2290        assert_eq!(r.module.version, Some(1));
2291        assert_eq!(r.diagnostics.len(), 0);
2292    }
2293
2294    #[test]
2295    fn version_missing_warns() {
2296        let r = parse("entity User {}");
2297        assert_eq!(r.module.version, None);
2298        assert_eq!(r.diagnostics.len(), 1);
2299        assert_eq!(r.diagnostics[0].severity, Severity::Warning);
2300        assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
2301    }
2302
2303    #[test]
2304    fn version_unsupported_errors() {
2305        let r = parse("-- allium: 99\nentity User {}");
2306        assert_eq!(r.module.version, Some(99));
2307        assert!(r.diagnostics.iter().any(|d|
2308            d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
2309        ), "expected unsupported version error, got: {:?}", r.diagnostics);
2310    }
2311
2312    #[test]
2313    fn empty_entity() {
2314        let r = parse_ok("entity User {}");
2315        assert_eq!(r.diagnostics.len(), 0);
2316        assert_eq!(r.module.declarations.len(), 1);
2317        match &r.module.declarations[0] {
2318            Decl::Block(b) => {
2319                assert_eq!(b.kind, BlockKind::Entity);
2320                assert_eq!(b.name.as_ref().unwrap().name, "User");
2321            }
2322            other => panic!("expected Block, got {other:?}"),
2323        }
2324    }
2325
2326    #[test]
2327    fn entity_with_fields() {
2328        let src = r#"entity Order {
2329    customer: Customer
2330    status: pending | active | completed
2331    total: Decimal
2332}"#;
2333        let r = parse_ok(src);
2334        assert_eq!(r.diagnostics.len(), 0);
2335        match &r.module.declarations[0] {
2336            Decl::Block(b) => {
2337                assert_eq!(b.items.len(), 3);
2338            }
2339            other => panic!("expected Block, got {other:?}"),
2340        }
2341    }
2342
2343    #[test]
2344    fn use_declaration() {
2345        let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
2346        assert_eq!(r.diagnostics.len(), 0);
2347        match &r.module.declarations[0] {
2348            Decl::Use(u) => {
2349                assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
2350            }
2351            other => panic!("expected Use, got {other:?}"),
2352        }
2353    }
2354
2355    #[test]
2356    fn enum_declaration() {
2357        let src = "enum OrderStatus { pending | shipped | delivered }";
2358        let r = parse_ok(src);
2359        assert_eq!(r.diagnostics.len(), 0);
2360    }
2361
2362    #[test]
2363    fn config_block() {
2364        let src = r#"config {
2365    max_retries: Integer = 3
2366    timeout: Duration = 24.hours
2367}"#;
2368        // Config entries are `name: Type = default`. The parser sees
2369        // `name: Type = default` as an assignment where the value is
2370        // `Type = default` (comparison with Eq). That's fine for the
2371        // parse tree — semantic pass separates type from default.
2372        let r = parse_ok(src);
2373        assert_eq!(r.diagnostics.len(), 0);
2374    }
2375
2376    #[test]
2377    fn rule_declaration() {
2378        let src = r#"rule PlaceOrder {
2379    when: CustomerPlacesOrder(customer, items, total)
2380    requires: total > 0
2381    ensures: Order.created(customer: customer, status: pending, total: total)
2382}"#;
2383        let r = parse_ok(src);
2384        assert_eq!(r.diagnostics.len(), 0);
2385        match &r.module.declarations[0] {
2386            Decl::Block(b) => {
2387                assert_eq!(b.kind, BlockKind::Rule);
2388                assert_eq!(b.items.len(), 3);
2389            }
2390            other => panic!("expected Block, got {other:?}"),
2391        }
2392    }
2393
2394    #[test]
2395    fn expression_precedence() {
2396        let r = parse_ok("rule T { v: a + b * c }");
2397        // The value should be Add(a, Mul(b, c))
2398        match &r.module.declarations[0] {
2399            Decl::Block(b) => match &b.items[0].kind {
2400                BlockItemKind::Assignment { value, .. } => match value {
2401                    Expr::BinaryOp { op, right, .. } => {
2402                        assert_eq!(*op, BinaryOp::Add);
2403                        assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2404                    }
2405                    other => panic!("expected BinaryOp, got {other:?}"),
2406                },
2407                other => panic!("expected Assignment, got {other:?}"),
2408            },
2409            other => panic!("expected Block, got {other:?}"),
2410        }
2411    }
2412
2413    #[test]
2414    fn default_declaration() {
2415        let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2416        let r = parse_ok(src);
2417        assert_eq!(r.diagnostics.len(), 0);
2418    }
2419
2420    #[test]
2421    fn open_question() {
2422        let src = r#"open question "Should admins be role-specific?""#;
2423        let r = parse_ok(src);
2424        assert_eq!(r.diagnostics.len(), 0);
2425    }
2426
2427    #[test]
2428    fn external_entity() {
2429        let src = "external entity Customer { email: String }";
2430        let r = parse_ok(src);
2431        assert_eq!(r.diagnostics.len(), 0);
2432        match &r.module.declarations[0] {
2433            Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2434            other => panic!("expected Block, got {other:?}"),
2435        }
2436    }
2437
2438    #[test]
2439    fn where_expression() {
2440        let src = "entity E { active: items where status = active }";
2441        let r = parse_ok(src);
2442        assert_eq!(r.diagnostics.len(), 0);
2443    }
2444
2445    #[test]
2446    fn with_expression() {
2447        let src = "entity E { slots: InterviewSlot with candidacy = this }";
2448        let r = parse_ok(src);
2449        assert_eq!(r.diagnostics.len(), 0);
2450    }
2451
2452    #[test]
2453    fn lambda_expression() {
2454        let src = "entity E { v: items.any(i => i.active) }";
2455        let r = parse_ok(src);
2456        assert_eq!(r.diagnostics.len(), 0);
2457    }
2458
2459    #[test]
2460    fn deferred() {
2461        let src = "deferred InterviewerMatching.suggest";
2462        let r = parse_ok(src);
2463        assert_eq!(r.diagnostics.len(), 0);
2464    }
2465
2466    #[test]
2467    fn variant_declaration() {
2468        let src = "variant Email : Notification { subject: String }";
2469        let r = parse_ok(src);
2470        assert_eq!(r.diagnostics.len(), 0);
2471    }
2472
2473    // -- projection mapping -----------------------------------------------
2474
2475    #[test]
2476    fn projection_arrow() {
2477        let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2478        let r = parse_ok(src);
2479        assert_eq!(r.diagnostics.len(), 0);
2480    }
2481
2482    // -- transitions_to / becomes ------------------------------------------
2483
2484    #[test]
2485    fn transitions_to_trigger() {
2486        let src = "rule R { when: Interview.status transitions_to scheduled\n    ensures: Notification.created() }";
2487        let r = parse_ok(src);
2488        assert_eq!(r.diagnostics.len(), 0);
2489    }
2490
2491    #[test]
2492    fn becomes_trigger() {
2493        let src = "rule R { when: Interview.status becomes scheduled\n    ensures: Notification.created() }";
2494        let r = parse_ok(src);
2495        assert_eq!(r.diagnostics.len(), 0);
2496    }
2497
2498    // -- binding colon in clause values ------------------------------------
2499
2500    #[test]
2501    fn when_binding() {
2502        let src = "rule R {\n    when: interview: Interview.status transitions_to scheduled\n    ensures: Notification.created()\n}";
2503        let r = parse_ok(src);
2504        assert_eq!(r.diagnostics.len(), 0);
2505        // The when clause value should be a Binding wrapping a TransitionsTo
2506        let decl = &r.module.declarations[0];
2507        if let Decl::Block(b) = decl {
2508            if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2509                assert_eq!(keyword, "when");
2510                assert!(matches!(value, Expr::Binding { .. }));
2511            } else {
2512                panic!("expected clause");
2513            }
2514        } else {
2515            panic!("expected block decl");
2516        }
2517    }
2518
2519    #[test]
2520    fn when_binding_temporal() {
2521        let src = "rule R {\n    when: invitation: Invitation.expires_at <= now\n    ensures: Invitation.expired()\n}";
2522        let r = parse_ok(src);
2523        assert_eq!(r.diagnostics.len(), 0);
2524    }
2525
2526    #[test]
2527    fn when_binding_created() {
2528        let src = "rule R {\n    when: batch: DigestBatch.created\n    ensures: Email.created()\n}";
2529        let r = parse_ok(src);
2530        assert_eq!(r.diagnostics.len(), 0);
2531    }
2532
2533    #[test]
2534    fn facing_binding() {
2535        let src = "surface S {\n    facing viewer: Interviewer\n    exposes: InterviewList\n}";
2536        let r = parse_ok(src);
2537        assert_eq!(r.diagnostics.len(), 0);
2538    }
2539
2540    #[test]
2541    fn context_binding() {
2542        let src = "surface S {\n    facing viewer: Interviewer\n    context assignment: SlotConfirmation where interviewer = viewer\n}";
2543        let r = parse_ok(src);
2544        assert_eq!(r.diagnostics.len(), 0);
2545    }
2546
2547    // -- rule-level for block item -----------------------------------------
2548
2549    #[test]
2550    fn rule_level_for() {
2551        let src = r#"rule ProcessDigests {
2552    when: schedule: DigestSchedule.next_run_at <= now
2553    for user in Users where notification_setting.digest_enabled:
2554        ensures: DigestBatch.created(user: user)
2555}"#;
2556        let r = parse_ok(src);
2557        assert_eq!(r.diagnostics.len(), 0);
2558        if let Decl::Block(b) = &r.module.declarations[0] {
2559            // Should have when clause + for block item
2560            assert!(b.items.len() >= 2);
2561            assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2562        } else {
2563            panic!("expected block decl");
2564        }
2565    }
2566
2567    // -- let inside ensures blocks -----------------------------------------
2568
2569    #[test]
2570    fn let_in_ensures_block() {
2571        let src = r#"rule R {
2572    when: ScheduleInterview(candidacy, time, interviewers)
2573    ensures:
2574        let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2575        for interviewer in interviewers:
2576            SlotConfirmation.created(slot: slot, interviewer: interviewer)
2577}"#;
2578        let r = parse_ok(src);
2579        assert_eq!(r.diagnostics.len(), 0);
2580    }
2581
2582    // -- when guard on provides items --------------------------------------
2583
2584    #[test]
2585    fn provides_when_guard() {
2586        let src = "surface S {\n    facing viewer: Interviewer\n    provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2587        let r = parse_ok(src);
2588        assert_eq!(r.diagnostics.len(), 0);
2589    }
2590
2591    // -- optional type suffix ----------------------------------------------
2592
2593    #[test]
2594    fn optional_type_suffix() {
2595        let src = "entity E { locked_until: Timestamp? }";
2596        let r = parse_ok(src);
2597        assert_eq!(r.diagnostics.len(), 0);
2598    }
2599
2600    #[test]
2601    fn optional_trigger_param() {
2602        let src = "rule R { when: Report(interviewer, interview, reason, details?)\n    ensures: Done() }";
2603        let r = parse_ok(src);
2604        assert_eq!(r.diagnostics.len(), 0);
2605    }
2606
2607    // -- qualified name with config ----------------------------------------
2608
2609    #[test]
2610    fn qualified_config_access() {
2611        let src = "entity E { duration: oauth/config.session_duration }";
2612        let r = parse_ok(src);
2613        assert_eq!(r.diagnostics.len(), 0);
2614    }
2615
2616    // -- comprehensive integration test ------------------------------------
2617
2618    #[test]
2619    fn realistic_spec() {
2620        let src = r#"-- allium: 1
2621
2622enum OrderStatus { pending | shipped | delivered }
2623
2624external entity Customer {
2625    email: String
2626    name: String
2627}
2628
2629entity Order {
2630    customer: Customer
2631    status: OrderStatus
2632    total: Decimal
2633    items: OrderItem with order = this
2634    shipped_items: items where status = shipped
2635    confirmed_items: items where status = confirmed -> item
2636    is_complete: status = delivered
2637    locked_until: Timestamp?
2638}
2639
2640config {
2641    max_retries: Integer = 3
2642    timeout: Duration = 24.hours
2643}
2644
2645rule PlaceOrder {
2646    when: CustomerPlacesOrder(customer, items, total)
2647    requires: total > 0
2648    ensures: Order.created(customer: customer, status: pending, total: total)
2649}
2650
2651rule ShipOrder {
2652    when: order: Order.status transitions_to shipped
2653    ensures: Email.created(to: order.customer.email, template: order_shipped)
2654}
2655
2656open question "How do we handle partial shipments?"
2657"#;
2658        let r = parse_ok(src);
2659        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2660        assert_eq!(r.module.version, Some(1));
2661        assert_eq!(r.module.declarations.len(), 7);
2662    }
2663
2664    #[test]
2665    fn extension_behaviour_excerpt() {
2666        // Exercises: inline enums, generic types, or-triggers, named call
2667        // args, config with typed defaults, module declaration.
2668        let src = r#"value Document {
2669    uri: String
2670    text: String
2671}
2672
2673entity Finding {
2674    code: String
2675    severity: error | warning | info
2676    range: FindingRange
2677}
2678
2679entity DiagnosticsMode {
2680    value: strict | relaxed
2681}
2682
2683config {
2684    duplicateKey: String = "allium.config.duplicateKey"
2685}
2686
2687rule RefreshDiagnostics {
2688    when: DocumentOpened(document) or DocumentChanged(document)
2689    requires: document.language_id = "allium"
2690    ensures: FindingsComputed(document)
2691}
2692
2693surface DiagnosticsDashboard {
2694    facing viewer: Developer
2695    context doc: Document where viewer.active_document = doc
2696    provides: RunChecks(viewer) when doc.language_id = "allium"
2697    exposes: FindingList
2698}
2699
2700rule ProcessDigests {
2701    when: schedule: DigestSchedule.next_run_at <= now
2702    for user in Users where notification_setting.digest_enabled:
2703        let settings = user.notification_setting
2704        ensures: DigestBatch.created(user: user)
2705}
2706"#;
2707        let r = parse_ok(src);
2708        assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2709        // value + entity + entity + config + rule + surface + rule = 7
2710        assert_eq!(r.module.declarations.len(), 7);
2711    }
2712
2713    #[test]
2714    fn exists_as_identifier() {
2715        let src = r#"rule R {
2716    when: X()
2717    ensures: CompletionItemAvailable(label: exists)
2718}"#;
2719        let r = parse_ok(src);
2720        assert_eq!(r.diagnostics.len(), 0);
2721    }
2722
2723    // -- pipe precedence: tighter than boolean ops ----------------------------
2724
2725    #[test]
2726    fn pipe_binds_tighter_than_or() {
2727        // `a or b | c` should parse as `a or (b | c)`, not `(a or b) | c`
2728        let src = "entity E { v: a or b | c }";
2729        let r = parse_ok(src);
2730        assert_eq!(r.diagnostics.len(), 0);
2731        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2732        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2733        // Top-level should be LogicalOp(Or)
2734        let Expr::LogicalOp { op, right, .. } = value else {
2735            panic!("expected LogicalOp, got {value:?}");
2736        };
2737        assert_eq!(*op, LogicalOp::Or);
2738        // Right side should be Pipe(b, c)
2739        assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2740    }
2741
2742    // -- variant with expression base -----------------------------------------
2743
2744    #[test]
2745    fn variant_with_pipe_base() {
2746        let src = "variant Mixed : TypeA | TypeB";
2747        let r = parse_ok(src);
2748        assert_eq!(r.diagnostics.len(), 0);
2749        let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2750        assert!(matches!(v.base, Expr::Pipe { .. }));
2751    }
2752
2753    // -- for-block with comparison in where filter ----------------------------
2754
2755    #[test]
2756    fn for_block_where_comparison() {
2757        let src = r#"rule R {
2758    when: X()
2759    for item in Items where item.status = active:
2760        ensures: Processed(item: item)
2761}"#;
2762        let r = parse_ok(src);
2763        assert_eq!(r.diagnostics.len(), 0);
2764        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2765        let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2766        assert!(filter.is_some());
2767        assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2768    }
2769
2770    // -- for-expression with comparison in where filter -----------------------
2771
2772    #[test]
2773    fn for_expr_where_comparison() {
2774        let src = r#"rule R {
2775    when: X()
2776    ensures:
2777        for item in Items where item.active = true:
2778            Processed(item: item)
2779}"#;
2780        let r = parse_ok(src);
2781        assert_eq!(r.diagnostics.len(), 0);
2782    }
2783
2784    // -- if/else if/else chain ------------------------------------------------
2785
2786    #[test]
2787    fn if_else_if_else() {
2788        let src = r#"rule R {
2789    when: X(v)
2790    ensures:
2791        if v < 10: Small()
2792        else if v < 100: Medium()
2793        else: Large()
2794}"#;
2795        let r = parse_ok(src);
2796        assert_eq!(r.diagnostics.len(), 0);
2797    }
2798
2799    // -- null coalescing and optional chaining --------------------------------
2800
2801    #[test]
2802    fn null_coalesce_and_optional_chain() {
2803        let src = "entity E { v: a?.b ?? fallback }";
2804        let r = parse_ok(src);
2805        assert_eq!(r.diagnostics.len(), 0);
2806        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2807        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2808        // Top-level should be NullCoalesce
2809        assert!(matches!(value, Expr::NullCoalesce { .. }));
2810    }
2811
2812    // -- generic types --------------------------------------------------------
2813
2814    #[test]
2815    fn generic_type_nested() {
2816        let src = "entity E { v: List<Set<String>> }";
2817        let r = parse_ok(src);
2818        assert_eq!(r.diagnostics.len(), 0);
2819    }
2820
2821    // -- set literal, list literal, object literal ----------------------------
2822
2823    #[test]
2824    fn collection_literals() {
2825        let src = r#"rule R {
2826    when: X()
2827    ensures:
2828        let s = {a, b, c}
2829        let o = {name: "test", count: 42}
2830        Done()
2831}"#;
2832        let r = parse_ok(src);
2833        assert_eq!(r.diagnostics.len(), 0);
2834    }
2835
2836    #[test]
2837    fn spec_reject_list_literal() {
2838        // The spec does not define `[...]` list literal syntax.
2839        let src = r#"rule R {
2840    when: X()
2841    ensures:
2842        let l = [1, 2, 3]
2843        Done()
2844}"#;
2845        let r = parse_ok(src);
2846        assert!(
2847            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2848            "expected error for `[...]` list literal (not in spec), but parsed without errors"
2849        );
2850    }
2851
2852    // -- given block ----------------------------------------------------------
2853
2854    #[test]
2855    fn given_block() {
2856        let src = "given { viewer: User\n    time: Timestamp }";
2857        let r = parse_ok(src);
2858        assert_eq!(r.diagnostics.len(), 0);
2859        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2860        assert_eq!(b.kind, BlockKind::Given);
2861        assert!(b.name.is_none());
2862    }
2863
2864    // -- actor block ----------------------------------------------------------
2865
2866    #[test]
2867    fn actor_block() {
2868        let src = "actor Admin { identified_by: User where role = admin }";
2869        let r = parse_ok(src);
2870        assert_eq!(r.diagnostics.len(), 0);
2871        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2872        assert_eq!(b.kind, BlockKind::Actor);
2873    }
2874
2875    // -- join lookup ----------------------------------------------------------
2876
2877    #[test]
2878    fn join_lookup() {
2879        let src = "entity E { match: Other{field_a, field_b: value} }";
2880        let r = parse_ok(src);
2881        assert_eq!(r.diagnostics.len(), 0);
2882        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2883        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2884        assert!(matches!(value, Expr::JoinLookup { .. }));
2885    }
2886
2887    // -- in / not in with set literal -----------------------------------------
2888
2889    #[test]
2890    fn in_not_in_set() {
2891        let src = r#"rule R {
2892    when: X(s)
2893    requires: s in {a, b, c}
2894    requires: s not in {d, e}
2895    ensures: Done()
2896}"#;
2897        let r = parse_ok(src);
2898        assert_eq!(r.diagnostics.len(), 0);
2899    }
2900
2901    // -- comprehensive fixture file -------------------------------------------
2902
2903    #[test]
2904    fn comprehensive_fixture() {
2905        let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
2906        let r = parse(src);
2907        assert_eq!(
2908            r.diagnostics.len(),
2909            0,
2910            "expected no errors in comprehensive fixture, got: {:?}",
2911            r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
2912        );
2913        assert!(r.module.declarations.len() > 30, "expected many declarations");
2914    }
2915
2916    // -- error message quality ------------------------------------------------
2917
2918    #[test]
2919    fn error_expected_declaration() {
2920        let r = parse("-- allium: 1\n+ invalid");
2921        assert!(r.diagnostics.len() >= 1);
2922        let msg = &r.diagnostics[0].message;
2923        assert!(msg.contains("expected declaration"), "got: {msg}");
2924        assert!(msg.contains("entity"), "should list valid options, got: {msg}");
2925        assert!(msg.contains("rule"), "should list valid options, got: {msg}");
2926    }
2927
2928    #[test]
2929    fn error_expected_expression() {
2930        let r = parse("-- allium: 1\nentity E { v: }");
2931        assert!(r.diagnostics.len() >= 1);
2932        let msg = &r.diagnostics[0].message;
2933        assert!(msg.contains("expected expression"), "got: {msg}");
2934        assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
2935    }
2936
2937    #[test]
2938    fn error_expected_block_item() {
2939        let r = parse("-- allium: 1\nentity E { + }");
2940        assert!(r.diagnostics.len() >= 1);
2941        let msg = &r.diagnostics[0].message;
2942        assert!(msg.contains("expected block item"), "got: {msg}");
2943    }
2944
2945    #[test]
2946    fn error_expected_identifier() {
2947        let r = parse("-- allium: 1\nentity 123 {}");
2948        assert!(r.diagnostics.len() >= 1);
2949        let msg = &r.diagnostics[0].message;
2950        // Context-aware: says "entity name" not generic "identifier"
2951        assert!(msg.contains("expected entity name"), "got: {msg}");
2952        // Human-friendly: says "number" not "Number" or "TokenKind::Number"
2953        assert!(msg.contains("number"), "should say what was found, got: {msg}");
2954    }
2955
2956    #[test]
2957    fn error_missing_brace() {
2958        let r = parse("entity E {");
2959        assert!(r.diagnostics.len() >= 1);
2960        let msg = &r.diagnostics[0].message;
2961        assert!(msg.contains("expected"), "got: {msg}");
2962    }
2963
2964    #[test]
2965    fn error_recovery_multiple() {
2966        // Parser should recover and report multiple errors (on separate lines)
2967        let r = parse("entity E { + }\nentity F { - }");
2968        assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
2969    }
2970
2971    #[test]
2972    fn error_dedup_same_line() {
2973        // Multiple bad tokens on a single line should produce only one error
2974        let r = parse("-- allium: 1\n+ - * /");
2975        let errors: Vec<_> = r.diagnostics.iter()
2976            .filter(|d| d.severity == crate::diagnostic::Severity::Error)
2977            .collect();
2978        assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
2979    }
2980
2981    #[test]
2982    fn for_block() {
2983        let src = r#"rule R {
2984    when: X()
2985    for user in Users where user.active:
2986        ensures: Notified(user: user)
2987}"#;
2988        let r = parse_ok(src);
2989        assert_eq!(r.diagnostics.len(), 0);
2990        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2991        assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2992    }
2993
2994    #[test]
2995    fn for_expr() {
2996        let src = r#"rule R {
2997    when: X(project)
2998    ensures:
2999        let total = for task in project.tasks: task.effort
3000        Done(total: total)
3001}"#;
3002        let r = parse_ok(src);
3003        assert_eq!(r.diagnostics.len(), 0);
3004    }
3005
3006    #[test]
3007    fn for_where() {
3008        let src = r#"rule R {
3009    when: X()
3010    for item in Items where item.active:
3011        ensures: Processed(item: item)
3012}"#;
3013        let r = parse_ok(src);
3014        assert_eq!(r.diagnostics.len(), 0);
3015    }
3016
3017    #[test]
3018    fn spec_reject_for_with_filter() {
3019        // The spec uses `where` for iteration filtering; `with` is for
3020        // relationship declarations only.
3021        let src = r#"rule R {
3022    when: X()
3023    for slot in Slot with slot.role = reviewer:
3024        ensures: Reviewed(slot: slot)
3025}"#;
3026        let r = parse_ok(src);
3027        assert!(
3028            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3029            "expected error for `for ... with` (spec uses `where`), but parsed without errors"
3030        );
3031    }
3032
3033    #[test]
3034    fn block_level_if() {
3035        let src = r#"rule R {
3036    when: X(task)
3037    if task.priority = high:
3038        ensures: Escalated(task: task)
3039}"#;
3040        let r = parse_ok(src);
3041        assert_eq!(r.diagnostics.len(), 0);
3042        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3043        let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3044            panic!("expected IfBlock, got {:?}", b.items[1].kind);
3045        };
3046        assert_eq!(branches.len(), 1);
3047        assert!(else_items.is_none());
3048    }
3049
3050    #[test]
3051    fn block_level_if_else() {
3052        let src = r#"rule R {
3053    when: X(score)
3054    if score > 80:
3055        ensures: High()
3056    else if score > 40:
3057        ensures: Medium()
3058    else:
3059        ensures: Low()
3060}"#;
3061        let r = parse_ok(src);
3062        assert_eq!(r.diagnostics.len(), 0);
3063        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3064        let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3065            panic!("expected IfBlock, got {:?}", b.items[1].kind);
3066        };
3067        assert_eq!(branches.len(), 2);
3068        assert!(else_items.is_some());
3069    }
3070
3071    #[test]
3072    fn wildcard_type_parameter() {
3073        let src = "entity E { codec: Codec<*> }";
3074        let r = parse_ok(src);
3075        assert_eq!(r.diagnostics.len(), 0);
3076        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3077        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3078        if let Expr::GenericType { args, .. } = value {
3079            assert_eq!(args.len(), 1);
3080            if let Expr::Ident(id) = &args[0] {
3081                assert_eq!(id.name, "*");
3082            } else {
3083                panic!("expected wildcard ident, got {:?}", args[0]);
3084            }
3085        } else {
3086            panic!("expected GenericType, got {:?}", value);
3087        }
3088    }
3089
3090    #[test]
3091    fn guidance_clause_comment_only_value_migration() {
3092        // Old `guidance:` colon form should emit a migration diagnostic
3093        let src = "-- allium: 1\nrule R {\n    ensures: Done()\n    guidance: -- just a comment\n}";
3094        let r = parse(src);
3095        assert!(
3096            r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
3097            "expected migration diagnostic, got: {:?}",
3098            r.diagnostics
3099        );
3100    }
3101
3102    #[test]
3103    fn spec_reject_for_expr_with_filter() {
3104        // Expression-level `for` also only accepts `where`, not `with`.
3105        let src = r#"rule R {
3106    when: X(project)
3107    ensures:
3108        let total = for task in project.tasks with task.active: task.effort
3109        Done(total: total)
3110}"#;
3111        let r = parse_ok(src);
3112        assert!(
3113            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3114            "expected error for `for ... with` in expression (spec uses `where`), but parsed without errors"
3115        );
3116    }
3117
3118    #[test]
3119    fn for_destructured_binding() {
3120        let src = r#"rule R {
3121    when: X()
3122    for (key, value) in Pairs where key != null:
3123        ensures: Processed(key: key, value: value)
3124}"#;
3125        let r = parse_ok(src);
3126        assert_eq!(r.diagnostics.len(), 0);
3127        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3128        let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
3129        assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
3130    }
3131
3132    #[test]
3133    fn dot_path_assignment() {
3134        let src = r#"entity Shard {
3135    ShardGroup.shard_cache: Shard with group = this
3136}"#;
3137        let r = parse_ok(src);
3138        assert_eq!(r.diagnostics.len(), 0);
3139        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3140        let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
3141            panic!("expected PathAssignment, got {:?}", b.items[0].kind);
3142        };
3143        assert!(matches!(path, Expr::MemberAccess { .. }));
3144    }
3145
3146    #[test]
3147    fn language_reference_fixture() {
3148        let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
3149        let r = parse(src);
3150        let errors: Vec<_> = r.diagnostics.iter()
3151            .filter(|d| d.severity == Severity::Error)
3152            .collect();
3153        assert_eq!(
3154            errors.len(),
3155            0,
3156            "expected no errors in language-reference fixture, got: {:?}",
3157            errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
3158        );
3159    }
3160
3161    // =====================================================================
3162    // V1 SPEC CONFORMANCE TESTS
3163    //
3164    // These tests verify that the parser conforms to the Allium V1 language
3165    // reference (docs/allium-v1-language-reference.md). Each test is tagged
3166    // with the finding number from the audit.
3167    //
3168    // Tests marked "should reject" are expected to FAIL until the parser
3169    // is updated to reject non-spec constructs.
3170    //
3171    // Tests marked "should parse" are expected to FAIL until the parser
3172    // is updated to handle spec-defined constructs.
3173    // =====================================================================
3174
3175    // -- Finding 1: spec uses `for`, not `for each` ---------------------------
3176
3177    #[test]
3178    fn spec_for_bare_form() {
3179        // The spec uses bare `for` exclusively. This must parse cleanly.
3180        let src = r#"rule ProcessDigests {
3181    when: schedule: DigestSchedule.next_run_at <= now
3182    for user in Users where notification_setting.digest_enabled:
3183        let settings = user.notification_setting
3184        ensures: DigestBatch.created(user: user)
3185}"#;
3186        let r = parse_ok(src);
3187        assert_eq!(r.diagnostics.len(), 0);
3188    }
3189
3190    #[test]
3191    fn spec_reject_for_each() {
3192        // `for each` is not in the spec. The parser should reject it.
3193        let src = r#"rule R {
3194    when: X()
3195    for each user in Users where user.active:
3196        ensures: Notified(user: user)
3197}"#;
3198        let r = parse_ok(src);
3199        assert!(
3200            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3201            "expected error for `for each` (not in spec), but parsed without errors"
3202        );
3203    }
3204
3205    // -- Finding 2: spec uses `=`, not `==` -----------------------------------
3206
3207    #[test]
3208    fn spec_reject_double_equals() {
3209        // The spec uses `=` for equality. `==` should not be accepted.
3210        let src = "rule R { when: X(a)\n    requires: a.status == active\n    ensures: Done() }";
3211        let r = parse_ok(src);
3212        assert!(
3213            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3214            "expected error for `==` (not in spec), but parsed without errors"
3215        );
3216    }
3217
3218    // -- Finding 3: `system` blocks are not in the spec -----------------------
3219
3220    #[test]
3221    fn spec_reject_system_block() {
3222        // `system` is not a declaration type in the V1 spec.
3223        let src = "system PaymentGateway {\n    timeout: 30.seconds\n}";
3224        let r = parse_ok(src);
3225        assert!(
3226            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3227            "expected error for `system` block (not in spec), but parsed without errors"
3228        );
3229    }
3230
3231    // -- Finding 6: `tags` clause is not in the spec --------------------------
3232
3233    #[test]
3234    fn spec_reject_tags_clause() {
3235        // `tags:` is not a clause keyword in the V1 spec.
3236        let src = r#"rule R {
3237    when: MigrationTriggered()
3238    tags: infrastructure, migration
3239    ensures: MigrationComplete()
3240}"#;
3241        let r = parse_ok(src);
3242        assert!(
3243            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3244            "expected error for `tags:` clause (not in spec), but parsed without errors"
3245        );
3246    }
3247
3248    // -- Finding 7: `includes`/`excludes` are not in the spec -----------------
3249
3250    #[test]
3251    fn spec_reject_includes_operator() {
3252        // The spec uses `x in collection`, not `collection includes x`.
3253        let src = r#"rule R {
3254    when: X(a, b)
3255    requires: a.items includes b
3256    ensures: Done()
3257}"#;
3258        let r = parse_ok(src);
3259        assert!(
3260            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3261            "expected error for `includes` operator (not in spec), but parsed without errors"
3262        );
3263    }
3264
3265    #[test]
3266    fn spec_reject_excludes_operator() {
3267        // The spec uses `x not in collection`, not `collection excludes x`.
3268        let src = r#"rule R {
3269    when: X(a, b)
3270    requires: a.items excludes b
3271    ensures: Done()
3272}"#;
3273        let r = parse_ok(src);
3274        assert!(
3275            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3276            "expected error for `excludes` operator (not in spec), but parsed without errors"
3277        );
3278    }
3279
3280    // -- Finding 8: range literals (`..`) are not in the spec -----------------
3281
3282    #[test]
3283    fn spec_reject_range_literal() {
3284        // The `..` range operator is not defined in the V1 spec.
3285        let src = r#"rule R {
3286    when: X(v)
3287    requires: v in [1..100]
3288    ensures: Done()
3289}"#;
3290        let r = parse_ok(src);
3291        assert!(
3292            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3293            "expected error for `..` range (not in spec), but parsed without errors"
3294        );
3295    }
3296
3297    // -- Finding 9: `within` is only for actors, not rules --------------------
3298
3299    #[test]
3300    fn spec_within_in_actor() {
3301        // The spec defines `within:` as an actor clause.
3302        let src = r#"actor WorkspaceAdmin {
3303    within: Workspace
3304    identified_by: User where role = admin
3305}"#;
3306        let r = parse_ok(src);
3307        assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
3308    }
3309
3310    // -- Finding 10: `module` declaration is not in the spec ------------------
3311
3312    #[test]
3313    fn spec_reject_module_declaration() {
3314        // `module Name` is not a declaration in the V1 spec.
3315        let src = "module my_spec";
3316        let r = parse_ok(src);
3317        assert!(
3318            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3319            "expected error for `module` declaration (not in spec), but parsed without errors"
3320        );
3321    }
3322
3323    // -- Finding 11: `guidance` at module level is not in the spec ------------
3324
3325    #[test]
3326    fn spec_reject_module_level_guidance() {
3327        // The spec shows `guidance:` only as a surface clause.
3328        let src = r#"guidance: "All rules must be idempotent""#;
3329        let r = parse_ok(src);
3330        assert!(
3331            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3332            "expected error for module-level `guidance:` (not in spec), but parsed without errors"
3333        );
3334    }
3335
3336    // -- Finding 12: `guarantee`/`timeout` in rules is not in the spec --------
3337
3338    #[test]
3339    fn spec_guarantee_in_surface_migration() {
3340        // Old `guarantee:` colon form should emit a migration diagnostic
3341        let src = "-- allium: 1\nsurface S {\n    facing viewer: User\n    guarantee: DataIntegrity\n}";
3342        let r = parse(src);
3343        assert!(
3344            r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
3345            "expected migration diagnostic, got: {:?}",
3346            r.diagnostics
3347        );
3348    }
3349
3350    #[test]
3351    fn spec_timeout_in_surface() {
3352        // The spec defines `timeout:` as a surface clause with rule name syntax.
3353        let src = r#"surface InvitationView {
3354    facing recipient: Candidate
3355    context invitation: ResourceInvitation where email = recipient.email
3356    timeout: InvitationExpires
3357}"#;
3358        let r = parse_ok(src);
3359        assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
3360    }
3361
3362    #[test]
3363    fn spec_timeout_in_surface_with_when() {
3364        // The spec shows `timeout: RuleName when condition`.
3365        let src = r#"surface InvitationView {
3366    facing recipient: Candidate
3367    context invitation: ResourceInvitation where email = recipient.email
3368    timeout: InvitationExpires when invitation.expires_at <= now
3369}"#;
3370        let r = parse_ok(src);
3371        assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
3372    }
3373
3374    // -- Finding 15: suffix predicates are not in the spec --------------------
3375
3376    #[test]
3377    fn spec_reject_suffix_predicate() {
3378        // The spec does not define suffix predicate syntax like `starts_with`.
3379        let src = r#"rule R {
3380    when: X()
3381    requires: finding.code starts_with "allium."
3382    ensures: Done()
3383}"#;
3384        let r = parse_ok(src);
3385        assert!(
3386            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3387            "expected error for suffix predicate (not in spec), but parsed without errors"
3388        );
3389    }
3390
3391    // -- Finding 17: `.add()`/`.remove()` are in the spec ---------------------
3392
3393    #[test]
3394    fn spec_add_remove_in_ensures() {
3395        // The spec documents `.add()` and `.remove()` as ensures-only mutations.
3396        // These parse as regular method calls, which is correct.
3397        let src = r#"rule R {
3398    when: AssignInterviewer(interview, new_interviewer)
3399    ensures:
3400        interview.interviewers.add(new_interviewer)
3401}"#;
3402        let r = parse_ok(src);
3403        assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
3404    }
3405
3406    #[test]
3407    fn spec_remove_in_ensures() {
3408        let src = r#"rule R {
3409    when: RemoveInterviewer(interview, leaving)
3410    ensures:
3411        interview.interviewers.remove(leaving)
3412}"#;
3413        let r = parse_ok(src);
3414        assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3415    }
3416
3417    // -- Finding 18: `.first`/`.last` are in the spec -------------------------
3418
3419    #[test]
3420    fn spec_first_last_access() {
3421        // The spec documents `.first` and `.last` for ordered collections.
3422        let src = "entity E { latest: attempts.last\n    earliest: attempts.first }";
3423        let r = parse_ok(src);
3424        assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3425    }
3426
3427    // -- Finding 19: set arithmetic is in the spec ----------------------------
3428
3429    #[test]
3430    fn spec_set_arithmetic() {
3431        // The spec documents `+` and `-` on collections as set arithmetic.
3432        let src = r#"entity Role {
3433    permissions: Set<String>
3434    inherited: Set<String>
3435    all_permissions: permissions + inherited
3436    removed: old_mentions - new_mentions
3437}"#;
3438        let r = parse_ok(src);
3439        assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3440    }
3441
3442    // -- Finding 20: discard binding `_` is in the spec -----------------------
3443
3444    #[test]
3445    fn spec_discard_binding_in_trigger() {
3446        // The spec shows `when: _: LogProcessor.last_flush_check + ...`
3447        let src = r#"rule R {
3448    when: _: LogProcessor.last_flush_check <= now
3449    ensures: Flushed()
3450}"#;
3451        let r = parse_ok(src);
3452        assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3453    }
3454
3455    #[test]
3456    fn spec_discard_in_trigger_params() {
3457        // The spec shows `when: SomeEvent(_, slot)`
3458        let src = r#"rule R {
3459    when: SomeEvent(_, slot)
3460    ensures: Processed(slot: slot)
3461}"#;
3462        let r = parse_ok(src);
3463        assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3464    }
3465
3466    #[test]
3467    fn spec_discard_in_for() {
3468        // The spec shows `for _ in items: Counted(batch)`
3469        let src = r#"rule R {
3470    when: X(items)
3471    ensures:
3472        for _ in items: Counted()
3473}"#;
3474        let r = parse_ok(src);
3475        assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3476    }
3477
3478    // -- Finding 21: default with object literal is in the spec ---------------
3479
3480    #[test]
3481    fn spec_default_with_object_literal() {
3482        // The spec shows: default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }
3483        let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3484        let r = parse_ok(src);
3485        assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3486    }
3487
3488    #[test]
3489    fn spec_default_multiline_object() {
3490        // The spec shows multi-line defaults with object literals.
3491        let src = r#"default Role viewer = {
3492    name: "viewer",
3493    permissions: { "documents.read" }
3494}"#;
3495        let r = parse_ok(src);
3496        assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3497    }
3498
3499    // -- Spec surface features: related, let, guarantee, timeout --------------
3500
3501    #[test]
3502    fn spec_surface_related_clause() {
3503        // The spec shows `related:` with surface references.
3504        let src = r#"surface InterviewerDashboard {
3505    facing viewer: Interviewer
3506    context assignment: SlotConfirmation where interviewer = viewer
3507    related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3508}"#;
3509        let r = parse_ok(src);
3510        assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3511    }
3512
3513    #[test]
3514    fn spec_surface_let_binding() {
3515        // The spec shows `let` bindings inside surfaces.
3516        let src = r#"surface S {
3517    facing viewer: User
3518    let comments = Comments where parent = viewer
3519    exposes: CommentList
3520}"#;
3521        let r = parse_ok(src);
3522        assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3523    }
3524
3525    #[test]
3526    fn spec_surface_multiline_context_where() {
3527        // The spec shows context with where on a continuation line.
3528        let src = r#"surface InterviewerPendingAssignments {
3529    facing viewer: Interviewer
3530    context assignment: InterviewAssignment
3531        where interviewer = viewer and status = pending
3532    exposes: AssignmentList
3533}"#;
3534        let r = parse_ok(src);
3535        assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3536    }
3537
3538    // -- Spec: `for` inside surfaces ------------------------------------------
3539
3540    #[test]
3541    fn spec_for_in_surface_provides() {
3542        // The spec shows for iteration inside surface provides.
3543        let src = r#"surface TaskBoard {
3544    facing viewer: User
3545    for task in Task where task.assignee = viewer:
3546        provides: CompleteTask(viewer, task) when task.status = in_progress
3547    exposes: KanbanBoard
3548}"#;
3549        let r = parse_ok(src);
3550        assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3551    }
3552
3553    // -- Spec: `use` without alias --------------------------------------------
3554
3555    #[test]
3556    fn spec_use_without_alias() {
3557        // The spec shows `use` both with and without `as alias`.
3558        let src = r#"use "github.com/specs/notifications/def456""#;
3559        let r = parse_ok(src);
3560        assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3561    }
3562
3563    // -- Spec: empty external entity ------------------------------------------
3564
3565    #[test]
3566    fn spec_empty_external_entity() {
3567        // The spec shows external entities with empty bodies as type placeholders.
3568        let src = "external entity Commentable {}";
3569        let r = parse_ok(src);
3570        assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3571    }
3572
3573    // -- Spec: multi-line provides block in surface ---------------------------
3574
3575    #[test]
3576    fn spec_surface_multiline_provides() {
3577        // The spec shows provides as a multi-line block.
3578        let src = r#"surface ProjectDashboard {
3579    facing viewer: ProjectManager
3580    context project: Project where owner = viewer
3581    provides:
3582        CreateTask(viewer, project) when project.status = active
3583        ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3584    exposes: TaskList
3585}"#;
3586        let r = parse_ok(src);
3587        assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3588    }
3589
3590    // -- Spec: multi-line exposes block in surface ----------------------------
3591
3592    #[test]
3593    fn spec_surface_multiline_exposes() {
3594        // The spec shows exposes as a multi-line block.
3595        let src = r#"surface InterviewerDashboard {
3596    facing viewer: Interviewer
3597    context assignment: SlotConfirmation where interviewer = viewer
3598    exposes:
3599        assignment.slot.time
3600        assignment.status
3601}"#;
3602        let r = parse_ok(src);
3603        assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3604    }
3605
3606    // =====================================================================
3607    // COVERAGE GAP TESTS
3608    //
3609    // Dedicated unit tests for spec constructs that previously only had
3610    // fixture-file coverage.
3611    // =====================================================================
3612
3613    // -- Composite or-triggers ------------------------------------------------
3614
3615    #[test]
3616    fn composite_or_trigger() {
3617        let src = r#"rule R {
3618    when: EventA(x) or EventB(x) or EventC(x)
3619    ensures: Done()
3620}"#;
3621        let r = parse_ok(src);
3622        assert_eq!(r.diagnostics.len(), 0);
3623        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3624        let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
3625        assert_eq!(keyword, "when");
3626        // Top-level should be LogicalOp(Or) wrapping another Or
3627        let Expr::LogicalOp { op, left, .. } = value else {
3628            panic!("expected LogicalOp, got {value:?}");
3629        };
3630        assert_eq!(*op, LogicalOp::Or);
3631        assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
3632    }
3633
3634    // -- Value type declaration -----------------------------------------------
3635
3636    #[test]
3637    fn value_type_declaration() {
3638        let src = r#"value TimeRange {
3639    start: Timestamp
3640    end: Timestamp
3641    duration: end - start
3642}"#;
3643        let r = parse_ok(src);
3644        assert_eq!(r.diagnostics.len(), 0);
3645        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3646        assert_eq!(b.kind, BlockKind::Value);
3647        assert_eq!(b.name.as_ref().unwrap().name, "TimeRange");
3648        assert_eq!(b.items.len(), 3);
3649    }
3650
3651    // -- Qualified config block -----------------------------------------------
3652
3653    #[test]
3654    fn qualified_config_block() {
3655        let src = r#"use "github.com/specs/oauth/abc123" as oauth
3656oauth/config {
3657    session_duration: Duration = 24.hours
3658}"#;
3659        let r = parse_ok(src);
3660        assert_eq!(r.diagnostics.len(), 0);
3661        assert_eq!(r.module.declarations.len(), 2);
3662    }
3663
3664    // -- String interpolation -------------------------------------------------
3665
3666    #[test]
3667    fn string_interpolation_parts() {
3668        let src = r#"rule R {
3669    when: X(name, action)
3670    ensures: Log.created(message: "User {name} did {action}")
3671}"#;
3672        let r = parse_ok(src);
3673        assert_eq!(r.diagnostics.len(), 0);
3674        // Dig into the message arg to verify interpolation parts
3675        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3676        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3677        let Expr::Call { args, .. } = value else { panic!() };
3678        let CallArg::Named(arg) = &args[0] else { panic!() };
3679        let Expr::StringLiteral(s) = &arg.value else { panic!() };
3680        assert_eq!(s.parts.len(), 4, "expected 4 string parts: text, interp, text, interp");
3681        assert!(matches!(&s.parts[0], StringPart::Text(t) if t == "User "));
3682        assert!(matches!(&s.parts[1], StringPart::Interpolation(id) if id.name == "name"));
3683        assert!(matches!(&s.parts[2], StringPart::Text(t) if t == " did "));
3684        assert!(matches!(&s.parts[3], StringPart::Interpolation(id) if id.name == "action"));
3685    }
3686
3687    // -- `this` keyword as expression -----------------------------------------
3688
3689    #[test]
3690    fn this_keyword_expression() {
3691        // `Item with parent = this` parses as With(Item, Eq(parent, this))
3692        // because `with` binds looser than `=`, capturing the full predicate.
3693        let src = "entity E { items: Item with parent = this }";
3694        let r = parse_ok(src);
3695        assert_eq!(r.diagnostics.len(), 0);
3696        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3697        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3698        let Expr::With { predicate, .. } = value else {
3699            panic!("expected With, got {value:?}");
3700        };
3701        let Expr::Comparison { op, right, .. } = predicate.as_ref() else {
3702            panic!("expected Comparison in with predicate, got {predicate:?}");
3703        };
3704        assert_eq!(*op, ComparisonOp::Eq);
3705        assert!(matches!(right.as_ref(), Expr::This { .. }));
3706    }
3707
3708    // -- `not` prefix operator (standalone) -----------------------------------
3709
3710    #[test]
3711    fn not_prefix_standalone() {
3712        let src = r#"rule R {
3713    when: X(user)
3714    requires: not user.is_locked
3715    ensures: Done()
3716}"#;
3717        let r = parse_ok(src);
3718        assert_eq!(r.diagnostics.len(), 0);
3719        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3720        let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3721        assert_eq!(keyword, "requires");
3722        assert!(matches!(value, Expr::Not { .. }));
3723    }
3724
3725    // -- Unary minus ----------------------------------------------------------
3726
3727    #[test]
3728    fn unary_minus() {
3729        let src = "entity E { offset: -1 }";
3730        let r = parse_ok(src);
3731        assert_eq!(r.diagnostics.len(), 0);
3732        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3733        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3734        assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }
3735                        | Expr::NumberLiteral { .. }), "expected negation, got {value:?}");
3736    }
3737
3738    // -- Parenthesised expression grouping ------------------------------------
3739
3740    #[test]
3741    fn parenthesised_expression() {
3742        let src = "entity E { v: (a + b) * c }";
3743        let r = parse_ok(src);
3744        assert_eq!(r.diagnostics.len(), 0);
3745        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3746        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3747        // Top level should be Mul, with left being Add (grouped by parens)
3748        let Expr::BinaryOp { op, left, .. } = value else {
3749            panic!("expected BinaryOp, got {value:?}");
3750        };
3751        assert_eq!(*op, BinaryOp::Mul);
3752        assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3753    }
3754
3755    // -- Boolean literals -----------------------------------------------------
3756
3757    #[test]
3758    fn boolean_literals() {
3759        let src = r#"rule R {
3760    when: X(item)
3761    ensures:
3762        item.active = true
3763        item.deleted = false
3764}"#;
3765        let r = parse_ok(src);
3766        assert_eq!(r.diagnostics.len(), 0);
3767    }
3768
3769    // -- `null` literal -------------------------------------------------------
3770
3771    #[test]
3772    fn null_literal() {
3773        let src = "entity E { v: parent ?? null }";
3774        let r = parse_ok(src);
3775        assert_eq!(r.diagnostics.len(), 0);
3776        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3777        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3778        let Expr::NullCoalesce { right, .. } = value else { panic!() };
3779        assert!(matches!(right.as_ref(), Expr::Null { .. }));
3780    }
3781
3782    // -- Empty set literal ----------------------------------------------------
3783
3784    #[test]
3785    fn empty_set_literal() {
3786        let src = "entity E { tags: Set<String>\n    default_tags: {} }";
3787        let r = parse_ok(src);
3788        assert_eq!(r.diagnostics.len(), 0);
3789        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3790        let BlockItemKind::Assignment { value, .. } = &b.items[1].kind else { panic!() };
3791        let Expr::SetLiteral { elements, .. } = value else { panic!("expected SetLiteral, got {value:?}") };
3792        assert!(elements.is_empty());
3793    }
3794
3795    // =====================================================================
3796    // PRE-1.0 COVERAGE TESTS
3797    //
3798    // Additional tests addressing gaps identified during the pre-release
3799    // review: parameterised derived values, operator precedence matrix,
3800    // indentation-based multi-line detection, and set/object literal
3801    // disambiguation.
3802    // =====================================================================
3803
3804    // -- Parameterised derived values (ParamAssignment) --------------------
3805
3806    #[test]
3807    fn param_assignment_single() {
3808        let src = "entity Plan { can_use(feature): feature in features }";
3809        let r = parse_ok(src);
3810        assert_eq!(r.diagnostics.len(), 0);
3811        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3812        let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
3813            panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3814        };
3815        assert_eq!(name.name, "can_use");
3816        assert_eq!(params.len(), 1);
3817        assert_eq!(params[0].name, "feature");
3818        assert!(matches!(value, Expr::In { .. }));
3819    }
3820
3821    #[test]
3822    fn param_assignment_multiple() {
3823        let src = "entity E { distance(x, y): (x * x + y * y) }";
3824        let r = parse_ok(src);
3825        assert_eq!(r.diagnostics.len(), 0);
3826        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3827        let BlockItemKind::ParamAssignment { name, params, .. } = &b.items[0].kind else {
3828            panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3829        };
3830        assert_eq!(name.name, "distance");
3831        assert_eq!(params.len(), 2);
3832        assert_eq!(params[0].name, "x");
3833        assert_eq!(params[1].name, "y");
3834    }
3835
3836    #[test]
3837    fn param_assignment_simple_expression() {
3838        let src = "entity Task { remaining_effort(total): total - effort }";
3839        let r = parse_ok(src);
3840        assert_eq!(r.diagnostics.len(), 0);
3841        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3842        let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
3843            panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3844        };
3845        assert_eq!(name.name, "remaining_effort");
3846        assert_eq!(params.len(), 1);
3847        assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }));
3848    }
3849
3850    // -- Operator precedence matrix ----------------------------------------
3851
3852    #[test]
3853    fn precedence_logical_and_binds_tighter_than_or() {
3854        // `a or b and c` => Or(a, And(b, c))
3855        let src = "entity E { v: a or b and c }";
3856        let r = parse_ok(src);
3857        assert_eq!(r.diagnostics.len(), 0);
3858        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3859        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3860        let Expr::LogicalOp { op, right, .. } = value else {
3861            panic!("expected LogicalOp, got {value:?}");
3862        };
3863        assert_eq!(*op, LogicalOp::Or);
3864        assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3865    }
3866
3867    #[test]
3868    fn precedence_comparison_binds_tighter_than_and() {
3869        // `a = b and c != d` => And(Eq(a, b), NotEq(c, d))
3870        let src = "entity E { v: a = b and c != d }";
3871        let r = parse_ok(src);
3872        assert_eq!(r.diagnostics.len(), 0);
3873        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3874        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3875        let Expr::LogicalOp { op, left, right, .. } = value else {
3876            panic!("expected LogicalOp, got {value:?}");
3877        };
3878        assert_eq!(*op, LogicalOp::And);
3879        assert!(matches!(left.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
3880        assert!(matches!(right.as_ref(), Expr::Comparison { op: ComparisonOp::NotEq, .. }));
3881    }
3882
3883    #[test]
3884    fn precedence_arithmetic_binds_tighter_than_comparison() {
3885        // `a + b > c * d` => Gt(Add(a, b), Mul(c, d))
3886        let src = "entity E { v: a + b > c * d }";
3887        let r = parse_ok(src);
3888        assert_eq!(r.diagnostics.len(), 0);
3889        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3890        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3891        let Expr::Comparison { op, left, right, .. } = value else {
3892            panic!("expected Comparison, got {value:?}");
3893        };
3894        assert_eq!(*op, ComparisonOp::Gt);
3895        assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3896        assert!(matches!(right.as_ref(), Expr::BinaryOp { op: BinaryOp::Mul, .. }));
3897    }
3898
3899    #[test]
3900    fn precedence_null_coalesce_binds_tighter_than_comparison() {
3901        // `a ?? b = c` => Eq(NullCoalesce(a, b), c)
3902        let src = "entity E { v: a ?? b = c }";
3903        let r = parse_ok(src);
3904        assert_eq!(r.diagnostics.len(), 0);
3905        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3906        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3907        let Expr::Comparison { op, left, .. } = value else {
3908            panic!("expected Comparison, got {value:?}");
3909        };
3910        assert_eq!(*op, ComparisonOp::Eq);
3911        assert!(matches!(left.as_ref(), Expr::NullCoalesce { .. }));
3912    }
3913
3914    #[test]
3915    fn precedence_not_binds_tighter_than_and() {
3916        // `not a and b` => And(Not(a), b)
3917        let src = r#"rule R {
3918    when: X(a, b)
3919    requires: not a and b
3920    ensures: Done()
3921}"#;
3922        let r = parse_ok(src);
3923        assert_eq!(r.diagnostics.len(), 0);
3924        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3925        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3926        let Expr::LogicalOp { op, left, .. } = value else {
3927            panic!("expected LogicalOp, got {value:?}");
3928        };
3929        assert_eq!(*op, LogicalOp::And);
3930        assert!(matches!(left.as_ref(), Expr::Not { .. }));
3931    }
3932
3933    #[test]
3934    fn precedence_where_captures_full_condition() {
3935        // `items where status = active` => Where(items, Eq(status, active))
3936        // where (BP 7) binds looser than comparison (BP 30), so the full
3937        // condition `status = active` is captured as the where predicate.
3938        let src = "entity E { v: items where status = active }";
3939        let r = parse_ok(src);
3940        assert_eq!(r.diagnostics.len(), 0);
3941        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3942        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3943        let Expr::Where { condition, .. } = value else {
3944            panic!("expected Where, got {value:?}");
3945        };
3946        assert!(matches!(condition.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
3947    }
3948
3949    #[test]
3950    fn precedence_where_captures_and_or_conditions() {
3951        // `items where status = active and count > 0` =>
3952        //   Where(items, And(Eq(status, active), Gt(count, 0)))
3953        let src = "entity E { v: items where status = active and count > 0 }";
3954        let r = parse_ok(src);
3955        assert_eq!(r.diagnostics.len(), 0);
3956        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3957        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3958        let Expr::Where { condition, .. } = value else {
3959            panic!("expected Where, got {value:?}");
3960        };
3961        assert!(matches!(condition.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3962    }
3963
3964    #[test]
3965    fn precedence_projection_applies_to_where_result() {
3966        // `items where status = confirmed -> interviewer` =>
3967        //   ProjectionMap(Where(items, Eq(status, confirmed)), interviewer)
3968        let src = "entity E { v: items where status = confirmed -> interviewer }";
3969        let r = parse_ok(src);
3970        assert_eq!(r.diagnostics.len(), 0);
3971        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3972        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3973        let Expr::ProjectionMap { source, field, .. } = value else {
3974            panic!("expected ProjectionMap, got {value:?}");
3975        };
3976        assert_eq!(field.name, "interviewer");
3977        assert!(matches!(source.as_ref(), Expr::Where { .. }));
3978    }
3979
3980    #[test]
3981    fn precedence_lambda_binds_loosest() {
3982        // `items.any(i => i.active and i.valid)` => Lambda(i, And(active, valid))
3983        let src = "entity E { v: items.any(i => i.active and i.valid) }";
3984        let r = parse_ok(src);
3985        assert_eq!(r.diagnostics.len(), 0);
3986        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3987        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3988        let Expr::Call { args, .. } = value else { panic!() };
3989        let CallArg::Positional(Expr::Lambda { body, .. }) = &args[0] else { panic!() };
3990        assert!(matches!(body.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3991    }
3992
3993    #[test]
3994    fn precedence_in_binds_at_comparison_level() {
3995        // `x in {a, b} and y not in {c}` => And(In(x, {a,b}), NotIn(y, {c}))
3996        let src = r#"rule R {
3997    when: X(x, y)
3998    requires: x in {a, b} and y not in {c}
3999    ensures: Done()
4000}"#;
4001        let r = parse_ok(src);
4002        assert_eq!(r.diagnostics.len(), 0);
4003        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4004        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4005        let Expr::LogicalOp { op, left, right, .. } = value else {
4006            panic!("expected LogicalOp, got {value:?}");
4007        };
4008        assert_eq!(*op, LogicalOp::And);
4009        assert!(matches!(left.as_ref(), Expr::In { .. }));
4010        assert!(matches!(right.as_ref(), Expr::NotIn { .. }));
4011    }
4012
4013    // -- Multi-line clause value detection ----------------------------------
4014
4015    #[test]
4016    fn multiline_ensures_block() {
4017        let src = r#"rule R {
4018    when: X(doc)
4019    ensures:
4020        doc.status = published
4021        Notification.created(to: doc.author)
4022}"#;
4023        let r = parse_ok(src);
4024        assert_eq!(r.diagnostics.len(), 0);
4025        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4026        let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4027        assert_eq!(keyword, "ensures");
4028        let Expr::Block { items, .. } = value else {
4029            panic!("expected Block for multi-line ensures, got {value:?}");
4030        };
4031        assert_eq!(items.len(), 2);
4032    }
4033
4034    #[test]
4035    fn singleline_ensures_value() {
4036        let src = r#"rule R {
4037    when: X(doc)
4038    ensures: doc.status = published
4039}"#;
4040        let r = parse_ok(src);
4041        assert_eq!(r.diagnostics.len(), 0);
4042        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4043        let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4044        assert_eq!(keyword, "ensures");
4045        // Single-line value should NOT be wrapped in a Block
4046        assert!(!matches!(value, Expr::Block { .. }), "single-line ensures should not be Block");
4047    }
4048
4049    #[test]
4050    fn multiline_requires_with_continuation() {
4051        let src = r#"rule R {
4052    when: X(a)
4053    requires:
4054        a.count >= 2
4055        or a.items.any(i => i.can_solo)
4056    ensures: Done()
4057}"#;
4058        let r = parse_ok(src);
4059        assert_eq!(r.diagnostics.len(), 0);
4060    }
4061
4062    // -- Set vs object literal disambiguation ------------------------------
4063
4064    #[test]
4065    fn object_literal_single_field() {
4066        let src = r#"rule R {
4067    when: X()
4068    ensures:
4069        let o = {name: "test"}
4070        Done()
4071}"#;
4072        let r = parse_ok(src);
4073        assert_eq!(r.diagnostics.len(), 0);
4074        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4075        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4076        let Expr::Block { items, .. } = value else { panic!() };
4077        let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4078        assert!(matches!(let_val.as_ref(), Expr::ObjectLiteral { .. }));
4079    }
4080
4081    #[test]
4082    fn set_literal_single_element() {
4083        let src = r#"rule R {
4084    when: X()
4085    ensures:
4086        let s = {active}
4087        Done()
4088}"#;
4089        let r = parse_ok(src);
4090        assert_eq!(r.diagnostics.len(), 0);
4091        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4092        let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4093        let Expr::Block { items, .. } = value else { panic!() };
4094        let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4095        assert!(matches!(let_val.as_ref(), Expr::SetLiteral { .. }),
4096            "bare {{ident}} should parse as set literal, got {:?}", let_val);
4097    }
4098
4099    // -- Lambda variations -------------------------------------------------
4100
4101    #[test]
4102    fn lambda_with_chained_access() {
4103        let src = "entity E { v: items.all(t => t.item.status = active) }";
4104        let r = parse_ok(src);
4105        assert_eq!(r.diagnostics.len(), 0);
4106    }
4107
4108    #[test]
4109    fn nested_lambda() {
4110        let src = "entity E { v: groups.any(g => g.items.all(i => i.valid)) }";
4111        let r = parse_ok(src);
4112        assert_eq!(r.diagnostics.len(), 0);
4113    }
4114
4115    // -- Qualified name variations -----------------------------------------
4116
4117    #[test]
4118    fn qualified_name_with_member_access() {
4119        let src = "entity E { v: shared/Validator.check }";
4120        let r = parse_ok(src);
4121        assert_eq!(r.diagnostics.len(), 0);
4122        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4123        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4124        let Expr::MemberAccess { object, field, .. } = value else {
4125            panic!("expected MemberAccess, got {value:?}");
4126        };
4127        assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
4128        assert_eq!(field.name, "check");
4129    }
4130
4131    #[test]
4132    fn qualified_name_in_call() {
4133        let src = r#"rule R {
4134    when: X(item)
4135    requires: shared/Validator.check(item: item)
4136    ensures: Done()
4137}"#;
4138        let r = parse_ok(src);
4139        assert_eq!(r.diagnostics.len(), 0);
4140    }
4141
4142    // -- Nested control flow -----------------------------------------------
4143
4144    #[test]
4145    fn nested_if_inside_for() {
4146        let src = r#"rule R {
4147    when: X()
4148    for user in Users where user.active:
4149        if user.role = admin:
4150            ensures: AdminNotified(user: user)
4151        else:
4152            ensures: UserNotified(user: user)
4153}"#;
4154        let r = parse_ok(src);
4155        assert_eq!(r.diagnostics.len(), 0);
4156        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4157        let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4158        assert!(matches!(items[0].kind, BlockItemKind::IfBlock { .. }));
4159    }
4160
4161    #[test]
4162    fn for_with_let_before_ensures() {
4163        let src = r#"rule R {
4164    when: schedule: DigestSchedule.next_run_at <= now
4165    for user in Users where user.active:
4166        let pending = user.tasks where status = pending
4167        ensures: DigestEmail.created(to: user.email, tasks: pending)
4168}"#;
4169        let r = parse_ok(src);
4170        assert_eq!(r.diagnostics.len(), 0);
4171        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4172        let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4173        assert_eq!(items.len(), 2, "for body should have let + ensures");
4174        assert!(matches!(items[0].kind, BlockItemKind::Let { .. }));
4175        assert!(matches!(items[1].kind, BlockItemKind::Clause { .. }));
4176    }
4177
4178    // -- Join lookup variations --------------------------------------------
4179
4180    #[test]
4181    fn join_lookup_all_unnamed() {
4182        let src = "entity E { match: Other{a, b, c} }";
4183        let r = parse_ok(src);
4184        assert_eq!(r.diagnostics.len(), 0);
4185        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4186        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4187        let Expr::JoinLookup { fields, .. } = value else { panic!() };
4188        assert_eq!(fields.len(), 3);
4189        assert!(fields.iter().all(|f| f.value.is_none()));
4190    }
4191
4192    #[test]
4193    fn join_lookup_all_named() {
4194        let src = "entity E { match: Membership{user: actor, workspace: ws} }";
4195        let r = parse_ok(src);
4196        assert_eq!(r.diagnostics.len(), 0);
4197        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4198        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4199        let Expr::JoinLookup { fields, .. } = value else { panic!() };
4200        assert_eq!(fields.len(), 2);
4201        assert!(fields.iter().all(|f| f.value.is_some()));
4202    }
4203
4204    #[test]
4205    fn join_lookup_in_requires() {
4206        let src = r#"rule R {
4207    when: X(user, workspace)
4208    requires: exists WorkspaceMembership{user: user, workspace: workspace}
4209    ensures: Done()
4210}"#;
4211        let r = parse_ok(src);
4212        assert_eq!(r.diagnostics.len(), 0);
4213    }
4214
4215    #[test]
4216    fn join_lookup_negated_in_requires() {
4217        let src = r#"rule R {
4218    when: X(email)
4219    requires: not exists User{email: email}
4220    ensures: Done()
4221}"#;
4222        let r = parse_ok(src);
4223        assert_eq!(r.diagnostics.len(), 0);
4224    }
4225
4226    // -----------------------------------------------------------------------
4227    // ALP-11: implies operator
4228    // -----------------------------------------------------------------------
4229
4230    #[test]
4231    fn implies_basic() {
4232        let src = "rule R { requires: a implies b }";
4233        let r = parse_ok(src);
4234        assert_eq!(r.diagnostics.len(), 0);
4235        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4236        let BlockItemKind::Clause { value, .. } = &b.items[0].kind else { panic!() };
4237        let Expr::LogicalOp { op, .. } = value else { panic!("expected LogicalOp, got {value:?}") };
4238        assert_eq!(*op, LogicalOp::Implies);
4239    }
4240
4241    #[test]
4242    fn implies_precedence_and_binds_tighter() {
4243        // `a and b implies c` → `(a and b) implies c`
4244        let src = "rule R { v: a and b implies c }";
4245        let r = parse_ok(src);
4246        assert_eq!(r.diagnostics.len(), 0);
4247        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4248        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4249        let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4250        assert_eq!(*op, LogicalOp::Implies);
4251        assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4252    }
4253
4254    #[test]
4255    fn implies_precedence_or_binds_tighter() {
4256        // `a or b implies c` → `(a or b) implies c`
4257        let src = "rule R { v: a or b implies c }";
4258        let r = parse_ok(src);
4259        assert_eq!(r.diagnostics.len(), 0);
4260        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4261        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4262        let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4263        assert_eq!(*op, LogicalOp::Implies);
4264        assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4265    }
4266
4267    #[test]
4268    fn implies_precedence_implies_above_or() {
4269        // `a implies b or c` → `a implies (b or c)`
4270        let src = "rule R { v: a implies b or c }";
4271        let r = parse_ok(src);
4272        assert_eq!(r.diagnostics.len(), 0);
4273        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4274        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4275        let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4276        assert_eq!(*op, LogicalOp::Implies);
4277        assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4278    }
4279
4280    #[test]
4281    fn implies_precedence_not_binds_tighter() {
4282        // `not a implies b` → `(not a) implies b`
4283        let src = "rule R { v: not a implies b }";
4284        let r = parse_ok(src);
4285        assert_eq!(r.diagnostics.len(), 0);
4286        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4287        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4288        let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4289        assert_eq!(*op, LogicalOp::Implies);
4290        assert!(matches!(left.as_ref(), Expr::Not { .. }));
4291    }
4292
4293    #[test]
4294    fn implies_right_associative() {
4295        // `a implies b implies c` → `a implies (b implies c)`
4296        let src = "rule R { v: a implies b implies c }";
4297        let r = parse_ok(src);
4298        assert_eq!(r.diagnostics.len(), 0);
4299        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4300        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4301        let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4302        assert_eq!(*op, LogicalOp::Implies);
4303        assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4304    }
4305
4306    #[test]
4307    fn implies_is_keyword_parsed_as_operator() {
4308        // `implies` is a keyword — in infix position it's the operator, not an ident.
4309        // `a implies b` parses as LogicalOp, never as two adjacent identifiers.
4310        let src = "entity E { v: a implies b }";
4311        let r = parse_ok(src);
4312        assert_eq!(r.diagnostics.len(), 0);
4313        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4314        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4315        assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4316    }
4317
4318    #[test]
4319    fn implies_in_ensures() {
4320        let src = r#"rule R {
4321    when: X()
4322    ensures: a implies b
4323}"#;
4324        let r = parse_ok(src);
4325        assert_eq!(r.diagnostics.len(), 0);
4326    }
4327
4328    #[test]
4329    fn implies_in_derived_value() {
4330        let src = "entity E { v: a implies b }";
4331        let r = parse_ok(src);
4332        assert_eq!(r.diagnostics.len(), 0);
4333        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4334        let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4335        assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4336    }
4337
4338    // -----------------------------------------------------------------------
4339    // ALP-7: guidance clause ordering
4340    // -----------------------------------------------------------------------
4341
4342    #[test]
4343    fn guidance_ordering_tests_removed() {
4344        // Guidance ordering validation was moved to the structural validator.
4345        // The old `guidance:` colon form now emits migration diagnostics.
4346        // See guidance_colon_form_migration and annotation_guidance_in_rule tests.
4347    }
4348
4349    // -----------------------------------------------------------------------
4350    // ALP-9: contract declarations
4351    // -----------------------------------------------------------------------
4352
4353    #[test]
4354    fn contract_signatures_only() {
4355        let src = r#"contract Auditable {
4356    last_modified_by: Actor
4357    last_modified_at: Timestamp
4358}"#;
4359        let r = parse_ok(src);
4360        assert_eq!(r.diagnostics.len(), 0);
4361        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4362        assert_eq!(b.kind, BlockKind::Contract);
4363        assert_eq!(b.name.as_ref().unwrap().name, "Auditable");
4364        assert_eq!(b.items.len(), 2);
4365    }
4366
4367    #[test]
4368    fn contract_with_annotations() {
4369        let src = r#"contract Versioned {
4370    version: Integer
4371    @invariant Monotonic
4372        -- versions must increase
4373    @guidance
4374        -- use semantic versioning
4375}"#;
4376        let r = parse_ok(src);
4377        assert_eq!(r.diagnostics.len(), 0);
4378        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4379        assert_eq!(b.kind, BlockKind::Contract);
4380        assert_eq!(b.items.len(), 3);
4381    }
4382
4383    #[test]
4384    fn contract_with_any_type() {
4385        let src = r#"contract Identifiable {
4386    id: Any
4387}"#;
4388        let r = parse_ok(src);
4389        assert_eq!(r.diagnostics.len(), 0);
4390    }
4391
4392    #[test]
4393    fn contract_lowercase_name_rejected() {
4394        let src = "-- allium: 1\ncontract bad {}";
4395        let r = parse(src);
4396        assert!(
4397            r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4398            "expected uppercase error, got: {:?}",
4399            r.diagnostics
4400        );
4401    }
4402
4403    #[test]
4404    fn contract_colon_body_rejected() {
4405        let src = "-- allium: 1\ncontract Bad: something";
4406        let r = parse(src);
4407        assert!(
4408            r.diagnostics.iter().any(|d| d.message.contains("braces")),
4409            "expected braces error, got: {:?}",
4410            r.diagnostics
4411        );
4412    }
4413
4414    // -----------------------------------------------------------------------
4415    // ALP-15: contracts clause
4416    // -----------------------------------------------------------------------
4417
4418    #[test]
4419    fn contracts_clause_single_demands() {
4420        let src = "surface S {\n    contracts:\n        demands Auditable\n}";
4421        let r = parse_ok(src);
4422        assert_eq!(r.diagnostics.len(), 0);
4423        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4424        let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4425            panic!("expected ContractsClause, got {:?}", b.items[0].kind)
4426        };
4427        assert_eq!(entries.len(), 1);
4428        assert!(matches!(entries[0].direction, ContractDirection::Demands));
4429        assert_eq!(entries[0].name.name, "Auditable");
4430    }
4431
4432    #[test]
4433    fn contracts_clause_single_fulfils() {
4434        let src = "surface S {\n    contracts:\n        fulfils EventSubmitter\n}";
4435        let r = parse_ok(src);
4436        assert_eq!(r.diagnostics.len(), 0);
4437        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4438        let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4439            panic!("expected ContractsClause")
4440        };
4441        assert_eq!(entries.len(), 1);
4442        assert!(matches!(entries[0].direction, ContractDirection::Fulfils));
4443        assert_eq!(entries[0].name.name, "EventSubmitter");
4444    }
4445
4446    #[test]
4447    fn contracts_clause_mixed() {
4448        let src = "surface S {\n    contracts:\n        demands Auditable\n        fulfils EventSubmitter\n}";
4449        let r = parse_ok(src);
4450        assert_eq!(r.diagnostics.len(), 0);
4451        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4452        let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4453            panic!("expected ContractsClause")
4454        };
4455        assert_eq!(entries.len(), 2);
4456        assert!(matches!(entries[0].direction, ContractDirection::Demands));
4457        assert!(matches!(entries[1].direction, ContractDirection::Fulfils));
4458    }
4459
4460    #[test]
4461    fn contracts_with_other_clauses() {
4462        let src = r#"surface S {
4463    facing user: User
4464    contracts:
4465        demands Auditable
4466    exposes:
4467        user.name
4468}"#;
4469        let r = parse_ok(src);
4470        assert_eq!(r.diagnostics.len(), 0);
4471        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4472        assert_eq!(b.items.len(), 3);
4473    }
4474
4475    #[test]
4476    fn contracts_only_surface() {
4477        let src = "surface S {\n    contracts:\n        demands Foo\n        fulfils Bar\n}";
4478        let r = parse_ok(src);
4479        assert_eq!(r.diagnostics.len(), 0);
4480        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4481        assert_eq!(b.items.len(), 1);
4482    }
4483
4484    #[test]
4485    fn contracts_empty_rejected() {
4486        let src = "-- allium: 1\nsurface S {\n    contracts:\n}";
4487        let r = parse(src);
4488        assert!(
4489            r.diagnostics.iter().any(|d| d.message.contains("Empty `contracts:`")),
4490            "expected empty contracts error, got: {:?}",
4491            r.diagnostics
4492        );
4493    }
4494
4495    #[test]
4496    fn contracts_inline_block_rejected() {
4497        let src = "-- allium: 1\nsurface S {\n    contracts:\n        demands Foo {\n        }\n}";
4498        let r = parse(src);
4499        assert!(
4500            r.diagnostics.iter().any(|d| d.message.contains("Inline contract blocks")),
4501            "expected inline block error, got: {:?}",
4502            r.diagnostics
4503        );
4504    }
4505
4506    #[test]
4507    fn contracts_unknown_direction_rejected() {
4508        let src = "-- allium: 1\nsurface S {\n    contracts:\n        requires Foo\n}";
4509        let r = parse(src);
4510        assert!(
4511            r.diagnostics.iter().any(|d| d.message.contains("Unknown direction")),
4512            "expected unknown direction error, got: {:?}",
4513            r.diagnostics
4514        );
4515    }
4516
4517    // -----------------------------------------------------------------------
4518    // ALP-16: annotations
4519    // -----------------------------------------------------------------------
4520
4521    #[test]
4522    fn annotation_invariant() {
4523        let src = "contract C {\n    @invariant Determinism\n        -- all evaluations must be deterministic\n}";
4524        let r = parse_ok(src);
4525        assert_eq!(r.diagnostics.len(), 0);
4526        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4527        let BlockItemKind::Annotation(ann) = &b.items[0].kind else {
4528            panic!("expected Annotation, got {:?}", b.items[0].kind)
4529        };
4530        assert!(matches!(ann.kind, AnnotationKind::Invariant));
4531        assert_eq!(ann.name.as_ref().unwrap().name, "Determinism");
4532        assert_eq!(ann.body.len(), 1);
4533        assert_eq!(ann.body[0], "all evaluations must be deterministic");
4534    }
4535
4536    #[test]
4537    fn annotation_multiple_invariants() {
4538        let src = "contract C {\n    @invariant A\n        -- first\n    @invariant B\n        -- second\n}";
4539        let r = parse_ok(src);
4540        assert_eq!(r.diagnostics.len(), 0);
4541        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4542        assert_eq!(b.items.len(), 2);
4543        assert!(matches!(&b.items[0].kind, BlockItemKind::Annotation(_)));
4544        assert!(matches!(&b.items[1].kind, BlockItemKind::Annotation(_)));
4545    }
4546
4547    #[test]
4548    fn annotation_invariant_then_guidance() {
4549        let src = "contract C {\n    @invariant Safety\n        -- must be safe\n    @guidance\n        -- implementation notes\n}";
4550        let r = parse_ok(src);
4551        assert_eq!(r.diagnostics.len(), 0);
4552        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4553        assert_eq!(b.items.len(), 2);
4554    }
4555
4556    #[test]
4557    fn annotation_guidance_in_rule() {
4558        let src = "rule R {\n    when: Event.created\n    ensures: something\n    @guidance\n        -- do it this way\n}";
4559        let r = parse_ok(src);
4560        assert_eq!(r.diagnostics.len(), 0);
4561        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4562        let last = b.items.last().unwrap();
4563        let BlockItemKind::Annotation(ann) = &last.kind else { panic!() };
4564        assert!(matches!(ann.kind, AnnotationKind::Guidance));
4565        assert!(ann.name.is_none());
4566    }
4567
4568    #[test]
4569    fn annotation_guarantee() {
4570        let src = "surface S {\n    @guarantee ResponseTime\n        -- must respond within 100ms\n}";
4571        let r = parse_ok(src);
4572        assert_eq!(r.diagnostics.len(), 0);
4573        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4574        let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
4575        assert!(matches!(ann.kind, AnnotationKind::Guarantee));
4576        assert_eq!(ann.name.as_ref().unwrap().name, "ResponseTime");
4577    }
4578
4579    #[test]
4580    fn annotation_guarantee_then_guidance() {
4581        let src = "surface S {\n    @guarantee Fast\n        -- sub-second\n    @guidance\n        -- cache aggressively\n}";
4582        let r = parse_ok(src);
4583        assert_eq!(r.diagnostics.len(), 0);
4584        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4585        assert_eq!(b.items.len(), 2);
4586    }
4587
4588    #[test]
4589    fn annotation_contracts_guarantee_guidance() {
4590        let src = r#"surface S {
4591    contracts:
4592        demands Auditable
4593    @guarantee ResponseTime
4594        -- fast
4595    @guidance
4596        -- notes
4597}"#;
4598        let r = parse_ok(src);
4599        assert_eq!(r.diagnostics.len(), 0);
4600        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4601        assert_eq!(b.items.len(), 3);
4602    }
4603
4604    #[test]
4605    fn annotation_multiline_body() {
4606        let src = "contract C {\n    @invariant Multi\n        -- line one\n        -- line two\n        -- line three\n}";
4607        let r = parse_ok(src);
4608        assert_eq!(r.diagnostics.len(), 0);
4609        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4610        let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
4611        assert_eq!(ann.body.len(), 3);
4612        assert_eq!(ann.body[0], "line one");
4613        assert_eq!(ann.body[2], "line three");
4614    }
4615
4616    #[test]
4617    fn annotation_empty_body_rejected() {
4618        let src = "-- allium: 1\ncontract C {\n    @invariant NoBody\n}";
4619        let r = parse(src);
4620        assert!(
4621            r.diagnostics.iter().any(|d| d.message.contains("at least one indented comment line")),
4622            "expected empty body error, got: {:?}",
4623            r.diagnostics
4624        );
4625    }
4626
4627    #[test]
4628    fn annotation_unknown_keyword_rejected() {
4629        let src = "-- allium: 1\ncontract C {\n    @note Something\n        -- text\n}";
4630        let r = parse(src);
4631        assert!(
4632            r.diagnostics.iter().any(|d| d.message.contains("Unknown annotation")),
4633            "expected unknown annotation error, got: {:?}",
4634            r.diagnostics
4635        );
4636    }
4637
4638    #[test]
4639    fn expression_invariant_still_works() {
4640        let src = r#"entity E {
4641    status: pending | active
4642    invariant AllValid {
4643        this.status = active
4644    }
4645}"#;
4646        let r = parse_ok(src);
4647        assert_eq!(r.diagnostics.len(), 0);
4648        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4649        // Find the invariant block item
4650        let inv = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. }));
4651        assert!(inv.is_some(), "expression-bearing invariant should still parse");
4652    }
4653
4654    #[test]
4655    fn invariant_colon_form_migration() {
4656        let src = "-- allium: 1\ncontract C {\n    invariant: SomeName\n}";
4657        let r = parse(src);
4658        assert!(
4659            r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
4660            "expected migration diagnostic, got: {:?}",
4661            r.diagnostics
4662        );
4663    }
4664
4665    #[test]
4666    fn guidance_colon_form_migration() {
4667        let src = "-- allium: 1\nrule R {\n    when: Event.created\n    ensures: something\n    guidance: \"do it\"\n}";
4668        let r = parse(src);
4669        assert!(
4670            r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
4671            "expected migration diagnostic, got: {:?}",
4672            r.diagnostics
4673        );
4674    }
4675
4676    #[test]
4677    fn guarantee_colon_form_migration() {
4678        let src = "-- allium: 1\nsurface S {\n    guarantee: \"fast\"\n}";
4679        let r = parse(src);
4680        assert!(
4681            r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
4682            "expected migration diagnostic, got: {:?}",
4683            r.diagnostics
4684        );
4685    }
4686
4687    #[test]
4688    fn annotation_guidance_with_name_rejected() {
4689        let src = "-- allium: 1\ncontract C {\n    @guidance Named\n        -- text\n}";
4690        let r = parse(src);
4691        assert!(
4692            r.diagnostics.iter().any(|d| d.message.contains("does not take a name")),
4693            "expected guidance name error, got: {:?}",
4694            r.diagnostics
4695        );
4696    }
4697
4698    // -----------------------------------------------------------------------
4699    // ALP-11 part 2: expression-bearing invariants
4700    // -----------------------------------------------------------------------
4701
4702    #[test]
4703    fn invariant_top_level_simple() {
4704        let src = r#"invariant PositiveBalance {
4705    this.balance > 0
4706}"#;
4707        let r = parse_ok(src);
4708        assert_eq!(r.diagnostics.len(), 0);
4709        let Decl::Invariant(inv) = &r.module.declarations[0] else {
4710            panic!("expected Invariant, got {:?}", r.module.declarations[0])
4711        };
4712        assert_eq!(inv.name.name, "PositiveBalance");
4713    }
4714
4715    #[test]
4716    fn invariant_top_level_for_quantifier() {
4717        let src = r#"invariant AllPositive {
4718    for item in items: item.value > 0
4719}"#;
4720        let r = parse_ok(src);
4721        assert_eq!(r.diagnostics.len(), 0);
4722        let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
4723        assert!(matches!(inv.body, Expr::For { .. }));
4724    }
4725
4726    #[test]
4727    fn invariant_top_level_nested_for() {
4728        let src = r#"invariant NestedFor {
4729    for a in items: for b in a.children: b.valid = true
4730}"#;
4731        let r = parse_ok(src);
4732        assert_eq!(r.diagnostics.len(), 0);
4733    }
4734
4735    #[test]
4736    fn invariant_top_level_implies() {
4737        let src = r#"invariant ImpliesTest {
4738    this.active implies this.balance > 0
4739}"#;
4740        let r = parse_ok(src);
4741        assert_eq!(r.diagnostics.len(), 0);
4742        let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
4743        assert!(matches!(inv.body, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4744    }
4745
4746    #[test]
4747    fn invariant_top_level_let_binding() {
4748        let src = r#"invariant WithLet {
4749    let total = this.items.count()
4750    total > 0
4751}"#;
4752        let r = parse_ok(src);
4753        assert_eq!(r.diagnostics.len(), 0);
4754    }
4755
4756    #[test]
4757    fn invariant_top_level_collection_ops() {
4758        let src = r#"invariant CollectionOps {
4759    this.items where active = true
4760}"#;
4761        let r = parse_ok(src);
4762        assert_eq!(r.diagnostics.len(), 0);
4763    }
4764
4765    #[test]
4766    fn invariant_top_level_exists() {
4767        let src = r#"invariant ExistsCheck {
4768    exists this.primary_contact
4769}"#;
4770        let r = parse_ok(src);
4771        assert_eq!(r.diagnostics.len(), 0);
4772    }
4773
4774    #[test]
4775    fn invariant_top_level_not_exists() {
4776        let src = r#"invariant NotExistsCheck {
4777    not exists this.deleted_at
4778}"#;
4779        let r = parse_ok(src);
4780        assert_eq!(r.diagnostics.len(), 0);
4781    }
4782
4783    #[test]
4784    fn invariant_top_level_optional_navigation() {
4785        let src = r#"invariant OptionalNav {
4786    this.owner?.email ?? "none" != "none"
4787}"#;
4788        let r = parse_ok(src);
4789        assert_eq!(r.diagnostics.len(), 0);
4790    }
4791
4792    #[test]
4793    fn invariant_top_level_lowercase_rejected() {
4794        let src = "-- allium: 1\ninvariant bad { true }";
4795        let r = parse(src);
4796        assert!(
4797            r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4798            "expected uppercase error, got: {:?}",
4799            r.diagnostics
4800        );
4801    }
4802
4803    #[test]
4804    fn invariant_entity_level() {
4805        let src = r#"entity Account {
4806    balance: Decimal
4807    invariant NonNegative { this.balance >= 0 }
4808}"#;
4809        let r = parse_ok(src);
4810        assert_eq!(r.diagnostics.len(), 0);
4811        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4812        let BlockItemKind::InvariantBlock { name, body: _ } = &b.items[1].kind else {
4813            panic!("expected InvariantBlock, got {:?}", b.items[1].kind)
4814        };
4815        assert_eq!(name.name, "NonNegative");
4816    }
4817
4818    #[test]
4819    fn invariant_entity_level_this_ref() {
4820        let src = r#"entity Order {
4821    total: Decimal
4822    invariant PositiveTotal { this.total > 0 }
4823}"#;
4824        let r = parse_ok(src);
4825        assert_eq!(r.diagnostics.len(), 0);
4826    }
4827
4828    #[test]
4829    fn invariant_entity_level_implies() {
4830        let src = r#"entity Subscription {
4831    active: Boolean
4832    balance: Decimal
4833    invariant ActiveMeansPositive { this.active implies this.balance > 0 }
4834}"#;
4835        let r = parse_ok(src);
4836        assert_eq!(r.diagnostics.len(), 0);
4837    }
4838
4839    #[test]
4840    fn invariant_entity_level_lowercase_rejected() {
4841        let src = "-- allium: 1\nentity E { invariant bad { true } }";
4842        let r = parse(src);
4843        assert!(
4844            r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4845            "expected uppercase error, got: {:?}",
4846            r.diagnostics
4847        );
4848    }
4849
4850    #[test]
4851    fn invariant_colon_form_in_entity_migration() {
4852        // Old `invariant:` colon form should emit migration diagnostic
4853        let src = "-- allium: 1\nentity E {\n    invariant: -- must be valid\n}";
4854        let r = parse(src);
4855        assert!(
4856            r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
4857            "expected migration diagnostic, got: {:?}",
4858            r.diagnostics
4859        );
4860    }
4861
4862    #[test]
4863    fn invariant_top_level_colon_rejected() {
4864        // Colon-delimited body at top level is wrong syntax
4865        let src = "-- allium: 1\ninvariant Bad: some text";
4866        let r = parse(src);
4867        assert!(
4868            r.diagnostics.iter().any(|d| d.severity == Severity::Error),
4869            "expected error for colon-delimited invariant at top level, got: {:?}",
4870            r.diagnostics
4871        );
4872    }
4873
4874    #[test]
4875    fn invariant_same_name_different_scopes() {
4876        // Top-level and entity-level invariants with same name are valid (parser doesn't check)
4877        let src = r#"invariant SameName { true }
4878entity E {
4879    invariant SameName { true }
4880}"#;
4881        let r = parse_ok(src);
4882        assert_eq!(r.diagnostics.len(), 0);
4883    }
4884
4885    // -----------------------------------------------------------------------
4886    // ALP-10: cross-module config references
4887    // -----------------------------------------------------------------------
4888
4889    #[test]
4890    fn config_qualified_reference() {
4891        // `core/config.max_batch_size` should parse as MemberAccess(QualifiedName, field)
4892        let src = r#"config {
4893    param: Integer = core/config.max_batch_size
4894}"#;
4895        let r = parse_ok(src);
4896        assert_eq!(r.diagnostics.len(), 0);
4897    }
4898
4899    #[test]
4900    fn config_multiple_qualified_refs() {
4901        let src = r#"config {
4902    param_a: Integer = core/config.max_batch_size
4903    param_b: Duration = core/config.default_delay
4904}"#;
4905        let r = parse_ok(src);
4906        assert_eq!(r.diagnostics.len(), 0);
4907    }
4908
4909    #[test]
4910    fn config_qualified_ref_with_type() {
4911        let src = r#"config {
4912    publish_delay: Duration = core/config.default_delay
4913}"#;
4914        let r = parse_ok(src);
4915        assert_eq!(r.diagnostics.len(), 0);
4916    }
4917
4918    #[test]
4919    fn config_qualified_chain() {
4920        // Parameter with qualified default that itself could have a qualified default
4921        let src = r#"config {
4922    first: Integer = core/config.base
4923    second: Integer = first
4924}"#;
4925        let r = parse_ok(src);
4926        assert_eq!(r.diagnostics.len(), 0);
4927    }
4928
4929    #[test]
4930    fn config_renamed_param_with_qualified_ref() {
4931        let src = r#"config {
4932    my_timeout: Duration = core/config.base_timeout
4933}"#;
4934        let r = parse_ok(src);
4935        assert_eq!(r.diagnostics.len(), 0);
4936    }
4937
4938    // -----------------------------------------------------------------------
4939    // ALP-13: expression-form config defaults
4940    // -----------------------------------------------------------------------
4941
4942    #[test]
4943    fn config_default_arithmetic() {
4944        let src = r#"config {
4945    param: Integer = other_param + 1
4946}"#;
4947        let r = parse_ok(src);
4948        assert_eq!(r.diagnostics.len(), 0);
4949    }
4950
4951    #[test]
4952    fn config_default_qualified_arithmetic() {
4953        let src = r#"config {
4954    param: Duration = core/config.timeout * 2
4955}"#;
4956        let r = parse_ok(src);
4957        assert_eq!(r.diagnostics.len(), 0);
4958    }
4959
4960    #[test]
4961    fn config_default_parenthesised() {
4962        let src = r#"config {
4963    param: Integer = (base + 1) * factor
4964}"#;
4965        let r = parse_ok(src);
4966        assert_eq!(r.diagnostics.len(), 0);
4967    }
4968
4969    #[test]
4970    fn config_default_two_qualified_refs() {
4971        let src = r#"config {
4972    param: Duration = core/config.a + core/config.b
4973}"#;
4974        let r = parse_ok(src);
4975        assert_eq!(r.diagnostics.len(), 0);
4976    }
4977
4978    #[test]
4979    fn config_default_literal_only() {
4980        let src = r#"config {
4981    param: Integer = 5
4982}"#;
4983        let r = parse_ok(src);
4984        assert_eq!(r.diagnostics.len(), 0);
4985    }
4986
4987    #[test]
4988    fn config_default_decimal_literal() {
4989        let src = r#"config {
4990    param: Decimal = price * 1.5
4991}"#;
4992        let r = parse_ok(src);
4993        assert_eq!(r.diagnostics.len(), 0);
4994    }
4995
4996    #[test]
4997    fn config_default_mixed_operators() {
4998        let src = r#"config {
4999    param: Duration = timeout * 2 + 1.minute
5000}"#;
5001        let r = parse_ok(src);
5002        assert_eq!(r.diagnostics.len(), 0);
5003    }
5004
5005    #[test]
5006    fn config_default_operator_precedence() {
5007        // a + b * c should be Add(a, Mul(b, c))
5008        let src = r#"config {
5009    param: Integer = a + b * c
5010}"#;
5011        let r = parse_ok(src);
5012        assert_eq!(r.diagnostics.len(), 0);
5013    }
5014
5015    // -----------------------------------------------------------------------
5016    // Version 2 marker
5017    // -----------------------------------------------------------------------
5018
5019    #[test]
5020    fn version_2_accepted() {
5021        let r = parse("-- allium: 2\nentity User {}");
5022        assert_eq!(r.module.version, Some(2));
5023        assert_eq!(r.diagnostics.len(), 0);
5024    }
5025
5026    #[test]
5027    fn version_99_still_rejected() {
5028        let r = parse("-- allium: 99\nentity User {}");
5029        assert!(r.diagnostics.iter().any(|d|
5030            d.severity == Severity::Error && d.message.contains("unsupported")
5031        ));
5032    }
5033
5034    #[test]
5035    fn contract_typed_signature() {
5036        let src = r#"contract Codec {
5037    serialize: (value: Any) -> ByteArray
5038}"#;
5039        let r = parse_ok(src);
5040        assert_eq!(r.diagnostics.len(), 0);
5041        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5042        assert_eq!(b.kind, BlockKind::Contract);
5043        let BlockItemKind::Assignment { name, value } = &b.items[0].kind else { panic!() };
5044        assert_eq!(name.name, "serialize");
5045        assert!(matches!(value, Expr::ProjectionMap { .. }));
5046    }
5047
5048    #[test]
5049    fn contract_multi_param_signature() {
5050        let src = r#"contract Codec {
5051    serialize: (value: Any, format: String) -> ByteArray
5052}"#;
5053        let r = parse_ok(src);
5054        assert_eq!(r.diagnostics.len(), 0);
5055    }
5056
5057    #[test]
5058    fn comma_separated_entity_fields() {
5059        let src = "entity Point { x: Decimal, y: Decimal }";
5060        let r = parse_ok(src);
5061        assert_eq!(r.diagnostics.len(), 0);
5062        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5063        assert_eq!(b.items.len(), 2);
5064        assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "x"));
5065        assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "y"));
5066    }
5067
5068    #[test]
5069    fn comma_separated_value_fields() {
5070        let src = "value Coord { x: Integer, y: Integer }";
5071        let r = parse_ok(src);
5072        assert_eq!(r.diagnostics.len(), 0);
5073        let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5074        assert_eq!(b.items.len(), 2);
5075    }
5076}