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 | TokenKind::Transitions
253 )
254}
255
256fn extract_when_clause(expr: &Expr) -> Option<(Expr, WhenClause)> {
261 if let Expr::WhenGuard { action, condition, span } = expr {
262 if let Expr::Comparison {
264 left,
265 op: ComparisonOp::Eq,
266 right,
267 span: _cond_span,
268 } = condition.as_ref()
269 {
270 if let Expr::Ident(status_field) = left.as_ref() {
271 let mut qualifying_states = Vec::new();
272 collect_pipe_idents(right, &mut qualifying_states);
273 if !qualifying_states.is_empty() {
274 return Some((
275 *action.clone(),
276 WhenClause {
277 span: *span,
278 status_field: status_field.clone(),
279 qualifying_states,
280 },
281 ));
282 }
283 }
284 }
285 }
288 None
294}
295
296fn collect_pipe_idents(expr: &Expr, out: &mut Vec<Ident>) {
297 match expr {
298 Expr::Ident(id) => out.push(id.clone()),
299 Expr::Pipe { left, right, .. } => {
300 collect_pipe_idents(left, out);
301 collect_pipe_idents(right, out);
302 }
303 _ => {}
304 }
305}
306
307impl<'s> Parser<'s> {
312 fn parse_module(&mut self) -> Module {
313 let start = self.peek().span;
314 let version = detect_version(self.source);
317
318 match version {
319 None => {
320 self.diagnostics.push(Diagnostic::warning(
321 start,
322 "missing version marker; expected '-- allium: 1' as the first line",
323 ));
324 }
325 Some(1) | Some(2) | Some(3) => {}
326 Some(v) => {
327 self.diagnostics.push(Diagnostic::error(
328 start,
329 format!("unsupported allium version {v}; this parser supports versions 1, 2 and 3"),
330 ));
331 }
332 }
333
334 let mut decls = Vec::new();
335 while !self.at_eof() {
336 if let Some(d) = self.parse_decl() {
337 decls.push(d);
338 } else {
339 self.advance();
341 }
342 }
343 let end = self.peek().span;
344 Module {
345 span: start.merge(end),
346 version,
347 declarations: decls,
348 }
349 }
350}
351
352fn detect_version(source: &str) -> Option<u32> {
353 for line in source.lines() {
354 let trimmed = line.trim();
355 if trimmed.is_empty() {
356 continue;
357 }
358 if let Some(rest) = trimmed.strip_prefix("--") {
359 let rest = rest.trim();
360 if let Some(ver) = rest.strip_prefix("allium:") {
361 return ver.trim().parse().ok();
362 }
363 }
364 break; }
366 None
367}
368
369impl<'s> Parser<'s> {
374 fn parse_decl(&mut self) -> Option<Decl> {
375 match self.peek_kind() {
376 TokenKind::Use => self.parse_use_decl().map(Decl::Use),
377 TokenKind::Rule => self.parse_block(BlockKind::Rule).map(Decl::Block),
378 TokenKind::Entity => self.parse_block(BlockKind::Entity).map(Decl::Block),
379 TokenKind::External => {
380 let start = self.advance().span;
381 if self.at(TokenKind::Entity) {
382 self.parse_block_from(start, BlockKind::ExternalEntity)
383 .map(Decl::Block)
384 } else {
385 self.error(self.peek().span, "expected 'entity' after 'external'");
386 None
387 }
388 }
389 TokenKind::Value => self.parse_block(BlockKind::Value).map(Decl::Block),
390 TokenKind::Enum => self.parse_block(BlockKind::Enum).map(Decl::Block),
391 TokenKind::Given => self.parse_anonymous_block(BlockKind::Given).map(Decl::Block),
392 TokenKind::Config => self.parse_anonymous_block(BlockKind::Config).map(Decl::Block),
393 TokenKind::Surface => self.parse_block(BlockKind::Surface).map(Decl::Block),
394 TokenKind::Actor => self.parse_block(BlockKind::Actor).map(Decl::Block),
395 TokenKind::Contract => self.parse_contract_decl().map(Decl::Block),
396 TokenKind::Invariant => self.parse_invariant_decl().map(Decl::Invariant),
397 TokenKind::Default => self.parse_default_decl().map(Decl::Default),
398 TokenKind::Variant => self.parse_variant_decl().map(Decl::Variant),
399 TokenKind::Deferred => self.parse_deferred_decl().map(Decl::Deferred),
400 TokenKind::Open => self.parse_open_question_decl().map(Decl::OpenQuestion),
401 TokenKind::Ident
403 if self.peek_at(1).kind == TokenKind::Slash
404 && self.text(self.peek_at(2).span) == "config" =>
405 {
406 self.parse_qualified_config().map(Decl::Block)
407 }
408 _ => {
409 self.error(
410 self.peek().span,
411 format!(
412 "expected declaration (entity, rule, enum, value, config, surface, actor, \
413 given, default, variant, deferred, use, open question, contract, invariant), found {}",
414 self.peek_kind(),
415 ),
416 );
417 None
418 }
419 }
420 }
421
422 fn parse_use_decl(&mut self) -> Option<UseDecl> {
427 let start = self.expect(TokenKind::Use)?.span;
428 let path = self.parse_string()?;
429 let alias = if self.eat(TokenKind::As).is_some() {
430 Some(self.parse_ident_in("import alias")?)
431 } else {
432 None
433 };
434 let end = alias
435 .as_ref()
436 .map(|a| a.span)
437 .unwrap_or(path.span);
438 Some(UseDecl {
439 span: start.merge(end),
440 path,
441 alias,
442 })
443 }
444
445 fn parse_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
448 let start = self.advance().span; self.parse_block_from(start, kind)
450 }
451
452 fn parse_block_from(&mut self, start: Span, kind: BlockKind) -> Option<BlockDecl> {
453 if kind == BlockKind::ExternalEntity {
456 self.expect(TokenKind::Entity)?;
457 }
458 let context = match kind {
459 BlockKind::Entity | BlockKind::ExternalEntity => "entity name",
460 BlockKind::Rule => "rule name",
461 BlockKind::Surface => "surface name",
462 BlockKind::Actor => "actor name",
463 BlockKind::Value => "value type name",
464 BlockKind::Enum => "enum name",
465 _ => "block name",
466 };
467 let name = Some(self.parse_ident_in(context)?);
468 self.expect(TokenKind::LBrace)?;
469 let items = if kind == BlockKind::Enum {
470 self.parse_enum_body()
471 } else {
472 self.parse_block_items(kind)
473 };
474 let end = self.expect(TokenKind::RBrace)?.span;
475 Some(BlockDecl {
476 span: start.merge(end),
477 kind,
478 name,
479 items,
480 })
481 }
482
483 fn parse_enum_body(&mut self) -> Vec<BlockItem> {
488 let mut items = Vec::new();
489 while !self.at(TokenKind::RBrace) && !self.at_eof() {
490 if self.eat(TokenKind::Pipe).is_some() {
491 continue;
492 }
493 if self.at(TokenKind::BacktickLiteral) {
494 let t = self.advance();
495 let raw = self.text(t.span);
496 let value = raw[1..raw.len() - 1].to_string();
497 items.push(BlockItem {
498 span: t.span,
499 kind: BlockItemKind::EnumVariant {
500 name: Ident { span: t.span, name: value },
501 backtick_quoted: true,
502 },
503 });
504 } else if let Some(ident) = self.parse_ident_in("enum variant") {
505 items.push(BlockItem {
506 span: ident.span,
507 kind: BlockItemKind::EnumVariant { name: ident, backtick_quoted: false },
508 });
509 } else {
510 self.advance(); }
512 }
513 items
514 }
515
516 fn parse_anonymous_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
517 let start = self.advance().span;
518 self.expect(TokenKind::LBrace)?;
519 let items = self.parse_block_items(kind);
520 let end = self.expect(TokenKind::RBrace)?.span;
521 Some(BlockDecl {
522 span: start.merge(end),
523 kind,
524 name: None,
525 items,
526 })
527 }
528
529 fn parse_qualified_config(&mut self) -> Option<BlockDecl> {
532 let alias = self.parse_ident_in("config qualifier")?;
533 let start = alias.span;
534 self.expect(TokenKind::Slash)?;
535 self.advance(); self.expect(TokenKind::LBrace)?;
537 let items = self.parse_block_items(BlockKind::Config);
538 let end = self.expect(TokenKind::RBrace)?.span;
539 Some(BlockDecl {
540 span: start.merge(end),
541 kind: BlockKind::Config,
542 name: Some(alias),
543 items,
544 })
545 }
546
547 fn parse_default_decl(&mut self) -> Option<DefaultDecl> {
550 let start = self.expect(TokenKind::Default)?.span;
551
552 let (type_name, name) = if self.peek_kind().is_word()
556 && self.peek_at(1).kind.is_word()
557 && self.peek_at(2).kind == TokenKind::Eq
558 {
559 let t = self.parse_ident_in("type name")?;
560 let n = self.parse_ident_in("default name")?;
561 (Some(t), n)
562 } else {
563 (None, self.parse_ident_in("default name")?)
564 };
565
566 self.expect(TokenKind::Eq)?;
567 let value = self.parse_expr(0)?;
568 Some(DefaultDecl {
569 span: start.merge(value.span()),
570 type_name,
571 name,
572 value,
573 })
574 }
575
576 fn parse_variant_decl(&mut self) -> Option<VariantDecl> {
579 let start = self.expect(TokenKind::Variant)?.span;
580 let name = self.parse_ident_in("variant name")?;
581 self.expect(TokenKind::Colon)?;
582 let base = self.parse_expr(0)?;
583
584 let items = if self.eat(TokenKind::LBrace).is_some() {
585 let items = self.parse_block_items(BlockKind::Entity);
586 self.expect(TokenKind::RBrace)?;
587 items
588 } else {
589 Vec::new()
590 };
591
592 let end = if let Some(last) = items.last() {
593 last.span
594 } else {
595 base.span()
596 };
597 Some(VariantDecl {
598 span: start.merge(end),
599 name,
600 base,
601 items,
602 })
603 }
604
605 fn parse_deferred_decl(&mut self) -> Option<DeferredDecl> {
608 let start = self.expect(TokenKind::Deferred)?.span;
609 let path = self.parse_deferred_path()?;
610
611 let location_hint = if self.at(TokenKind::String)
615 && self.same_line(path.span().end, self.peek().span.start)
616 {
617 self.parse_string()
618 } else {
619 None
620 };
621
622 let end = location_hint
623 .as_ref()
624 .map_or(path.span(), |hint| hint.span);
625 Some(DeferredDecl {
626 span: start.merge(end),
627 path,
628 location_hint,
629 })
630 }
631
632 fn parse_deferred_path(&mut self) -> Option<Expr> {
649 let first = self.parse_ident_in("deferred name")?;
650
651 let mut expr = if self.at(TokenKind::Slash)
652 && self.same_line(first.span.end, self.peek().span.start)
653 {
654 self.advance(); if !self.peek_kind().is_word()
656 || !self.same_line(first.span.end, self.peek().span.start)
657 {
658 let tok = self.peek();
659 self.error(
660 tok.span,
661 format!("expected deferred name after '/', found {}", tok.kind),
662 );
663 return None;
664 }
665 let name = self.parse_ident_in("deferred name after '/'")?;
666 Expr::QualifiedName(QualifiedName {
667 span: first.span.merge(name.span),
668 qualifier: Some(first.name),
669 name: name.name,
670 })
671 } else {
672 Expr::Ident(first)
673 };
674
675 while self.at(TokenKind::Dot)
676 && self.peek_at(1).kind.is_word()
677 && self.same_line(expr.span().end, self.peek_at(1).span.start)
678 {
679 self.advance(); let field = self.parse_ident_in("field name")?;
681 expr = Expr::MemberAccess {
682 span: expr.span().merge(field.span),
683 object: Box::new(expr),
684 field,
685 };
686 }
687 Some(expr)
688 }
689
690 fn same_line(&self, from: usize, to: usize) -> bool {
692 !self.source[from..to].contains(['\n', '\r'])
693 }
694
695 fn parse_open_question_decl(&mut self) -> Option<OpenQuestionDecl> {
698 let start = self.expect(TokenKind::Open)?.span;
699 self.expect(TokenKind::Question)?;
700 let text = self.parse_string()?;
701 Some(OpenQuestionDecl {
702 span: start.merge(text.span),
703 text,
704 })
705 }
706
707 fn parse_contract_decl(&mut self) -> Option<BlockDecl> {
710 let start = self.advance().span; let name = self.parse_ident_in("contract name")?;
712
713 if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
715 self.diagnostics.push(Diagnostic::error(
716 name.span,
717 "contract name must start with an uppercase letter",
718 ));
719 }
720
721 if self.at(TokenKind::Colon) {
723 self.error(
724 self.peek().span,
725 "contract body must use braces { }, not a colon",
726 );
727 return None;
728 }
729
730 self.expect(TokenKind::LBrace)?;
731 let items = self.parse_block_items(BlockKind::Contract);
732 let end = self.expect(TokenKind::RBrace)?.span;
733 Some(BlockDecl {
734 span: start.merge(end),
735 kind: BlockKind::Contract,
736 name: Some(name),
737 items,
738 })
739 }
740
741 fn parse_invariant_decl(&mut self) -> Option<InvariantDecl> {
744 let start = self.advance().span; let name = self.parse_ident_in("invariant name")?;
746
747 if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
749 self.diagnostics.push(Diagnostic::error(
750 name.span,
751 "invariant name must start with an uppercase letter",
752 ));
753 }
754
755 self.expect(TokenKind::LBrace)?;
756 let body = self.parse_invariant_body()?;
757 let end = self.expect(TokenKind::RBrace)?.span;
758 Some(InvariantDecl {
759 span: start.merge(end),
760 name,
761 body,
762 })
763 }
764
765 fn parse_invariant_body(&mut self) -> Option<Expr> {
768 let start = self.peek().span;
769 let mut items = Vec::new();
770
771 while !self.at(TokenKind::RBrace) && !self.at_eof() {
772 if self.at(TokenKind::Let) {
773 let let_start = self.advance().span;
774 let name = self.parse_ident_in("binding name")?;
775 self.expect(TokenKind::Eq)?;
776 let value = self.parse_expr(0)?;
777 items.push(Expr::LetExpr {
778 span: let_start.merge(value.span()),
779 name,
780 value: Box::new(value),
781 });
782 } else if let Some(expr) = self.parse_expr(0) {
783 items.push(expr);
784 } else {
785 self.advance();
786 break;
787 }
788 }
789
790 if items.len() == 1 {
791 Some(items.pop().unwrap())
792 } else {
793 let end = items.last().map(|e| e.span()).unwrap_or(start);
794 Some(Expr::Block {
795 span: start.merge(end),
796 items,
797 })
798 }
799 }
800}
801
802impl<'s> Parser<'s> {
807 fn parse_block_items(&mut self, block_kind: BlockKind) -> Vec<BlockItem> {
808 let mut items = Vec::new();
809 while !self.at(TokenKind::RBrace) && !self.at_eof() {
810 if let Some(item) = self.parse_block_item(block_kind) {
811 items.push(item);
812 self.eat(TokenKind::Comma);
813 } else {
814 self.advance();
816 }
817 }
818 items
819 }
820
821 fn parse_block_item(&mut self, block_kind: BlockKind) -> Option<BlockItem> {
822 let start = self.peek().span;
823
824 if self.at(TokenKind::Let) {
826 return self.parse_let_item(start);
827 }
828
829 if self.at(TokenKind::For) {
831 return self.parse_for_block_item(start);
832 }
833
834 if self.at(TokenKind::If) {
836 return self.parse_if_block_item(start);
837 }
838
839 if self.at(TokenKind::At) {
841 return self.parse_annotation(start);
842 }
843
844 if self.at(TokenKind::Invariant) && self.peek_at(1).kind.is_word()
846 && self.peek_at(2).kind != TokenKind::Colon
847 {
848 return self.parse_invariant_block_item(start);
849 }
850
851 if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
853 self.advance(); self.advance(); let text = self.parse_string()?;
856 return Some(BlockItem {
857 span: start.merge(text.span),
858 kind: BlockItemKind::OpenQuestion { text },
859 });
860 }
861
862 if self.at(TokenKind::Transitions)
864 && self.peek_at(1).kind.is_word()
865 && self.peek_at(2).kind == TokenKind::LBrace
866 {
867 return self.parse_transitions_block(start);
868 }
869
870 if self.peek_kind() == TokenKind::Ident {
872 let word = self.text(self.peek().span);
873 if (word == "guidance" || word == "guarantee")
874 && self.peek_at(1).kind == TokenKind::Colon
875 {
876 let kw = word.to_string();
877 self.error(
878 self.peek().span,
879 format!(
880 "`{kw}:` syntax was replaced by `@{kw}`. Use `@{kw}` followed by indented comment lines."
881 ),
882 );
883 }
885 }
886
887 if self.at(TokenKind::Invariant) && self.peek_at(1).kind == TokenKind::Colon {
889 self.error(
890 self.peek().span,
891 "`invariant:` syntax was replaced by `@invariant`. Use `@invariant Name` followed by indented comment lines.",
892 );
893 }
895
896 if self.peek_kind().is_word() {
899 if self.text(self.peek().span) == "contracts"
901 && self.peek_at(1).kind == TokenKind::Colon
902 {
903 return self.parse_contracts_clause(start);
904 }
905
906 if is_binding_clause_keyword(self.text(self.peek().span))
909 && self.peek_at(1).kind.is_word()
910 && self.peek_at(2).kind == TokenKind::Colon
911 {
912 return self.parse_binding_clause_item(start);
913 }
914
915 if self.peek_at(1).kind == TokenKind::Dot
917 && self.peek_at(2).kind.is_word()
918 && self.peek_at(3).kind == TokenKind::Colon
919 {
920 return self.parse_path_assignment_item(start);
921 }
922
923 if self.peek_at(1).kind == TokenKind::LParen {
925 return self.parse_param_or_clause_item(start);
926 }
927
928 if block_kind == BlockKind::Rule
930 && (self.at(TokenKind::Produces) || self.at(TokenKind::Consumes))
931 && self.peek_at(1).kind == TokenKind::Colon
932 {
933 return self.parse_legacy_field_list_clause(start);
934 }
935
936 if self.peek_at(1).kind == TokenKind::Colon {
938 return self.parse_assign_or_clause_item(start);
939 }
940 }
941
942 if token_is_clause_keyword(self.peek_kind()) && self.peek_at(1).kind == TokenKind::Colon {
944 return self.parse_assign_or_clause_item(start);
945 }
946
947 self.error(
948 start,
949 format!(
950 "expected block item (name: value, let name = value, when:/requires:/ensures: clause, \
951 for ... in ...:, or open question), found {}",
952 self.peek_kind(),
953 ),
954 );
955 None
956 }
957
958 fn parse_transitions_block(&mut self, start: Span) -> Option<BlockItem> {
960 self.advance(); let field = self.parse_ident_in("transition field name")?;
962 self.expect(TokenKind::LBrace)?;
963
964 let mut edges = Vec::new();
965 let mut terminal = Vec::new();
966
967 while !self.at(TokenKind::RBrace) && !self.at_eof() {
968 if self.at(TokenKind::Terminal) && self.peek_at(1).kind == TokenKind::Colon {
970 self.advance(); self.advance(); loop {
973 let state = self.parse_ident_in("terminal state")?;
974 terminal.push(state);
975 if self.eat(TokenKind::Comma).is_none() {
976 break;
977 }
978 if self.at(TokenKind::RBrace) {
980 break;
981 }
982 }
983 continue;
984 }
985
986 let from = self.parse_ident_in("source state")?;
988 if self.expect(TokenKind::ThinArrow).is_none() {
989 while !self.at(TokenKind::RBrace) && !self.at_eof() {
991 let cur_line = self.line_of(self.peek().span);
992 self.advance();
993 if self.line_of(self.peek().span) != cur_line {
994 break;
995 }
996 }
997 continue;
998 }
999 let to = self.parse_ident_in("target state")?;
1000 let edge_span = from.span.merge(to.span);
1001 edges.push(TransitionEdge {
1002 span: edge_span,
1003 from,
1004 to,
1005 });
1006
1007 self.eat(TokenKind::Comma);
1009 }
1010
1011 let end = self.expect(TokenKind::RBrace)?.span;
1012
1013 Some(BlockItem {
1014 span: start.merge(end),
1015 kind: BlockItemKind::TransitionsBlock(TransitionGraph {
1016 span: start.merge(end),
1017 field,
1018 edges,
1019 terminal,
1020 }),
1021 })
1022 }
1023
1024 fn parse_legacy_field_list_clause(&mut self, start: Span) -> Option<BlockItem> {
1027 let keyword_tok = self.advance(); let keyword = self.text(keyword_tok.span).to_string();
1029 self.advance(); let clause_line = self.line_of(start);
1033 loop {
1034 if self.at(TokenKind::RBrace) || self.at_eof() {
1035 break;
1036 }
1037 if self.line_of(self.peek().span) > clause_line {
1038 break;
1039 }
1040 self.advance();
1041 }
1042
1043 self.diagnostics.push(Diagnostic::warning(
1044 start.merge(keyword_tok.span),
1045 format!(
1046 "`{keyword}:` clauses are removed in v3; use `when` clauses on entity fields instead"
1047 ),
1048 ));
1049
1050 self.parse_block_item(BlockKind::Rule)
1052 }
1053
1054 fn parse_let_item(&mut self, start: Span) -> Option<BlockItem> {
1055 self.advance(); let name = self.parse_ident_in("binding name")?;
1057 self.expect(TokenKind::Eq)?;
1058 let value = self.parse_clause_value(start)?;
1059 Some(BlockItem {
1060 span: start.merge(value.span()),
1061 kind: BlockItemKind::Let { name, value },
1062 })
1063 }
1064
1065 fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
1068 let keyword_tok = self.advance(); let keyword = self.text(keyword_tok.span).to_string();
1070 let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
1071 self.advance(); let type_expr = self.parse_clause_value(start)?;
1073 let value_span = type_expr.span();
1074 let value = Expr::Binding {
1075 span: binding_name.span.merge(value_span),
1076 name: binding_name,
1077 value: Box::new(type_expr),
1078 };
1079 Some(BlockItem {
1080 span: start.merge(value_span),
1081 kind: BlockItemKind::Clause { keyword, value },
1082 })
1083 }
1084
1085 fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
1088 self.advance(); let binding = self.parse_for_binding()?;
1090 self.expect(TokenKind::In)?;
1091
1092 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1093
1094 let filter = if self.eat(TokenKind::Where).is_some() {
1095 Some(self.parse_expr(0)?)
1098 } else {
1099 None
1100 };
1101
1102 self.expect(TokenKind::Colon)?;
1103
1104 let for_line = self.line_of(start);
1106 let next_line = self.line_of(self.peek().span);
1107
1108 let items = if next_line > for_line {
1109 let base_col = self.col_of(self.peek().span);
1110 self.parse_indented_block_items(base_col)
1111 } else {
1112 let mut items = Vec::new();
1114 if let Some(item) = self.parse_block_item(BlockKind::Entity) {
1115 items.push(item);
1116 }
1117 items
1118 };
1119
1120 let end = items
1121 .last()
1122 .map(|i| i.span)
1123 .unwrap_or(start);
1124
1125 Some(BlockItem {
1126 span: start.merge(end),
1127 kind: BlockItemKind::ForBlock {
1128 binding,
1129 collection,
1130 filter,
1131 items,
1132 },
1133 })
1134 }
1135
1136 fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
1138 let mut items = Vec::new();
1139 while !self.at_eof()
1140 && !self.at(TokenKind::RBrace)
1141 && self.col_of(self.peek().span) >= base_col
1142 {
1143 if let Some(item) = self.parse_block_item(BlockKind::Entity) {
1144 items.push(item);
1145 } else {
1146 self.advance();
1147 break;
1148 }
1149 }
1150 items
1151 }
1152
1153 fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
1155 self.advance(); let mut branches = Vec::new();
1157
1158 let condition = self.parse_expr(0)?;
1160 self.expect(TokenKind::Colon)?;
1161 let if_line = self.line_of(start);
1162 let items = self.parse_if_block_body(if_line);
1163 branches.push(CondBlockBranch {
1164 span: start.merge(items.last().map(|i| i.span).unwrap_or(start)),
1165 condition,
1166 items,
1167 });
1168
1169 let mut else_items = None;
1171 while self.at(TokenKind::Else) {
1172 let else_tok = self.advance();
1173 if self.at(TokenKind::If) {
1174 let if_start = self.advance().span;
1175 let cond = self.parse_expr(0)?;
1176 self.expect(TokenKind::Colon)?;
1177 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
1178 branches.push(CondBlockBranch {
1179 span: if_start.merge(body_items.last().map(|i| i.span).unwrap_or(if_start)),
1180 condition: cond,
1181 items: body_items,
1182 });
1183 } else {
1184 self.expect(TokenKind::Colon)?;
1185 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
1186 else_items = Some(body_items);
1187 break;
1188 }
1189 }
1190
1191 let end = else_items
1192 .as_ref()
1193 .and_then(|items| items.last().map(|i| i.span))
1194 .or_else(|| branches.last().and_then(|b| b.items.last().map(|i| i.span)))
1195 .unwrap_or(start);
1196
1197 Some(BlockItem {
1198 span: start.merge(end),
1199 kind: BlockItemKind::IfBlock {
1200 branches,
1201 else_items,
1202 },
1203 })
1204 }
1205
1206 fn parse_if_block_body(&mut self, keyword_line: u32) -> Vec<BlockItem> {
1208 let next_line = self.line_of(self.peek().span);
1209 if next_line > keyword_line {
1210 let base_col = self.col_of(self.peek().span);
1211 self.parse_indented_block_items(base_col)
1212 } else {
1213 let mut items = Vec::new();
1215 if let Some(item) = self.parse_block_item(BlockKind::Entity) {
1216 items.push(item);
1217 }
1218 items
1219 }
1220 }
1221
1222 fn parse_contracts_clause(&mut self, start: Span) -> Option<BlockItem> {
1224 self.advance(); self.advance(); let contracts_col = self.col_of(start);
1228 let mut entries = Vec::new();
1229
1230 while !self.at_eof()
1231 && !self.at(TokenKind::RBrace)
1232 && self.col_of(self.peek().span) > contracts_col
1233 {
1234 if !self.peek_kind().is_word() {
1235 break;
1236 }
1237
1238 let entry_start = self.peek().span;
1239 let direction_tok = self.advance();
1240 let direction_text = self.text(direction_tok.span);
1241
1242 let direction = match direction_text {
1243 "demands" => ContractDirection::Demands,
1244 "fulfils" => ContractDirection::Fulfils,
1245 other => {
1246 self.error(
1247 direction_tok.span,
1248 format!(
1249 "Unknown direction '{other}' in contracts clause. Use `demands` or `fulfils`."
1250 ),
1251 );
1252 if self.peek_kind().is_word() {
1254 self.advance();
1255 }
1256 continue;
1257 }
1258 };
1259
1260 let first = self.parse_ident_in("contract name")?;
1261
1262 let (qualifier, name) = if self.at(TokenKind::Slash) {
1268 self.advance(); let name = self.parse_ident_in("contract name after '/'")?;
1270 (Some(first.name), name)
1271 } else {
1272 (None, first)
1273 };
1274
1275 if self.at(TokenKind::LBrace) {
1277 self.error(
1278 self.peek().span,
1279 "Inline contract blocks are not allowed in `contracts:`. Declare the contract at module level.",
1280 );
1281 return None;
1282 }
1283
1284 let end = name.span;
1285 entries.push(ContractBinding {
1286 direction,
1287 qualifier,
1288 name,
1289 span: entry_start.merge(end),
1290 });
1291 }
1292
1293 if entries.is_empty() {
1294 self.error(
1295 start,
1296 "Empty `contracts:` clause. Add at least one `demands` or `fulfils` entry.",
1297 );
1298 return None;
1299 }
1300
1301 let end = entries.last().unwrap().span;
1302 Some(BlockItem {
1303 span: start.merge(end),
1304 kind: BlockItemKind::ContractsClause { entries },
1305 })
1306 }
1307
1308 fn parse_annotation(&mut self, start: Span) -> Option<BlockItem> {
1310 let at_tok = self.advance(); let at_col = self.col_of(at_tok.span);
1312
1313 if !self.peek_kind().is_word() {
1314 self.error(
1315 self.peek().span,
1316 format!("expected annotation keyword after `@`, found {}", self.peek_kind()),
1317 );
1318 return None;
1319 }
1320
1321 let keyword_tok = self.advance();
1322 let keyword_text = self.text(keyword_tok.span);
1323
1324 let kind = match keyword_text {
1325 "invariant" => AnnotationKind::Invariant,
1326 "guidance" => AnnotationKind::Guidance,
1327 "guarantee" => AnnotationKind::Guarantee,
1328 other => {
1329 self.error(
1330 keyword_tok.span,
1331 format!(
1332 "Unknown annotation `@{other}`. Use `@invariant`, `@guidance` or `@guarantee`."
1333 ),
1334 );
1335 return None;
1336 }
1337 };
1338
1339 let name = match &kind {
1341 AnnotationKind::Invariant | AnnotationKind::Guarantee => {
1342 let n = self.parse_ident_in("annotation name")?;
1343 if n.name.chars().next().is_some_and(|c| c.is_lowercase()) {
1344 self.diagnostics.push(Diagnostic::error(
1345 n.span,
1346 "Annotation names must be PascalCase.",
1347 ));
1348 }
1349 Some(n)
1350 }
1351 AnnotationKind::Guidance => {
1352 if self.peek_kind().is_word()
1354 && self.line_of(self.peek().span) == self.line_of(keyword_tok.span)
1355 {
1356 self.error(
1357 self.peek().span,
1358 "`@guidance` does not take a name. Remove the name after `@guidance`.",
1359 );
1360 return None;
1361 }
1362 None
1363 }
1364 };
1365
1366 let last_header_span = name.as_ref().map(|n| n.span).unwrap_or(keyword_tok.span);
1369 let header_line = self.line_of(last_header_span);
1370 let body = self.parse_annotation_body(at_col, header_line);
1371
1372 if body.is_empty() {
1373 self.error(
1374 last_header_span,
1375 "Annotations must be followed by at least one indented comment line.",
1376 );
1377 return None;
1378 }
1379
1380 Some(BlockItem {
1381 span: start.merge(last_header_span),
1382 kind: BlockItemKind::Annotation(Annotation {
1383 kind,
1384 name,
1385 body,
1386 span: start.merge(last_header_span),
1387 }),
1388 })
1389 }
1390
1391 fn parse_annotation_body(&self, at_col: u32, header_line: u32) -> Vec<String> {
1395 let mut body = Vec::new();
1396 let lines: Vec<&str> = self.source.lines().collect();
1397 let mut line_idx = (header_line + 1) as usize;
1398
1399 while line_idx < lines.len() {
1400 let line = lines[line_idx];
1401 let trimmed = line.trim_start();
1402
1403 if trimmed.is_empty() {
1404 if !body.is_empty() {
1405 body.push(String::new());
1406 }
1407 line_idx += 1;
1408 continue;
1409 }
1410
1411 let indent = (line.len() - trimmed.len()) as u32;
1412 if indent <= at_col {
1413 break;
1414 }
1415
1416 if let Some(comment) = trimmed.strip_prefix("-- ") {
1417 body.push(comment.to_string());
1418 } else if trimmed == "--" {
1419 body.push(String::new());
1420 } else {
1421 break;
1422 }
1423
1424 line_idx += 1;
1425 }
1426
1427 while body.last().is_some_and(|l| l.is_empty()) {
1429 body.pop();
1430 }
1431
1432 body
1433 }
1434
1435 fn parse_invariant_block_item(&mut self, start: Span) -> Option<BlockItem> {
1437 self.advance(); let name = self.parse_ident_in("invariant name")?;
1439
1440 if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
1442 self.diagnostics.push(Diagnostic::error(
1443 name.span,
1444 "invariant name must start with an uppercase letter",
1445 ));
1446 }
1447
1448 self.expect(TokenKind::LBrace)?;
1449 let body = self.parse_invariant_body()?;
1450 let end = self.expect(TokenKind::RBrace)?.span;
1451 Some(BlockItem {
1452 span: start.merge(end),
1453 kind: BlockItemKind::InvariantBlock { name, body },
1454 })
1455 }
1456
1457 fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
1458 let name_tok = self.advance(); let name_text = self.text(name_tok.span).to_string();
1460 self.advance(); let allows_binding = clause_allows_binding(&name_text);
1463 let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
1464 let value_span = value.span();
1465
1466 let kind = if is_clause_keyword(&name_text) {
1467 BlockItemKind::Clause {
1468 keyword: name_text,
1469 value,
1470 }
1471 } else if let Some((inner_value, when_clause)) = extract_when_clause(&value) {
1472 BlockItemKind::FieldWithWhen {
1473 name: Ident {
1474 span: name_tok.span,
1475 name: name_text,
1476 },
1477 value: inner_value,
1478 when_clause,
1479 }
1480 } else {
1481 BlockItemKind::Assignment {
1482 name: Ident {
1483 span: name_tok.span,
1484 name: name_text,
1485 },
1486 value,
1487 }
1488 };
1489
1490 Some(BlockItem {
1491 span: start.merge(value_span),
1492 kind,
1493 })
1494 }
1495
1496 fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
1498 let obj_tok = self.advance(); self.advance(); let field = self.parse_ident_in("field name")?;
1501 self.advance(); let path = Expr::MemberAccess {
1504 span: obj_tok.span.merge(field.span),
1505 object: Box::new(Expr::Ident(Ident {
1506 span: obj_tok.span,
1507 name: self.text(obj_tok.span).to_string(),
1508 })),
1509 field,
1510 };
1511
1512 let value = self.parse_clause_value(start)?;
1513 let value_span = value.span();
1514 Some(BlockItem {
1515 span: start.merge(value_span),
1516 kind: BlockItemKind::PathAssignment { path, value },
1517 })
1518 }
1519
1520 fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
1521 let saved_pos = self.pos;
1525 let _name_tok = self.advance();
1526 self.advance(); let mut depth = 1u32;
1530 while !self.at_eof() && depth > 0 {
1531 match self.peek_kind() {
1532 TokenKind::LParen => {
1533 depth += 1;
1534 self.advance();
1535 }
1536 TokenKind::RParen => {
1537 depth -= 1;
1538 self.advance();
1539 }
1540 _ => {
1541 self.advance();
1542 }
1543 }
1544 }
1545
1546 if self.at(TokenKind::Colon) {
1547 self.pos = saved_pos;
1549 let name = self.parse_ident_in("derived value name")?;
1550 self.expect(TokenKind::LParen)?;
1551 let params = self.parse_ident_list()?;
1552 self.expect(TokenKind::RParen)?;
1553 self.expect(TokenKind::Colon)?;
1554 let value = self.parse_clause_value(start)?;
1555 Some(BlockItem {
1556 span: start.merge(value.span()),
1557 kind: BlockItemKind::ParamAssignment {
1558 name,
1559 params,
1560 value,
1561 },
1562 })
1563 } else {
1564 self.pos = saved_pos;
1566 if self.peek_at(1).kind == TokenKind::Colon {
1568 }
1569 self.parse_assign_or_clause_item(start)
1571 }
1572 }
1573
1574 fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
1575 let mut params = Vec::new();
1576 if !self.at(TokenKind::RParen) {
1577 params.push(self.parse_ident_in("parameter name")?);
1578 while self.eat(TokenKind::Comma).is_some() {
1579 params.push(self.parse_ident_in("parameter name")?);
1580 }
1581 }
1582 Some(params)
1583 }
1584
1585 fn parse_for_binding(&mut self) -> Option<ForBinding> {
1587 if self.at(TokenKind::LParen) {
1588 let start = self.advance().span; let mut idents = Vec::new();
1590 idents.push(self.parse_ident_in("loop variable")?);
1591 while self.eat(TokenKind::Comma).is_some() {
1592 idents.push(self.parse_ident_in("loop variable")?);
1593 }
1594 let end = self.expect(TokenKind::RParen)?.span;
1595 Some(ForBinding::Destructured(idents, start.merge(end)))
1596 } else {
1597 let ident = self.parse_ident_in("loop variable")?;
1598 Some(ForBinding::Single(ident))
1599 }
1600 }
1601
1602 fn parse_clause_value_maybe_binding(
1606 &mut self,
1607 clause_start: Span,
1608 allow_binding: bool,
1609 ) -> Option<Expr> {
1610 if allow_binding
1611 && self.peek_kind().is_word()
1612 && self.peek_at(1).kind == TokenKind::Colon
1613 {
1614 let clause_line = self.line_of(clause_start);
1617 let next_line = self.line_of(self.peek().span);
1618 let colon_is_block_item = next_line > clause_line
1619 && self.peek_at(2).kind != TokenKind::Eof
1620 && self.line_of(self.peek_at(2).span) == next_line;
1621
1622 if next_line == clause_line || colon_is_block_item {
1623 let name = self.parse_ident_in("binding name")?;
1624 self.advance(); let inner = self.parse_clause_value(clause_start)?;
1626 return Some(Expr::Binding {
1627 span: name.span.merge(inner.span()),
1628 name,
1629 value: Box::new(inner),
1630 });
1631 }
1632 }
1633 self.parse_clause_value(clause_start)
1634 }
1635
1636 fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1639 let clause_line = self.line_of(clause_start);
1640 let next = self.peek();
1641 let next_line = self.line_of(next.span);
1642
1643 if next_line > clause_line {
1644 let base_col = self.col_of(next.span);
1649 let clause_col = self.col_of(clause_start);
1650 if base_col <= clause_col {
1651 return Some(Expr::Block {
1652 span: clause_start,
1653 items: Vec::new(),
1654 });
1655 }
1656 self.parse_indented_block(base_col)
1657 } else {
1658 self.parse_expr(0)
1660 }
1661 }
1662
1663 fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1666 let start = self.peek().span;
1667 let mut items = Vec::new();
1668
1669 while !self.at_eof()
1670 && !self.at(TokenKind::RBrace)
1671 && self.col_of(self.peek().span) >= base_col
1672 {
1673 if self.at(TokenKind::Let) {
1675 let let_start = self.advance().span;
1676 if let Some(name) = self.parse_ident_in("binding name") {
1677 if self.expect(TokenKind::Eq).is_some() {
1678 if let Some(value) = self.parse_expr(0) {
1679 items.push(Expr::LetExpr {
1680 span: let_start.merge(value.span()),
1681 name,
1682 value: Box::new(value),
1683 });
1684 continue;
1685 }
1686 }
1687 }
1688 break;
1689 }
1690
1691 if let Some(expr) = self.parse_expr(0) {
1692 items.push(expr);
1693 } else {
1694 self.advance();
1695 break;
1696 }
1697 }
1698
1699 if items.len() == 1 {
1700 Some(items.pop().unwrap())
1701 } else {
1702 let end = items.last().map(|e| e.span()).unwrap_or(start);
1703 Some(Expr::Block {
1704 span: start.merge(end),
1705 items,
1706 })
1707 }
1708 }
1709}
1710
1711const BP_LAMBDA: u8 = 4;
1717const BP_WHEN_GUARD: u8 = 5;
1718const BP_PROJECTION: u8 = 6;
1719const BP_WITH_WHERE: u8 = 7;
1720const BP_IMPLIES: u8 = 8;
1721const BP_OR: u8 = 10;
1722const BP_AND: u8 = 20;
1723const BP_COMPARE: u8 = 30;
1724const BP_TRANSITION: u8 = 32;
1725const BP_NULL_COALESCE: u8 = 40;
1726const BP_ADD: u8 = 50;
1727const BP_MUL: u8 = 60;
1728const BP_PIPE: u8 = 65;
1729const BP_PREFIX: u8 = 70;
1730const BP_POSTFIX: u8 = 80;
1731
1732impl<'s> Parser<'s> {
1733 pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1734 let mut lhs = self.parse_prefix()?;
1735
1736 loop {
1737 if let Some((l_bp, r_bp)) = self.infix_bp() {
1738 if l_bp < min_bp {
1739 break;
1740 }
1741 lhs = self.parse_infix(lhs, r_bp)?;
1742 } else if let Some(l_bp) = self.postfix_bp() {
1743 if l_bp < min_bp {
1744 break;
1745 }
1746 lhs = self.parse_postfix(lhs)?;
1747 } else {
1748 break;
1749 }
1750 }
1751
1752 Some(lhs)
1753 }
1754
1755 fn parse_prefix(&mut self) -> Option<Expr> {
1758 match self.peek_kind() {
1759 TokenKind::Not => {
1760 let start = self.advance().span;
1761 if self.at(TokenKind::Exists) {
1762 self.advance();
1763 let operand = self.parse_expr(BP_PREFIX)?;
1764 Some(Expr::NotExists {
1765 span: start.merge(operand.span()),
1766 operand: Box::new(operand),
1767 })
1768 } else {
1769 let operand = self.parse_expr(BP_PREFIX)?;
1770 Some(Expr::Not {
1771 span: start.merge(operand.span()),
1772 operand: Box::new(operand),
1773 })
1774 }
1775 }
1776 TokenKind::Exists => {
1777 let next = self.peek_at(1).kind;
1780 if matches!(
1781 next,
1782 TokenKind::RParen
1783 | TokenKind::RBrace
1784 | TokenKind::RBracket
1785 | TokenKind::Comma
1786 | TokenKind::Eof
1787 ) {
1788 let id = self.parse_ident()?;
1789 return Some(Expr::Ident(id));
1790 }
1791 let start = self.advance().span;
1792 let operand = self.parse_expr(BP_PREFIX)?;
1793 Some(Expr::Exists {
1794 span: start.merge(operand.span()),
1795 operand: Box::new(operand),
1796 })
1797 }
1798 TokenKind::If => self.parse_if_expr(),
1799 TokenKind::For => self.parse_for_expr(),
1800 TokenKind::LBrace => self.parse_brace_expr(),
1801 TokenKind::LBracket => {
1802 let t = self.advance();
1803 self.error(t.span, "list literals `[...]` are not supported; use `Set<T>` type annotation or `{...}` set literal");
1804 None
1805 }
1806 TokenKind::LParen => self.parse_paren_expr(),
1807 TokenKind::Number => {
1808 let t = self.advance();
1809 Some(Expr::NumberLiteral {
1810 span: t.span,
1811 value: self.text(t.span).to_string(),
1812 })
1813 }
1814 TokenKind::Duration => {
1815 let t = self.advance();
1816 Some(Expr::DurationLiteral {
1817 span: t.span,
1818 value: self.text(t.span).to_string(),
1819 })
1820 }
1821 TokenKind::String => {
1822 let sl = self.parse_string()?;
1823 Some(Expr::StringLiteral(sl))
1824 }
1825 TokenKind::BacktickLiteral => {
1826 let t = self.advance();
1827 let raw = self.text(t.span);
1828 let value = raw[1..raw.len() - 1].to_string();
1830 Some(Expr::BacktickLiteral {
1831 span: t.span,
1832 value,
1833 })
1834 }
1835 TokenKind::True => {
1836 let t = self.advance();
1837 Some(Expr::BoolLiteral {
1838 span: t.span,
1839 value: true,
1840 })
1841 }
1842 TokenKind::False => {
1843 let t = self.advance();
1844 Some(Expr::BoolLiteral {
1845 span: t.span,
1846 value: false,
1847 })
1848 }
1849 TokenKind::Null => {
1850 let t = self.advance();
1851 Some(Expr::Null { span: t.span })
1852 }
1853 TokenKind::Now => {
1854 let t = self.advance();
1855 Some(Expr::Now { span: t.span })
1856 }
1857 TokenKind::This => {
1858 let t = self.advance();
1859 Some(Expr::This { span: t.span })
1860 }
1861 TokenKind::Within => {
1862 let t = self.advance();
1863 Some(Expr::Within { span: t.span })
1864 }
1865 k if k.is_word() => {
1866 let id = self.parse_ident()?;
1867 Some(Expr::Ident(id))
1868 }
1869 TokenKind::Star => {
1870 let t = self.advance();
1872 Some(Expr::Ident(Ident {
1873 span: t.span,
1874 name: "*".into(),
1875 }))
1876 }
1877 TokenKind::Minus => {
1878 let start = self.advance().span;
1880 let operand = self.parse_expr(BP_PREFIX)?;
1881 Some(Expr::BinaryOp {
1882 span: start.merge(operand.span()),
1883 left: Box::new(Expr::NumberLiteral {
1884 span: start,
1885 value: "0".into(),
1886 }),
1887 op: BinaryOp::Sub,
1888 right: Box::new(operand),
1889 })
1890 }
1891 _ => {
1892 self.error(
1893 self.peek().span,
1894 format!(
1895 "expected expression (identifier, number, string, true/false, null, \
1896 if/for/not/exists, '(', '{{', '['), found {}",
1897 self.peek_kind(),
1898 ),
1899 );
1900 None
1901 }
1902 }
1903 }
1904
1905 fn infix_bp(&self) -> Option<(u8, u8)> {
1908 match self.peek_kind() {
1909 TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1912 TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1913 TokenKind::Implies => Some((BP_IMPLIES, BP_IMPLIES - 1)), TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1915 TokenKind::And => Some((BP_AND, BP_AND + 1)),
1916 TokenKind::Eq | TokenKind::BangEq => {
1917 Some((BP_COMPARE, BP_COMPARE + 1))
1918 }
1919 TokenKind::Lt => {
1920 if self.pos > 0 {
1923 let prev = self.tokens[self.pos - 1];
1924 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1925 return None;
1926 }
1927 }
1928 Some((BP_COMPARE, BP_COMPARE + 1))
1929 }
1930 TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1931 Some((BP_COMPARE, BP_COMPARE + 1))
1932 }
1933 TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1934 TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1936 Some((BP_COMPARE, BP_COMPARE + 1))
1937 }
1938 TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1939 TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1940 TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1941 TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1942 TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1943 TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1944 TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1945 TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1946 _ => None,
1947 }
1948 }
1949
1950 fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1951 let op_tok = self.advance();
1952 match op_tok.kind {
1953 TokenKind::FatArrow => {
1954 let body = self.parse_expr(r_bp)?;
1955 Some(Expr::Lambda {
1956 span: lhs.span().merge(body.span()),
1957 param: Box::new(lhs),
1958 body: Box::new(body),
1959 })
1960 }
1961 TokenKind::Pipe => {
1962 let rhs = self.parse_expr(r_bp)?;
1963 Some(Expr::Pipe {
1964 span: lhs.span().merge(rhs.span()),
1965 left: Box::new(lhs),
1966 right: Box::new(rhs),
1967 })
1968 }
1969 TokenKind::Implies => {
1970 let rhs = self.parse_expr(r_bp)?;
1971 Some(Expr::LogicalOp {
1972 span: lhs.span().merge(rhs.span()),
1973 left: Box::new(lhs),
1974 op: LogicalOp::Implies,
1975 right: Box::new(rhs),
1976 })
1977 }
1978 TokenKind::Or => {
1979 let rhs = self.parse_expr(r_bp)?;
1980 Some(Expr::LogicalOp {
1981 span: lhs.span().merge(rhs.span()),
1982 left: Box::new(lhs),
1983 op: LogicalOp::Or,
1984 right: Box::new(rhs),
1985 })
1986 }
1987 TokenKind::And => {
1988 let rhs = self.parse_expr(r_bp)?;
1989 Some(Expr::LogicalOp {
1990 span: lhs.span().merge(rhs.span()),
1991 left: Box::new(lhs),
1992 op: LogicalOp::And,
1993 right: Box::new(rhs),
1994 })
1995 }
1996 TokenKind::Eq => {
1997 let rhs = self.parse_expr(r_bp)?;
1998 Some(Expr::Comparison {
1999 span: lhs.span().merge(rhs.span()),
2000 left: Box::new(lhs),
2001 op: ComparisonOp::Eq,
2002 right: Box::new(rhs),
2003 })
2004 }
2005 TokenKind::BangEq => {
2006 let rhs = self.parse_expr(r_bp)?;
2007 Some(Expr::Comparison {
2008 span: lhs.span().merge(rhs.span()),
2009 left: Box::new(lhs),
2010 op: ComparisonOp::NotEq,
2011 right: Box::new(rhs),
2012 })
2013 }
2014 TokenKind::Lt => {
2015 let rhs = self.parse_expr(r_bp)?;
2016 Some(Expr::Comparison {
2017 span: lhs.span().merge(rhs.span()),
2018 left: Box::new(lhs),
2019 op: ComparisonOp::Lt,
2020 right: Box::new(rhs),
2021 })
2022 }
2023 TokenKind::LtEq => {
2024 let rhs = self.parse_expr(r_bp)?;
2025 Some(Expr::Comparison {
2026 span: lhs.span().merge(rhs.span()),
2027 left: Box::new(lhs),
2028 op: ComparisonOp::LtEq,
2029 right: Box::new(rhs),
2030 })
2031 }
2032 TokenKind::Gt => {
2033 let rhs = self.parse_expr(r_bp)?;
2034 Some(Expr::Comparison {
2035 span: lhs.span().merge(rhs.span()),
2036 left: Box::new(lhs),
2037 op: ComparisonOp::Gt,
2038 right: Box::new(rhs),
2039 })
2040 }
2041 TokenKind::GtEq => {
2042 let rhs = self.parse_expr(r_bp)?;
2043 Some(Expr::Comparison {
2044 span: lhs.span().merge(rhs.span()),
2045 left: Box::new(lhs),
2046 op: ComparisonOp::GtEq,
2047 right: Box::new(rhs),
2048 })
2049 }
2050 TokenKind::In => {
2051 let rhs = self.parse_expr(r_bp)?;
2052 Some(Expr::In {
2053 span: lhs.span().merge(rhs.span()),
2054 element: Box::new(lhs),
2055 collection: Box::new(rhs),
2056 })
2057 }
2058 TokenKind::Not => {
2059 self.expect(TokenKind::In)?;
2061 let rhs = self.parse_expr(r_bp)?;
2062 Some(Expr::NotIn {
2063 span: lhs.span().merge(rhs.span()),
2064 element: Box::new(lhs),
2065 collection: Box::new(rhs),
2066 })
2067 }
2068 TokenKind::Where => {
2069 let rhs = self.parse_expr(r_bp)?;
2070 Some(Expr::Where {
2071 span: lhs.span().merge(rhs.span()),
2072 source: Box::new(lhs),
2073 condition: Box::new(rhs),
2074 })
2075 }
2076 TokenKind::With => {
2077 let rhs = self.parse_expr(r_bp)?;
2078 Some(Expr::With {
2079 span: lhs.span().merge(rhs.span()),
2080 source: Box::new(lhs),
2081 predicate: Box::new(rhs),
2082 })
2083 }
2084 TokenKind::QuestionQuestion => {
2085 let rhs = self.parse_expr(r_bp)?;
2086 Some(Expr::NullCoalesce {
2087 span: lhs.span().merge(rhs.span()),
2088 left: Box::new(lhs),
2089 right: Box::new(rhs),
2090 })
2091 }
2092 TokenKind::Plus => {
2093 let rhs = self.parse_expr(r_bp)?;
2094 Some(Expr::BinaryOp {
2095 span: lhs.span().merge(rhs.span()),
2096 left: Box::new(lhs),
2097 op: BinaryOp::Add,
2098 right: Box::new(rhs),
2099 })
2100 }
2101 TokenKind::Minus => {
2102 let rhs = self.parse_expr(r_bp)?;
2103 Some(Expr::BinaryOp {
2104 span: lhs.span().merge(rhs.span()),
2105 left: Box::new(lhs),
2106 op: BinaryOp::Sub,
2107 right: Box::new(rhs),
2108 })
2109 }
2110 TokenKind::Star => {
2111 let rhs = self.parse_expr(r_bp)?;
2112 Some(Expr::BinaryOp {
2113 span: lhs.span().merge(rhs.span()),
2114 left: Box::new(lhs),
2115 op: BinaryOp::Mul,
2116 right: Box::new(rhs),
2117 })
2118 }
2119 TokenKind::Slash => {
2120 if let Expr::Ident(ref id) = lhs {
2125 if self.peek_kind().is_word() {
2126 let next_text = self.text(self.peek().span);
2127 let is_qualified = next_text
2128 .chars()
2129 .next()
2130 .is_some_and(|c| c.is_uppercase())
2131 || matches!(
2132 self.peek_kind(),
2133 TokenKind::Config | TokenKind::Entity | TokenKind::Value
2134 );
2135 if is_qualified {
2136 let name_tok = self.advance();
2137 return Some(Expr::QualifiedName(QualifiedName {
2138 span: lhs.span().merge(name_tok.span),
2139 qualifier: Some(id.name.clone()),
2140 name: self.text(name_tok.span).to_string(),
2141 }));
2142 }
2143 }
2144 }
2145 let rhs = self.parse_expr(r_bp)?;
2146 Some(Expr::BinaryOp {
2147 span: lhs.span().merge(rhs.span()),
2148 left: Box::new(lhs),
2149 op: BinaryOp::Div,
2150 right: Box::new(rhs),
2151 })
2152 }
2153 TokenKind::ThinArrow => {
2154 let field = self.parse_ident_in("projection field")?;
2155 Some(Expr::ProjectionMap {
2156 span: lhs.span().merge(field.span),
2157 source: Box::new(lhs),
2158 field,
2159 })
2160 }
2161 TokenKind::TransitionsTo => {
2162 let rhs = self.parse_expr(r_bp)?;
2163 Some(Expr::TransitionsTo {
2164 span: lhs.span().merge(rhs.span()),
2165 subject: Box::new(lhs),
2166 new_state: Box::new(rhs),
2167 })
2168 }
2169 TokenKind::Becomes => {
2170 let rhs = self.parse_expr(r_bp)?;
2171 Some(Expr::Becomes {
2172 span: lhs.span().merge(rhs.span()),
2173 subject: Box::new(lhs),
2174 new_state: Box::new(rhs),
2175 })
2176 }
2177 TokenKind::When => {
2178 let rhs = self.parse_expr(r_bp)?;
2180 Some(Expr::WhenGuard {
2181 span: lhs.span().merge(rhs.span()),
2182 action: Box::new(lhs),
2183 condition: Box::new(rhs),
2184 })
2185 }
2186 _ => {
2187 self.error(
2188 op_tok.span,
2189 format!("unexpected infix operator {}", op_tok.kind),
2190 );
2191 None
2192 }
2193 }
2194 }
2195
2196 fn postfix_bp(&self) -> Option<u8> {
2199 match self.peek_kind() {
2200 TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
2201 TokenKind::QuestionMark => Some(BP_POSTFIX),
2202 TokenKind::Lt => {
2205 if self.pos > 0 {
2206 let prev = self.tokens[self.pos - 1];
2207 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
2210 return Some(BP_POSTFIX);
2211 }
2212 }
2213 None
2214 }
2215 TokenKind::LParen => Some(BP_POSTFIX),
2216 TokenKind::LBrace => {
2217 let next = self.peek();
2223 let prev_end = if self.pos > 0 {
2224 self.tokens[self.pos - 1].span.end
2225 } else {
2226 0
2227 };
2228 if self.line_of(Span::new(prev_end, prev_end))
2230 == self.line_of(next.span)
2231 {
2232 Some(BP_POSTFIX)
2233 } else {
2234 None
2235 }
2236 }
2237 _ => None,
2238 }
2239 }
2240
2241 fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
2242 match self.peek_kind() {
2243 TokenKind::QuestionMark => {
2244 let end = self.advance().span;
2245 Some(Expr::TypeOptional {
2246 span: lhs.span().merge(end),
2247 inner: Box::new(lhs),
2248 })
2249 }
2250 TokenKind::Lt => {
2251 self.advance(); let mut args = Vec::new();
2254 while !self.at(TokenKind::Gt) && !self.at_eof() {
2256 args.push(self.parse_expr(BP_COMPARE + 1)?);
2257 self.eat(TokenKind::Comma);
2258 }
2259 let end = self.expect(TokenKind::Gt)?.span;
2260 Some(Expr::GenericType {
2261 span: lhs.span().merge(end),
2262 name: Box::new(lhs),
2263 args,
2264 })
2265 }
2266 TokenKind::Dot => {
2267 self.advance();
2268 let field = self.parse_ident_in("field name")?;
2269 Some(Expr::MemberAccess {
2270 span: lhs.span().merge(field.span),
2271 object: Box::new(lhs),
2272 field,
2273 })
2274 }
2275 TokenKind::QuestionDot => {
2276 self.advance();
2277 let field = self.parse_ident_in("field name")?;
2278 Some(Expr::OptionalAccess {
2279 span: lhs.span().merge(field.span),
2280 object: Box::new(lhs),
2281 field,
2282 })
2283 }
2284 TokenKind::LParen => {
2285 self.advance();
2286 let args = self.parse_call_args()?;
2287 let end = self.expect(TokenKind::RParen)?.span;
2288 Some(Expr::Call {
2289 span: lhs.span().merge(end),
2290 function: Box::new(lhs),
2291 args,
2292 })
2293 }
2294 TokenKind::LBrace => {
2295 self.advance();
2296 let fields = self.parse_join_fields()?;
2297 let end = self.expect(TokenKind::RBrace)?.span;
2298 Some(Expr::JoinLookup {
2299 span: lhs.span().merge(end),
2300 entity: Box::new(lhs),
2301 fields,
2302 })
2303 }
2304 _ => None,
2305 }
2306 }
2307
2308 fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
2311 let mut args = Vec::new();
2312 while !self.at(TokenKind::RParen) && !self.at_eof() {
2313 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2315 let name = self.parse_ident_in("argument name")?;
2316 self.advance(); let value = self.parse_expr(0)?;
2318 args.push(CallArg::Named(NamedArg {
2319 span: name.span.merge(value.span()),
2320 name,
2321 value,
2322 }));
2323 } else {
2324 let expr = self.parse_expr(0)?;
2325 args.push(CallArg::Positional(expr));
2326 }
2327 self.eat(TokenKind::Comma);
2328 }
2329 Some(args)
2330 }
2331
2332 fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
2335 let mut fields = Vec::new();
2336 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2337 let field = self.parse_ident_in("join field name")?;
2338 let value = if self.eat(TokenKind::Colon).is_some() {
2339 Some(self.parse_expr(0)?)
2340 } else {
2341 None
2342 };
2343 fields.push(JoinField {
2344 span: field.span.merge(
2345 value
2346 .as_ref()
2347 .map(|v| v.span())
2348 .unwrap_or(field.span),
2349 ),
2350 field,
2351 value,
2352 });
2353 self.eat(TokenKind::Comma);
2354 }
2355 Some(fields)
2356 }
2357
2358 fn parse_if_expr(&mut self) -> Option<Expr> {
2361 let start = self.advance().span; let mut branches = Vec::new();
2363
2364 let condition = self.parse_expr(0)?;
2366 self.expect(TokenKind::Colon)?;
2367 let body = self.parse_branch_body(start)?;
2368 branches.push(CondBranch {
2369 span: start.merge(body.span()),
2370 condition,
2371 body,
2372 });
2373
2374 let mut else_body = None;
2376 while self.at(TokenKind::Else) {
2377 let else_tok = self.advance();
2378 if self.at(TokenKind::If) {
2379 let if_start = self.advance().span;
2380 let cond = self.parse_expr(0)?;
2381 self.expect(TokenKind::Colon)?;
2382 let body = self.parse_branch_body(else_tok.span)?;
2383 branches.push(CondBranch {
2384 span: if_start.merge(body.span()),
2385 condition: cond,
2386 body,
2387 });
2388 } else {
2389 self.expect(TokenKind::Colon)?;
2390 let body = self.parse_branch_body(else_tok.span)?;
2391 else_body = Some(Box::new(body));
2392 break;
2393 }
2394 }
2395
2396 let end = else_body
2397 .as_ref()
2398 .map(|b| b.span())
2399 .or_else(|| branches.last().map(|b| b.body.span()))
2400 .unwrap_or(start);
2401
2402 Some(Expr::Conditional {
2403 span: start.merge(end),
2404 branches,
2405 else_body,
2406 })
2407 }
2408
2409 fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
2410 let keyword_line = self.line_of(keyword_span);
2411 let next_line = self.line_of(self.peek().span);
2412
2413 if next_line > keyword_line {
2414 let base_col = self.col_of(self.peek().span);
2415 self.parse_indented_block(base_col)
2416 } else {
2417 self.parse_expr(0)
2418 }
2419 }
2420
2421 fn parse_for_expr(&mut self) -> Option<Expr> {
2424 let start = self.advance().span; let binding = self.parse_for_binding()?;
2426 self.expect(TokenKind::In)?;
2427
2428 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
2430
2431 let filter = if self.eat(TokenKind::Where).is_some() {
2432 Some(Box::new(self.parse_expr(0)?))
2434 } else {
2435 None
2436 };
2437
2438 self.expect(TokenKind::Colon)?;
2439 let body = self.parse_branch_body(start)?;
2440
2441 Some(Expr::For {
2442 span: start.merge(body.span()),
2443 binding,
2444 collection: Box::new(collection),
2445 filter,
2446 body: Box::new(body),
2447 })
2448 }
2449
2450 fn parse_brace_expr(&mut self) -> Option<Expr> {
2453 let start = self.advance().span; if self.at(TokenKind::RBrace) {
2456 let end = self.advance().span;
2457 return Some(Expr::SetLiteral {
2458 span: start.merge(end),
2459 elements: Vec::new(),
2460 });
2461 }
2462
2463 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2465 return self.parse_object_literal(start);
2466 }
2467
2468 self.parse_set_literal(start)
2470 }
2471
2472
2473 fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
2474 let mut fields = Vec::new();
2475 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2476 let name = self.parse_ident_in("field name")?;
2477 self.expect(TokenKind::Colon)?;
2478 let value = self.parse_expr(0)?;
2479 fields.push(NamedArg {
2480 span: name.span.merge(value.span()),
2481 name,
2482 value,
2483 });
2484 self.eat(TokenKind::Comma);
2485 }
2486 let end = self.expect(TokenKind::RBrace)?.span;
2487 Some(Expr::ObjectLiteral {
2488 span: start.merge(end),
2489 fields,
2490 })
2491 }
2492
2493 fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
2494 let mut elements = Vec::new();
2495 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2496 elements.push(self.parse_expr(0)?);
2497 self.eat(TokenKind::Comma);
2498 }
2499 let end = self.expect(TokenKind::RBrace)?.span;
2500 Some(Expr::SetLiteral {
2501 span: start.merge(end),
2502 elements,
2503 })
2504 }
2505
2506 fn parse_paren_expr(&mut self) -> Option<Expr> {
2509 let start = self.advance().span; if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2513 let mut bindings = Vec::new();
2514 while !self.at(TokenKind::RParen) && !self.at_eof() {
2515 let name = self.parse_ident_in("parameter name")?;
2516 self.expect(TokenKind::Colon)?;
2517 let value = self.parse_expr(0)?;
2518 bindings.push(Expr::Binding {
2519 span: name.span.merge(value.span()),
2520 name,
2521 value: Box::new(value),
2522 });
2523 self.eat(TokenKind::Comma);
2524 }
2525 self.expect(TokenKind::RParen)?;
2526 if bindings.len() == 1 {
2527 return Some(bindings.into_iter().next().unwrap());
2528 }
2529 let span = start.merge(bindings.last().unwrap().span());
2530 return Some(Expr::Block {
2531 span,
2532 items: bindings,
2533 });
2534 }
2535
2536 let expr = self.parse_expr(0)?;
2537 self.expect(TokenKind::RParen)?;
2538 Some(expr)
2539 }
2540}
2541
2542#[cfg(test)]
2547mod tests {
2548 use super::*;
2549 use crate::diagnostic::Severity;
2550
2551 fn parse_ok(src: &str) -> ParseResult {
2552 let owned;
2555 let input = if src.starts_with("-- allium:") {
2556 src
2557 } else {
2558 owned = format!("-- allium: 1\n{src}");
2559 &owned
2560 };
2561 let result = parse(input);
2562 if !result.diagnostics.is_empty() {
2563 for d in &result.diagnostics {
2564 eprintln!(
2565 " [{:?}] {} ({}..{})",
2566 d.severity, d.message, d.span.start, d.span.end
2567 );
2568 }
2569 }
2570 result
2571 }
2572
2573 #[test]
2574 fn version_marker() {
2575 let r = parse_ok("-- allium: 1\n");
2576 assert_eq!(r.module.version, Some(1));
2577 assert_eq!(r.diagnostics.len(), 0);
2578 }
2579
2580 #[test]
2581 fn version_missing_warns() {
2582 let r = parse("entity User {}");
2583 assert_eq!(r.module.version, None);
2584 assert_eq!(r.diagnostics.len(), 1);
2585 assert_eq!(r.diagnostics[0].severity, Severity::Warning);
2586 assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
2587 }
2588
2589 #[test]
2590 fn version_unsupported_errors() {
2591 let r = parse("-- allium: 99\nentity User {}");
2592 assert_eq!(r.module.version, Some(99));
2593 assert!(r.diagnostics.iter().any(|d|
2594 d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
2595 ), "expected unsupported version error, got: {:?}", r.diagnostics);
2596 }
2597
2598 #[test]
2599 fn empty_entity() {
2600 let r = parse_ok("entity User {}");
2601 assert_eq!(r.diagnostics.len(), 0);
2602 assert_eq!(r.module.declarations.len(), 1);
2603 match &r.module.declarations[0] {
2604 Decl::Block(b) => {
2605 assert_eq!(b.kind, BlockKind::Entity);
2606 assert_eq!(b.name.as_ref().unwrap().name, "User");
2607 }
2608 other => panic!("expected Block, got {other:?}"),
2609 }
2610 }
2611
2612 #[test]
2613 fn entity_with_fields() {
2614 let src = r#"entity Order {
2615 customer: Customer
2616 status: pending | active | completed
2617 total: Decimal
2618}"#;
2619 let r = parse_ok(src);
2620 assert_eq!(r.diagnostics.len(), 0);
2621 match &r.module.declarations[0] {
2622 Decl::Block(b) => {
2623 assert_eq!(b.items.len(), 3);
2624 }
2625 other => panic!("expected Block, got {other:?}"),
2626 }
2627 }
2628
2629 #[test]
2630 fn use_declaration() {
2631 let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
2632 assert_eq!(r.diagnostics.len(), 0);
2633 match &r.module.declarations[0] {
2634 Decl::Use(u) => {
2635 assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
2636 }
2637 other => panic!("expected Use, got {other:?}"),
2638 }
2639 }
2640
2641 #[test]
2642 fn enum_declaration() {
2643 let src = "enum OrderStatus { pending | shipped | delivered }";
2644 let r = parse_ok(src);
2645 assert_eq!(r.diagnostics.len(), 0);
2646 }
2647
2648 #[test]
2649 fn config_block() {
2650 let src = r#"config {
2651 max_retries: Integer = 3
2652 timeout: Duration = 24.hours
2653}"#;
2654 let r = parse_ok(src);
2659 assert_eq!(r.diagnostics.len(), 0);
2660 }
2661
2662 #[test]
2663 fn rule_declaration() {
2664 let src = r#"rule PlaceOrder {
2665 when: CustomerPlacesOrder(customer, items, total)
2666 requires: total > 0
2667 ensures: Order.created(customer: customer, status: pending, total: total)
2668}"#;
2669 let r = parse_ok(src);
2670 assert_eq!(r.diagnostics.len(), 0);
2671 match &r.module.declarations[0] {
2672 Decl::Block(b) => {
2673 assert_eq!(b.kind, BlockKind::Rule);
2674 assert_eq!(b.items.len(), 3);
2675 }
2676 other => panic!("expected Block, got {other:?}"),
2677 }
2678 }
2679
2680 #[test]
2681 fn expression_precedence() {
2682 let r = parse_ok("rule T { v: a + b * c }");
2683 match &r.module.declarations[0] {
2685 Decl::Block(b) => match &b.items[0].kind {
2686 BlockItemKind::Assignment { value, .. } => match value {
2687 Expr::BinaryOp { op, right, .. } => {
2688 assert_eq!(*op, BinaryOp::Add);
2689 assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2690 }
2691 other => panic!("expected BinaryOp, got {other:?}"),
2692 },
2693 other => panic!("expected Assignment, got {other:?}"),
2694 },
2695 other => panic!("expected Block, got {other:?}"),
2696 }
2697 }
2698
2699 #[test]
2700 fn default_declaration() {
2701 let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2702 let r = parse_ok(src);
2703 assert_eq!(r.diagnostics.len(), 0);
2704 }
2705
2706 #[test]
2707 fn open_question() {
2708 let src = r#"open question "Should admins be role-specific?""#;
2709 let r = parse_ok(src);
2710 assert_eq!(r.diagnostics.len(), 0);
2711 }
2712
2713 #[test]
2714 fn external_entity() {
2715 let src = "external entity Customer { email: String }";
2716 let r = parse_ok(src);
2717 assert_eq!(r.diagnostics.len(), 0);
2718 match &r.module.declarations[0] {
2719 Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2720 other => panic!("expected Block, got {other:?}"),
2721 }
2722 }
2723
2724 #[test]
2725 fn where_expression() {
2726 let src = "entity E { active: items where status = active }";
2727 let r = parse_ok(src);
2728 assert_eq!(r.diagnostics.len(), 0);
2729 }
2730
2731 #[test]
2732 fn with_expression() {
2733 let src = "entity E { slots: InterviewSlot with candidacy = this }";
2734 let r = parse_ok(src);
2735 assert_eq!(r.diagnostics.len(), 0);
2736 }
2737
2738 #[test]
2739 fn lambda_expression() {
2740 let src = "entity E { v: items.any(i => i.active) }";
2741 let r = parse_ok(src);
2742 assert_eq!(r.diagnostics.len(), 0);
2743 }
2744
2745 #[test]
2746 fn deferred() {
2747 let src = "deferred InterviewerMatching.suggest";
2748 let r = parse_ok(src);
2749 assert_eq!(r.diagnostics.len(), 0);
2750 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2751 assert!(matches!(&d.path, Expr::MemberAccess { .. }));
2752 assert!(d.location_hint.is_none());
2753 }
2754
2755 #[test]
2756 fn deferred_qualified_path() {
2757 let src = "deferred billing/InvoiceWorkflow";
2758 let r = parse_ok(src);
2759 assert_eq!(r.diagnostics.len(), 0);
2760 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2761 let Expr::QualifiedName(q) = &d.path else {
2762 panic!("expected QualifiedName, got {:?}", d.path)
2763 };
2764 assert_eq!(q.qualifier.as_deref(), Some("billing"));
2765 assert_eq!(q.name, "InvoiceWorkflow");
2766 }
2767
2768 #[test]
2769 fn deferred_qualified_path_with_member() {
2770 let src = "deferred billing/InvoiceWorkflow.initiate";
2771 let r = parse_ok(src);
2772 assert_eq!(r.diagnostics.len(), 0);
2773 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2774 let Expr::MemberAccess { object, field, .. } = &d.path else {
2775 panic!("expected MemberAccess, got {:?}", d.path)
2776 };
2777 assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
2778 assert_eq!(field.name, "initiate");
2779 }
2780
2781 #[test]
2782 fn deferred_with_quoted_location_hint() {
2783 let src = "deferred Foo.bar \"detailed/foo.allium\"";
2784 let r = parse_ok(src);
2785 assert_eq!(r.diagnostics.len(), 0);
2786 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2787 let hint = d.location_hint.as_ref().expect("location hint parsed");
2788 assert_eq!(hint.text(), "detailed/foo.allium");
2789 assert_eq!(d.span.end, hint.span.end, "declaration span covers the hint");
2790 }
2791
2792 #[test]
2793 fn deferred_hint_string_must_share_the_line() {
2794 let src = "deferred Foo\n\"detailed/foo.allium\"";
2797 let r = parse_ok(src);
2798 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2799 assert!(d.location_hint.is_none());
2800 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected declaration")));
2801 }
2802
2803 #[test]
2804 fn deferred_path_rejects_expression_shapes() {
2805 for src in ["deferred Foo(\"x\")", "deferred Foo = \"x\""] {
2809 let r = parse_ok(src);
2810 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2811 assert!(matches!(&d.path, Expr::Ident(id) if id.name == "Foo"));
2812 assert!(
2813 r.diagnostics.iter().any(|d| d.message.contains("expected declaration")),
2814 "leftover tokens must error in {src:?}"
2815 );
2816 }
2817 }
2818
2819 #[test]
2820 fn deferred_parenthesised_path_errors() {
2821 let r = parse_ok("deferred (Foo)");
2822 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected deferred name")));
2823 }
2824
2825 #[test]
2826 fn deferred_trailing_dot_keeps_declaration() {
2827 let r = parse_ok("deferred Dangling.");
2830 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2831 assert!(matches!(&d.path, Expr::Ident(id) if id.name == "Dangling"));
2832 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected declaration")));
2833 }
2834
2835 #[test]
2836 fn deferred_dangling_qualifier_errors() {
2837 let r = parse_ok("deferred billing/");
2838 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected deferred name after '/'")));
2839 }
2840
2841 #[test]
2842 fn deferred_path_does_not_cross_lines() {
2843 let src = "deferred Dangling.\ndeferred Next.step";
2847 let r = parse_ok(src);
2848 let deferreds: Vec<_> = r
2849 .module
2850 .declarations
2851 .iter()
2852 .filter(|d| matches!(d, Decl::Deferred(_)))
2853 .collect();
2854 assert_eq!(deferreds.len(), 2, "both declarations survive");
2855 let Decl::Deferred(d) = deferreds[0] else { panic!() };
2856 assert!(matches!(&d.path, Expr::Ident(id) if id.name == "Dangling"));
2857 }
2858
2859 #[test]
2860 fn variant_declaration() {
2861 let src = "variant Email : Notification { subject: String }";
2862 let r = parse_ok(src);
2863 assert_eq!(r.diagnostics.len(), 0);
2864 }
2865
2866 #[test]
2869 fn projection_arrow() {
2870 let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2871 let r = parse_ok(src);
2872 assert_eq!(r.diagnostics.len(), 0);
2873 }
2874
2875 #[test]
2878 fn transitions_to_trigger() {
2879 let src = "rule R { when: Interview.status transitions_to scheduled\n ensures: Notification.created() }";
2880 let r = parse_ok(src);
2881 assert_eq!(r.diagnostics.len(), 0);
2882 }
2883
2884 #[test]
2885 fn becomes_trigger() {
2886 let src = "rule R { when: Interview.status becomes scheduled\n ensures: Notification.created() }";
2887 let r = parse_ok(src);
2888 assert_eq!(r.diagnostics.len(), 0);
2889 }
2890
2891 #[test]
2894 fn when_binding() {
2895 let src = "rule R {\n when: interview: Interview.status transitions_to scheduled\n ensures: Notification.created()\n}";
2896 let r = parse_ok(src);
2897 assert_eq!(r.diagnostics.len(), 0);
2898 let decl = &r.module.declarations[0];
2900 if let Decl::Block(b) = decl {
2901 if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2902 assert_eq!(keyword, "when");
2903 assert!(matches!(value, Expr::Binding { .. }));
2904 } else {
2905 panic!("expected clause");
2906 }
2907 } else {
2908 panic!("expected block decl");
2909 }
2910 }
2911
2912 #[test]
2913 fn when_binding_temporal() {
2914 let src = "rule R {\n when: invitation: Invitation.expires_at <= now\n ensures: Invitation.expired()\n}";
2915 let r = parse_ok(src);
2916 assert_eq!(r.diagnostics.len(), 0);
2917 }
2918
2919 #[test]
2920 fn when_binding_created() {
2921 let src = "rule R {\n when: batch: DigestBatch.created\n ensures: Email.created()\n}";
2922 let r = parse_ok(src);
2923 assert_eq!(r.diagnostics.len(), 0);
2924 }
2925
2926 #[test]
2927 fn facing_binding() {
2928 let src = "surface S {\n facing viewer: Interviewer\n exposes: InterviewList\n}";
2929 let r = parse_ok(src);
2930 assert_eq!(r.diagnostics.len(), 0);
2931 }
2932
2933 #[test]
2934 fn context_binding() {
2935 let src = "surface S {\n facing viewer: Interviewer\n context assignment: SlotConfirmation where interviewer = viewer\n}";
2936 let r = parse_ok(src);
2937 assert_eq!(r.diagnostics.len(), 0);
2938 }
2939
2940 #[test]
2943 fn rule_level_for() {
2944 let src = r#"rule ProcessDigests {
2945 when: schedule: DigestSchedule.next_run_at <= now
2946 for user in Users where notification_setting.digest_enabled:
2947 ensures: DigestBatch.created(user: user)
2948}"#;
2949 let r = parse_ok(src);
2950 assert_eq!(r.diagnostics.len(), 0);
2951 if let Decl::Block(b) = &r.module.declarations[0] {
2952 assert!(b.items.len() >= 2);
2954 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2955 } else {
2956 panic!("expected block decl");
2957 }
2958 }
2959
2960 #[test]
2963 fn let_in_ensures_block() {
2964 let src = r#"rule R {
2965 when: ScheduleInterview(candidacy, time, interviewers)
2966 ensures:
2967 let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2968 for interviewer in interviewers:
2969 SlotConfirmation.created(slot: slot, interviewer: interviewer)
2970}"#;
2971 let r = parse_ok(src);
2972 assert_eq!(r.diagnostics.len(), 0);
2973 }
2974
2975 #[test]
2978 fn provides_when_guard() {
2979 let src = "surface S {\n facing viewer: Interviewer\n provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2980 let r = parse_ok(src);
2981 assert_eq!(r.diagnostics.len(), 0);
2982 }
2983
2984 #[test]
2987 fn optional_type_suffix() {
2988 let src = "entity E { locked_until: Timestamp? }";
2989 let r = parse_ok(src);
2990 assert_eq!(r.diagnostics.len(), 0);
2991 }
2992
2993 #[test]
2994 fn optional_trigger_param() {
2995 let src = "rule R { when: Report(interviewer, interview, reason, details?)\n ensures: Done() }";
2996 let r = parse_ok(src);
2997 assert_eq!(r.diagnostics.len(), 0);
2998 }
2999
3000 #[test]
3003 fn qualified_config_access() {
3004 let src = "entity E { duration: oauth/config.session_duration }";
3005 let r = parse_ok(src);
3006 assert_eq!(r.diagnostics.len(), 0);
3007 }
3008
3009 #[test]
3012 fn realistic_spec() {
3013 let src = r#"-- allium: 1
3014
3015enum OrderStatus { pending | shipped | delivered }
3016
3017external entity Customer {
3018 email: String
3019 name: String
3020}
3021
3022entity Order {
3023 customer: Customer
3024 status: OrderStatus
3025 total: Decimal
3026 items: OrderItem with order = this
3027 shipped_items: items where status = shipped
3028 confirmed_items: items where status = confirmed -> item
3029 is_complete: status = delivered
3030 locked_until: Timestamp?
3031}
3032
3033config {
3034 max_retries: Integer = 3
3035 timeout: Duration = 24.hours
3036}
3037
3038rule PlaceOrder {
3039 when: CustomerPlacesOrder(customer, items, total)
3040 requires: total > 0
3041 ensures: Order.created(customer: customer, status: pending, total: total)
3042}
3043
3044rule ShipOrder {
3045 when: order: Order.status transitions_to shipped
3046 ensures: Email.created(to: order.customer.email, template: order_shipped)
3047}
3048
3049open question "How do we handle partial shipments?"
3050"#;
3051 let r = parse_ok(src);
3052 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
3053 assert_eq!(r.module.version, Some(1));
3054 assert_eq!(r.module.declarations.len(), 7);
3055 }
3056
3057 #[test]
3058 fn extension_behaviour_excerpt() {
3059 let src = r#"value Document {
3062 uri: String
3063 text: String
3064}
3065
3066entity Finding {
3067 code: String
3068 severity: error | warning | info
3069 range: FindingRange
3070}
3071
3072entity DiagnosticsMode {
3073 value: strict | relaxed
3074}
3075
3076config {
3077 duplicateKey: String = "allium.config.duplicateKey"
3078}
3079
3080rule RefreshDiagnostics {
3081 when: DocumentOpened(document) or DocumentChanged(document)
3082 requires: document.language_id = "allium"
3083 ensures: FindingsComputed(document)
3084}
3085
3086surface DiagnosticsDashboard {
3087 facing viewer: Developer
3088 context doc: Document where viewer.active_document = doc
3089 provides: RunChecks(viewer) when doc.language_id = "allium"
3090 exposes: FindingList
3091}
3092
3093rule ProcessDigests {
3094 when: schedule: DigestSchedule.next_run_at <= now
3095 for user in Users where notification_setting.digest_enabled:
3096 let settings = user.notification_setting
3097 ensures: DigestBatch.created(user: user)
3098}
3099"#;
3100 let r = parse_ok(src);
3101 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
3102 assert_eq!(r.module.declarations.len(), 7);
3104 }
3105
3106 #[test]
3107 fn exists_as_identifier() {
3108 let src = r#"rule R {
3109 when: X()
3110 ensures: CompletionItemAvailable(label: exists)
3111}"#;
3112 let r = parse_ok(src);
3113 assert_eq!(r.diagnostics.len(), 0);
3114 }
3115
3116 #[test]
3119 fn pipe_binds_tighter_than_or() {
3120 let src = "entity E { v: a or b | c }";
3122 let r = parse_ok(src);
3123 assert_eq!(r.diagnostics.len(), 0);
3124 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3125 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3126 let Expr::LogicalOp { op, right, .. } = value else {
3128 panic!("expected LogicalOp, got {value:?}");
3129 };
3130 assert_eq!(*op, LogicalOp::Or);
3131 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
3133 }
3134
3135 #[test]
3138 fn variant_with_pipe_base() {
3139 let src = "variant Mixed : TypeA | TypeB";
3140 let r = parse_ok(src);
3141 assert_eq!(r.diagnostics.len(), 0);
3142 let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
3143 assert!(matches!(v.base, Expr::Pipe { .. }));
3144 }
3145
3146 #[test]
3149 fn for_block_where_comparison() {
3150 let src = r#"rule R {
3151 when: X()
3152 for item in Items where item.status = active:
3153 ensures: Processed(item: item)
3154}"#;
3155 let r = parse_ok(src);
3156 assert_eq!(r.diagnostics.len(), 0);
3157 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3158 let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
3159 assert!(filter.is_some());
3160 assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
3161 }
3162
3163 #[test]
3166 fn for_expr_where_comparison() {
3167 let src = r#"rule R {
3168 when: X()
3169 ensures:
3170 for item in Items where item.active = true:
3171 Processed(item: item)
3172}"#;
3173 let r = parse_ok(src);
3174 assert_eq!(r.diagnostics.len(), 0);
3175 }
3176
3177 #[test]
3180 fn if_else_if_else() {
3181 let src = r#"rule R {
3182 when: X(v)
3183 ensures:
3184 if v < 10: Small()
3185 else if v < 100: Medium()
3186 else: Large()
3187}"#;
3188 let r = parse_ok(src);
3189 assert_eq!(r.diagnostics.len(), 0);
3190 }
3191
3192 #[test]
3195 fn null_coalesce_and_optional_chain() {
3196 let src = "entity E { v: a?.b ?? fallback }";
3197 let r = parse_ok(src);
3198 assert_eq!(r.diagnostics.len(), 0);
3199 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3200 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3201 assert!(matches!(value, Expr::NullCoalesce { .. }));
3203 }
3204
3205 #[test]
3208 fn generic_type_nested() {
3209 let src = "entity E { v: List<Set<String>> }";
3210 let r = parse_ok(src);
3211 assert_eq!(r.diagnostics.len(), 0);
3212 }
3213
3214 #[test]
3217 fn collection_literals() {
3218 let src = r#"rule R {
3219 when: X()
3220 ensures:
3221 let s = {a, b, c}
3222 let o = {name: "test", count: 42}
3223 Done()
3224}"#;
3225 let r = parse_ok(src);
3226 assert_eq!(r.diagnostics.len(), 0);
3227 }
3228
3229 #[test]
3230 fn spec_reject_list_literal() {
3231 let src = r#"rule R {
3233 when: X()
3234 ensures:
3235 let l = [1, 2, 3]
3236 Done()
3237}"#;
3238 let r = parse_ok(src);
3239 assert!(
3240 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3241 "expected error for `[...]` list literal (not in spec), but parsed without errors"
3242 );
3243 }
3244
3245 #[test]
3248 fn given_block() {
3249 let src = "given { viewer: User\n time: Timestamp }";
3250 let r = parse_ok(src);
3251 assert_eq!(r.diagnostics.len(), 0);
3252 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3253 assert_eq!(b.kind, BlockKind::Given);
3254 assert!(b.name.is_none());
3255 }
3256
3257 #[test]
3260 fn actor_block() {
3261 let src = "actor Admin { identified_by: User where role = admin }";
3262 let r = parse_ok(src);
3263 assert_eq!(r.diagnostics.len(), 0);
3264 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3265 assert_eq!(b.kind, BlockKind::Actor);
3266 }
3267
3268 #[test]
3271 fn join_lookup() {
3272 let src = "entity E { match: Other{field_a, field_b: value} }";
3273 let r = parse_ok(src);
3274 assert_eq!(r.diagnostics.len(), 0);
3275 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3276 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3277 assert!(matches!(value, Expr::JoinLookup { .. }));
3278 }
3279
3280 #[test]
3283 fn in_not_in_set() {
3284 let src = r#"rule R {
3285 when: X(s)
3286 requires: s in {a, b, c}
3287 requires: s not in {d, e}
3288 ensures: Done()
3289}"#;
3290 let r = parse_ok(src);
3291 assert_eq!(r.diagnostics.len(), 0);
3292 }
3293
3294 #[test]
3297 fn comprehensive_fixture() {
3298 let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
3299 let r = parse(src);
3300 assert_eq!(
3301 r.diagnostics.len(),
3302 0,
3303 "expected no errors in comprehensive fixture, got: {:?}",
3304 r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
3305 );
3306 assert!(r.module.declarations.len() > 30, "expected many declarations");
3307 }
3308
3309 #[test]
3312 fn error_expected_declaration() {
3313 let r = parse("-- allium: 1\n+ invalid");
3314 assert!(r.diagnostics.len() >= 1);
3315 let msg = &r.diagnostics[0].message;
3316 assert!(msg.contains("expected declaration"), "got: {msg}");
3317 assert!(msg.contains("entity"), "should list valid options, got: {msg}");
3318 assert!(msg.contains("rule"), "should list valid options, got: {msg}");
3319 }
3320
3321 #[test]
3322 fn error_expected_expression() {
3323 let r = parse("-- allium: 1\nentity E { v: }");
3324 assert!(r.diagnostics.len() >= 1);
3325 let msg = &r.diagnostics[0].message;
3326 assert!(msg.contains("expected expression"), "got: {msg}");
3327 assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
3328 }
3329
3330 #[test]
3331 fn error_expected_block_item() {
3332 let r = parse("-- allium: 1\nentity E { + }");
3333 assert!(r.diagnostics.len() >= 1);
3334 let msg = &r.diagnostics[0].message;
3335 assert!(msg.contains("expected block item"), "got: {msg}");
3336 }
3337
3338 #[test]
3339 fn error_expected_identifier() {
3340 let r = parse("-- allium: 1\nentity 123 {}");
3341 assert!(r.diagnostics.len() >= 1);
3342 let msg = &r.diagnostics[0].message;
3343 assert!(msg.contains("expected entity name"), "got: {msg}");
3345 assert!(msg.contains("number"), "should say what was found, got: {msg}");
3347 }
3348
3349 #[test]
3350 fn error_missing_brace() {
3351 let r = parse("entity E {");
3352 assert!(r.diagnostics.len() >= 1);
3353 let msg = &r.diagnostics[0].message;
3354 assert!(msg.contains("expected"), "got: {msg}");
3355 }
3356
3357 #[test]
3358 fn error_recovery_multiple() {
3359 let r = parse("entity E { + }\nentity F { - }");
3361 assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
3362 }
3363
3364 #[test]
3365 fn error_dedup_same_line() {
3366 let r = parse("-- allium: 1\n+ - * /");
3368 let errors: Vec<_> = r.diagnostics.iter()
3369 .filter(|d| d.severity == crate::diagnostic::Severity::Error)
3370 .collect();
3371 assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
3372 }
3373
3374 #[test]
3375 fn for_block() {
3376 let src = r#"rule R {
3377 when: X()
3378 for user in Users where user.active:
3379 ensures: Notified(user: user)
3380}"#;
3381 let r = parse_ok(src);
3382 assert_eq!(r.diagnostics.len(), 0);
3383 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3384 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
3385 }
3386
3387 #[test]
3388 fn for_expr() {
3389 let src = r#"rule R {
3390 when: X(project)
3391 ensures:
3392 let total = for task in project.tasks: task.effort
3393 Done(total: total)
3394}"#;
3395 let r = parse_ok(src);
3396 assert_eq!(r.diagnostics.len(), 0);
3397 }
3398
3399 #[test]
3400 fn for_where() {
3401 let src = r#"rule R {
3402 when: X()
3403 for item in Items where item.active:
3404 ensures: Processed(item: item)
3405}"#;
3406 let r = parse_ok(src);
3407 assert_eq!(r.diagnostics.len(), 0);
3408 }
3409
3410 #[test]
3411 fn spec_reject_for_with_filter() {
3412 let src = r#"rule R {
3415 when: X()
3416 for slot in Slot with slot.role = reviewer:
3417 ensures: Reviewed(slot: slot)
3418}"#;
3419 let r = parse_ok(src);
3420 assert!(
3421 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3422 "expected error for `for ... with` (spec uses `where`), but parsed without errors"
3423 );
3424 }
3425
3426 #[test]
3427 fn block_level_if() {
3428 let src = r#"rule R {
3429 when: X(task)
3430 if task.priority = high:
3431 ensures: Escalated(task: task)
3432}"#;
3433 let r = parse_ok(src);
3434 assert_eq!(r.diagnostics.len(), 0);
3435 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3436 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3437 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3438 };
3439 assert_eq!(branches.len(), 1);
3440 assert!(else_items.is_none());
3441 }
3442
3443 #[test]
3444 fn block_level_if_else() {
3445 let src = r#"rule R {
3446 when: X(score)
3447 if score > 80:
3448 ensures: High()
3449 else if score > 40:
3450 ensures: Medium()
3451 else:
3452 ensures: Low()
3453}"#;
3454 let r = parse_ok(src);
3455 assert_eq!(r.diagnostics.len(), 0);
3456 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3457 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3458 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3459 };
3460 assert_eq!(branches.len(), 2);
3461 assert!(else_items.is_some());
3462 }
3463
3464 #[test]
3465 fn wildcard_type_parameter() {
3466 let src = "entity E { codec: Codec<*> }";
3467 let r = parse_ok(src);
3468 assert_eq!(r.diagnostics.len(), 0);
3469 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3470 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3471 if let Expr::GenericType { args, .. } = value {
3472 assert_eq!(args.len(), 1);
3473 if let Expr::Ident(id) = &args[0] {
3474 assert_eq!(id.name, "*");
3475 } else {
3476 panic!("expected wildcard ident, got {:?}", args[0]);
3477 }
3478 } else {
3479 panic!("expected GenericType, got {:?}", value);
3480 }
3481 }
3482
3483 #[test]
3484 fn guidance_clause_comment_only_value_migration() {
3485 let src = "-- allium: 1\nrule R {\n ensures: Done()\n guidance: -- just a comment\n}";
3487 let r = parse(src);
3488 assert!(
3489 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
3490 "expected migration diagnostic, got: {:?}",
3491 r.diagnostics
3492 );
3493 }
3494
3495 #[test]
3496 fn spec_reject_for_expr_with_filter() {
3497 let src = r#"rule R {
3499 when: X(project)
3500 ensures:
3501 let total = for task in project.tasks with task.active: task.effort
3502 Done(total: total)
3503}"#;
3504 let r = parse_ok(src);
3505 assert!(
3506 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3507 "expected error for `for ... with` in expression (spec uses `where`), but parsed without errors"
3508 );
3509 }
3510
3511 #[test]
3512 fn for_destructured_binding() {
3513 let src = r#"rule R {
3514 when: X()
3515 for (key, value) in Pairs where key != null:
3516 ensures: Processed(key: key, value: value)
3517}"#;
3518 let r = parse_ok(src);
3519 assert_eq!(r.diagnostics.len(), 0);
3520 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3521 let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
3522 assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
3523 }
3524
3525 #[test]
3526 fn dot_path_assignment() {
3527 let src = r#"entity Shard {
3528 ShardGroup.shard_cache: Shard with group = this
3529}"#;
3530 let r = parse_ok(src);
3531 assert_eq!(r.diagnostics.len(), 0);
3532 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3533 let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
3534 panic!("expected PathAssignment, got {:?}", b.items[0].kind);
3535 };
3536 assert!(matches!(path, Expr::MemberAccess { .. }));
3537 }
3538
3539 #[test]
3540 fn language_reference_fixture() {
3541 let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
3542 let r = parse(src);
3543 let errors: Vec<_> = r.diagnostics.iter()
3544 .filter(|d| d.severity == Severity::Error)
3545 .collect();
3546 assert_eq!(
3547 errors.len(),
3548 0,
3549 "expected no errors in language-reference fixture, got: {:?}",
3550 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
3551 );
3552 }
3553
3554 #[test]
3571 fn spec_for_bare_form() {
3572 let src = r#"rule ProcessDigests {
3574 when: schedule: DigestSchedule.next_run_at <= now
3575 for user in Users where notification_setting.digest_enabled:
3576 let settings = user.notification_setting
3577 ensures: DigestBatch.created(user: user)
3578}"#;
3579 let r = parse_ok(src);
3580 assert_eq!(r.diagnostics.len(), 0);
3581 }
3582
3583 #[test]
3584 fn spec_reject_for_each() {
3585 let src = r#"rule R {
3587 when: X()
3588 for each user in Users where user.active:
3589 ensures: Notified(user: user)
3590}"#;
3591 let r = parse_ok(src);
3592 assert!(
3593 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3594 "expected error for `for each` (not in spec), but parsed without errors"
3595 );
3596 }
3597
3598 #[test]
3601 fn spec_reject_double_equals() {
3602 let src = "rule R { when: X(a)\n requires: a.status == active\n ensures: Done() }";
3604 let r = parse_ok(src);
3605 assert!(
3606 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3607 "expected error for `==` (not in spec), but parsed without errors"
3608 );
3609 }
3610
3611 #[test]
3614 fn spec_reject_system_block() {
3615 let src = "system PaymentGateway {\n timeout: 30.seconds\n}";
3617 let r = parse_ok(src);
3618 assert!(
3619 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3620 "expected error for `system` block (not in spec), but parsed without errors"
3621 );
3622 }
3623
3624 #[test]
3627 fn spec_reject_tags_clause() {
3628 let src = r#"rule R {
3630 when: MigrationTriggered()
3631 tags: infrastructure, migration
3632 ensures: MigrationComplete()
3633}"#;
3634 let r = parse_ok(src);
3635 assert!(
3636 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3637 "expected error for `tags:` clause (not in spec), but parsed without errors"
3638 );
3639 }
3640
3641 #[test]
3644 fn spec_reject_includes_operator() {
3645 let src = r#"rule R {
3647 when: X(a, b)
3648 requires: a.items includes b
3649 ensures: Done()
3650}"#;
3651 let r = parse_ok(src);
3652 assert!(
3653 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3654 "expected error for `includes` operator (not in spec), but parsed without errors"
3655 );
3656 }
3657
3658 #[test]
3659 fn spec_reject_excludes_operator() {
3660 let src = r#"rule R {
3662 when: X(a, b)
3663 requires: a.items excludes b
3664 ensures: Done()
3665}"#;
3666 let r = parse_ok(src);
3667 assert!(
3668 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3669 "expected error for `excludes` operator (not in spec), but parsed without errors"
3670 );
3671 }
3672
3673 #[test]
3676 fn spec_reject_range_literal() {
3677 let src = r#"rule R {
3679 when: X(v)
3680 requires: v in [1..100]
3681 ensures: Done()
3682}"#;
3683 let r = parse_ok(src);
3684 assert!(
3685 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3686 "expected error for `..` range (not in spec), but parsed without errors"
3687 );
3688 }
3689
3690 #[test]
3693 fn spec_within_in_actor() {
3694 let src = r#"actor WorkspaceAdmin {
3696 within: Workspace
3697 identified_by: User where role = admin
3698}"#;
3699 let r = parse_ok(src);
3700 assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
3701 }
3702
3703 #[test]
3706 fn spec_reject_module_declaration() {
3707 let src = "module my_spec";
3709 let r = parse_ok(src);
3710 assert!(
3711 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3712 "expected error for `module` declaration (not in spec), but parsed without errors"
3713 );
3714 }
3715
3716 #[test]
3719 fn spec_reject_module_level_guidance() {
3720 let src = r#"guidance: "All rules must be idempotent""#;
3722 let r = parse_ok(src);
3723 assert!(
3724 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3725 "expected error for module-level `guidance:` (not in spec), but parsed without errors"
3726 );
3727 }
3728
3729 #[test]
3732 fn spec_guarantee_in_surface_migration() {
3733 let src = "-- allium: 1\nsurface S {\n facing viewer: User\n guarantee: DataIntegrity\n}";
3735 let r = parse(src);
3736 assert!(
3737 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
3738 "expected migration diagnostic, got: {:?}",
3739 r.diagnostics
3740 );
3741 }
3742
3743 #[test]
3744 fn spec_timeout_in_surface() {
3745 let src = r#"surface InvitationView {
3747 facing recipient: Candidate
3748 context invitation: ResourceInvitation where email = recipient.email
3749 timeout: InvitationExpires
3750}"#;
3751 let r = parse_ok(src);
3752 assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
3753 }
3754
3755 #[test]
3756 fn spec_timeout_in_surface_with_when() {
3757 let src = r#"surface InvitationView {
3759 facing recipient: Candidate
3760 context invitation: ResourceInvitation where email = recipient.email
3761 timeout: InvitationExpires when invitation.expires_at <= now
3762}"#;
3763 let r = parse_ok(src);
3764 assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
3765 }
3766
3767 #[test]
3770 fn spec_reject_suffix_predicate() {
3771 let src = r#"rule R {
3773 when: X()
3774 requires: finding.code starts_with "allium."
3775 ensures: Done()
3776}"#;
3777 let r = parse_ok(src);
3778 assert!(
3779 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3780 "expected error for suffix predicate (not in spec), but parsed without errors"
3781 );
3782 }
3783
3784 #[test]
3787 fn spec_add_remove_in_ensures() {
3788 let src = r#"rule R {
3791 when: AssignInterviewer(interview, new_interviewer)
3792 ensures:
3793 interview.interviewers.add(new_interviewer)
3794}"#;
3795 let r = parse_ok(src);
3796 assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
3797 }
3798
3799 #[test]
3800 fn spec_remove_in_ensures() {
3801 let src = r#"rule R {
3802 when: RemoveInterviewer(interview, leaving)
3803 ensures:
3804 interview.interviewers.remove(leaving)
3805}"#;
3806 let r = parse_ok(src);
3807 assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3808 }
3809
3810 #[test]
3813 fn spec_first_last_access() {
3814 let src = "entity E { latest: attempts.last\n earliest: attempts.first }";
3816 let r = parse_ok(src);
3817 assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3818 }
3819
3820 #[test]
3823 fn spec_set_arithmetic() {
3824 let src = r#"entity Role {
3826 permissions: Set<String>
3827 inherited: Set<String>
3828 all_permissions: permissions + inherited
3829 removed: old_mentions - new_mentions
3830}"#;
3831 let r = parse_ok(src);
3832 assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3833 }
3834
3835 #[test]
3838 fn spec_discard_binding_in_trigger() {
3839 let src = r#"rule R {
3841 when: _: LogProcessor.last_flush_check <= now
3842 ensures: Flushed()
3843}"#;
3844 let r = parse_ok(src);
3845 assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3846 }
3847
3848 #[test]
3849 fn spec_discard_in_trigger_params() {
3850 let src = r#"rule R {
3852 when: SomeEvent(_, slot)
3853 ensures: Processed(slot: slot)
3854}"#;
3855 let r = parse_ok(src);
3856 assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3857 }
3858
3859 #[test]
3860 fn spec_discard_in_for() {
3861 let src = r#"rule R {
3863 when: X(items)
3864 ensures:
3865 for _ in items: Counted()
3866}"#;
3867 let r = parse_ok(src);
3868 assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3869 }
3870
3871 #[test]
3874 fn spec_default_with_object_literal() {
3875 let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3877 let r = parse_ok(src);
3878 assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3879 }
3880
3881 #[test]
3882 fn spec_default_multiline_object() {
3883 let src = r#"default Role viewer = {
3885 name: "viewer",
3886 permissions: { "documents.read" }
3887}"#;
3888 let r = parse_ok(src);
3889 assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3890 }
3891
3892 #[test]
3895 fn spec_surface_related_clause() {
3896 let src = r#"surface InterviewerDashboard {
3898 facing viewer: Interviewer
3899 context assignment: SlotConfirmation where interviewer = viewer
3900 related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3901}"#;
3902 let r = parse_ok(src);
3903 assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3904 }
3905
3906 #[test]
3907 fn spec_surface_let_binding() {
3908 let src = r#"surface S {
3910 facing viewer: User
3911 let comments = Comments where parent = viewer
3912 exposes: CommentList
3913}"#;
3914 let r = parse_ok(src);
3915 assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3916 }
3917
3918 #[test]
3919 fn spec_surface_multiline_context_where() {
3920 let src = r#"surface InterviewerPendingAssignments {
3922 facing viewer: Interviewer
3923 context assignment: InterviewAssignment
3924 where interviewer = viewer and status = pending
3925 exposes: AssignmentList
3926}"#;
3927 let r = parse_ok(src);
3928 assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3929 }
3930
3931 #[test]
3934 fn spec_for_in_surface_provides() {
3935 let src = r#"surface TaskBoard {
3937 facing viewer: User
3938 for task in Task where task.assignee = viewer:
3939 provides: CompleteTask(viewer, task) when task.status = in_progress
3940 exposes: KanbanBoard
3941}"#;
3942 let r = parse_ok(src);
3943 assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3944 }
3945
3946 #[test]
3949 fn spec_use_without_alias() {
3950 let src = r#"use "github.com/specs/notifications/def456""#;
3952 let r = parse_ok(src);
3953 assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3954 }
3955
3956 #[test]
3959 fn spec_empty_external_entity() {
3960 let src = "external entity Commentable {}";
3962 let r = parse_ok(src);
3963 assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3964 }
3965
3966 #[test]
3969 fn spec_surface_multiline_provides() {
3970 let src = r#"surface ProjectDashboard {
3972 facing viewer: ProjectManager
3973 context project: Project where owner = viewer
3974 provides:
3975 CreateTask(viewer, project) when project.status = active
3976 ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3977 exposes: TaskList
3978}"#;
3979 let r = parse_ok(src);
3980 assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3981 }
3982
3983 #[test]
3986 fn spec_surface_multiline_exposes() {
3987 let src = r#"surface InterviewerDashboard {
3989 facing viewer: Interviewer
3990 context assignment: SlotConfirmation where interviewer = viewer
3991 exposes:
3992 assignment.slot.time
3993 assignment.status
3994}"#;
3995 let r = parse_ok(src);
3996 assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3997 }
3998
3999 #[test]
4009 fn composite_or_trigger() {
4010 let src = r#"rule R {
4011 when: EventA(x) or EventB(x) or EventC(x)
4012 ensures: Done()
4013}"#;
4014 let r = parse_ok(src);
4015 assert_eq!(r.diagnostics.len(), 0);
4016 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4017 let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
4018 assert_eq!(keyword, "when");
4019 let Expr::LogicalOp { op, left, .. } = value else {
4021 panic!("expected LogicalOp, got {value:?}");
4022 };
4023 assert_eq!(*op, LogicalOp::Or);
4024 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4025 }
4026
4027 #[test]
4030 fn value_type_declaration() {
4031 let src = r#"value TimeRange {
4032 start: Timestamp
4033 end: Timestamp
4034 duration: end - start
4035}"#;
4036 let r = parse_ok(src);
4037 assert_eq!(r.diagnostics.len(), 0);
4038 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4039 assert_eq!(b.kind, BlockKind::Value);
4040 assert_eq!(b.name.as_ref().unwrap().name, "TimeRange");
4041 assert_eq!(b.items.len(), 3);
4042 }
4043
4044 #[test]
4047 fn qualified_config_block() {
4048 let src = r#"use "github.com/specs/oauth/abc123" as oauth
4049oauth/config {
4050 session_duration: Duration = 24.hours
4051}"#;
4052 let r = parse_ok(src);
4053 assert_eq!(r.diagnostics.len(), 0);
4054 assert_eq!(r.module.declarations.len(), 2);
4055 }
4056
4057 #[test]
4060 fn string_interpolation_parts() {
4061 let src = r#"rule R {
4062 when: X(name, action)
4063 ensures: Log.created(message: "User {name} did {action}")
4064}"#;
4065 let r = parse_ok(src);
4066 assert_eq!(r.diagnostics.len(), 0);
4067 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4069 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4070 let Expr::Call { args, .. } = value else { panic!() };
4071 let CallArg::Named(arg) = &args[0] else { panic!() };
4072 let Expr::StringLiteral(s) = &arg.value else { panic!() };
4073 assert_eq!(s.parts.len(), 4, "expected 4 string parts: text, interp, text, interp");
4074 assert!(matches!(&s.parts[0], StringPart::Text(t) if t == "User "));
4075 assert!(matches!(&s.parts[1], StringPart::Interpolation(id) if id.name == "name"));
4076 assert!(matches!(&s.parts[2], StringPart::Text(t) if t == " did "));
4077 assert!(matches!(&s.parts[3], StringPart::Interpolation(id) if id.name == "action"));
4078 }
4079
4080 #[test]
4083 fn this_keyword_expression() {
4084 let src = "entity E { items: Item with parent = this }";
4087 let r = parse_ok(src);
4088 assert_eq!(r.diagnostics.len(), 0);
4089 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4090 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4091 let Expr::With { predicate, .. } = value else {
4092 panic!("expected With, got {value:?}");
4093 };
4094 let Expr::Comparison { op, right, .. } = predicate.as_ref() else {
4095 panic!("expected Comparison in with predicate, got {predicate:?}");
4096 };
4097 assert_eq!(*op, ComparisonOp::Eq);
4098 assert!(matches!(right.as_ref(), Expr::This { .. }));
4099 }
4100
4101 #[test]
4104 fn not_prefix_standalone() {
4105 let src = r#"rule R {
4106 when: X(user)
4107 requires: not user.is_locked
4108 ensures: Done()
4109}"#;
4110 let r = parse_ok(src);
4111 assert_eq!(r.diagnostics.len(), 0);
4112 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4113 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4114 assert_eq!(keyword, "requires");
4115 assert!(matches!(value, Expr::Not { .. }));
4116 }
4117
4118 #[test]
4121 fn unary_minus() {
4122 let src = "entity E { offset: -1 }";
4123 let r = parse_ok(src);
4124 assert_eq!(r.diagnostics.len(), 0);
4125 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4126 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4127 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }
4128 | Expr::NumberLiteral { .. }), "expected negation, got {value:?}");
4129 }
4130
4131 #[test]
4134 fn parenthesised_expression() {
4135 let src = "entity E { v: (a + b) * c }";
4136 let r = parse_ok(src);
4137 assert_eq!(r.diagnostics.len(), 0);
4138 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4139 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4140 let Expr::BinaryOp { op, left, .. } = value else {
4142 panic!("expected BinaryOp, got {value:?}");
4143 };
4144 assert_eq!(*op, BinaryOp::Mul);
4145 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
4146 }
4147
4148 #[test]
4151 fn boolean_literals() {
4152 let src = r#"rule R {
4153 when: X(item)
4154 ensures:
4155 item.active = true
4156 item.deleted = false
4157}"#;
4158 let r = parse_ok(src);
4159 assert_eq!(r.diagnostics.len(), 0);
4160 }
4161
4162 #[test]
4165 fn null_literal() {
4166 let src = "entity E { v: parent ?? null }";
4167 let r = parse_ok(src);
4168 assert_eq!(r.diagnostics.len(), 0);
4169 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4170 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4171 let Expr::NullCoalesce { right, .. } = value else { panic!() };
4172 assert!(matches!(right.as_ref(), Expr::Null { .. }));
4173 }
4174
4175 #[test]
4178 fn empty_set_literal() {
4179 let src = "entity E { tags: Set<String>\n default_tags: {} }";
4180 let r = parse_ok(src);
4181 assert_eq!(r.diagnostics.len(), 0);
4182 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4183 let BlockItemKind::Assignment { value, .. } = &b.items[1].kind else { panic!() };
4184 let Expr::SetLiteral { elements, .. } = value else { panic!("expected SetLiteral, got {value:?}") };
4185 assert!(elements.is_empty());
4186 }
4187
4188 #[test]
4200 fn param_assignment_single() {
4201 let src = "entity Plan { can_use(feature): feature in features }";
4202 let r = parse_ok(src);
4203 assert_eq!(r.diagnostics.len(), 0);
4204 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4205 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4206 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4207 };
4208 assert_eq!(name.name, "can_use");
4209 assert_eq!(params.len(), 1);
4210 assert_eq!(params[0].name, "feature");
4211 assert!(matches!(value, Expr::In { .. }));
4212 }
4213
4214 #[test]
4215 fn param_assignment_multiple() {
4216 let src = "entity E { distance(x, y): (x * x + y * y) }";
4217 let r = parse_ok(src);
4218 assert_eq!(r.diagnostics.len(), 0);
4219 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4220 let BlockItemKind::ParamAssignment { name, params, .. } = &b.items[0].kind else {
4221 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4222 };
4223 assert_eq!(name.name, "distance");
4224 assert_eq!(params.len(), 2);
4225 assert_eq!(params[0].name, "x");
4226 assert_eq!(params[1].name, "y");
4227 }
4228
4229 #[test]
4230 fn param_assignment_simple_expression() {
4231 let src = "entity Task { remaining_effort(total): total - effort }";
4232 let r = parse_ok(src);
4233 assert_eq!(r.diagnostics.len(), 0);
4234 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4235 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4236 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4237 };
4238 assert_eq!(name.name, "remaining_effort");
4239 assert_eq!(params.len(), 1);
4240 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }));
4241 }
4242
4243 #[test]
4246 fn precedence_logical_and_binds_tighter_than_or() {
4247 let src = "entity E { v: a or b and c }";
4249 let r = parse_ok(src);
4250 assert_eq!(r.diagnostics.len(), 0);
4251 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4252 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4253 let Expr::LogicalOp { op, right, .. } = value else {
4254 panic!("expected LogicalOp, got {value:?}");
4255 };
4256 assert_eq!(*op, LogicalOp::Or);
4257 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4258 }
4259
4260 #[test]
4261 fn precedence_comparison_binds_tighter_than_and() {
4262 let src = "entity E { v: a = b and c != d }";
4264 let r = parse_ok(src);
4265 assert_eq!(r.diagnostics.len(), 0);
4266 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4267 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4268 let Expr::LogicalOp { op, left, right, .. } = value else {
4269 panic!("expected LogicalOp, got {value:?}");
4270 };
4271 assert_eq!(*op, LogicalOp::And);
4272 assert!(matches!(left.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4273 assert!(matches!(right.as_ref(), Expr::Comparison { op: ComparisonOp::NotEq, .. }));
4274 }
4275
4276 #[test]
4277 fn precedence_arithmetic_binds_tighter_than_comparison() {
4278 let src = "entity E { v: a + b > c * d }";
4280 let r = parse_ok(src);
4281 assert_eq!(r.diagnostics.len(), 0);
4282 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4283 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4284 let Expr::Comparison { op, left, right, .. } = value else {
4285 panic!("expected Comparison, got {value:?}");
4286 };
4287 assert_eq!(*op, ComparisonOp::Gt);
4288 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
4289 assert!(matches!(right.as_ref(), Expr::BinaryOp { op: BinaryOp::Mul, .. }));
4290 }
4291
4292 #[test]
4293 fn precedence_null_coalesce_binds_tighter_than_comparison() {
4294 let src = "entity E { v: a ?? b = c }";
4296 let r = parse_ok(src);
4297 assert_eq!(r.diagnostics.len(), 0);
4298 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4299 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4300 let Expr::Comparison { op, left, .. } = value else {
4301 panic!("expected Comparison, got {value:?}");
4302 };
4303 assert_eq!(*op, ComparisonOp::Eq);
4304 assert!(matches!(left.as_ref(), Expr::NullCoalesce { .. }));
4305 }
4306
4307 #[test]
4308 fn precedence_not_binds_tighter_than_and() {
4309 let src = r#"rule R {
4311 when: X(a, b)
4312 requires: not a and b
4313 ensures: Done()
4314}"#;
4315 let r = parse_ok(src);
4316 assert_eq!(r.diagnostics.len(), 0);
4317 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4318 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4319 let Expr::LogicalOp { op, left, .. } = value else {
4320 panic!("expected LogicalOp, got {value:?}");
4321 };
4322 assert_eq!(*op, LogicalOp::And);
4323 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4324 }
4325
4326 #[test]
4327 fn precedence_where_captures_full_condition() {
4328 let src = "entity E { v: items where status = active }";
4332 let r = parse_ok(src);
4333 assert_eq!(r.diagnostics.len(), 0);
4334 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4335 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4336 let Expr::Where { condition, .. } = value else {
4337 panic!("expected Where, got {value:?}");
4338 };
4339 assert!(matches!(condition.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4340 }
4341
4342 #[test]
4343 fn precedence_where_captures_and_or_conditions() {
4344 let src = "entity E { v: items where status = active and count > 0 }";
4347 let r = parse_ok(src);
4348 assert_eq!(r.diagnostics.len(), 0);
4349 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4350 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4351 let Expr::Where { condition, .. } = value else {
4352 panic!("expected Where, got {value:?}");
4353 };
4354 assert!(matches!(condition.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4355 }
4356
4357 #[test]
4358 fn precedence_projection_applies_to_where_result() {
4359 let src = "entity E { v: items where status = confirmed -> interviewer }";
4362 let r = parse_ok(src);
4363 assert_eq!(r.diagnostics.len(), 0);
4364 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4365 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4366 let Expr::ProjectionMap { source, field, .. } = value else {
4367 panic!("expected ProjectionMap, got {value:?}");
4368 };
4369 assert_eq!(field.name, "interviewer");
4370 assert!(matches!(source.as_ref(), Expr::Where { .. }));
4371 }
4372
4373 #[test]
4374 fn precedence_lambda_binds_loosest() {
4375 let src = "entity E { v: items.any(i => i.active and i.valid) }";
4377 let r = parse_ok(src);
4378 assert_eq!(r.diagnostics.len(), 0);
4379 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4380 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4381 let Expr::Call { args, .. } = value else { panic!() };
4382 let CallArg::Positional(Expr::Lambda { body, .. }) = &args[0] else { panic!() };
4383 assert!(matches!(body.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4384 }
4385
4386 #[test]
4387 fn precedence_in_binds_at_comparison_level() {
4388 let src = r#"rule R {
4390 when: X(x, y)
4391 requires: x in {a, b} and y not in {c}
4392 ensures: Done()
4393}"#;
4394 let r = parse_ok(src);
4395 assert_eq!(r.diagnostics.len(), 0);
4396 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4397 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4398 let Expr::LogicalOp { op, left, right, .. } = value else {
4399 panic!("expected LogicalOp, got {value:?}");
4400 };
4401 assert_eq!(*op, LogicalOp::And);
4402 assert!(matches!(left.as_ref(), Expr::In { .. }));
4403 assert!(matches!(right.as_ref(), Expr::NotIn { .. }));
4404 }
4405
4406 #[test]
4409 fn multiline_ensures_block() {
4410 let src = r#"rule R {
4411 when: X(doc)
4412 ensures:
4413 doc.status = published
4414 Notification.created(to: doc.author)
4415}"#;
4416 let r = parse_ok(src);
4417 assert_eq!(r.diagnostics.len(), 0);
4418 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4419 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4420 assert_eq!(keyword, "ensures");
4421 let Expr::Block { items, .. } = value else {
4422 panic!("expected Block for multi-line ensures, got {value:?}");
4423 };
4424 assert_eq!(items.len(), 2);
4425 }
4426
4427 #[test]
4428 fn singleline_ensures_value() {
4429 let src = r#"rule R {
4430 when: X(doc)
4431 ensures: doc.status = published
4432}"#;
4433 let r = parse_ok(src);
4434 assert_eq!(r.diagnostics.len(), 0);
4435 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4436 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4437 assert_eq!(keyword, "ensures");
4438 assert!(!matches!(value, Expr::Block { .. }), "single-line ensures should not be Block");
4440 }
4441
4442 #[test]
4443 fn multiline_requires_with_continuation() {
4444 let src = r#"rule R {
4445 when: X(a)
4446 requires:
4447 a.count >= 2
4448 or a.items.any(i => i.can_solo)
4449 ensures: Done()
4450}"#;
4451 let r = parse_ok(src);
4452 assert_eq!(r.diagnostics.len(), 0);
4453 }
4454
4455 #[test]
4458 fn object_literal_single_field() {
4459 let src = r#"rule R {
4460 when: X()
4461 ensures:
4462 let o = {name: "test"}
4463 Done()
4464}"#;
4465 let r = parse_ok(src);
4466 assert_eq!(r.diagnostics.len(), 0);
4467 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4468 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4469 let Expr::Block { items, .. } = value else { panic!() };
4470 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4471 assert!(matches!(let_val.as_ref(), Expr::ObjectLiteral { .. }));
4472 }
4473
4474 #[test]
4475 fn set_literal_single_element() {
4476 let src = r#"rule R {
4477 when: X()
4478 ensures:
4479 let s = {active}
4480 Done()
4481}"#;
4482 let r = parse_ok(src);
4483 assert_eq!(r.diagnostics.len(), 0);
4484 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4485 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4486 let Expr::Block { items, .. } = value else { panic!() };
4487 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4488 assert!(matches!(let_val.as_ref(), Expr::SetLiteral { .. }),
4489 "bare {{ident}} should parse as set literal, got {:?}", let_val);
4490 }
4491
4492 #[test]
4495 fn lambda_with_chained_access() {
4496 let src = "entity E { v: items.all(t => t.item.status = active) }";
4497 let r = parse_ok(src);
4498 assert_eq!(r.diagnostics.len(), 0);
4499 }
4500
4501 #[test]
4502 fn nested_lambda() {
4503 let src = "entity E { v: groups.any(g => g.items.all(i => i.valid)) }";
4504 let r = parse_ok(src);
4505 assert_eq!(r.diagnostics.len(), 0);
4506 }
4507
4508 #[test]
4511 fn qualified_name_with_member_access() {
4512 let src = "entity E { v: shared/Validator.check }";
4513 let r = parse_ok(src);
4514 assert_eq!(r.diagnostics.len(), 0);
4515 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4516 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4517 let Expr::MemberAccess { object, field, .. } = value else {
4518 panic!("expected MemberAccess, got {value:?}");
4519 };
4520 assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
4521 assert_eq!(field.name, "check");
4522 }
4523
4524 #[test]
4525 fn qualified_name_in_call() {
4526 let src = r#"rule R {
4527 when: X(item)
4528 requires: shared/Validator.check(item: item)
4529 ensures: Done()
4530}"#;
4531 let r = parse_ok(src);
4532 assert_eq!(r.diagnostics.len(), 0);
4533 }
4534
4535 #[test]
4538 fn nested_if_inside_for() {
4539 let src = r#"rule R {
4540 when: X()
4541 for user in Users where user.active:
4542 if user.role = admin:
4543 ensures: AdminNotified(user: user)
4544 else:
4545 ensures: UserNotified(user: user)
4546}"#;
4547 let r = parse_ok(src);
4548 assert_eq!(r.diagnostics.len(), 0);
4549 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4550 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4551 assert!(matches!(items[0].kind, BlockItemKind::IfBlock { .. }));
4552 }
4553
4554 #[test]
4555 fn for_with_let_before_ensures() {
4556 let src = r#"rule R {
4557 when: schedule: DigestSchedule.next_run_at <= now
4558 for user in Users where user.active:
4559 let pending = user.tasks where status = pending
4560 ensures: DigestEmail.created(to: user.email, tasks: pending)
4561}"#;
4562 let r = parse_ok(src);
4563 assert_eq!(r.diagnostics.len(), 0);
4564 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4565 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4566 assert_eq!(items.len(), 2, "for body should have let + ensures");
4567 assert!(matches!(items[0].kind, BlockItemKind::Let { .. }));
4568 assert!(matches!(items[1].kind, BlockItemKind::Clause { .. }));
4569 }
4570
4571 #[test]
4574 fn join_lookup_all_unnamed() {
4575 let src = "entity E { match: Other{a, b, c} }";
4576 let r = parse_ok(src);
4577 assert_eq!(r.diagnostics.len(), 0);
4578 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4579 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4580 let Expr::JoinLookup { fields, .. } = value else { panic!() };
4581 assert_eq!(fields.len(), 3);
4582 assert!(fields.iter().all(|f| f.value.is_none()));
4583 }
4584
4585 #[test]
4586 fn join_lookup_all_named() {
4587 let src = "entity E { match: Membership{user: actor, workspace: ws} }";
4588 let r = parse_ok(src);
4589 assert_eq!(r.diagnostics.len(), 0);
4590 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4591 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4592 let Expr::JoinLookup { fields, .. } = value else { panic!() };
4593 assert_eq!(fields.len(), 2);
4594 assert!(fields.iter().all(|f| f.value.is_some()));
4595 }
4596
4597 #[test]
4598 fn join_lookup_in_requires() {
4599 let src = r#"rule R {
4600 when: X(user, workspace)
4601 requires: exists WorkspaceMembership{user: user, workspace: workspace}
4602 ensures: Done()
4603}"#;
4604 let r = parse_ok(src);
4605 assert_eq!(r.diagnostics.len(), 0);
4606 }
4607
4608 #[test]
4609 fn join_lookup_negated_in_requires() {
4610 let src = r#"rule R {
4611 when: X(email)
4612 requires: not exists User{email: email}
4613 ensures: Done()
4614}"#;
4615 let r = parse_ok(src);
4616 assert_eq!(r.diagnostics.len(), 0);
4617 }
4618
4619 #[test]
4624 fn implies_basic() {
4625 let src = "rule R { requires: a implies b }";
4626 let r = parse_ok(src);
4627 assert_eq!(r.diagnostics.len(), 0);
4628 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4629 let BlockItemKind::Clause { value, .. } = &b.items[0].kind else { panic!() };
4630 let Expr::LogicalOp { op, .. } = value else { panic!("expected LogicalOp, got {value:?}") };
4631 assert_eq!(*op, LogicalOp::Implies);
4632 }
4633
4634 #[test]
4635 fn implies_precedence_and_binds_tighter() {
4636 let src = "rule R { v: a and b implies c }";
4638 let r = parse_ok(src);
4639 assert_eq!(r.diagnostics.len(), 0);
4640 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4641 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4642 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4643 assert_eq!(*op, LogicalOp::Implies);
4644 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4645 }
4646
4647 #[test]
4648 fn implies_precedence_or_binds_tighter() {
4649 let src = "rule R { v: a or b implies c }";
4651 let r = parse_ok(src);
4652 assert_eq!(r.diagnostics.len(), 0);
4653 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4654 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4655 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4656 assert_eq!(*op, LogicalOp::Implies);
4657 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4658 }
4659
4660 #[test]
4661 fn implies_precedence_implies_above_or() {
4662 let src = "rule R { v: a implies b or c }";
4664 let r = parse_ok(src);
4665 assert_eq!(r.diagnostics.len(), 0);
4666 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4667 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4668 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4669 assert_eq!(*op, LogicalOp::Implies);
4670 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4671 }
4672
4673 #[test]
4674 fn implies_precedence_not_binds_tighter() {
4675 let src = "rule R { v: not a implies b }";
4677 let r = parse_ok(src);
4678 assert_eq!(r.diagnostics.len(), 0);
4679 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4680 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4681 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4682 assert_eq!(*op, LogicalOp::Implies);
4683 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4684 }
4685
4686 #[test]
4687 fn implies_right_associative() {
4688 let src = "rule R { v: a implies b implies c }";
4690 let r = parse_ok(src);
4691 assert_eq!(r.diagnostics.len(), 0);
4692 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4693 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4694 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4695 assert_eq!(*op, LogicalOp::Implies);
4696 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4697 }
4698
4699 #[test]
4700 fn implies_is_keyword_parsed_as_operator() {
4701 let src = "entity E { v: a implies b }";
4704 let r = parse_ok(src);
4705 assert_eq!(r.diagnostics.len(), 0);
4706 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4707 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4708 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4709 }
4710
4711 #[test]
4712 fn implies_in_ensures() {
4713 let src = r#"rule R {
4714 when: X()
4715 ensures: a implies b
4716}"#;
4717 let r = parse_ok(src);
4718 assert_eq!(r.diagnostics.len(), 0);
4719 }
4720
4721 #[test]
4722 fn implies_in_derived_value() {
4723 let src = "entity E { v: a implies b }";
4724 let r = parse_ok(src);
4725 assert_eq!(r.diagnostics.len(), 0);
4726 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4727 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4728 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4729 }
4730
4731 #[test]
4736 fn guidance_ordering_tests_removed() {
4737 }
4741
4742 #[test]
4747 fn contract_signatures_only() {
4748 let src = r#"contract Auditable {
4749 last_modified_by: Actor
4750 last_modified_at: Timestamp
4751}"#;
4752 let r = parse_ok(src);
4753 assert_eq!(r.diagnostics.len(), 0);
4754 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4755 assert_eq!(b.kind, BlockKind::Contract);
4756 assert_eq!(b.name.as_ref().unwrap().name, "Auditable");
4757 assert_eq!(b.items.len(), 2);
4758 }
4759
4760 #[test]
4761 fn contract_with_annotations() {
4762 let src = r#"contract Versioned {
4763 version: Integer
4764 @invariant Monotonic
4765 -- versions must increase
4766 @guidance
4767 -- use semantic versioning
4768}"#;
4769 let r = parse_ok(src);
4770 assert_eq!(r.diagnostics.len(), 0);
4771 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4772 assert_eq!(b.kind, BlockKind::Contract);
4773 assert_eq!(b.items.len(), 3);
4774 }
4775
4776 #[test]
4777 fn contract_with_any_type() {
4778 let src = r#"contract Identifiable {
4779 id: Any
4780}"#;
4781 let r = parse_ok(src);
4782 assert_eq!(r.diagnostics.len(), 0);
4783 }
4784
4785 #[test]
4786 fn contract_lowercase_name_rejected() {
4787 let src = "-- allium: 1\ncontract bad {}";
4788 let r = parse(src);
4789 assert!(
4790 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4791 "expected uppercase error, got: {:?}",
4792 r.diagnostics
4793 );
4794 }
4795
4796 #[test]
4797 fn contract_colon_body_rejected() {
4798 let src = "-- allium: 1\ncontract Bad: something";
4799 let r = parse(src);
4800 assert!(
4801 r.diagnostics.iter().any(|d| d.message.contains("braces")),
4802 "expected braces error, got: {:?}",
4803 r.diagnostics
4804 );
4805 }
4806
4807 #[test]
4812 fn contracts_clause_single_demands() {
4813 let src = "surface S {\n contracts:\n demands Auditable\n}";
4814 let r = parse_ok(src);
4815 assert_eq!(r.diagnostics.len(), 0);
4816 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4817 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4818 panic!("expected ContractsClause, got {:?}", b.items[0].kind)
4819 };
4820 assert_eq!(entries.len(), 1);
4821 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4822 assert_eq!(entries[0].name.name, "Auditable");
4823 }
4824
4825 #[test]
4826 fn contracts_clause_single_fulfils() {
4827 let src = "surface S {\n contracts:\n fulfils EventSubmitter\n}";
4828 let r = parse_ok(src);
4829 assert_eq!(r.diagnostics.len(), 0);
4830 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4831 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4832 panic!("expected ContractsClause")
4833 };
4834 assert_eq!(entries.len(), 1);
4835 assert!(matches!(entries[0].direction, ContractDirection::Fulfils));
4836 assert_eq!(entries[0].name.name, "EventSubmitter");
4837 }
4838
4839 #[test]
4840 fn contracts_clause_qualified_fulfils() {
4841 let src = "surface S {\n contracts:\n fulfils base/MyContract\n}";
4842 let r = parse_ok(src);
4843 assert_eq!(r.diagnostics.len(), 0);
4844 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4845 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4846 panic!("expected ContractsClause, got {:?}", b.items[0].kind)
4847 };
4848 assert_eq!(entries.len(), 1);
4849 assert!(matches!(entries[0].direction, ContractDirection::Fulfils));
4850 assert_eq!(entries[0].qualifier.as_deref(), Some("base"));
4851 assert_eq!(entries[0].name.name, "MyContract");
4852 }
4853
4854 #[test]
4855 fn contracts_clause_qualified_demands() {
4856 let src = "surface S {\n contracts:\n demands base/MyContract\n fulfils Local\n}";
4857 let r = parse_ok(src);
4858 assert_eq!(r.diagnostics.len(), 0);
4859 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4860 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4861 panic!("expected ContractsClause")
4862 };
4863 assert_eq!(entries.len(), 2);
4864 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4865 assert_eq!(entries[0].qualifier.as_deref(), Some("base"));
4866 assert_eq!(entries[0].name.name, "MyContract");
4867 assert_eq!(entries[1].qualifier, None);
4868 assert_eq!(entries[1].name.name, "Local");
4869 }
4870
4871 #[test]
4872 fn contracts_clause_qualified_missing_name_errors() {
4873 let src = "surface S {\n contracts:\n fulfils base/\n}";
4874 let r = parse(src);
4875 assert!(
4876 r.diagnostics
4877 .iter()
4878 .any(|d| d.message.contains("contract name after '/'")),
4879 "expected an error about the missing name, got {:?}",
4880 r.diagnostics
4881 );
4882 }
4883
4884 #[test]
4885 fn contracts_clause_mixed() {
4886 let src = "surface S {\n contracts:\n demands Auditable\n fulfils EventSubmitter\n}";
4887 let r = parse_ok(src);
4888 assert_eq!(r.diagnostics.len(), 0);
4889 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4890 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4891 panic!("expected ContractsClause")
4892 };
4893 assert_eq!(entries.len(), 2);
4894 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4895 assert!(matches!(entries[1].direction, ContractDirection::Fulfils));
4896 }
4897
4898 #[test]
4899 fn contracts_with_other_clauses() {
4900 let src = r#"surface S {
4901 facing user: User
4902 contracts:
4903 demands Auditable
4904 exposes:
4905 user.name
4906}"#;
4907 let r = parse_ok(src);
4908 assert_eq!(r.diagnostics.len(), 0);
4909 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4910 assert_eq!(b.items.len(), 3);
4911 }
4912
4913 #[test]
4914 fn contracts_only_surface() {
4915 let src = "surface S {\n contracts:\n demands Foo\n fulfils Bar\n}";
4916 let r = parse_ok(src);
4917 assert_eq!(r.diagnostics.len(), 0);
4918 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4919 assert_eq!(b.items.len(), 1);
4920 }
4921
4922 #[test]
4923 fn contracts_empty_rejected() {
4924 let src = "-- allium: 1\nsurface S {\n contracts:\n}";
4925 let r = parse(src);
4926 assert!(
4927 r.diagnostics.iter().any(|d| d.message.contains("Empty `contracts:`")),
4928 "expected empty contracts error, got: {:?}",
4929 r.diagnostics
4930 );
4931 }
4932
4933 #[test]
4934 fn contracts_inline_block_rejected() {
4935 let src = "-- allium: 1\nsurface S {\n contracts:\n demands Foo {\n }\n}";
4936 let r = parse(src);
4937 assert!(
4938 r.diagnostics.iter().any(|d| d.message.contains("Inline contract blocks")),
4939 "expected inline block error, got: {:?}",
4940 r.diagnostics
4941 );
4942 }
4943
4944 #[test]
4945 fn contracts_unknown_direction_rejected() {
4946 let src = "-- allium: 1\nsurface S {\n contracts:\n requires Foo\n}";
4947 let r = parse(src);
4948 assert!(
4949 r.diagnostics.iter().any(|d| d.message.contains("Unknown direction")),
4950 "expected unknown direction error, got: {:?}",
4951 r.diagnostics
4952 );
4953 }
4954
4955 #[test]
4960 fn annotation_invariant() {
4961 let src = "contract C {\n @invariant Determinism\n -- all evaluations must be deterministic\n}";
4962 let r = parse_ok(src);
4963 assert_eq!(r.diagnostics.len(), 0);
4964 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4965 let BlockItemKind::Annotation(ann) = &b.items[0].kind else {
4966 panic!("expected Annotation, got {:?}", b.items[0].kind)
4967 };
4968 assert!(matches!(ann.kind, AnnotationKind::Invariant));
4969 assert_eq!(ann.name.as_ref().unwrap().name, "Determinism");
4970 assert_eq!(ann.body.len(), 1);
4971 assert_eq!(ann.body[0], "all evaluations must be deterministic");
4972 }
4973
4974 #[test]
4975 fn annotation_multiple_invariants() {
4976 let src = "contract C {\n @invariant A\n -- first\n @invariant B\n -- second\n}";
4977 let r = parse_ok(src);
4978 assert_eq!(r.diagnostics.len(), 0);
4979 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4980 assert_eq!(b.items.len(), 2);
4981 assert!(matches!(&b.items[0].kind, BlockItemKind::Annotation(_)));
4982 assert!(matches!(&b.items[1].kind, BlockItemKind::Annotation(_)));
4983 }
4984
4985 #[test]
4986 fn annotation_invariant_then_guidance() {
4987 let src = "contract C {\n @invariant Safety\n -- must be safe\n @guidance\n -- implementation notes\n}";
4988 let r = parse_ok(src);
4989 assert_eq!(r.diagnostics.len(), 0);
4990 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4991 assert_eq!(b.items.len(), 2);
4992 }
4993
4994 #[test]
4995 fn annotation_guidance_in_rule() {
4996 let src = "rule R {\n when: Event.created\n ensures: something\n @guidance\n -- do it this way\n}";
4997 let r = parse_ok(src);
4998 assert_eq!(r.diagnostics.len(), 0);
4999 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5000 let last = b.items.last().unwrap();
5001 let BlockItemKind::Annotation(ann) = &last.kind else { panic!() };
5002 assert!(matches!(ann.kind, AnnotationKind::Guidance));
5003 assert!(ann.name.is_none());
5004 }
5005
5006 #[test]
5007 fn annotation_guarantee() {
5008 let src = "surface S {\n @guarantee ResponseTime\n -- must respond within 100ms\n}";
5009 let r = parse_ok(src);
5010 assert_eq!(r.diagnostics.len(), 0);
5011 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5012 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
5013 assert!(matches!(ann.kind, AnnotationKind::Guarantee));
5014 assert_eq!(ann.name.as_ref().unwrap().name, "ResponseTime");
5015 }
5016
5017 #[test]
5018 fn annotation_guarantee_then_guidance() {
5019 let src = "surface S {\n @guarantee Fast\n -- sub-second\n @guidance\n -- cache aggressively\n}";
5020 let r = parse_ok(src);
5021 assert_eq!(r.diagnostics.len(), 0);
5022 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5023 assert_eq!(b.items.len(), 2);
5024 }
5025
5026 #[test]
5027 fn annotation_contracts_guarantee_guidance() {
5028 let src = r#"surface S {
5029 contracts:
5030 demands Auditable
5031 @guarantee ResponseTime
5032 -- fast
5033 @guidance
5034 -- notes
5035}"#;
5036 let r = parse_ok(src);
5037 assert_eq!(r.diagnostics.len(), 0);
5038 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5039 assert_eq!(b.items.len(), 3);
5040 }
5041
5042 #[test]
5043 fn annotation_multiline_body() {
5044 let src = "contract C {\n @invariant Multi\n -- line one\n -- line two\n -- line three\n}";
5045 let r = parse_ok(src);
5046 assert_eq!(r.diagnostics.len(), 0);
5047 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5048 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
5049 assert_eq!(ann.body.len(), 3);
5050 assert_eq!(ann.body[0], "line one");
5051 assert_eq!(ann.body[2], "line three");
5052 }
5053
5054 #[test]
5055 fn annotation_empty_body_rejected() {
5056 let src = "-- allium: 1\ncontract C {\n @invariant NoBody\n}";
5057 let r = parse(src);
5058 assert!(
5059 r.diagnostics.iter().any(|d| d.message.contains("at least one indented comment line")),
5060 "expected empty body error, got: {:?}",
5061 r.diagnostics
5062 );
5063 }
5064
5065 #[test]
5066 fn annotation_unknown_keyword_rejected() {
5067 let src = "-- allium: 1\ncontract C {\n @note Something\n -- text\n}";
5068 let r = parse(src);
5069 assert!(
5070 r.diagnostics.iter().any(|d| d.message.contains("Unknown annotation")),
5071 "expected unknown annotation error, got: {:?}",
5072 r.diagnostics
5073 );
5074 }
5075
5076 #[test]
5077 fn expression_invariant_still_works() {
5078 let src = r#"entity E {
5079 status: pending | active
5080 invariant AllValid {
5081 this.status = active
5082 }
5083}"#;
5084 let r = parse_ok(src);
5085 assert_eq!(r.diagnostics.len(), 0);
5086 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5087 let inv = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. }));
5089 assert!(inv.is_some(), "expression-bearing invariant should still parse");
5090 }
5091
5092 #[test]
5093 fn invariant_colon_form_migration() {
5094 let src = "-- allium: 1\ncontract C {\n invariant: SomeName\n}";
5095 let r = parse(src);
5096 assert!(
5097 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
5098 "expected migration diagnostic, got: {:?}",
5099 r.diagnostics
5100 );
5101 }
5102
5103 #[test]
5104 fn guidance_colon_form_migration() {
5105 let src = "-- allium: 1\nrule R {\n when: Event.created\n ensures: something\n guidance: \"do it\"\n}";
5106 let r = parse(src);
5107 assert!(
5108 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
5109 "expected migration diagnostic, got: {:?}",
5110 r.diagnostics
5111 );
5112 }
5113
5114 #[test]
5115 fn guarantee_colon_form_migration() {
5116 let src = "-- allium: 1\nsurface S {\n guarantee: \"fast\"\n}";
5117 let r = parse(src);
5118 assert!(
5119 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
5120 "expected migration diagnostic, got: {:?}",
5121 r.diagnostics
5122 );
5123 }
5124
5125 #[test]
5126 fn annotation_guidance_with_name_rejected() {
5127 let src = "-- allium: 1\ncontract C {\n @guidance Named\n -- text\n}";
5128 let r = parse(src);
5129 assert!(
5130 r.diagnostics.iter().any(|d| d.message.contains("does not take a name")),
5131 "expected guidance name error, got: {:?}",
5132 r.diagnostics
5133 );
5134 }
5135
5136 #[test]
5141 fn invariant_top_level_simple() {
5142 let src = r#"invariant PositiveBalance {
5143 this.balance > 0
5144}"#;
5145 let r = parse_ok(src);
5146 assert_eq!(r.diagnostics.len(), 0);
5147 let Decl::Invariant(inv) = &r.module.declarations[0] else {
5148 panic!("expected Invariant, got {:?}", r.module.declarations[0])
5149 };
5150 assert_eq!(inv.name.name, "PositiveBalance");
5151 }
5152
5153 #[test]
5154 fn invariant_top_level_for_quantifier() {
5155 let src = r#"invariant AllPositive {
5156 for item in items: item.value > 0
5157}"#;
5158 let r = parse_ok(src);
5159 assert_eq!(r.diagnostics.len(), 0);
5160 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
5161 assert!(matches!(inv.body, Expr::For { .. }));
5162 }
5163
5164 #[test]
5165 fn invariant_top_level_nested_for() {
5166 let src = r#"invariant NestedFor {
5167 for a in items: for b in a.children: b.valid = true
5168}"#;
5169 let r = parse_ok(src);
5170 assert_eq!(r.diagnostics.len(), 0);
5171 }
5172
5173 #[test]
5174 fn invariant_top_level_implies() {
5175 let src = r#"invariant ImpliesTest {
5176 this.active implies this.balance > 0
5177}"#;
5178 let r = parse_ok(src);
5179 assert_eq!(r.diagnostics.len(), 0);
5180 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
5181 assert!(matches!(inv.body, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
5182 }
5183
5184 #[test]
5185 fn invariant_top_level_let_binding() {
5186 let src = r#"invariant WithLet {
5187 let total = this.items.count()
5188 total > 0
5189}"#;
5190 let r = parse_ok(src);
5191 assert_eq!(r.diagnostics.len(), 0);
5192 }
5193
5194 #[test]
5195 fn invariant_top_level_collection_ops() {
5196 let src = r#"invariant CollectionOps {
5197 this.items where active = true
5198}"#;
5199 let r = parse_ok(src);
5200 assert_eq!(r.diagnostics.len(), 0);
5201 }
5202
5203 #[test]
5204 fn invariant_top_level_exists() {
5205 let src = r#"invariant ExistsCheck {
5206 exists this.primary_contact
5207}"#;
5208 let r = parse_ok(src);
5209 assert_eq!(r.diagnostics.len(), 0);
5210 }
5211
5212 #[test]
5213 fn invariant_top_level_not_exists() {
5214 let src = r#"invariant NotExistsCheck {
5215 not exists this.deleted_at
5216}"#;
5217 let r = parse_ok(src);
5218 assert_eq!(r.diagnostics.len(), 0);
5219 }
5220
5221 #[test]
5222 fn invariant_top_level_optional_navigation() {
5223 let src = r#"invariant OptionalNav {
5224 this.owner?.email ?? "none" != "none"
5225}"#;
5226 let r = parse_ok(src);
5227 assert_eq!(r.diagnostics.len(), 0);
5228 }
5229
5230 #[test]
5231 fn invariant_top_level_lowercase_rejected() {
5232 let src = "-- allium: 1\ninvariant bad { true }";
5233 let r = parse(src);
5234 assert!(
5235 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
5236 "expected uppercase error, got: {:?}",
5237 r.diagnostics
5238 );
5239 }
5240
5241 #[test]
5242 fn invariant_entity_level() {
5243 let src = r#"entity Account {
5244 balance: Decimal
5245 invariant NonNegative { this.balance >= 0 }
5246}"#;
5247 let r = parse_ok(src);
5248 assert_eq!(r.diagnostics.len(), 0);
5249 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5250 let BlockItemKind::InvariantBlock { name, body: _ } = &b.items[1].kind else {
5251 panic!("expected InvariantBlock, got {:?}", b.items[1].kind)
5252 };
5253 assert_eq!(name.name, "NonNegative");
5254 }
5255
5256 #[test]
5257 fn invariant_entity_level_this_ref() {
5258 let src = r#"entity Order {
5259 total: Decimal
5260 invariant PositiveTotal { this.total > 0 }
5261}"#;
5262 let r = parse_ok(src);
5263 assert_eq!(r.diagnostics.len(), 0);
5264 }
5265
5266 #[test]
5267 fn invariant_entity_level_implies() {
5268 let src = r#"entity Subscription {
5269 active: Boolean
5270 balance: Decimal
5271 invariant ActiveMeansPositive { this.active implies this.balance > 0 }
5272}"#;
5273 let r = parse_ok(src);
5274 assert_eq!(r.diagnostics.len(), 0);
5275 }
5276
5277 #[test]
5278 fn invariant_entity_level_lowercase_rejected() {
5279 let src = "-- allium: 1\nentity E { invariant bad { true } }";
5280 let r = parse(src);
5281 assert!(
5282 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
5283 "expected uppercase error, got: {:?}",
5284 r.diagnostics
5285 );
5286 }
5287
5288 #[test]
5289 fn invariant_colon_form_in_entity_migration() {
5290 let src = "-- allium: 1\nentity E {\n invariant: -- must be valid\n}";
5292 let r = parse(src);
5293 assert!(
5294 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
5295 "expected migration diagnostic, got: {:?}",
5296 r.diagnostics
5297 );
5298 }
5299
5300 #[test]
5301 fn invariant_top_level_colon_rejected() {
5302 let src = "-- allium: 1\ninvariant Bad: some text";
5304 let r = parse(src);
5305 assert!(
5306 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5307 "expected error for colon-delimited invariant at top level, got: {:?}",
5308 r.diagnostics
5309 );
5310 }
5311
5312 #[test]
5313 fn invariant_same_name_different_scopes() {
5314 let src = r#"invariant SameName { true }
5316entity E {
5317 invariant SameName { true }
5318}"#;
5319 let r = parse_ok(src);
5320 assert_eq!(r.diagnostics.len(), 0);
5321 }
5322
5323 #[test]
5328 fn config_qualified_reference() {
5329 let src = r#"config {
5331 param: Integer = core/config.max_batch_size
5332}"#;
5333 let r = parse_ok(src);
5334 assert_eq!(r.diagnostics.len(), 0);
5335 }
5336
5337 #[test]
5338 fn config_multiple_qualified_refs() {
5339 let src = r#"config {
5340 param_a: Integer = core/config.max_batch_size
5341 param_b: Duration = core/config.default_delay
5342}"#;
5343 let r = parse_ok(src);
5344 assert_eq!(r.diagnostics.len(), 0);
5345 }
5346
5347 #[test]
5348 fn config_qualified_ref_with_type() {
5349 let src = r#"config {
5350 publish_delay: Duration = core/config.default_delay
5351}"#;
5352 let r = parse_ok(src);
5353 assert_eq!(r.diagnostics.len(), 0);
5354 }
5355
5356 #[test]
5357 fn config_qualified_chain() {
5358 let src = r#"config {
5360 first: Integer = core/config.base
5361 second: Integer = first
5362}"#;
5363 let r = parse_ok(src);
5364 assert_eq!(r.diagnostics.len(), 0);
5365 }
5366
5367 #[test]
5368 fn config_renamed_param_with_qualified_ref() {
5369 let src = r#"config {
5370 my_timeout: Duration = core/config.base_timeout
5371}"#;
5372 let r = parse_ok(src);
5373 assert_eq!(r.diagnostics.len(), 0);
5374 }
5375
5376 #[test]
5381 fn config_default_arithmetic() {
5382 let src = r#"config {
5383 param: Integer = other_param + 1
5384}"#;
5385 let r = parse_ok(src);
5386 assert_eq!(r.diagnostics.len(), 0);
5387 }
5388
5389 #[test]
5390 fn config_default_qualified_arithmetic() {
5391 let src = r#"config {
5392 param: Duration = core/config.timeout * 2
5393}"#;
5394 let r = parse_ok(src);
5395 assert_eq!(r.diagnostics.len(), 0);
5396 }
5397
5398 #[test]
5399 fn config_default_parenthesised() {
5400 let src = r#"config {
5401 param: Integer = (base + 1) * factor
5402}"#;
5403 let r = parse_ok(src);
5404 assert_eq!(r.diagnostics.len(), 0);
5405 }
5406
5407 #[test]
5408 fn config_default_two_qualified_refs() {
5409 let src = r#"config {
5410 param: Duration = core/config.a + core/config.b
5411}"#;
5412 let r = parse_ok(src);
5413 assert_eq!(r.diagnostics.len(), 0);
5414 }
5415
5416 #[test]
5417 fn config_default_literal_only() {
5418 let src = r#"config {
5419 param: Integer = 5
5420}"#;
5421 let r = parse_ok(src);
5422 assert_eq!(r.diagnostics.len(), 0);
5423 }
5424
5425 #[test]
5426 fn config_default_decimal_literal() {
5427 let src = r#"config {
5428 param: Decimal = price * 1.5
5429}"#;
5430 let r = parse_ok(src);
5431 assert_eq!(r.diagnostics.len(), 0);
5432 }
5433
5434 #[test]
5435 fn config_default_mixed_operators() {
5436 let src = r#"config {
5437 param: Duration = timeout * 2 + 1.minute
5438}"#;
5439 let r = parse_ok(src);
5440 assert_eq!(r.diagnostics.len(), 0);
5441 }
5442
5443 #[test]
5444 fn config_default_operator_precedence() {
5445 let src = r#"config {
5447 param: Integer = a + b * c
5448}"#;
5449 let r = parse_ok(src);
5450 assert_eq!(r.diagnostics.len(), 0);
5451 }
5452
5453 #[test]
5458 fn version_2_accepted() {
5459 let r = parse("-- allium: 2\nentity User {}");
5460 assert_eq!(r.module.version, Some(2));
5461 assert_eq!(r.diagnostics.len(), 0);
5462 }
5463
5464 #[test]
5465 fn version_99_still_rejected() {
5466 let r = parse("-- allium: 99\nentity User {}");
5467 assert!(r.diagnostics.iter().any(|d|
5468 d.severity == Severity::Error && d.message.contains("unsupported")
5469 ));
5470 }
5471
5472 #[test]
5473 fn contract_typed_signature() {
5474 let src = r#"contract Codec {
5475 serialize: (value: Any) -> ByteArray
5476}"#;
5477 let r = parse_ok(src);
5478 assert_eq!(r.diagnostics.len(), 0);
5479 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5480 assert_eq!(b.kind, BlockKind::Contract);
5481 let BlockItemKind::Assignment { name, value } = &b.items[0].kind else { panic!() };
5482 assert_eq!(name.name, "serialize");
5483 assert!(matches!(value, Expr::ProjectionMap { .. }));
5484 }
5485
5486 #[test]
5487 fn contract_multi_param_signature() {
5488 let src = r#"contract Codec {
5489 serialize: (value: Any, format: String) -> ByteArray
5490}"#;
5491 let r = parse_ok(src);
5492 assert_eq!(r.diagnostics.len(), 0);
5493 }
5494
5495 #[test]
5496 fn comma_separated_entity_fields() {
5497 let src = "entity Point { x: Decimal, y: Decimal }";
5498 let r = parse_ok(src);
5499 assert_eq!(r.diagnostics.len(), 0);
5500 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5501 assert_eq!(b.items.len(), 2);
5502 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "x"));
5503 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "y"));
5504 }
5505
5506 #[test]
5507 fn comma_separated_value_fields() {
5508 let src = "value Coord { x: Integer, y: Integer }";
5509 let r = parse_ok(src);
5510 assert_eq!(r.diagnostics.len(), 0);
5511 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5512 assert_eq!(b.items.len(), 2);
5513 }
5514
5515 #[test]
5520 fn version_3_accepted() {
5521 let r = parse("-- allium: 3\nentity User {}");
5522 assert_eq!(r.module.version, Some(3));
5523 assert_eq!(r.diagnostics.len(), 0);
5524 }
5525
5526 #[test]
5527 fn transitions_block_basic() {
5528 let src = r#"-- allium: 3
5529entity Order {
5530 status: pending | confirmed | shipped | delivered | cancelled
5531
5532 transitions status {
5533 pending -> confirmed
5534 confirmed -> shipped
5535 shipped -> delivered
5536 pending -> cancelled
5537 confirmed -> cancelled
5538 terminal: delivered, cancelled
5539 }
5540}"#;
5541 let r = parse(src);
5542 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5543 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5544 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5546 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5547 };
5548 assert_eq!(graph.field.name, "status");
5549 assert_eq!(graph.edges.len(), 5);
5550 assert_eq!(graph.edges[0].from.name, "pending");
5551 assert_eq!(graph.edges[0].to.name, "confirmed");
5552 assert_eq!(graph.terminal.len(), 2);
5553 assert_eq!(graph.terminal[0].name, "delivered");
5554 assert_eq!(graph.terminal[1].name, "cancelled");
5555 }
5556
5557 #[test]
5558 fn transitions_block_no_terminal() {
5559 let src = r#"-- allium: 3
5560entity Task {
5561 status: open | closed
5562 transitions status {
5563 open -> closed
5564 }
5565}"#;
5566 let r = parse(src);
5567 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5568 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5569 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5570 panic!("expected TransitionsBlock")
5571 };
5572 assert_eq!(graph.edges.len(), 1);
5573 assert!(graph.terminal.is_empty());
5574 }
5575
5576 #[test]
5577 fn produces_emits_migration_warning() {
5578 let src = r#"-- allium: 3
5579rule ShipOrder {
5580 when: ShipOrder(order, tracking)
5581 requires: order.status = picking
5582 produces: tracking_number, shipped_at
5583 ensures: order.status = shipped
5584}"#;
5585 let r = parse(src);
5586 let warnings: Vec<_> = r.diagnostics.iter()
5587 .filter(|d| d.severity == Severity::Warning)
5588 .collect();
5589 assert!(
5590 warnings.iter().any(|d| d.message.contains("`produces:` clauses are removed")),
5591 "expected migration warning for produces, got: {:?}", warnings
5592 );
5593 }
5594
5595 #[test]
5596 fn consumes_emits_migration_warning() {
5597 let src = r#"-- allium: 3
5598rule ReadOrder {
5599 when: Check(order)
5600 consumes: warehouse_assignment
5601 ensures: order.verified = true
5602}"#;
5603 let r = parse(src);
5604 let warnings: Vec<_> = r.diagnostics.iter()
5605 .filter(|d| d.severity == Severity::Warning)
5606 .collect();
5607 assert!(
5608 warnings.iter().any(|d| d.message.contains("`consumes:` clauses are removed")),
5609 "expected migration warning for consumes, got: {:?}", warnings
5610 );
5611 }
5612
5613 #[test]
5614 fn when_clause_on_field() {
5615 let src = r#"-- allium: 3
5616entity Order {
5617 status: pending | shipped | delivered
5618 tracking_number: String when status = shipped | delivered
5619 transitions status {
5620 pending -> shipped
5621 shipped -> delivered
5622 terminal: delivered
5623 }
5624}"#;
5625 let r = parse(src);
5626 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5627 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5628 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5629 let field_with_when = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }));
5630 assert!(field_with_when.is_some(), "expected FieldWithWhen item");
5631 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &field_with_when.unwrap().kind {
5632 assert_eq!(name.name, "tracking_number");
5633 assert_eq!(when_clause.status_field.name, "status");
5634 assert_eq!(when_clause.qualifying_states.len(), 2);
5635 assert_eq!(when_clause.qualifying_states[0].name, "shipped");
5636 assert_eq!(when_clause.qualifying_states[1].name, "delivered");
5637 }
5638 }
5639
5640 #[test]
5641 fn when_clause_single_state() {
5642 let src = r#"-- allium: 3
5643entity Order {
5644 status: active | cancelled
5645 cancelled_at: Timestamp when status = cancelled
5646 transitions status {
5647 active -> cancelled
5648 terminal: cancelled
5649 }
5650}"#;
5651 let r = parse(src);
5652 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5653 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5654 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5655 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &b.items[1].kind {
5656 assert_eq!(name.name, "cancelled_at");
5657 assert_eq!(when_clause.qualifying_states.len(), 1);
5658 assert_eq!(when_clause.qualifying_states[0].name, "cancelled");
5659 } else {
5660 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5661 }
5662 }
5663
5664 #[test]
5665 fn when_clause_with_optional() {
5666 let src = r#"-- allium: 3
5667entity Order {
5668 status: active | cancelled
5669 notes: String? when status = cancelled
5670 transitions status {
5671 active -> cancelled
5672 terminal: cancelled
5673 }
5674}"#;
5675 let r = parse(src);
5676 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5677 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5678 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5679 if let BlockItemKind::FieldWithWhen { name, value, when_clause } = &b.items[1].kind {
5680 assert_eq!(name.name, "notes");
5681 assert!(matches!(value, Expr::TypeOptional { .. }), "expected TypeOptional");
5682 assert_eq!(when_clause.qualifying_states.len(), 1);
5683 } else {
5684 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5685 }
5686 }
5687
5688 #[test]
5689 fn transitions_in_json_output() {
5690 let src = r#"-- allium: 3
5691entity Order {
5692 status: pending | done
5693 transitions status {
5694 pending -> done
5695 terminal: done
5696 }
5697}"#;
5698 let r = parse(src);
5699 let json = serde_json::to_string(&r.module).unwrap();
5700 assert!(json.contains("TransitionsBlock"), "JSON should contain TransitionsBlock: {}", json);
5701 assert!(json.contains("pending"), "JSON should contain 'pending'");
5702 }
5703
5704 #[test]
5705 fn transitions_block_with_commas() {
5706 let src = r#"-- allium: 3
5707entity Order {
5708 status: a | b | c
5709 transitions status {
5710 a -> b,
5711 b -> c,
5712 terminal: c,
5713 }
5714}"#;
5715 let r = parse(src);
5716 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5717 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5718 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5719 panic!("expected TransitionsBlock")
5720 };
5721 assert_eq!(graph.edges.len(), 2);
5722 assert_eq!(graph.terminal.len(), 1);
5723 }
5724
5725 #[test]
5726 fn v3_full_entity_with_transitions_and_rule() {
5727 let src = r#"-- allium: 3
5728entity Order {
5729 status: pending | shipped | delivered
5730 tracking: String when status = shipped | delivered
5731 shipped_at: Timestamp when status = shipped | delivered
5732
5733 transitions status {
5734 pending -> shipped
5735 shipped -> delivered
5736 terminal: delivered
5737 }
5738}
5739
5740rule ShipOrder {
5741 when: ShipOrder(order, tracking)
5742 requires: order.status = pending
5743 ensures:
5744 order.status = shipped
5745 order.tracking = tracking
5746 order.shipped_at = now
5747}"#;
5748 let r = parse(src);
5749 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5750 assert_eq!(r.module.declarations.len(), 2);
5751 }
5752
5753 #[test]
5758 fn transitions_empty_block() {
5759 let src = "-- allium: 3\nentity E {\n status: a | b\n transitions status {}\n}";
5760 let r = parse(src);
5761 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5762 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5763 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5764 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5765 };
5766 assert!(graph.edges.is_empty());
5767 assert!(graph.terminal.is_empty());
5768 }
5769
5770 #[test]
5771 fn transitions_terminal_only() {
5772 let src = r#"-- allium: 3
5773entity E {
5774 status: done
5775 transitions status {
5776 terminal: done
5777 }
5778}"#;
5779 let r = parse(src);
5780 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5781 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5782 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5783 assert!(graph.edges.is_empty());
5784 assert_eq!(graph.terminal.len(), 1);
5785 assert_eq!(graph.terminal[0].name, "done");
5786 }
5787
5788 #[test]
5789 fn transitions_terminal_before_edges() {
5790 let src = r#"-- allium: 3
5791entity E {
5792 status: a | b | c
5793 transitions status {
5794 terminal: c
5795 a -> b
5796 b -> c
5797 }
5798}"#;
5799 let r = parse(src);
5800 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5801 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5802 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5803 assert_eq!(graph.edges.len(), 2);
5804 assert_eq!(graph.terminal.len(), 1);
5805 assert_eq!(graph.terminal[0].name, "c");
5806 }
5807
5808 #[test]
5809 fn transitions_self_loop() {
5810 let src = r#"-- allium: 3
5811entity E {
5812 status: running | stopped
5813 transitions status {
5814 running -> running
5815 running -> stopped
5816 terminal: stopped
5817 }
5818}"#;
5819 let r = parse(src);
5820 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5821 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5822 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5823 assert_eq!(graph.edges.len(), 2);
5824 assert_eq!(graph.edges[0].from.name, "running");
5825 assert_eq!(graph.edges[0].to.name, "running");
5826 }
5827
5828 #[test]
5829 fn transitions_single_edge() {
5830 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s { a -> b }\n}";
5831 let r = parse(src);
5832 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5833 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5834 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5835 assert_eq!(graph.field.name, "s");
5836 assert_eq!(graph.edges.len(), 1);
5837 }
5838
5839 #[test]
5840 fn transitions_multiple_terminal_values() {
5841 let src = r#"-- allium: 3
5842entity E {
5843 status: a | b | c | d | e
5844 transitions status {
5845 a -> b
5846 b -> c
5847 terminal: c, d, e
5848 }
5849}"#;
5850 let r = parse(src);
5851 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5852 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5853 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5854 assert_eq!(graph.terminal.len(), 3);
5855 }
5856
5857 #[test]
5858 fn transitions_trailing_comma_in_terminal() {
5859 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n terminal: b,\n }\n}";
5860 let r = parse(src);
5861 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5862 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5863 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5864 assert_eq!(graph.terminal.len(), 1);
5865 }
5866
5867 #[test]
5868 fn transitions_among_other_entity_items() {
5869 let src = r#"-- allium: 3
5871entity Order {
5872 status: pending | shipped | delivered
5873 customer: Customer
5874 tracking: String?
5875
5876 transitions status {
5877 pending -> shipped
5878 shipped -> delivered
5879 terminal: delivered
5880 }
5881
5882 active_items: items where status = active
5883 invariant Positive { this.total > 0 }
5884}"#;
5885 let r = parse(src);
5886 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5887 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5888 assert_eq!(b.items.len(), 6);
5890 assert!(matches!(&b.items[3].kind, BlockItemKind::TransitionsBlock(_)));
5891 assert!(matches!(&b.items[5].kind, BlockItemKind::InvariantBlock { .. }));
5892 }
5893
5894 #[test]
5895 fn transitions_error_recovery_missing_arrow() {
5896 let src = r#"-- allium: 3
5897entity E {
5898 status: a | b | c
5899 transitions status {
5900 a b
5901 b -> c
5902 }
5903}"#;
5904 let r = parse(src);
5905 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5907 "expected error for missing arrow, got: {:?}", r.diagnostics);
5908 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5910 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5911 panic!("expected TransitionsBlock")
5912 };
5913 assert_eq!(graph.edges.len(), 1, "should recover and parse second edge");
5914 assert_eq!(graph.edges[0].from.name, "b");
5915 }
5916
5917 #[test]
5918 fn transitions_field_name_preserved() {
5919 let src = "-- allium: 3\nentity E {\n phase: x | y\n transitions phase { x -> y }\n}";
5920 let r = parse(src);
5921 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5922 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5923 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5924 assert_eq!(graph.field.name, "phase");
5925 }
5926
5927 #[test]
5928 fn transitions_diamond_topology() {
5929 let src = r#"-- allium: 3
5931entity E {
5932 status: new | path_a | path_b | done
5933 transitions status {
5934 new -> path_a
5935 new -> path_b
5936 path_a -> done
5937 path_b -> done
5938 terminal: done
5939 }
5940}"#;
5941 let r = parse(src);
5942 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5943 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5944 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5945 assert_eq!(graph.edges.len(), 4);
5946 }
5947
5948 #[test]
5949 fn transitions_edge_span_is_from_to_range() {
5950 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n }\n}";
5951 let r = parse(src);
5952 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5953 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5954 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5955 let edge = &graph.edges[0];
5956 assert!(edge.span.start <= edge.from.span.start);
5958 assert!(edge.span.end >= edge.to.span.end);
5959 }
5960
5961 #[test]
5966 fn when_clause_multiple_fields() {
5967 let src = r#"-- allium: 3
5968entity Order {
5969 status: pending | shipped | delivered
5970 tracking: String when status = shipped | delivered
5971 shipped_at: Timestamp when status = shipped | delivered
5972 delivered_at: Timestamp when status = delivered
5973 transitions status {
5974 pending -> shipped
5975 shipped -> delivered
5976 terminal: delivered
5977 }
5978}"#;
5979 let r = parse(src);
5980 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5981 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5982 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5983 let when_count = b.items.iter()
5984 .filter(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }))
5985 .count();
5986 assert_eq!(when_count, 3);
5987 }
5988
5989 #[test]
5990 fn legacy_produces_consumes_skipped_with_warnings() {
5991 let src = r#"-- allium: 3
5992rule R {
5993 when: Go(x)
5994 produces: field_a
5995 consumes: field_b
5996 ensures: x.done = true
5997}"#;
5998 let r = parse(src);
5999 let warnings: Vec<_> = r.diagnostics.iter()
6000 .filter(|d| d.severity == Severity::Warning)
6001 .collect();
6002 assert!(warnings.len() >= 2, "expected at least 2 migration warnings, got {}", warnings.len());
6003 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6005 assert!(
6006 !b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. })),
6007 "legacy produces/consumes should not become FieldWithWhen"
6008 );
6009 }
6010
6011 #[test]
6016 fn v3_entity_with_transitions_and_invariant() {
6017 let src = r#"-- allium: 3
6018entity Account {
6019 status: open | frozen | closed
6020 balance: Decimal
6021
6022 transitions status {
6023 open -> frozen
6024 frozen -> open
6025 open -> closed
6026 frozen -> closed
6027 terminal: closed
6028 }
6029
6030 invariant NonNegative { this.balance >= 0 }
6031}"#;
6032 let r = parse(src);
6033 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6034 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6035 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::TransitionsBlock(_))));
6036 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. })));
6037 }
6038
6039 #[test]
6040 fn v3_rule_with_multiple_ensures() {
6041 let src = r#"-- allium: 3
6042rule CompleteOrder {
6043 when: Complete(order)
6044 requires: order.status = shipped
6045 ensures: order.status = delivered
6046 ensures: order.completed_at = now
6047 ensures: order.receipt_number = generate_receipt()
6048}"#;
6049 let r = parse(src);
6050 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6051 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6052 let ensures_count = b.items.iter()
6053 .filter(|i| matches!(&i.kind, BlockItemKind::Clause { keyword, .. } if keyword == "ensures"))
6054 .count();
6055 assert_eq!(ensures_count, 3);
6056 }
6057
6058 #[test]
6059 fn v3_rule_with_if_block() {
6060 let src = r#"-- allium: 3
6061rule Cancel {
6062 when: Cancel(order, reason)
6063 requires: order.status != delivered
6064 ensures:
6065 order.status = cancelled
6066 order.cancelled_at = now
6067 if reason = customer_request:
6068 order.cancelled_by = order.customer
6069}"#;
6070 let r = parse(src);
6071 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6072 }
6073
6074 #[test]
6075 fn v3_complete_lifecycle_spec() {
6076 let src = r#"-- allium: 3
6077
6078entity Subscription {
6079 status: trial | active | past_due | cancelled
6080 started_at: Timestamp when status = active | past_due | cancelled
6081 cancelled_at: Timestamp when status = cancelled
6082 balance: Decimal
6083
6084 transitions status {
6085 trial -> active
6086 active -> past_due
6087 past_due -> active
6088 active -> cancelled
6089 past_due -> cancelled
6090 terminal: cancelled
6091 }
6092
6093 invariant NonNegative { this.balance >= 0 }
6094}
6095
6096config {
6097 trial_period: Duration = 14.days
6098}
6099
6100rule ActivateSubscription {
6101 when: Activate(sub)
6102 requires: sub.status = trial
6103 ensures:
6104 sub.status = active
6105 sub.started_at = now
6106}
6107
6108rule CancelSubscription {
6109 when: Cancel(sub)
6110 requires: sub.status != cancelled
6111 ensures:
6112 sub.status = cancelled
6113 sub.cancelled_at = now
6114}
6115
6116invariant AllCancelledHaveTimestamp {
6117 for sub in Subscriptions where status = cancelled:
6118 sub.cancelled_at != null
6119}
6120
6121surface SubscriptionDashboard {
6122 facing user: User
6123 context sub: Subscription where owner = user
6124 exposes:
6125 sub.status
6126 sub.balance
6127 provides:
6128 Cancel(sub) when sub.status != cancelled
6129}
6130"#;
6131 let r = parse(src);
6132 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6133 assert_eq!(r.module.declarations.len(), 6);
6135 }
6136
6137 #[test]
6138 fn v3_produces_consumes_are_field_names_in_entities() {
6139 let src = r#"-- allium: 3
6141entity Factory {
6142 produces: widget_a
6143 consumes: raw_material
6144}"#;
6145 let r = parse(src);
6146 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6147 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6148 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "produces"));
6149 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "consumes"));
6150 }
6151
6152 #[test]
6153 fn v3_legacy_produces_consumes_emit_warnings_in_rules() {
6154 let src = r#"-- allium: 3
6155rule Ship {
6156 when: Ship(order)
6157 produces: tracking_number
6158 consumes: warehouse
6159 ensures: order.status = shipped
6160}"#;
6161 let r = parse(src);
6162 let warnings: Vec<_> = r.diagnostics.iter()
6163 .filter(|d| d.severity == Severity::Warning)
6164 .collect();
6165 assert!(warnings.len() >= 2, "expected migration warnings, got {:?}", warnings);
6166 }
6167
6168 #[test]
6169 fn v3_version_preserved_in_module() {
6170 let src = "-- allium: 3\nentity E {}";
6171 let r = parse(src);
6172 assert_eq!(r.module.version, Some(3));
6173 }
6174
6175 #[test]
6176 fn v3_version_4_still_rejected() {
6177 let src = "-- allium: 4\nentity E {}";
6178 let r = parse(src);
6179 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error
6180 && d.message.contains("unsupported")));
6181 }
6182
6183 #[test]
6188 fn backtick_in_named_enum() {
6189 let src = "-- allium: 3\nenum Locale { en | fr | `de-CH-1996` | `no-cache` }";
6190 let r = parse(src);
6191 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6192 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6193 assert_eq!(b.items.len(), 4);
6194 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[0].kind else { panic!() };
6196 assert_eq!(name.name, "en");
6197 assert!(!backtick_quoted);
6198 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[2].kind else { panic!() };
6200 assert_eq!(name.name, "de-CH-1996");
6201 assert!(backtick_quoted);
6202 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[3].kind else { panic!() };
6204 assert_eq!(name.name, "no-cache");
6205 assert!(backtick_quoted);
6206 }
6207
6208 #[test]
6209 fn backtick_in_inline_enum() {
6210 let src = "-- allium: 3\nentity E { cache: `no-cache` | `no-store` | `public` }";
6211 let r = parse(src);
6212 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6213 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6214 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
6215 assert!(matches!(value, Expr::Pipe { .. }));
6217 }
6218
6219 #[test]
6220 fn backtick_in_comparison() {
6221 let src = r#"-- allium: 3
6222rule R {
6223 when: Check(item)
6224 requires: item.locale = `de-CH-1996`
6225 ensures: Done()
6226}"#;
6227 let r = parse(src);
6228 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6229 }
6230
6231 #[test]
6232 fn backtick_mixed_with_unquoted() {
6233 let src = "-- allium: 3\nenum CacheDirective { `no-cache` | `no-store` | public | private }";
6234 let r = parse(src);
6235 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6236 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6237 assert_eq!(b.items.len(), 4);
6238 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[0].kind else { panic!() };
6239 assert!(backtick_quoted);
6240 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[2].kind else { panic!() };
6241 assert!(!backtick_quoted);
6242 }
6243
6244 #[test]
6249 fn v3_lifecycle_fixture() {
6250 let src = include_str!("../tests/fixtures/v3-lifecycle.allium");
6251 let r = parse(src);
6252 let errors: Vec<_> = r.diagnostics.iter()
6253 .filter(|d| d.severity == Severity::Error)
6254 .collect();
6255 assert_eq!(
6256 errors.len(),
6257 0,
6258 "expected no errors in v3 lifecycle fixture, got: {:?}",
6259 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
6260 );
6261 }
6262}