1use serde::Serialize;
8
9use crate::ast::*;
10use crate::diagnostic::Diagnostic;
11use crate::lexer::{lex, SourceMap, Token, TokenKind};
12use crate::Span;
13
14#[derive(Debug, Serialize)]
19pub struct ParseResult {
20 pub module: Module,
21 pub diagnostics: Vec<Diagnostic>,
22}
23
24pub 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
42struct Parser<'s> {
47 source: &'s str,
48 tokens: Vec<Token>,
49 pos: usize,
50 source_map: SourceMap,
51 diagnostics: Vec<Diagnostic>,
52}
53
54impl<'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 fn parse_ident(&mut self) -> Option<Ident> {
134 self.parse_ident_in("identifier")
135 }
136
137 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 fn parse_string(&mut self) -> Option<StringLiteral> {
157 let tok = self.expect(TokenKind::String)?;
158 let raw = self.text(tok.span);
159 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
169fn 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; 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; }
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
211fn 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
235fn clause_allows_binding(keyword: &str) -> bool {
237 matches!(keyword, "when")
238}
239
240fn is_binding_clause_keyword(text: &str) -> bool {
243 matches!(text, "facing" | "context")
244}
245
246fn 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
255impl<'s> Parser<'s> {
260 fn parse_module(&mut self) -> Module {
261 let start = self.peek().span;
262 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 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; }
314 None
315}
316
317impl<'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 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 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 fn parse_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
396 let start = self.advance().span; self.parse_block_from(start, kind)
398 }
399
400 fn parse_block_from(&mut self, start: Span, kind: BlockKind) -> Option<BlockDecl> {
401 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 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(); }
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 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(); 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 fn parse_default_decl(&mut self) -> Option<DefaultDecl> {
487 let start = self.expect(TokenKind::Default)?.span;
488
489 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 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 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 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 fn parse_contract_decl(&mut self) -> Option<BlockDecl> {
568 let start = self.advance().span; let name = self.parse_ident_in("contract name")?;
570
571 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 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 fn parse_invariant_decl(&mut self) -> Option<InvariantDecl> {
602 let start = self.advance().span; let name = self.parse_ident_in("invariant name")?;
604
605 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 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
660impl<'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 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 if self.at(TokenKind::Let) {
684 return self.parse_let_item(start);
685 }
686
687 if self.at(TokenKind::For) {
689 return self.parse_for_block_item(start);
690 }
691
692 if self.at(TokenKind::If) {
694 return self.parse_if_block_item(start);
695 }
696
697 if self.at(TokenKind::At) {
699 return self.parse_annotation(start);
700 }
701
702 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 if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
711 self.advance(); self.advance(); let text = self.parse_string()?;
714 return Some(BlockItem {
715 span: start.merge(text.span),
716 kind: BlockItemKind::OpenQuestion { text },
717 });
718 }
719
720 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 }
735 }
736
737 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 }
745
746 if self.peek_kind().is_word() {
749 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 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 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 if self.peek_at(1).kind == TokenKind::LParen {
775 return self.parse_param_or_clause_item(start);
776 }
777
778 if self.peek_at(1).kind == TokenKind::Colon {
780 return self.parse_assign_or_clause_item(start);
781 }
782 }
783
784 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(); 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 fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
814 let keyword_tok = self.advance(); let keyword = self.text(keyword_tok.span).to_string();
816 let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
817 self.advance(); 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 fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
834 self.advance(); 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 Some(self.parse_expr(0)?)
844 } else {
845 None
846 };
847
848 self.expect(TokenKind::Colon)?;
849
850 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 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 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 fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
901 self.advance(); let mut branches = Vec::new();
903
904 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 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 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 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 fn parse_contracts_clause(&mut self, start: Span) -> Option<BlockItem> {
970 self.advance(); self.advance(); 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 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 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 fn parse_annotation(&mut self, start: Span) -> Option<BlockItem> {
1042 let at_tok = self.advance(); 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 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 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 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 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 while body.last().is_some_and(|l| l.is_empty()) {
1161 body.pop();
1162 }
1163
1164 body
1165 }
1166
1167 fn parse_invariant_block_item(&mut self, start: Span) -> Option<BlockItem> {
1169 self.advance(); let name = self.parse_ident_in("invariant name")?;
1171
1172 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(); let name_text = self.text(name_tok.span).to_string();
1192 self.advance(); 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 fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
1221 let obj_tok = self.advance(); self.advance(); let field = self.parse_ident_in("field name")?;
1224 self.advance(); 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 let saved_pos = self.pos;
1248 let _name_tok = self.advance();
1249 self.advance(); 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 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 self.pos = saved_pos;
1289 if self.peek_at(1).kind == TokenKind::Colon {
1291 }
1293 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 fn parse_for_binding(&mut self) -> Option<ForBinding> {
1311 if self.at(TokenKind::LParen) {
1312 let start = self.advance().span; 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 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 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(); 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 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 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 self.parse_expr(0)
1384 }
1385 }
1386
1387 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 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
1435const 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 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 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 let t = self.advance();
1586 Some(Expr::Ident(Ident {
1587 span: t.span,
1588 name: "*".into(),
1589 }))
1590 }
1591 TokenKind::Minus => {
1592 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 fn infix_bp(&self) -> Option<(u8, u8)> {
1622 match self.peek_kind() {
1623 TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), 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)), 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 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 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 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 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 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 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 TokenKind::Lt => {
1919 if self.pos > 0 {
1920 let prev = self.tokens[self.pos - 1];
1921 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 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 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 self.advance(); let mut args = Vec::new();
1968 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 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 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(); 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 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 fn parse_if_expr(&mut self) -> Option<Expr> {
2075 let start = self.advance().span; let mut branches = Vec::new();
2077
2078 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 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 fn parse_for_expr(&mut self) -> Option<Expr> {
2138 let start = self.advance().span; let binding = self.parse_for_binding()?;
2140 self.expect(TokenKind::In)?;
2141
2142 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
2144
2145 let filter = if self.eat(TokenKind::Where).is_some() {
2146 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 fn parse_brace_expr(&mut self) -> Option<Expr> {
2167 let start = self.advance().span; 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 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2179 return self.parse_object_literal(start);
2180 }
2181
2182 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 fn parse_paren_expr(&mut self) -> Option<Expr> {
2223 let start = self.advance().span; 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#[cfg(test)]
2261mod tests {
2262 use super::*;
2263 use crate::diagnostic::Severity;
2264
2265 fn parse_ok(src: &str) -> ParseResult {
2266 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 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 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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 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 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 #[test]
2726 fn pipe_binds_tighter_than_or() {
2727 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 let Expr::LogicalOp { op, right, .. } = value else {
2735 panic!("expected LogicalOp, got {value:?}");
2736 };
2737 assert_eq!(*op, LogicalOp::Or);
2738 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2740 }
2741
2742 #[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 #[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 #[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 #[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 #[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 assert!(matches!(value, Expr::NullCoalesce { .. }));
2810 }
2811
2812 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 assert!(msg.contains("expected entity name"), "got: {msg}");
2952 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 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 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 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 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 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 #[test]
3178 fn spec_for_bare_form() {
3179 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 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 #[test]
3208 fn spec_reject_double_equals() {
3209 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 #[test]
3221 fn spec_reject_system_block() {
3222 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 #[test]
3234 fn spec_reject_tags_clause() {
3235 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 #[test]
3251 fn spec_reject_includes_operator() {
3252 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 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 #[test]
3283 fn spec_reject_range_literal() {
3284 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 #[test]
3300 fn spec_within_in_actor() {
3301 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 #[test]
3313 fn spec_reject_module_declaration() {
3314 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 #[test]
3326 fn spec_reject_module_level_guidance() {
3327 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 #[test]
3339 fn spec_guarantee_in_surface_migration() {
3340 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 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 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 #[test]
3377 fn spec_reject_suffix_predicate() {
3378 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 #[test]
3394 fn spec_add_remove_in_ensures() {
3395 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 #[test]
3420 fn spec_first_last_access() {
3421 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 #[test]
3430 fn spec_set_arithmetic() {
3431 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 #[test]
3445 fn spec_discard_binding_in_trigger() {
3446 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 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 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 #[test]
3481 fn spec_default_with_object_literal() {
3482 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 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 #[test]
3502 fn spec_surface_related_clause() {
3503 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 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 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 #[test]
3541 fn spec_for_in_surface_provides() {
3542 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 #[test]
3556 fn spec_use_without_alias() {
3557 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 #[test]
3566 fn spec_empty_external_entity() {
3567 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 #[test]
3576 fn spec_surface_multiline_provides() {
3577 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 #[test]
3593 fn spec_surface_multiline_exposes() {
3594 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 #[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 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 #[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 #[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 #[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 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 #[test]
3690 fn this_keyword_expression() {
3691 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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[test]
3853 fn precedence_logical_and_binds_tighter_than_or() {
3854 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 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 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 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 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 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 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 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 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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 #[test]
4343 fn guidance_ordering_tests_removed() {
4344 }
4348
4349 #[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 #[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 #[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 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 #[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 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 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 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 #[test]
4890 fn config_qualified_reference() {
4891 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 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 #[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 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 #[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}