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_expr(0)?;
610 Some(DeferredDecl {
611 span: start.merge(path.span()),
612 path,
613 })
614 }
615
616 fn parse_open_question_decl(&mut self) -> Option<OpenQuestionDecl> {
619 let start = self.expect(TokenKind::Open)?.span;
620 self.expect(TokenKind::Question)?;
621 let text = self.parse_string()?;
622 Some(OpenQuestionDecl {
623 span: start.merge(text.span),
624 text,
625 })
626 }
627
628 fn parse_contract_decl(&mut self) -> Option<BlockDecl> {
631 let start = self.advance().span; let name = self.parse_ident_in("contract name")?;
633
634 if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
636 self.diagnostics.push(Diagnostic::error(
637 name.span,
638 "contract name must start with an uppercase letter",
639 ));
640 }
641
642 if self.at(TokenKind::Colon) {
644 self.error(
645 self.peek().span,
646 "contract body must use braces { }, not a colon",
647 );
648 return None;
649 }
650
651 self.expect(TokenKind::LBrace)?;
652 let items = self.parse_block_items(BlockKind::Contract);
653 let end = self.expect(TokenKind::RBrace)?.span;
654 Some(BlockDecl {
655 span: start.merge(end),
656 kind: BlockKind::Contract,
657 name: Some(name),
658 items,
659 })
660 }
661
662 fn parse_invariant_decl(&mut self) -> Option<InvariantDecl> {
665 let start = self.advance().span; let name = self.parse_ident_in("invariant name")?;
667
668 if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
670 self.diagnostics.push(Diagnostic::error(
671 name.span,
672 "invariant name must start with an uppercase letter",
673 ));
674 }
675
676 self.expect(TokenKind::LBrace)?;
677 let body = self.parse_invariant_body()?;
678 let end = self.expect(TokenKind::RBrace)?.span;
679 Some(InvariantDecl {
680 span: start.merge(end),
681 name,
682 body,
683 })
684 }
685
686 fn parse_invariant_body(&mut self) -> Option<Expr> {
689 let start = self.peek().span;
690 let mut items = Vec::new();
691
692 while !self.at(TokenKind::RBrace) && !self.at_eof() {
693 if self.at(TokenKind::Let) {
694 let let_start = self.advance().span;
695 let name = self.parse_ident_in("binding name")?;
696 self.expect(TokenKind::Eq)?;
697 let value = self.parse_expr(0)?;
698 items.push(Expr::LetExpr {
699 span: let_start.merge(value.span()),
700 name,
701 value: Box::new(value),
702 });
703 } else if let Some(expr) = self.parse_expr(0) {
704 items.push(expr);
705 } else {
706 self.advance();
707 break;
708 }
709 }
710
711 if items.len() == 1 {
712 Some(items.pop().unwrap())
713 } else {
714 let end = items.last().map(|e| e.span()).unwrap_or(start);
715 Some(Expr::Block {
716 span: start.merge(end),
717 items,
718 })
719 }
720 }
721}
722
723impl<'s> Parser<'s> {
728 fn parse_block_items(&mut self, block_kind: BlockKind) -> Vec<BlockItem> {
729 let mut items = Vec::new();
730 while !self.at(TokenKind::RBrace) && !self.at_eof() {
731 if let Some(item) = self.parse_block_item(block_kind) {
732 items.push(item);
733 self.eat(TokenKind::Comma);
734 } else {
735 self.advance();
737 }
738 }
739 items
740 }
741
742 fn parse_block_item(&mut self, block_kind: BlockKind) -> Option<BlockItem> {
743 let start = self.peek().span;
744
745 if self.at(TokenKind::Let) {
747 return self.parse_let_item(start);
748 }
749
750 if self.at(TokenKind::For) {
752 return self.parse_for_block_item(start);
753 }
754
755 if self.at(TokenKind::If) {
757 return self.parse_if_block_item(start);
758 }
759
760 if self.at(TokenKind::At) {
762 return self.parse_annotation(start);
763 }
764
765 if self.at(TokenKind::Invariant) && self.peek_at(1).kind.is_word()
767 && self.peek_at(2).kind != TokenKind::Colon
768 {
769 return self.parse_invariant_block_item(start);
770 }
771
772 if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
774 self.advance(); self.advance(); let text = self.parse_string()?;
777 return Some(BlockItem {
778 span: start.merge(text.span),
779 kind: BlockItemKind::OpenQuestion { text },
780 });
781 }
782
783 if self.at(TokenKind::Transitions)
785 && self.peek_at(1).kind.is_word()
786 && self.peek_at(2).kind == TokenKind::LBrace
787 {
788 return self.parse_transitions_block(start);
789 }
790
791 if self.peek_kind() == TokenKind::Ident {
793 let word = self.text(self.peek().span);
794 if (word == "guidance" || word == "guarantee")
795 && self.peek_at(1).kind == TokenKind::Colon
796 {
797 let kw = word.to_string();
798 self.error(
799 self.peek().span,
800 format!(
801 "`{kw}:` syntax was replaced by `@{kw}`. Use `@{kw}` followed by indented comment lines."
802 ),
803 );
804 }
806 }
807
808 if self.at(TokenKind::Invariant) && self.peek_at(1).kind == TokenKind::Colon {
810 self.error(
811 self.peek().span,
812 "`invariant:` syntax was replaced by `@invariant`. Use `@invariant Name` followed by indented comment lines.",
813 );
814 }
816
817 if self.peek_kind().is_word() {
820 if self.text(self.peek().span) == "contracts"
822 && self.peek_at(1).kind == TokenKind::Colon
823 {
824 return self.parse_contracts_clause(start);
825 }
826
827 if is_binding_clause_keyword(self.text(self.peek().span))
830 && self.peek_at(1).kind.is_word()
831 && self.peek_at(2).kind == TokenKind::Colon
832 {
833 return self.parse_binding_clause_item(start);
834 }
835
836 if self.peek_at(1).kind == TokenKind::Dot
838 && self.peek_at(2).kind.is_word()
839 && self.peek_at(3).kind == TokenKind::Colon
840 {
841 return self.parse_path_assignment_item(start);
842 }
843
844 if self.peek_at(1).kind == TokenKind::LParen {
846 return self.parse_param_or_clause_item(start);
847 }
848
849 if block_kind == BlockKind::Rule
851 && (self.at(TokenKind::Produces) || self.at(TokenKind::Consumes))
852 && self.peek_at(1).kind == TokenKind::Colon
853 {
854 return self.parse_legacy_field_list_clause(start);
855 }
856
857 if self.peek_at(1).kind == TokenKind::Colon {
859 return self.parse_assign_or_clause_item(start);
860 }
861 }
862
863 if token_is_clause_keyword(self.peek_kind()) && self.peek_at(1).kind == TokenKind::Colon {
865 return self.parse_assign_or_clause_item(start);
866 }
867
868 self.error(
869 start,
870 format!(
871 "expected block item (name: value, let name = value, when:/requires:/ensures: clause, \
872 for ... in ...:, or open question), found {}",
873 self.peek_kind(),
874 ),
875 );
876 None
877 }
878
879 fn parse_transitions_block(&mut self, start: Span) -> Option<BlockItem> {
881 self.advance(); let field = self.parse_ident_in("transition field name")?;
883 self.expect(TokenKind::LBrace)?;
884
885 let mut edges = Vec::new();
886 let mut terminal = Vec::new();
887
888 while !self.at(TokenKind::RBrace) && !self.at_eof() {
889 if self.at(TokenKind::Terminal) && self.peek_at(1).kind == TokenKind::Colon {
891 self.advance(); self.advance(); loop {
894 let state = self.parse_ident_in("terminal state")?;
895 terminal.push(state);
896 if self.eat(TokenKind::Comma).is_none() {
897 break;
898 }
899 if self.at(TokenKind::RBrace) {
901 break;
902 }
903 }
904 continue;
905 }
906
907 let from = self.parse_ident_in("source state")?;
909 if self.expect(TokenKind::ThinArrow).is_none() {
910 while !self.at(TokenKind::RBrace) && !self.at_eof() {
912 let cur_line = self.line_of(self.peek().span);
913 self.advance();
914 if self.line_of(self.peek().span) != cur_line {
915 break;
916 }
917 }
918 continue;
919 }
920 let to = self.parse_ident_in("target state")?;
921 let edge_span = from.span.merge(to.span);
922 edges.push(TransitionEdge {
923 span: edge_span,
924 from,
925 to,
926 });
927
928 self.eat(TokenKind::Comma);
930 }
931
932 let end = self.expect(TokenKind::RBrace)?.span;
933
934 Some(BlockItem {
935 span: start.merge(end),
936 kind: BlockItemKind::TransitionsBlock(TransitionGraph {
937 span: start.merge(end),
938 field,
939 edges,
940 terminal,
941 }),
942 })
943 }
944
945 fn parse_legacy_field_list_clause(&mut self, start: Span) -> Option<BlockItem> {
948 let keyword_tok = self.advance(); let keyword = self.text(keyword_tok.span).to_string();
950 self.advance(); let clause_line = self.line_of(start);
954 loop {
955 if self.at(TokenKind::RBrace) || self.at_eof() {
956 break;
957 }
958 if self.line_of(self.peek().span) > clause_line {
959 break;
960 }
961 self.advance();
962 }
963
964 self.diagnostics.push(Diagnostic::warning(
965 start.merge(keyword_tok.span),
966 format!(
967 "`{keyword}:` clauses are removed in v3; use `when` clauses on entity fields instead"
968 ),
969 ));
970
971 self.parse_block_item(BlockKind::Rule)
973 }
974
975 fn parse_let_item(&mut self, start: Span) -> Option<BlockItem> {
976 self.advance(); let name = self.parse_ident_in("binding name")?;
978 self.expect(TokenKind::Eq)?;
979 let value = self.parse_clause_value(start)?;
980 Some(BlockItem {
981 span: start.merge(value.span()),
982 kind: BlockItemKind::Let { name, value },
983 })
984 }
985
986 fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
989 let keyword_tok = self.advance(); let keyword = self.text(keyword_tok.span).to_string();
991 let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
992 self.advance(); let type_expr = self.parse_clause_value(start)?;
994 let value_span = type_expr.span();
995 let value = Expr::Binding {
996 span: binding_name.span.merge(value_span),
997 name: binding_name,
998 value: Box::new(type_expr),
999 };
1000 Some(BlockItem {
1001 span: start.merge(value_span),
1002 kind: BlockItemKind::Clause { keyword, value },
1003 })
1004 }
1005
1006 fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
1009 self.advance(); let binding = self.parse_for_binding()?;
1011 self.expect(TokenKind::In)?;
1012
1013 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1014
1015 let filter = if self.eat(TokenKind::Where).is_some() {
1016 Some(self.parse_expr(0)?)
1019 } else {
1020 None
1021 };
1022
1023 self.expect(TokenKind::Colon)?;
1024
1025 let for_line = self.line_of(start);
1027 let next_line = self.line_of(self.peek().span);
1028
1029 let items = if next_line > for_line {
1030 let base_col = self.col_of(self.peek().span);
1031 self.parse_indented_block_items(base_col)
1032 } else {
1033 let mut items = Vec::new();
1035 if let Some(item) = self.parse_block_item(BlockKind::Entity) {
1036 items.push(item);
1037 }
1038 items
1039 };
1040
1041 let end = items
1042 .last()
1043 .map(|i| i.span)
1044 .unwrap_or(start);
1045
1046 Some(BlockItem {
1047 span: start.merge(end),
1048 kind: BlockItemKind::ForBlock {
1049 binding,
1050 collection,
1051 filter,
1052 items,
1053 },
1054 })
1055 }
1056
1057 fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
1059 let mut items = Vec::new();
1060 while !self.at_eof()
1061 && !self.at(TokenKind::RBrace)
1062 && self.col_of(self.peek().span) >= base_col
1063 {
1064 if let Some(item) = self.parse_block_item(BlockKind::Entity) {
1065 items.push(item);
1066 } else {
1067 self.advance();
1068 break;
1069 }
1070 }
1071 items
1072 }
1073
1074 fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
1076 self.advance(); let mut branches = Vec::new();
1078
1079 let condition = self.parse_expr(0)?;
1081 self.expect(TokenKind::Colon)?;
1082 let if_line = self.line_of(start);
1083 let items = self.parse_if_block_body(if_line);
1084 branches.push(CondBlockBranch {
1085 span: start.merge(items.last().map(|i| i.span).unwrap_or(start)),
1086 condition,
1087 items,
1088 });
1089
1090 let mut else_items = None;
1092 while self.at(TokenKind::Else) {
1093 let else_tok = self.advance();
1094 if self.at(TokenKind::If) {
1095 let if_start = self.advance().span;
1096 let cond = self.parse_expr(0)?;
1097 self.expect(TokenKind::Colon)?;
1098 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
1099 branches.push(CondBlockBranch {
1100 span: if_start.merge(body_items.last().map(|i| i.span).unwrap_or(if_start)),
1101 condition: cond,
1102 items: body_items,
1103 });
1104 } else {
1105 self.expect(TokenKind::Colon)?;
1106 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
1107 else_items = Some(body_items);
1108 break;
1109 }
1110 }
1111
1112 let end = else_items
1113 .as_ref()
1114 .and_then(|items| items.last().map(|i| i.span))
1115 .or_else(|| branches.last().and_then(|b| b.items.last().map(|i| i.span)))
1116 .unwrap_or(start);
1117
1118 Some(BlockItem {
1119 span: start.merge(end),
1120 kind: BlockItemKind::IfBlock {
1121 branches,
1122 else_items,
1123 },
1124 })
1125 }
1126
1127 fn parse_if_block_body(&mut self, keyword_line: u32) -> Vec<BlockItem> {
1129 let next_line = self.line_of(self.peek().span);
1130 if next_line > keyword_line {
1131 let base_col = self.col_of(self.peek().span);
1132 self.parse_indented_block_items(base_col)
1133 } else {
1134 let mut items = Vec::new();
1136 if let Some(item) = self.parse_block_item(BlockKind::Entity) {
1137 items.push(item);
1138 }
1139 items
1140 }
1141 }
1142
1143 fn parse_contracts_clause(&mut self, start: Span) -> Option<BlockItem> {
1145 self.advance(); self.advance(); let contracts_col = self.col_of(start);
1149 let mut entries = Vec::new();
1150
1151 while !self.at_eof()
1152 && !self.at(TokenKind::RBrace)
1153 && self.col_of(self.peek().span) > contracts_col
1154 {
1155 if !self.peek_kind().is_word() {
1156 break;
1157 }
1158
1159 let entry_start = self.peek().span;
1160 let direction_tok = self.advance();
1161 let direction_text = self.text(direction_tok.span);
1162
1163 let direction = match direction_text {
1164 "demands" => ContractDirection::Demands,
1165 "fulfils" => ContractDirection::Fulfils,
1166 other => {
1167 self.error(
1168 direction_tok.span,
1169 format!(
1170 "Unknown direction '{other}' in contracts clause. Use `demands` or `fulfils`."
1171 ),
1172 );
1173 if self.peek_kind().is_word() {
1175 self.advance();
1176 }
1177 continue;
1178 }
1179 };
1180
1181 let name = self.parse_ident_in("contract name")?;
1182
1183 if self.at(TokenKind::LBrace) {
1185 self.error(
1186 self.peek().span,
1187 "Inline contract blocks are not allowed in `contracts:`. Declare the contract at module level.",
1188 );
1189 return None;
1190 }
1191
1192 let end = name.span;
1193 entries.push(ContractBinding {
1194 direction,
1195 name,
1196 span: entry_start.merge(end),
1197 });
1198 }
1199
1200 if entries.is_empty() {
1201 self.error(
1202 start,
1203 "Empty `contracts:` clause. Add at least one `demands` or `fulfils` entry.",
1204 );
1205 return None;
1206 }
1207
1208 let end = entries.last().unwrap().span;
1209 Some(BlockItem {
1210 span: start.merge(end),
1211 kind: BlockItemKind::ContractsClause { entries },
1212 })
1213 }
1214
1215 fn parse_annotation(&mut self, start: Span) -> Option<BlockItem> {
1217 let at_tok = self.advance(); let at_col = self.col_of(at_tok.span);
1219
1220 if !self.peek_kind().is_word() {
1221 self.error(
1222 self.peek().span,
1223 format!("expected annotation keyword after `@`, found {}", self.peek_kind()),
1224 );
1225 return None;
1226 }
1227
1228 let keyword_tok = self.advance();
1229 let keyword_text = self.text(keyword_tok.span);
1230
1231 let kind = match keyword_text {
1232 "invariant" => AnnotationKind::Invariant,
1233 "guidance" => AnnotationKind::Guidance,
1234 "guarantee" => AnnotationKind::Guarantee,
1235 other => {
1236 self.error(
1237 keyword_tok.span,
1238 format!(
1239 "Unknown annotation `@{other}`. Use `@invariant`, `@guidance` or `@guarantee`."
1240 ),
1241 );
1242 return None;
1243 }
1244 };
1245
1246 let name = match &kind {
1248 AnnotationKind::Invariant | AnnotationKind::Guarantee => {
1249 let n = self.parse_ident_in("annotation name")?;
1250 if n.name.chars().next().is_some_and(|c| c.is_lowercase()) {
1251 self.diagnostics.push(Diagnostic::error(
1252 n.span,
1253 "Annotation names must be PascalCase.",
1254 ));
1255 }
1256 Some(n)
1257 }
1258 AnnotationKind::Guidance => {
1259 if self.peek_kind().is_word()
1261 && self.line_of(self.peek().span) == self.line_of(keyword_tok.span)
1262 {
1263 self.error(
1264 self.peek().span,
1265 "`@guidance` does not take a name. Remove the name after `@guidance`.",
1266 );
1267 return None;
1268 }
1269 None
1270 }
1271 };
1272
1273 let last_header_span = name.as_ref().map(|n| n.span).unwrap_or(keyword_tok.span);
1276 let header_line = self.line_of(last_header_span);
1277 let body = self.parse_annotation_body(at_col, header_line);
1278
1279 if body.is_empty() {
1280 self.error(
1281 last_header_span,
1282 "Annotations must be followed by at least one indented comment line.",
1283 );
1284 return None;
1285 }
1286
1287 Some(BlockItem {
1288 span: start.merge(last_header_span),
1289 kind: BlockItemKind::Annotation(Annotation {
1290 kind,
1291 name,
1292 body,
1293 span: start.merge(last_header_span),
1294 }),
1295 })
1296 }
1297
1298 fn parse_annotation_body(&self, at_col: u32, header_line: u32) -> Vec<String> {
1302 let mut body = Vec::new();
1303 let lines: Vec<&str> = self.source.lines().collect();
1304 let mut line_idx = (header_line + 1) as usize;
1305
1306 while line_idx < lines.len() {
1307 let line = lines[line_idx];
1308 let trimmed = line.trim_start();
1309
1310 if trimmed.is_empty() {
1311 if !body.is_empty() {
1312 body.push(String::new());
1313 }
1314 line_idx += 1;
1315 continue;
1316 }
1317
1318 let indent = (line.len() - trimmed.len()) as u32;
1319 if indent <= at_col {
1320 break;
1321 }
1322
1323 if let Some(comment) = trimmed.strip_prefix("-- ") {
1324 body.push(comment.to_string());
1325 } else if trimmed == "--" {
1326 body.push(String::new());
1327 } else {
1328 break;
1329 }
1330
1331 line_idx += 1;
1332 }
1333
1334 while body.last().is_some_and(|l| l.is_empty()) {
1336 body.pop();
1337 }
1338
1339 body
1340 }
1341
1342 fn parse_invariant_block_item(&mut self, start: Span) -> Option<BlockItem> {
1344 self.advance(); let name = self.parse_ident_in("invariant name")?;
1346
1347 if name.name.chars().next().is_some_and(|c| c.is_lowercase()) {
1349 self.diagnostics.push(Diagnostic::error(
1350 name.span,
1351 "invariant name must start with an uppercase letter",
1352 ));
1353 }
1354
1355 self.expect(TokenKind::LBrace)?;
1356 let body = self.parse_invariant_body()?;
1357 let end = self.expect(TokenKind::RBrace)?.span;
1358 Some(BlockItem {
1359 span: start.merge(end),
1360 kind: BlockItemKind::InvariantBlock { name, body },
1361 })
1362 }
1363
1364 fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
1365 let name_tok = self.advance(); let name_text = self.text(name_tok.span).to_string();
1367 self.advance(); let allows_binding = clause_allows_binding(&name_text);
1370 let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
1371 let value_span = value.span();
1372
1373 let kind = if is_clause_keyword(&name_text) {
1374 BlockItemKind::Clause {
1375 keyword: name_text,
1376 value,
1377 }
1378 } else if let Some((inner_value, when_clause)) = extract_when_clause(&value) {
1379 BlockItemKind::FieldWithWhen {
1380 name: Ident {
1381 span: name_tok.span,
1382 name: name_text,
1383 },
1384 value: inner_value,
1385 when_clause,
1386 }
1387 } else {
1388 BlockItemKind::Assignment {
1389 name: Ident {
1390 span: name_tok.span,
1391 name: name_text,
1392 },
1393 value,
1394 }
1395 };
1396
1397 Some(BlockItem {
1398 span: start.merge(value_span),
1399 kind,
1400 })
1401 }
1402
1403 fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
1405 let obj_tok = self.advance(); self.advance(); let field = self.parse_ident_in("field name")?;
1408 self.advance(); let path = Expr::MemberAccess {
1411 span: obj_tok.span.merge(field.span),
1412 object: Box::new(Expr::Ident(Ident {
1413 span: obj_tok.span,
1414 name: self.text(obj_tok.span).to_string(),
1415 })),
1416 field,
1417 };
1418
1419 let value = self.parse_clause_value(start)?;
1420 let value_span = value.span();
1421 Some(BlockItem {
1422 span: start.merge(value_span),
1423 kind: BlockItemKind::PathAssignment { path, value },
1424 })
1425 }
1426
1427 fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
1428 let saved_pos = self.pos;
1432 let _name_tok = self.advance();
1433 self.advance(); let mut depth = 1u32;
1437 while !self.at_eof() && depth > 0 {
1438 match self.peek_kind() {
1439 TokenKind::LParen => {
1440 depth += 1;
1441 self.advance();
1442 }
1443 TokenKind::RParen => {
1444 depth -= 1;
1445 self.advance();
1446 }
1447 _ => {
1448 self.advance();
1449 }
1450 }
1451 }
1452
1453 if self.at(TokenKind::Colon) {
1454 self.pos = saved_pos;
1456 let name = self.parse_ident_in("derived value name")?;
1457 self.expect(TokenKind::LParen)?;
1458 let params = self.parse_ident_list()?;
1459 self.expect(TokenKind::RParen)?;
1460 self.expect(TokenKind::Colon)?;
1461 let value = self.parse_clause_value(start)?;
1462 Some(BlockItem {
1463 span: start.merge(value.span()),
1464 kind: BlockItemKind::ParamAssignment {
1465 name,
1466 params,
1467 value,
1468 },
1469 })
1470 } else {
1471 self.pos = saved_pos;
1473 if self.peek_at(1).kind == TokenKind::Colon {
1475 }
1476 self.parse_assign_or_clause_item(start)
1478 }
1479 }
1480
1481 fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
1482 let mut params = Vec::new();
1483 if !self.at(TokenKind::RParen) {
1484 params.push(self.parse_ident_in("parameter name")?);
1485 while self.eat(TokenKind::Comma).is_some() {
1486 params.push(self.parse_ident_in("parameter name")?);
1487 }
1488 }
1489 Some(params)
1490 }
1491
1492 fn parse_for_binding(&mut self) -> Option<ForBinding> {
1494 if self.at(TokenKind::LParen) {
1495 let start = self.advance().span; let mut idents = Vec::new();
1497 idents.push(self.parse_ident_in("loop variable")?);
1498 while self.eat(TokenKind::Comma).is_some() {
1499 idents.push(self.parse_ident_in("loop variable")?);
1500 }
1501 let end = self.expect(TokenKind::RParen)?.span;
1502 Some(ForBinding::Destructured(idents, start.merge(end)))
1503 } else {
1504 let ident = self.parse_ident_in("loop variable")?;
1505 Some(ForBinding::Single(ident))
1506 }
1507 }
1508
1509 fn parse_clause_value_maybe_binding(
1513 &mut self,
1514 clause_start: Span,
1515 allow_binding: bool,
1516 ) -> Option<Expr> {
1517 if allow_binding
1518 && self.peek_kind().is_word()
1519 && self.peek_at(1).kind == TokenKind::Colon
1520 {
1521 let clause_line = self.line_of(clause_start);
1524 let next_line = self.line_of(self.peek().span);
1525 let colon_is_block_item = next_line > clause_line
1526 && self.peek_at(2).kind != TokenKind::Eof
1527 && self.line_of(self.peek_at(2).span) == next_line;
1528
1529 if next_line == clause_line || colon_is_block_item {
1530 let name = self.parse_ident_in("binding name")?;
1531 self.advance(); let inner = self.parse_clause_value(clause_start)?;
1533 return Some(Expr::Binding {
1534 span: name.span.merge(inner.span()),
1535 name,
1536 value: Box::new(inner),
1537 });
1538 }
1539 }
1540 self.parse_clause_value(clause_start)
1541 }
1542
1543 fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1546 let clause_line = self.line_of(clause_start);
1547 let next = self.peek();
1548 let next_line = self.line_of(next.span);
1549
1550 if next_line > clause_line {
1551 let base_col = self.col_of(next.span);
1556 let clause_col = self.col_of(clause_start);
1557 if base_col <= clause_col {
1558 return Some(Expr::Block {
1559 span: clause_start,
1560 items: Vec::new(),
1561 });
1562 }
1563 self.parse_indented_block(base_col)
1564 } else {
1565 self.parse_expr(0)
1567 }
1568 }
1569
1570 fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1573 let start = self.peek().span;
1574 let mut items = Vec::new();
1575
1576 while !self.at_eof()
1577 && !self.at(TokenKind::RBrace)
1578 && self.col_of(self.peek().span) >= base_col
1579 {
1580 if self.at(TokenKind::Let) {
1582 let let_start = self.advance().span;
1583 if let Some(name) = self.parse_ident_in("binding name") {
1584 if self.expect(TokenKind::Eq).is_some() {
1585 if let Some(value) = self.parse_expr(0) {
1586 items.push(Expr::LetExpr {
1587 span: let_start.merge(value.span()),
1588 name,
1589 value: Box::new(value),
1590 });
1591 continue;
1592 }
1593 }
1594 }
1595 break;
1596 }
1597
1598 if let Some(expr) = self.parse_expr(0) {
1599 items.push(expr);
1600 } else {
1601 self.advance();
1602 break;
1603 }
1604 }
1605
1606 if items.len() == 1 {
1607 Some(items.pop().unwrap())
1608 } else {
1609 let end = items.last().map(|e| e.span()).unwrap_or(start);
1610 Some(Expr::Block {
1611 span: start.merge(end),
1612 items,
1613 })
1614 }
1615 }
1616}
1617
1618const BP_LAMBDA: u8 = 4;
1624const BP_WHEN_GUARD: u8 = 5;
1625const BP_PROJECTION: u8 = 6;
1626const BP_WITH_WHERE: u8 = 7;
1627const BP_IMPLIES: u8 = 8;
1628const BP_OR: u8 = 10;
1629const BP_AND: u8 = 20;
1630const BP_COMPARE: u8 = 30;
1631const BP_TRANSITION: u8 = 32;
1632const BP_NULL_COALESCE: u8 = 40;
1633const BP_ADD: u8 = 50;
1634const BP_MUL: u8 = 60;
1635const BP_PIPE: u8 = 65;
1636const BP_PREFIX: u8 = 70;
1637const BP_POSTFIX: u8 = 80;
1638
1639impl<'s> Parser<'s> {
1640 pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1641 let mut lhs = self.parse_prefix()?;
1642
1643 loop {
1644 if let Some((l_bp, r_bp)) = self.infix_bp() {
1645 if l_bp < min_bp {
1646 break;
1647 }
1648 lhs = self.parse_infix(lhs, r_bp)?;
1649 } else if let Some(l_bp) = self.postfix_bp() {
1650 if l_bp < min_bp {
1651 break;
1652 }
1653 lhs = self.parse_postfix(lhs)?;
1654 } else {
1655 break;
1656 }
1657 }
1658
1659 Some(lhs)
1660 }
1661
1662 fn parse_prefix(&mut self) -> Option<Expr> {
1665 match self.peek_kind() {
1666 TokenKind::Not => {
1667 let start = self.advance().span;
1668 if self.at(TokenKind::Exists) {
1669 self.advance();
1670 let operand = self.parse_expr(BP_PREFIX)?;
1671 Some(Expr::NotExists {
1672 span: start.merge(operand.span()),
1673 operand: Box::new(operand),
1674 })
1675 } else {
1676 let operand = self.parse_expr(BP_PREFIX)?;
1677 Some(Expr::Not {
1678 span: start.merge(operand.span()),
1679 operand: Box::new(operand),
1680 })
1681 }
1682 }
1683 TokenKind::Exists => {
1684 let next = self.peek_at(1).kind;
1687 if matches!(
1688 next,
1689 TokenKind::RParen
1690 | TokenKind::RBrace
1691 | TokenKind::RBracket
1692 | TokenKind::Comma
1693 | TokenKind::Eof
1694 ) {
1695 let id = self.parse_ident()?;
1696 return Some(Expr::Ident(id));
1697 }
1698 let start = self.advance().span;
1699 let operand = self.parse_expr(BP_PREFIX)?;
1700 Some(Expr::Exists {
1701 span: start.merge(operand.span()),
1702 operand: Box::new(operand),
1703 })
1704 }
1705 TokenKind::If => self.parse_if_expr(),
1706 TokenKind::For => self.parse_for_expr(),
1707 TokenKind::LBrace => self.parse_brace_expr(),
1708 TokenKind::LBracket => {
1709 let t = self.advance();
1710 self.error(t.span, "list literals `[...]` are not supported; use `Set<T>` type annotation or `{...}` set literal");
1711 None
1712 }
1713 TokenKind::LParen => self.parse_paren_expr(),
1714 TokenKind::Number => {
1715 let t = self.advance();
1716 Some(Expr::NumberLiteral {
1717 span: t.span,
1718 value: self.text(t.span).to_string(),
1719 })
1720 }
1721 TokenKind::Duration => {
1722 let t = self.advance();
1723 Some(Expr::DurationLiteral {
1724 span: t.span,
1725 value: self.text(t.span).to_string(),
1726 })
1727 }
1728 TokenKind::String => {
1729 let sl = self.parse_string()?;
1730 Some(Expr::StringLiteral(sl))
1731 }
1732 TokenKind::BacktickLiteral => {
1733 let t = self.advance();
1734 let raw = self.text(t.span);
1735 let value = raw[1..raw.len() - 1].to_string();
1737 Some(Expr::BacktickLiteral {
1738 span: t.span,
1739 value,
1740 })
1741 }
1742 TokenKind::True => {
1743 let t = self.advance();
1744 Some(Expr::BoolLiteral {
1745 span: t.span,
1746 value: true,
1747 })
1748 }
1749 TokenKind::False => {
1750 let t = self.advance();
1751 Some(Expr::BoolLiteral {
1752 span: t.span,
1753 value: false,
1754 })
1755 }
1756 TokenKind::Null => {
1757 let t = self.advance();
1758 Some(Expr::Null { span: t.span })
1759 }
1760 TokenKind::Now => {
1761 let t = self.advance();
1762 Some(Expr::Now { span: t.span })
1763 }
1764 TokenKind::This => {
1765 let t = self.advance();
1766 Some(Expr::This { span: t.span })
1767 }
1768 TokenKind::Within => {
1769 let t = self.advance();
1770 Some(Expr::Within { span: t.span })
1771 }
1772 k if k.is_word() => {
1773 let id = self.parse_ident()?;
1774 Some(Expr::Ident(id))
1775 }
1776 TokenKind::Star => {
1777 let t = self.advance();
1779 Some(Expr::Ident(Ident {
1780 span: t.span,
1781 name: "*".into(),
1782 }))
1783 }
1784 TokenKind::Minus => {
1785 let start = self.advance().span;
1787 let operand = self.parse_expr(BP_PREFIX)?;
1788 Some(Expr::BinaryOp {
1789 span: start.merge(operand.span()),
1790 left: Box::new(Expr::NumberLiteral {
1791 span: start,
1792 value: "0".into(),
1793 }),
1794 op: BinaryOp::Sub,
1795 right: Box::new(operand),
1796 })
1797 }
1798 _ => {
1799 self.error(
1800 self.peek().span,
1801 format!(
1802 "expected expression (identifier, number, string, true/false, null, \
1803 if/for/not/exists, '(', '{{', '['), found {}",
1804 self.peek_kind(),
1805 ),
1806 );
1807 None
1808 }
1809 }
1810 }
1811
1812 fn infix_bp(&self) -> Option<(u8, u8)> {
1815 match self.peek_kind() {
1816 TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1819 TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1820 TokenKind::Implies => Some((BP_IMPLIES, BP_IMPLIES - 1)), TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1822 TokenKind::And => Some((BP_AND, BP_AND + 1)),
1823 TokenKind::Eq | TokenKind::BangEq => {
1824 Some((BP_COMPARE, BP_COMPARE + 1))
1825 }
1826 TokenKind::Lt => {
1827 if self.pos > 0 {
1830 let prev = self.tokens[self.pos - 1];
1831 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1832 return None;
1833 }
1834 }
1835 Some((BP_COMPARE, BP_COMPARE + 1))
1836 }
1837 TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1838 Some((BP_COMPARE, BP_COMPARE + 1))
1839 }
1840 TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1841 TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1843 Some((BP_COMPARE, BP_COMPARE + 1))
1844 }
1845 TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1846 TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1847 TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1848 TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1849 TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1850 TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1851 TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1852 TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1853 _ => None,
1854 }
1855 }
1856
1857 fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1858 let op_tok = self.advance();
1859 match op_tok.kind {
1860 TokenKind::FatArrow => {
1861 let body = self.parse_expr(r_bp)?;
1862 Some(Expr::Lambda {
1863 span: lhs.span().merge(body.span()),
1864 param: Box::new(lhs),
1865 body: Box::new(body),
1866 })
1867 }
1868 TokenKind::Pipe => {
1869 let rhs = self.parse_expr(r_bp)?;
1870 Some(Expr::Pipe {
1871 span: lhs.span().merge(rhs.span()),
1872 left: Box::new(lhs),
1873 right: Box::new(rhs),
1874 })
1875 }
1876 TokenKind::Implies => {
1877 let rhs = self.parse_expr(r_bp)?;
1878 Some(Expr::LogicalOp {
1879 span: lhs.span().merge(rhs.span()),
1880 left: Box::new(lhs),
1881 op: LogicalOp::Implies,
1882 right: Box::new(rhs),
1883 })
1884 }
1885 TokenKind::Or => {
1886 let rhs = self.parse_expr(r_bp)?;
1887 Some(Expr::LogicalOp {
1888 span: lhs.span().merge(rhs.span()),
1889 left: Box::new(lhs),
1890 op: LogicalOp::Or,
1891 right: Box::new(rhs),
1892 })
1893 }
1894 TokenKind::And => {
1895 let rhs = self.parse_expr(r_bp)?;
1896 Some(Expr::LogicalOp {
1897 span: lhs.span().merge(rhs.span()),
1898 left: Box::new(lhs),
1899 op: LogicalOp::And,
1900 right: Box::new(rhs),
1901 })
1902 }
1903 TokenKind::Eq => {
1904 let rhs = self.parse_expr(r_bp)?;
1905 Some(Expr::Comparison {
1906 span: lhs.span().merge(rhs.span()),
1907 left: Box::new(lhs),
1908 op: ComparisonOp::Eq,
1909 right: Box::new(rhs),
1910 })
1911 }
1912 TokenKind::BangEq => {
1913 let rhs = self.parse_expr(r_bp)?;
1914 Some(Expr::Comparison {
1915 span: lhs.span().merge(rhs.span()),
1916 left: Box::new(lhs),
1917 op: ComparisonOp::NotEq,
1918 right: Box::new(rhs),
1919 })
1920 }
1921 TokenKind::Lt => {
1922 let rhs = self.parse_expr(r_bp)?;
1923 Some(Expr::Comparison {
1924 span: lhs.span().merge(rhs.span()),
1925 left: Box::new(lhs),
1926 op: ComparisonOp::Lt,
1927 right: Box::new(rhs),
1928 })
1929 }
1930 TokenKind::LtEq => {
1931 let rhs = self.parse_expr(r_bp)?;
1932 Some(Expr::Comparison {
1933 span: lhs.span().merge(rhs.span()),
1934 left: Box::new(lhs),
1935 op: ComparisonOp::LtEq,
1936 right: Box::new(rhs),
1937 })
1938 }
1939 TokenKind::Gt => {
1940 let rhs = self.parse_expr(r_bp)?;
1941 Some(Expr::Comparison {
1942 span: lhs.span().merge(rhs.span()),
1943 left: Box::new(lhs),
1944 op: ComparisonOp::Gt,
1945 right: Box::new(rhs),
1946 })
1947 }
1948 TokenKind::GtEq => {
1949 let rhs = self.parse_expr(r_bp)?;
1950 Some(Expr::Comparison {
1951 span: lhs.span().merge(rhs.span()),
1952 left: Box::new(lhs),
1953 op: ComparisonOp::GtEq,
1954 right: Box::new(rhs),
1955 })
1956 }
1957 TokenKind::In => {
1958 let rhs = self.parse_expr(r_bp)?;
1959 Some(Expr::In {
1960 span: lhs.span().merge(rhs.span()),
1961 element: Box::new(lhs),
1962 collection: Box::new(rhs),
1963 })
1964 }
1965 TokenKind::Not => {
1966 self.expect(TokenKind::In)?;
1968 let rhs = self.parse_expr(r_bp)?;
1969 Some(Expr::NotIn {
1970 span: lhs.span().merge(rhs.span()),
1971 element: Box::new(lhs),
1972 collection: Box::new(rhs),
1973 })
1974 }
1975 TokenKind::Where => {
1976 let rhs = self.parse_expr(r_bp)?;
1977 Some(Expr::Where {
1978 span: lhs.span().merge(rhs.span()),
1979 source: Box::new(lhs),
1980 condition: Box::new(rhs),
1981 })
1982 }
1983 TokenKind::With => {
1984 let rhs = self.parse_expr(r_bp)?;
1985 Some(Expr::With {
1986 span: lhs.span().merge(rhs.span()),
1987 source: Box::new(lhs),
1988 predicate: Box::new(rhs),
1989 })
1990 }
1991 TokenKind::QuestionQuestion => {
1992 let rhs = self.parse_expr(r_bp)?;
1993 Some(Expr::NullCoalesce {
1994 span: lhs.span().merge(rhs.span()),
1995 left: Box::new(lhs),
1996 right: Box::new(rhs),
1997 })
1998 }
1999 TokenKind::Plus => {
2000 let rhs = self.parse_expr(r_bp)?;
2001 Some(Expr::BinaryOp {
2002 span: lhs.span().merge(rhs.span()),
2003 left: Box::new(lhs),
2004 op: BinaryOp::Add,
2005 right: Box::new(rhs),
2006 })
2007 }
2008 TokenKind::Minus => {
2009 let rhs = self.parse_expr(r_bp)?;
2010 Some(Expr::BinaryOp {
2011 span: lhs.span().merge(rhs.span()),
2012 left: Box::new(lhs),
2013 op: BinaryOp::Sub,
2014 right: Box::new(rhs),
2015 })
2016 }
2017 TokenKind::Star => {
2018 let rhs = self.parse_expr(r_bp)?;
2019 Some(Expr::BinaryOp {
2020 span: lhs.span().merge(rhs.span()),
2021 left: Box::new(lhs),
2022 op: BinaryOp::Mul,
2023 right: Box::new(rhs),
2024 })
2025 }
2026 TokenKind::Slash => {
2027 if let Expr::Ident(ref id) = lhs {
2032 if self.peek_kind().is_word() {
2033 let next_text = self.text(self.peek().span);
2034 let is_qualified = next_text
2035 .chars()
2036 .next()
2037 .is_some_and(|c| c.is_uppercase())
2038 || matches!(
2039 self.peek_kind(),
2040 TokenKind::Config | TokenKind::Entity | TokenKind::Value
2041 );
2042 if is_qualified {
2043 let name_tok = self.advance();
2044 return Some(Expr::QualifiedName(QualifiedName {
2045 span: lhs.span().merge(name_tok.span),
2046 qualifier: Some(id.name.clone()),
2047 name: self.text(name_tok.span).to_string(),
2048 }));
2049 }
2050 }
2051 }
2052 let rhs = self.parse_expr(r_bp)?;
2053 Some(Expr::BinaryOp {
2054 span: lhs.span().merge(rhs.span()),
2055 left: Box::new(lhs),
2056 op: BinaryOp::Div,
2057 right: Box::new(rhs),
2058 })
2059 }
2060 TokenKind::ThinArrow => {
2061 let field = self.parse_ident_in("projection field")?;
2062 Some(Expr::ProjectionMap {
2063 span: lhs.span().merge(field.span),
2064 source: Box::new(lhs),
2065 field,
2066 })
2067 }
2068 TokenKind::TransitionsTo => {
2069 let rhs = self.parse_expr(r_bp)?;
2070 Some(Expr::TransitionsTo {
2071 span: lhs.span().merge(rhs.span()),
2072 subject: Box::new(lhs),
2073 new_state: Box::new(rhs),
2074 })
2075 }
2076 TokenKind::Becomes => {
2077 let rhs = self.parse_expr(r_bp)?;
2078 Some(Expr::Becomes {
2079 span: lhs.span().merge(rhs.span()),
2080 subject: Box::new(lhs),
2081 new_state: Box::new(rhs),
2082 })
2083 }
2084 TokenKind::When => {
2085 let rhs = self.parse_expr(r_bp)?;
2087 Some(Expr::WhenGuard {
2088 span: lhs.span().merge(rhs.span()),
2089 action: Box::new(lhs),
2090 condition: Box::new(rhs),
2091 })
2092 }
2093 _ => {
2094 self.error(
2095 op_tok.span,
2096 format!("unexpected infix operator {}", op_tok.kind),
2097 );
2098 None
2099 }
2100 }
2101 }
2102
2103 fn postfix_bp(&self) -> Option<u8> {
2106 match self.peek_kind() {
2107 TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
2108 TokenKind::QuestionMark => Some(BP_POSTFIX),
2109 TokenKind::Lt => {
2112 if self.pos > 0 {
2113 let prev = self.tokens[self.pos - 1];
2114 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
2117 return Some(BP_POSTFIX);
2118 }
2119 }
2120 None
2121 }
2122 TokenKind::LParen => Some(BP_POSTFIX),
2123 TokenKind::LBrace => {
2124 let next = self.peek();
2130 let prev_end = if self.pos > 0 {
2131 self.tokens[self.pos - 1].span.end
2132 } else {
2133 0
2134 };
2135 if self.line_of(Span::new(prev_end, prev_end))
2137 == self.line_of(next.span)
2138 {
2139 Some(BP_POSTFIX)
2140 } else {
2141 None
2142 }
2143 }
2144 _ => None,
2145 }
2146 }
2147
2148 fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
2149 match self.peek_kind() {
2150 TokenKind::QuestionMark => {
2151 let end = self.advance().span;
2152 Some(Expr::TypeOptional {
2153 span: lhs.span().merge(end),
2154 inner: Box::new(lhs),
2155 })
2156 }
2157 TokenKind::Lt => {
2158 self.advance(); let mut args = Vec::new();
2161 while !self.at(TokenKind::Gt) && !self.at_eof() {
2163 args.push(self.parse_expr(BP_COMPARE + 1)?);
2164 self.eat(TokenKind::Comma);
2165 }
2166 let end = self.expect(TokenKind::Gt)?.span;
2167 Some(Expr::GenericType {
2168 span: lhs.span().merge(end),
2169 name: Box::new(lhs),
2170 args,
2171 })
2172 }
2173 TokenKind::Dot => {
2174 self.advance();
2175 let field = self.parse_ident_in("field name")?;
2176 Some(Expr::MemberAccess {
2177 span: lhs.span().merge(field.span),
2178 object: Box::new(lhs),
2179 field,
2180 })
2181 }
2182 TokenKind::QuestionDot => {
2183 self.advance();
2184 let field = self.parse_ident_in("field name")?;
2185 Some(Expr::OptionalAccess {
2186 span: lhs.span().merge(field.span),
2187 object: Box::new(lhs),
2188 field,
2189 })
2190 }
2191 TokenKind::LParen => {
2192 self.advance();
2193 let args = self.parse_call_args()?;
2194 let end = self.expect(TokenKind::RParen)?.span;
2195 Some(Expr::Call {
2196 span: lhs.span().merge(end),
2197 function: Box::new(lhs),
2198 args,
2199 })
2200 }
2201 TokenKind::LBrace => {
2202 self.advance();
2203 let fields = self.parse_join_fields()?;
2204 let end = self.expect(TokenKind::RBrace)?.span;
2205 Some(Expr::JoinLookup {
2206 span: lhs.span().merge(end),
2207 entity: Box::new(lhs),
2208 fields,
2209 })
2210 }
2211 _ => None,
2212 }
2213 }
2214
2215 fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
2218 let mut args = Vec::new();
2219 while !self.at(TokenKind::RParen) && !self.at_eof() {
2220 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2222 let name = self.parse_ident_in("argument name")?;
2223 self.advance(); let value = self.parse_expr(0)?;
2225 args.push(CallArg::Named(NamedArg {
2226 span: name.span.merge(value.span()),
2227 name,
2228 value,
2229 }));
2230 } else {
2231 let expr = self.parse_expr(0)?;
2232 args.push(CallArg::Positional(expr));
2233 }
2234 self.eat(TokenKind::Comma);
2235 }
2236 Some(args)
2237 }
2238
2239 fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
2242 let mut fields = Vec::new();
2243 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2244 let field = self.parse_ident_in("join field name")?;
2245 let value = if self.eat(TokenKind::Colon).is_some() {
2246 Some(self.parse_expr(0)?)
2247 } else {
2248 None
2249 };
2250 fields.push(JoinField {
2251 span: field.span.merge(
2252 value
2253 .as_ref()
2254 .map(|v| v.span())
2255 .unwrap_or(field.span),
2256 ),
2257 field,
2258 value,
2259 });
2260 self.eat(TokenKind::Comma);
2261 }
2262 Some(fields)
2263 }
2264
2265 fn parse_if_expr(&mut self) -> Option<Expr> {
2268 let start = self.advance().span; let mut branches = Vec::new();
2270
2271 let condition = self.parse_expr(0)?;
2273 self.expect(TokenKind::Colon)?;
2274 let body = self.parse_branch_body(start)?;
2275 branches.push(CondBranch {
2276 span: start.merge(body.span()),
2277 condition,
2278 body,
2279 });
2280
2281 let mut else_body = None;
2283 while self.at(TokenKind::Else) {
2284 let else_tok = self.advance();
2285 if self.at(TokenKind::If) {
2286 let if_start = self.advance().span;
2287 let cond = self.parse_expr(0)?;
2288 self.expect(TokenKind::Colon)?;
2289 let body = self.parse_branch_body(else_tok.span)?;
2290 branches.push(CondBranch {
2291 span: if_start.merge(body.span()),
2292 condition: cond,
2293 body,
2294 });
2295 } else {
2296 self.expect(TokenKind::Colon)?;
2297 let body = self.parse_branch_body(else_tok.span)?;
2298 else_body = Some(Box::new(body));
2299 break;
2300 }
2301 }
2302
2303 let end = else_body
2304 .as_ref()
2305 .map(|b| b.span())
2306 .or_else(|| branches.last().map(|b| b.body.span()))
2307 .unwrap_or(start);
2308
2309 Some(Expr::Conditional {
2310 span: start.merge(end),
2311 branches,
2312 else_body,
2313 })
2314 }
2315
2316 fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
2317 let keyword_line = self.line_of(keyword_span);
2318 let next_line = self.line_of(self.peek().span);
2319
2320 if next_line > keyword_line {
2321 let base_col = self.col_of(self.peek().span);
2322 self.parse_indented_block(base_col)
2323 } else {
2324 self.parse_expr(0)
2325 }
2326 }
2327
2328 fn parse_for_expr(&mut self) -> Option<Expr> {
2331 let start = self.advance().span; let binding = self.parse_for_binding()?;
2333 self.expect(TokenKind::In)?;
2334
2335 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
2337
2338 let filter = if self.eat(TokenKind::Where).is_some() {
2339 Some(Box::new(self.parse_expr(0)?))
2341 } else {
2342 None
2343 };
2344
2345 self.expect(TokenKind::Colon)?;
2346 let body = self.parse_branch_body(start)?;
2347
2348 Some(Expr::For {
2349 span: start.merge(body.span()),
2350 binding,
2351 collection: Box::new(collection),
2352 filter,
2353 body: Box::new(body),
2354 })
2355 }
2356
2357 fn parse_brace_expr(&mut self) -> Option<Expr> {
2360 let start = self.advance().span; if self.at(TokenKind::RBrace) {
2363 let end = self.advance().span;
2364 return Some(Expr::SetLiteral {
2365 span: start.merge(end),
2366 elements: Vec::new(),
2367 });
2368 }
2369
2370 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2372 return self.parse_object_literal(start);
2373 }
2374
2375 self.parse_set_literal(start)
2377 }
2378
2379
2380 fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
2381 let mut fields = Vec::new();
2382 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2383 let name = self.parse_ident_in("field name")?;
2384 self.expect(TokenKind::Colon)?;
2385 let value = self.parse_expr(0)?;
2386 fields.push(NamedArg {
2387 span: name.span.merge(value.span()),
2388 name,
2389 value,
2390 });
2391 self.eat(TokenKind::Comma);
2392 }
2393 let end = self.expect(TokenKind::RBrace)?.span;
2394 Some(Expr::ObjectLiteral {
2395 span: start.merge(end),
2396 fields,
2397 })
2398 }
2399
2400 fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
2401 let mut elements = Vec::new();
2402 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2403 elements.push(self.parse_expr(0)?);
2404 self.eat(TokenKind::Comma);
2405 }
2406 let end = self.expect(TokenKind::RBrace)?.span;
2407 Some(Expr::SetLiteral {
2408 span: start.merge(end),
2409 elements,
2410 })
2411 }
2412
2413 fn parse_paren_expr(&mut self) -> Option<Expr> {
2416 let start = self.advance().span; if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2420 let mut bindings = Vec::new();
2421 while !self.at(TokenKind::RParen) && !self.at_eof() {
2422 let name = self.parse_ident_in("parameter name")?;
2423 self.expect(TokenKind::Colon)?;
2424 let value = self.parse_expr(0)?;
2425 bindings.push(Expr::Binding {
2426 span: name.span.merge(value.span()),
2427 name,
2428 value: Box::new(value),
2429 });
2430 self.eat(TokenKind::Comma);
2431 }
2432 self.expect(TokenKind::RParen)?;
2433 if bindings.len() == 1 {
2434 return Some(bindings.into_iter().next().unwrap());
2435 }
2436 let span = start.merge(bindings.last().unwrap().span());
2437 return Some(Expr::Block {
2438 span,
2439 items: bindings,
2440 });
2441 }
2442
2443 let expr = self.parse_expr(0)?;
2444 self.expect(TokenKind::RParen)?;
2445 Some(expr)
2446 }
2447}
2448
2449#[cfg(test)]
2454mod tests {
2455 use super::*;
2456 use crate::diagnostic::Severity;
2457
2458 fn parse_ok(src: &str) -> ParseResult {
2459 let owned;
2462 let input = if src.starts_with("-- allium:") {
2463 src
2464 } else {
2465 owned = format!("-- allium: 1\n{src}");
2466 &owned
2467 };
2468 let result = parse(input);
2469 if !result.diagnostics.is_empty() {
2470 for d in &result.diagnostics {
2471 eprintln!(
2472 " [{:?}] {} ({}..{})",
2473 d.severity, d.message, d.span.start, d.span.end
2474 );
2475 }
2476 }
2477 result
2478 }
2479
2480 #[test]
2481 fn version_marker() {
2482 let r = parse_ok("-- allium: 1\n");
2483 assert_eq!(r.module.version, Some(1));
2484 assert_eq!(r.diagnostics.len(), 0);
2485 }
2486
2487 #[test]
2488 fn version_missing_warns() {
2489 let r = parse("entity User {}");
2490 assert_eq!(r.module.version, None);
2491 assert_eq!(r.diagnostics.len(), 1);
2492 assert_eq!(r.diagnostics[0].severity, Severity::Warning);
2493 assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
2494 }
2495
2496 #[test]
2497 fn version_unsupported_errors() {
2498 let r = parse("-- allium: 99\nentity User {}");
2499 assert_eq!(r.module.version, Some(99));
2500 assert!(r.diagnostics.iter().any(|d|
2501 d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
2502 ), "expected unsupported version error, got: {:?}", r.diagnostics);
2503 }
2504
2505 #[test]
2506 fn empty_entity() {
2507 let r = parse_ok("entity User {}");
2508 assert_eq!(r.diagnostics.len(), 0);
2509 assert_eq!(r.module.declarations.len(), 1);
2510 match &r.module.declarations[0] {
2511 Decl::Block(b) => {
2512 assert_eq!(b.kind, BlockKind::Entity);
2513 assert_eq!(b.name.as_ref().unwrap().name, "User");
2514 }
2515 other => panic!("expected Block, got {other:?}"),
2516 }
2517 }
2518
2519 #[test]
2520 fn entity_with_fields() {
2521 let src = r#"entity Order {
2522 customer: Customer
2523 status: pending | active | completed
2524 total: Decimal
2525}"#;
2526 let r = parse_ok(src);
2527 assert_eq!(r.diagnostics.len(), 0);
2528 match &r.module.declarations[0] {
2529 Decl::Block(b) => {
2530 assert_eq!(b.items.len(), 3);
2531 }
2532 other => panic!("expected Block, got {other:?}"),
2533 }
2534 }
2535
2536 #[test]
2537 fn use_declaration() {
2538 let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
2539 assert_eq!(r.diagnostics.len(), 0);
2540 match &r.module.declarations[0] {
2541 Decl::Use(u) => {
2542 assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
2543 }
2544 other => panic!("expected Use, got {other:?}"),
2545 }
2546 }
2547
2548 #[test]
2549 fn enum_declaration() {
2550 let src = "enum OrderStatus { pending | shipped | delivered }";
2551 let r = parse_ok(src);
2552 assert_eq!(r.diagnostics.len(), 0);
2553 }
2554
2555 #[test]
2556 fn config_block() {
2557 let src = r#"config {
2558 max_retries: Integer = 3
2559 timeout: Duration = 24.hours
2560}"#;
2561 let r = parse_ok(src);
2566 assert_eq!(r.diagnostics.len(), 0);
2567 }
2568
2569 #[test]
2570 fn rule_declaration() {
2571 let src = r#"rule PlaceOrder {
2572 when: CustomerPlacesOrder(customer, items, total)
2573 requires: total > 0
2574 ensures: Order.created(customer: customer, status: pending, total: total)
2575}"#;
2576 let r = parse_ok(src);
2577 assert_eq!(r.diagnostics.len(), 0);
2578 match &r.module.declarations[0] {
2579 Decl::Block(b) => {
2580 assert_eq!(b.kind, BlockKind::Rule);
2581 assert_eq!(b.items.len(), 3);
2582 }
2583 other => panic!("expected Block, got {other:?}"),
2584 }
2585 }
2586
2587 #[test]
2588 fn expression_precedence() {
2589 let r = parse_ok("rule T { v: a + b * c }");
2590 match &r.module.declarations[0] {
2592 Decl::Block(b) => match &b.items[0].kind {
2593 BlockItemKind::Assignment { value, .. } => match value {
2594 Expr::BinaryOp { op, right, .. } => {
2595 assert_eq!(*op, BinaryOp::Add);
2596 assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2597 }
2598 other => panic!("expected BinaryOp, got {other:?}"),
2599 },
2600 other => panic!("expected Assignment, got {other:?}"),
2601 },
2602 other => panic!("expected Block, got {other:?}"),
2603 }
2604 }
2605
2606 #[test]
2607 fn default_declaration() {
2608 let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2609 let r = parse_ok(src);
2610 assert_eq!(r.diagnostics.len(), 0);
2611 }
2612
2613 #[test]
2614 fn open_question() {
2615 let src = r#"open question "Should admins be role-specific?""#;
2616 let r = parse_ok(src);
2617 assert_eq!(r.diagnostics.len(), 0);
2618 }
2619
2620 #[test]
2621 fn external_entity() {
2622 let src = "external entity Customer { email: String }";
2623 let r = parse_ok(src);
2624 assert_eq!(r.diagnostics.len(), 0);
2625 match &r.module.declarations[0] {
2626 Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2627 other => panic!("expected Block, got {other:?}"),
2628 }
2629 }
2630
2631 #[test]
2632 fn where_expression() {
2633 let src = "entity E { active: items where status = active }";
2634 let r = parse_ok(src);
2635 assert_eq!(r.diagnostics.len(), 0);
2636 }
2637
2638 #[test]
2639 fn with_expression() {
2640 let src = "entity E { slots: InterviewSlot with candidacy = this }";
2641 let r = parse_ok(src);
2642 assert_eq!(r.diagnostics.len(), 0);
2643 }
2644
2645 #[test]
2646 fn lambda_expression() {
2647 let src = "entity E { v: items.any(i => i.active) }";
2648 let r = parse_ok(src);
2649 assert_eq!(r.diagnostics.len(), 0);
2650 }
2651
2652 #[test]
2653 fn deferred() {
2654 let src = "deferred InterviewerMatching.suggest";
2655 let r = parse_ok(src);
2656 assert_eq!(r.diagnostics.len(), 0);
2657 }
2658
2659 #[test]
2660 fn variant_declaration() {
2661 let src = "variant Email : Notification { subject: String }";
2662 let r = parse_ok(src);
2663 assert_eq!(r.diagnostics.len(), 0);
2664 }
2665
2666 #[test]
2669 fn projection_arrow() {
2670 let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2671 let r = parse_ok(src);
2672 assert_eq!(r.diagnostics.len(), 0);
2673 }
2674
2675 #[test]
2678 fn transitions_to_trigger() {
2679 let src = "rule R { when: Interview.status transitions_to scheduled\n ensures: Notification.created() }";
2680 let r = parse_ok(src);
2681 assert_eq!(r.diagnostics.len(), 0);
2682 }
2683
2684 #[test]
2685 fn becomes_trigger() {
2686 let src = "rule R { when: Interview.status becomes scheduled\n ensures: Notification.created() }";
2687 let r = parse_ok(src);
2688 assert_eq!(r.diagnostics.len(), 0);
2689 }
2690
2691 #[test]
2694 fn when_binding() {
2695 let src = "rule R {\n when: interview: Interview.status transitions_to scheduled\n ensures: Notification.created()\n}";
2696 let r = parse_ok(src);
2697 assert_eq!(r.diagnostics.len(), 0);
2698 let decl = &r.module.declarations[0];
2700 if let Decl::Block(b) = decl {
2701 if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2702 assert_eq!(keyword, "when");
2703 assert!(matches!(value, Expr::Binding { .. }));
2704 } else {
2705 panic!("expected clause");
2706 }
2707 } else {
2708 panic!("expected block decl");
2709 }
2710 }
2711
2712 #[test]
2713 fn when_binding_temporal() {
2714 let src = "rule R {\n when: invitation: Invitation.expires_at <= now\n ensures: Invitation.expired()\n}";
2715 let r = parse_ok(src);
2716 assert_eq!(r.diagnostics.len(), 0);
2717 }
2718
2719 #[test]
2720 fn when_binding_created() {
2721 let src = "rule R {\n when: batch: DigestBatch.created\n ensures: Email.created()\n}";
2722 let r = parse_ok(src);
2723 assert_eq!(r.diagnostics.len(), 0);
2724 }
2725
2726 #[test]
2727 fn facing_binding() {
2728 let src = "surface S {\n facing viewer: Interviewer\n exposes: InterviewList\n}";
2729 let r = parse_ok(src);
2730 assert_eq!(r.diagnostics.len(), 0);
2731 }
2732
2733 #[test]
2734 fn context_binding() {
2735 let src = "surface S {\n facing viewer: Interviewer\n context assignment: SlotConfirmation where interviewer = viewer\n}";
2736 let r = parse_ok(src);
2737 assert_eq!(r.diagnostics.len(), 0);
2738 }
2739
2740 #[test]
2743 fn rule_level_for() {
2744 let src = r#"rule ProcessDigests {
2745 when: schedule: DigestSchedule.next_run_at <= now
2746 for user in Users where notification_setting.digest_enabled:
2747 ensures: DigestBatch.created(user: user)
2748}"#;
2749 let r = parse_ok(src);
2750 assert_eq!(r.diagnostics.len(), 0);
2751 if let Decl::Block(b) = &r.module.declarations[0] {
2752 assert!(b.items.len() >= 2);
2754 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2755 } else {
2756 panic!("expected block decl");
2757 }
2758 }
2759
2760 #[test]
2763 fn let_in_ensures_block() {
2764 let src = r#"rule R {
2765 when: ScheduleInterview(candidacy, time, interviewers)
2766 ensures:
2767 let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2768 for interviewer in interviewers:
2769 SlotConfirmation.created(slot: slot, interviewer: interviewer)
2770}"#;
2771 let r = parse_ok(src);
2772 assert_eq!(r.diagnostics.len(), 0);
2773 }
2774
2775 #[test]
2778 fn provides_when_guard() {
2779 let src = "surface S {\n facing viewer: Interviewer\n provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2780 let r = parse_ok(src);
2781 assert_eq!(r.diagnostics.len(), 0);
2782 }
2783
2784 #[test]
2787 fn optional_type_suffix() {
2788 let src = "entity E { locked_until: Timestamp? }";
2789 let r = parse_ok(src);
2790 assert_eq!(r.diagnostics.len(), 0);
2791 }
2792
2793 #[test]
2794 fn optional_trigger_param() {
2795 let src = "rule R { when: Report(interviewer, interview, reason, details?)\n ensures: Done() }";
2796 let r = parse_ok(src);
2797 assert_eq!(r.diagnostics.len(), 0);
2798 }
2799
2800 #[test]
2803 fn qualified_config_access() {
2804 let src = "entity E { duration: oauth/config.session_duration }";
2805 let r = parse_ok(src);
2806 assert_eq!(r.diagnostics.len(), 0);
2807 }
2808
2809 #[test]
2812 fn realistic_spec() {
2813 let src = r#"-- allium: 1
2814
2815enum OrderStatus { pending | shipped | delivered }
2816
2817external entity Customer {
2818 email: String
2819 name: String
2820}
2821
2822entity Order {
2823 customer: Customer
2824 status: OrderStatus
2825 total: Decimal
2826 items: OrderItem with order = this
2827 shipped_items: items where status = shipped
2828 confirmed_items: items where status = confirmed -> item
2829 is_complete: status = delivered
2830 locked_until: Timestamp?
2831}
2832
2833config {
2834 max_retries: Integer = 3
2835 timeout: Duration = 24.hours
2836}
2837
2838rule PlaceOrder {
2839 when: CustomerPlacesOrder(customer, items, total)
2840 requires: total > 0
2841 ensures: Order.created(customer: customer, status: pending, total: total)
2842}
2843
2844rule ShipOrder {
2845 when: order: Order.status transitions_to shipped
2846 ensures: Email.created(to: order.customer.email, template: order_shipped)
2847}
2848
2849open question "How do we handle partial shipments?"
2850"#;
2851 let r = parse_ok(src);
2852 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2853 assert_eq!(r.module.version, Some(1));
2854 assert_eq!(r.module.declarations.len(), 7);
2855 }
2856
2857 #[test]
2858 fn extension_behaviour_excerpt() {
2859 let src = r#"value Document {
2862 uri: String
2863 text: String
2864}
2865
2866entity Finding {
2867 code: String
2868 severity: error | warning | info
2869 range: FindingRange
2870}
2871
2872entity DiagnosticsMode {
2873 value: strict | relaxed
2874}
2875
2876config {
2877 duplicateKey: String = "allium.config.duplicateKey"
2878}
2879
2880rule RefreshDiagnostics {
2881 when: DocumentOpened(document) or DocumentChanged(document)
2882 requires: document.language_id = "allium"
2883 ensures: FindingsComputed(document)
2884}
2885
2886surface DiagnosticsDashboard {
2887 facing viewer: Developer
2888 context doc: Document where viewer.active_document = doc
2889 provides: RunChecks(viewer) when doc.language_id = "allium"
2890 exposes: FindingList
2891}
2892
2893rule ProcessDigests {
2894 when: schedule: DigestSchedule.next_run_at <= now
2895 for user in Users where notification_setting.digest_enabled:
2896 let settings = user.notification_setting
2897 ensures: DigestBatch.created(user: user)
2898}
2899"#;
2900 let r = parse_ok(src);
2901 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2902 assert_eq!(r.module.declarations.len(), 7);
2904 }
2905
2906 #[test]
2907 fn exists_as_identifier() {
2908 let src = r#"rule R {
2909 when: X()
2910 ensures: CompletionItemAvailable(label: exists)
2911}"#;
2912 let r = parse_ok(src);
2913 assert_eq!(r.diagnostics.len(), 0);
2914 }
2915
2916 #[test]
2919 fn pipe_binds_tighter_than_or() {
2920 let src = "entity E { v: a or b | c }";
2922 let r = parse_ok(src);
2923 assert_eq!(r.diagnostics.len(), 0);
2924 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2925 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2926 let Expr::LogicalOp { op, right, .. } = value else {
2928 panic!("expected LogicalOp, got {value:?}");
2929 };
2930 assert_eq!(*op, LogicalOp::Or);
2931 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2933 }
2934
2935 #[test]
2938 fn variant_with_pipe_base() {
2939 let src = "variant Mixed : TypeA | TypeB";
2940 let r = parse_ok(src);
2941 assert_eq!(r.diagnostics.len(), 0);
2942 let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2943 assert!(matches!(v.base, Expr::Pipe { .. }));
2944 }
2945
2946 #[test]
2949 fn for_block_where_comparison() {
2950 let src = r#"rule R {
2951 when: X()
2952 for item in Items where item.status = active:
2953 ensures: Processed(item: item)
2954}"#;
2955 let r = parse_ok(src);
2956 assert_eq!(r.diagnostics.len(), 0);
2957 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2958 let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2959 assert!(filter.is_some());
2960 assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2961 }
2962
2963 #[test]
2966 fn for_expr_where_comparison() {
2967 let src = r#"rule R {
2968 when: X()
2969 ensures:
2970 for item in Items where item.active = true:
2971 Processed(item: item)
2972}"#;
2973 let r = parse_ok(src);
2974 assert_eq!(r.diagnostics.len(), 0);
2975 }
2976
2977 #[test]
2980 fn if_else_if_else() {
2981 let src = r#"rule R {
2982 when: X(v)
2983 ensures:
2984 if v < 10: Small()
2985 else if v < 100: Medium()
2986 else: Large()
2987}"#;
2988 let r = parse_ok(src);
2989 assert_eq!(r.diagnostics.len(), 0);
2990 }
2991
2992 #[test]
2995 fn null_coalesce_and_optional_chain() {
2996 let src = "entity E { v: a?.b ?? fallback }";
2997 let r = parse_ok(src);
2998 assert_eq!(r.diagnostics.len(), 0);
2999 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3000 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3001 assert!(matches!(value, Expr::NullCoalesce { .. }));
3003 }
3004
3005 #[test]
3008 fn generic_type_nested() {
3009 let src = "entity E { v: List<Set<String>> }";
3010 let r = parse_ok(src);
3011 assert_eq!(r.diagnostics.len(), 0);
3012 }
3013
3014 #[test]
3017 fn collection_literals() {
3018 let src = r#"rule R {
3019 when: X()
3020 ensures:
3021 let s = {a, b, c}
3022 let o = {name: "test", count: 42}
3023 Done()
3024}"#;
3025 let r = parse_ok(src);
3026 assert_eq!(r.diagnostics.len(), 0);
3027 }
3028
3029 #[test]
3030 fn spec_reject_list_literal() {
3031 let src = r#"rule R {
3033 when: X()
3034 ensures:
3035 let l = [1, 2, 3]
3036 Done()
3037}"#;
3038 let r = parse_ok(src);
3039 assert!(
3040 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3041 "expected error for `[...]` list literal (not in spec), but parsed without errors"
3042 );
3043 }
3044
3045 #[test]
3048 fn given_block() {
3049 let src = "given { viewer: User\n time: Timestamp }";
3050 let r = parse_ok(src);
3051 assert_eq!(r.diagnostics.len(), 0);
3052 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3053 assert_eq!(b.kind, BlockKind::Given);
3054 assert!(b.name.is_none());
3055 }
3056
3057 #[test]
3060 fn actor_block() {
3061 let src = "actor Admin { identified_by: User where role = admin }";
3062 let r = parse_ok(src);
3063 assert_eq!(r.diagnostics.len(), 0);
3064 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3065 assert_eq!(b.kind, BlockKind::Actor);
3066 }
3067
3068 #[test]
3071 fn join_lookup() {
3072 let src = "entity E { match: Other{field_a, field_b: value} }";
3073 let r = parse_ok(src);
3074 assert_eq!(r.diagnostics.len(), 0);
3075 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3076 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3077 assert!(matches!(value, Expr::JoinLookup { .. }));
3078 }
3079
3080 #[test]
3083 fn in_not_in_set() {
3084 let src = r#"rule R {
3085 when: X(s)
3086 requires: s in {a, b, c}
3087 requires: s not in {d, e}
3088 ensures: Done()
3089}"#;
3090 let r = parse_ok(src);
3091 assert_eq!(r.diagnostics.len(), 0);
3092 }
3093
3094 #[test]
3097 fn comprehensive_fixture() {
3098 let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
3099 let r = parse(src);
3100 assert_eq!(
3101 r.diagnostics.len(),
3102 0,
3103 "expected no errors in comprehensive fixture, got: {:?}",
3104 r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
3105 );
3106 assert!(r.module.declarations.len() > 30, "expected many declarations");
3107 }
3108
3109 #[test]
3112 fn error_expected_declaration() {
3113 let r = parse("-- allium: 1\n+ invalid");
3114 assert!(r.diagnostics.len() >= 1);
3115 let msg = &r.diagnostics[0].message;
3116 assert!(msg.contains("expected declaration"), "got: {msg}");
3117 assert!(msg.contains("entity"), "should list valid options, got: {msg}");
3118 assert!(msg.contains("rule"), "should list valid options, got: {msg}");
3119 }
3120
3121 #[test]
3122 fn error_expected_expression() {
3123 let r = parse("-- allium: 1\nentity E { v: }");
3124 assert!(r.diagnostics.len() >= 1);
3125 let msg = &r.diagnostics[0].message;
3126 assert!(msg.contains("expected expression"), "got: {msg}");
3127 assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
3128 }
3129
3130 #[test]
3131 fn error_expected_block_item() {
3132 let r = parse("-- allium: 1\nentity E { + }");
3133 assert!(r.diagnostics.len() >= 1);
3134 let msg = &r.diagnostics[0].message;
3135 assert!(msg.contains("expected block item"), "got: {msg}");
3136 }
3137
3138 #[test]
3139 fn error_expected_identifier() {
3140 let r = parse("-- allium: 1\nentity 123 {}");
3141 assert!(r.diagnostics.len() >= 1);
3142 let msg = &r.diagnostics[0].message;
3143 assert!(msg.contains("expected entity name"), "got: {msg}");
3145 assert!(msg.contains("number"), "should say what was found, got: {msg}");
3147 }
3148
3149 #[test]
3150 fn error_missing_brace() {
3151 let r = parse("entity E {");
3152 assert!(r.diagnostics.len() >= 1);
3153 let msg = &r.diagnostics[0].message;
3154 assert!(msg.contains("expected"), "got: {msg}");
3155 }
3156
3157 #[test]
3158 fn error_recovery_multiple() {
3159 let r = parse("entity E { + }\nentity F { - }");
3161 assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
3162 }
3163
3164 #[test]
3165 fn error_dedup_same_line() {
3166 let r = parse("-- allium: 1\n+ - * /");
3168 let errors: Vec<_> = r.diagnostics.iter()
3169 .filter(|d| d.severity == crate::diagnostic::Severity::Error)
3170 .collect();
3171 assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
3172 }
3173
3174 #[test]
3175 fn for_block() {
3176 let src = r#"rule R {
3177 when: X()
3178 for user in Users where user.active:
3179 ensures: Notified(user: user)
3180}"#;
3181 let r = parse_ok(src);
3182 assert_eq!(r.diagnostics.len(), 0);
3183 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3184 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
3185 }
3186
3187 #[test]
3188 fn for_expr() {
3189 let src = r#"rule R {
3190 when: X(project)
3191 ensures:
3192 let total = for task in project.tasks: task.effort
3193 Done(total: total)
3194}"#;
3195 let r = parse_ok(src);
3196 assert_eq!(r.diagnostics.len(), 0);
3197 }
3198
3199 #[test]
3200 fn for_where() {
3201 let src = r#"rule R {
3202 when: X()
3203 for item in Items where item.active:
3204 ensures: Processed(item: item)
3205}"#;
3206 let r = parse_ok(src);
3207 assert_eq!(r.diagnostics.len(), 0);
3208 }
3209
3210 #[test]
3211 fn spec_reject_for_with_filter() {
3212 let src = r#"rule R {
3215 when: X()
3216 for slot in Slot with slot.role = reviewer:
3217 ensures: Reviewed(slot: slot)
3218}"#;
3219 let r = parse_ok(src);
3220 assert!(
3221 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3222 "expected error for `for ... with` (spec uses `where`), but parsed without errors"
3223 );
3224 }
3225
3226 #[test]
3227 fn block_level_if() {
3228 let src = r#"rule R {
3229 when: X(task)
3230 if task.priority = high:
3231 ensures: Escalated(task: task)
3232}"#;
3233 let r = parse_ok(src);
3234 assert_eq!(r.diagnostics.len(), 0);
3235 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3236 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3237 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3238 };
3239 assert_eq!(branches.len(), 1);
3240 assert!(else_items.is_none());
3241 }
3242
3243 #[test]
3244 fn block_level_if_else() {
3245 let src = r#"rule R {
3246 when: X(score)
3247 if score > 80:
3248 ensures: High()
3249 else if score > 40:
3250 ensures: Medium()
3251 else:
3252 ensures: Low()
3253}"#;
3254 let r = parse_ok(src);
3255 assert_eq!(r.diagnostics.len(), 0);
3256 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3257 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3258 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3259 };
3260 assert_eq!(branches.len(), 2);
3261 assert!(else_items.is_some());
3262 }
3263
3264 #[test]
3265 fn wildcard_type_parameter() {
3266 let src = "entity E { codec: Codec<*> }";
3267 let r = parse_ok(src);
3268 assert_eq!(r.diagnostics.len(), 0);
3269 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3270 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3271 if let Expr::GenericType { args, .. } = value {
3272 assert_eq!(args.len(), 1);
3273 if let Expr::Ident(id) = &args[0] {
3274 assert_eq!(id.name, "*");
3275 } else {
3276 panic!("expected wildcard ident, got {:?}", args[0]);
3277 }
3278 } else {
3279 panic!("expected GenericType, got {:?}", value);
3280 }
3281 }
3282
3283 #[test]
3284 fn guidance_clause_comment_only_value_migration() {
3285 let src = "-- allium: 1\nrule R {\n ensures: Done()\n guidance: -- just a comment\n}";
3287 let r = parse(src);
3288 assert!(
3289 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
3290 "expected migration diagnostic, got: {:?}",
3291 r.diagnostics
3292 );
3293 }
3294
3295 #[test]
3296 fn spec_reject_for_expr_with_filter() {
3297 let src = r#"rule R {
3299 when: X(project)
3300 ensures:
3301 let total = for task in project.tasks with task.active: task.effort
3302 Done(total: total)
3303}"#;
3304 let r = parse_ok(src);
3305 assert!(
3306 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3307 "expected error for `for ... with` in expression (spec uses `where`), but parsed without errors"
3308 );
3309 }
3310
3311 #[test]
3312 fn for_destructured_binding() {
3313 let src = r#"rule R {
3314 when: X()
3315 for (key, value) in Pairs where key != null:
3316 ensures: Processed(key: key, value: value)
3317}"#;
3318 let r = parse_ok(src);
3319 assert_eq!(r.diagnostics.len(), 0);
3320 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3321 let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
3322 assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
3323 }
3324
3325 #[test]
3326 fn dot_path_assignment() {
3327 let src = r#"entity Shard {
3328 ShardGroup.shard_cache: Shard with group = this
3329}"#;
3330 let r = parse_ok(src);
3331 assert_eq!(r.diagnostics.len(), 0);
3332 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3333 let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
3334 panic!("expected PathAssignment, got {:?}", b.items[0].kind);
3335 };
3336 assert!(matches!(path, Expr::MemberAccess { .. }));
3337 }
3338
3339 #[test]
3340 fn language_reference_fixture() {
3341 let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
3342 let r = parse(src);
3343 let errors: Vec<_> = r.diagnostics.iter()
3344 .filter(|d| d.severity == Severity::Error)
3345 .collect();
3346 assert_eq!(
3347 errors.len(),
3348 0,
3349 "expected no errors in language-reference fixture, got: {:?}",
3350 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
3351 );
3352 }
3353
3354 #[test]
3371 fn spec_for_bare_form() {
3372 let src = r#"rule ProcessDigests {
3374 when: schedule: DigestSchedule.next_run_at <= now
3375 for user in Users where notification_setting.digest_enabled:
3376 let settings = user.notification_setting
3377 ensures: DigestBatch.created(user: user)
3378}"#;
3379 let r = parse_ok(src);
3380 assert_eq!(r.diagnostics.len(), 0);
3381 }
3382
3383 #[test]
3384 fn spec_reject_for_each() {
3385 let src = r#"rule R {
3387 when: X()
3388 for each user in Users where user.active:
3389 ensures: Notified(user: user)
3390}"#;
3391 let r = parse_ok(src);
3392 assert!(
3393 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3394 "expected error for `for each` (not in spec), but parsed without errors"
3395 );
3396 }
3397
3398 #[test]
3401 fn spec_reject_double_equals() {
3402 let src = "rule R { when: X(a)\n requires: a.status == active\n ensures: Done() }";
3404 let r = parse_ok(src);
3405 assert!(
3406 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3407 "expected error for `==` (not in spec), but parsed without errors"
3408 );
3409 }
3410
3411 #[test]
3414 fn spec_reject_system_block() {
3415 let src = "system PaymentGateway {\n timeout: 30.seconds\n}";
3417 let r = parse_ok(src);
3418 assert!(
3419 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3420 "expected error for `system` block (not in spec), but parsed without errors"
3421 );
3422 }
3423
3424 #[test]
3427 fn spec_reject_tags_clause() {
3428 let src = r#"rule R {
3430 when: MigrationTriggered()
3431 tags: infrastructure, migration
3432 ensures: MigrationComplete()
3433}"#;
3434 let r = parse_ok(src);
3435 assert!(
3436 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3437 "expected error for `tags:` clause (not in spec), but parsed without errors"
3438 );
3439 }
3440
3441 #[test]
3444 fn spec_reject_includes_operator() {
3445 let src = r#"rule R {
3447 when: X(a, b)
3448 requires: a.items includes b
3449 ensures: Done()
3450}"#;
3451 let r = parse_ok(src);
3452 assert!(
3453 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3454 "expected error for `includes` operator (not in spec), but parsed without errors"
3455 );
3456 }
3457
3458 #[test]
3459 fn spec_reject_excludes_operator() {
3460 let src = r#"rule R {
3462 when: X(a, b)
3463 requires: a.items excludes b
3464 ensures: Done()
3465}"#;
3466 let r = parse_ok(src);
3467 assert!(
3468 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3469 "expected error for `excludes` operator (not in spec), but parsed without errors"
3470 );
3471 }
3472
3473 #[test]
3476 fn spec_reject_range_literal() {
3477 let src = r#"rule R {
3479 when: X(v)
3480 requires: v in [1..100]
3481 ensures: Done()
3482}"#;
3483 let r = parse_ok(src);
3484 assert!(
3485 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3486 "expected error for `..` range (not in spec), but parsed without errors"
3487 );
3488 }
3489
3490 #[test]
3493 fn spec_within_in_actor() {
3494 let src = r#"actor WorkspaceAdmin {
3496 within: Workspace
3497 identified_by: User where role = admin
3498}"#;
3499 let r = parse_ok(src);
3500 assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
3501 }
3502
3503 #[test]
3506 fn spec_reject_module_declaration() {
3507 let src = "module my_spec";
3509 let r = parse_ok(src);
3510 assert!(
3511 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3512 "expected error for `module` declaration (not in spec), but parsed without errors"
3513 );
3514 }
3515
3516 #[test]
3519 fn spec_reject_module_level_guidance() {
3520 let src = r#"guidance: "All rules must be idempotent""#;
3522 let r = parse_ok(src);
3523 assert!(
3524 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3525 "expected error for module-level `guidance:` (not in spec), but parsed without errors"
3526 );
3527 }
3528
3529 #[test]
3532 fn spec_guarantee_in_surface_migration() {
3533 let src = "-- allium: 1\nsurface S {\n facing viewer: User\n guarantee: DataIntegrity\n}";
3535 let r = parse(src);
3536 assert!(
3537 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
3538 "expected migration diagnostic, got: {:?}",
3539 r.diagnostics
3540 );
3541 }
3542
3543 #[test]
3544 fn spec_timeout_in_surface() {
3545 let src = r#"surface InvitationView {
3547 facing recipient: Candidate
3548 context invitation: ResourceInvitation where email = recipient.email
3549 timeout: InvitationExpires
3550}"#;
3551 let r = parse_ok(src);
3552 assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
3553 }
3554
3555 #[test]
3556 fn spec_timeout_in_surface_with_when() {
3557 let src = r#"surface InvitationView {
3559 facing recipient: Candidate
3560 context invitation: ResourceInvitation where email = recipient.email
3561 timeout: InvitationExpires when invitation.expires_at <= now
3562}"#;
3563 let r = parse_ok(src);
3564 assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
3565 }
3566
3567 #[test]
3570 fn spec_reject_suffix_predicate() {
3571 let src = r#"rule R {
3573 when: X()
3574 requires: finding.code starts_with "allium."
3575 ensures: Done()
3576}"#;
3577 let r = parse_ok(src);
3578 assert!(
3579 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3580 "expected error for suffix predicate (not in spec), but parsed without errors"
3581 );
3582 }
3583
3584 #[test]
3587 fn spec_add_remove_in_ensures() {
3588 let src = r#"rule R {
3591 when: AssignInterviewer(interview, new_interviewer)
3592 ensures:
3593 interview.interviewers.add(new_interviewer)
3594}"#;
3595 let r = parse_ok(src);
3596 assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
3597 }
3598
3599 #[test]
3600 fn spec_remove_in_ensures() {
3601 let src = r#"rule R {
3602 when: RemoveInterviewer(interview, leaving)
3603 ensures:
3604 interview.interviewers.remove(leaving)
3605}"#;
3606 let r = parse_ok(src);
3607 assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3608 }
3609
3610 #[test]
3613 fn spec_first_last_access() {
3614 let src = "entity E { latest: attempts.last\n earliest: attempts.first }";
3616 let r = parse_ok(src);
3617 assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3618 }
3619
3620 #[test]
3623 fn spec_set_arithmetic() {
3624 let src = r#"entity Role {
3626 permissions: Set<String>
3627 inherited: Set<String>
3628 all_permissions: permissions + inherited
3629 removed: old_mentions - new_mentions
3630}"#;
3631 let r = parse_ok(src);
3632 assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3633 }
3634
3635 #[test]
3638 fn spec_discard_binding_in_trigger() {
3639 let src = r#"rule R {
3641 when: _: LogProcessor.last_flush_check <= now
3642 ensures: Flushed()
3643}"#;
3644 let r = parse_ok(src);
3645 assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3646 }
3647
3648 #[test]
3649 fn spec_discard_in_trigger_params() {
3650 let src = r#"rule R {
3652 when: SomeEvent(_, slot)
3653 ensures: Processed(slot: slot)
3654}"#;
3655 let r = parse_ok(src);
3656 assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3657 }
3658
3659 #[test]
3660 fn spec_discard_in_for() {
3661 let src = r#"rule R {
3663 when: X(items)
3664 ensures:
3665 for _ in items: Counted()
3666}"#;
3667 let r = parse_ok(src);
3668 assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3669 }
3670
3671 #[test]
3674 fn spec_default_with_object_literal() {
3675 let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3677 let r = parse_ok(src);
3678 assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3679 }
3680
3681 #[test]
3682 fn spec_default_multiline_object() {
3683 let src = r#"default Role viewer = {
3685 name: "viewer",
3686 permissions: { "documents.read" }
3687}"#;
3688 let r = parse_ok(src);
3689 assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3690 }
3691
3692 #[test]
3695 fn spec_surface_related_clause() {
3696 let src = r#"surface InterviewerDashboard {
3698 facing viewer: Interviewer
3699 context assignment: SlotConfirmation where interviewer = viewer
3700 related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3701}"#;
3702 let r = parse_ok(src);
3703 assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3704 }
3705
3706 #[test]
3707 fn spec_surface_let_binding() {
3708 let src = r#"surface S {
3710 facing viewer: User
3711 let comments = Comments where parent = viewer
3712 exposes: CommentList
3713}"#;
3714 let r = parse_ok(src);
3715 assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3716 }
3717
3718 #[test]
3719 fn spec_surface_multiline_context_where() {
3720 let src = r#"surface InterviewerPendingAssignments {
3722 facing viewer: Interviewer
3723 context assignment: InterviewAssignment
3724 where interviewer = viewer and status = pending
3725 exposes: AssignmentList
3726}"#;
3727 let r = parse_ok(src);
3728 assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3729 }
3730
3731 #[test]
3734 fn spec_for_in_surface_provides() {
3735 let src = r#"surface TaskBoard {
3737 facing viewer: User
3738 for task in Task where task.assignee = viewer:
3739 provides: CompleteTask(viewer, task) when task.status = in_progress
3740 exposes: KanbanBoard
3741}"#;
3742 let r = parse_ok(src);
3743 assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3744 }
3745
3746 #[test]
3749 fn spec_use_without_alias() {
3750 let src = r#"use "github.com/specs/notifications/def456""#;
3752 let r = parse_ok(src);
3753 assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3754 }
3755
3756 #[test]
3759 fn spec_empty_external_entity() {
3760 let src = "external entity Commentable {}";
3762 let r = parse_ok(src);
3763 assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3764 }
3765
3766 #[test]
3769 fn spec_surface_multiline_provides() {
3770 let src = r#"surface ProjectDashboard {
3772 facing viewer: ProjectManager
3773 context project: Project where owner = viewer
3774 provides:
3775 CreateTask(viewer, project) when project.status = active
3776 ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3777 exposes: TaskList
3778}"#;
3779 let r = parse_ok(src);
3780 assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3781 }
3782
3783 #[test]
3786 fn spec_surface_multiline_exposes() {
3787 let src = r#"surface InterviewerDashboard {
3789 facing viewer: Interviewer
3790 context assignment: SlotConfirmation where interviewer = viewer
3791 exposes:
3792 assignment.slot.time
3793 assignment.status
3794}"#;
3795 let r = parse_ok(src);
3796 assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3797 }
3798
3799 #[test]
3809 fn composite_or_trigger() {
3810 let src = r#"rule R {
3811 when: EventA(x) or EventB(x) or EventC(x)
3812 ensures: Done()
3813}"#;
3814 let r = parse_ok(src);
3815 assert_eq!(r.diagnostics.len(), 0);
3816 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3817 let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
3818 assert_eq!(keyword, "when");
3819 let Expr::LogicalOp { op, left, .. } = value else {
3821 panic!("expected LogicalOp, got {value:?}");
3822 };
3823 assert_eq!(*op, LogicalOp::Or);
3824 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
3825 }
3826
3827 #[test]
3830 fn value_type_declaration() {
3831 let src = r#"value TimeRange {
3832 start: Timestamp
3833 end: Timestamp
3834 duration: end - start
3835}"#;
3836 let r = parse_ok(src);
3837 assert_eq!(r.diagnostics.len(), 0);
3838 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3839 assert_eq!(b.kind, BlockKind::Value);
3840 assert_eq!(b.name.as_ref().unwrap().name, "TimeRange");
3841 assert_eq!(b.items.len(), 3);
3842 }
3843
3844 #[test]
3847 fn qualified_config_block() {
3848 let src = r#"use "github.com/specs/oauth/abc123" as oauth
3849oauth/config {
3850 session_duration: Duration = 24.hours
3851}"#;
3852 let r = parse_ok(src);
3853 assert_eq!(r.diagnostics.len(), 0);
3854 assert_eq!(r.module.declarations.len(), 2);
3855 }
3856
3857 #[test]
3860 fn string_interpolation_parts() {
3861 let src = r#"rule R {
3862 when: X(name, action)
3863 ensures: Log.created(message: "User {name} did {action}")
3864}"#;
3865 let r = parse_ok(src);
3866 assert_eq!(r.diagnostics.len(), 0);
3867 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3869 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3870 let Expr::Call { args, .. } = value else { panic!() };
3871 let CallArg::Named(arg) = &args[0] else { panic!() };
3872 let Expr::StringLiteral(s) = &arg.value else { panic!() };
3873 assert_eq!(s.parts.len(), 4, "expected 4 string parts: text, interp, text, interp");
3874 assert!(matches!(&s.parts[0], StringPart::Text(t) if t == "User "));
3875 assert!(matches!(&s.parts[1], StringPart::Interpolation(id) if id.name == "name"));
3876 assert!(matches!(&s.parts[2], StringPart::Text(t) if t == " did "));
3877 assert!(matches!(&s.parts[3], StringPart::Interpolation(id) if id.name == "action"));
3878 }
3879
3880 #[test]
3883 fn this_keyword_expression() {
3884 let src = "entity E { items: Item with parent = this }";
3887 let r = parse_ok(src);
3888 assert_eq!(r.diagnostics.len(), 0);
3889 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3890 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3891 let Expr::With { predicate, .. } = value else {
3892 panic!("expected With, got {value:?}");
3893 };
3894 let Expr::Comparison { op, right, .. } = predicate.as_ref() else {
3895 panic!("expected Comparison in with predicate, got {predicate:?}");
3896 };
3897 assert_eq!(*op, ComparisonOp::Eq);
3898 assert!(matches!(right.as_ref(), Expr::This { .. }));
3899 }
3900
3901 #[test]
3904 fn not_prefix_standalone() {
3905 let src = r#"rule R {
3906 when: X(user)
3907 requires: not user.is_locked
3908 ensures: Done()
3909}"#;
3910 let r = parse_ok(src);
3911 assert_eq!(r.diagnostics.len(), 0);
3912 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3913 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3914 assert_eq!(keyword, "requires");
3915 assert!(matches!(value, Expr::Not { .. }));
3916 }
3917
3918 #[test]
3921 fn unary_minus() {
3922 let src = "entity E { offset: -1 }";
3923 let r = parse_ok(src);
3924 assert_eq!(r.diagnostics.len(), 0);
3925 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3926 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3927 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }
3928 | Expr::NumberLiteral { .. }), "expected negation, got {value:?}");
3929 }
3930
3931 #[test]
3934 fn parenthesised_expression() {
3935 let src = "entity E { v: (a + b) * c }";
3936 let r = parse_ok(src);
3937 assert_eq!(r.diagnostics.len(), 0);
3938 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3939 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3940 let Expr::BinaryOp { op, left, .. } = value else {
3942 panic!("expected BinaryOp, got {value:?}");
3943 };
3944 assert_eq!(*op, BinaryOp::Mul);
3945 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3946 }
3947
3948 #[test]
3951 fn boolean_literals() {
3952 let src = r#"rule R {
3953 when: X(item)
3954 ensures:
3955 item.active = true
3956 item.deleted = false
3957}"#;
3958 let r = parse_ok(src);
3959 assert_eq!(r.diagnostics.len(), 0);
3960 }
3961
3962 #[test]
3965 fn null_literal() {
3966 let src = "entity E { v: parent ?? null }";
3967 let r = parse_ok(src);
3968 assert_eq!(r.diagnostics.len(), 0);
3969 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3970 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3971 let Expr::NullCoalesce { right, .. } = value else { panic!() };
3972 assert!(matches!(right.as_ref(), Expr::Null { .. }));
3973 }
3974
3975 #[test]
3978 fn empty_set_literal() {
3979 let src = "entity E { tags: Set<String>\n default_tags: {} }";
3980 let r = parse_ok(src);
3981 assert_eq!(r.diagnostics.len(), 0);
3982 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3983 let BlockItemKind::Assignment { value, .. } = &b.items[1].kind else { panic!() };
3984 let Expr::SetLiteral { elements, .. } = value else { panic!("expected SetLiteral, got {value:?}") };
3985 assert!(elements.is_empty());
3986 }
3987
3988 #[test]
4000 fn param_assignment_single() {
4001 let src = "entity Plan { can_use(feature): feature in features }";
4002 let r = parse_ok(src);
4003 assert_eq!(r.diagnostics.len(), 0);
4004 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4005 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4006 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4007 };
4008 assert_eq!(name.name, "can_use");
4009 assert_eq!(params.len(), 1);
4010 assert_eq!(params[0].name, "feature");
4011 assert!(matches!(value, Expr::In { .. }));
4012 }
4013
4014 #[test]
4015 fn param_assignment_multiple() {
4016 let src = "entity E { distance(x, y): (x * x + y * y) }";
4017 let r = parse_ok(src);
4018 assert_eq!(r.diagnostics.len(), 0);
4019 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4020 let BlockItemKind::ParamAssignment { name, params, .. } = &b.items[0].kind else {
4021 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4022 };
4023 assert_eq!(name.name, "distance");
4024 assert_eq!(params.len(), 2);
4025 assert_eq!(params[0].name, "x");
4026 assert_eq!(params[1].name, "y");
4027 }
4028
4029 #[test]
4030 fn param_assignment_simple_expression() {
4031 let src = "entity Task { remaining_effort(total): total - effort }";
4032 let r = parse_ok(src);
4033 assert_eq!(r.diagnostics.len(), 0);
4034 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4035 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4036 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4037 };
4038 assert_eq!(name.name, "remaining_effort");
4039 assert_eq!(params.len(), 1);
4040 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }));
4041 }
4042
4043 #[test]
4046 fn precedence_logical_and_binds_tighter_than_or() {
4047 let src = "entity E { v: a or b and c }";
4049 let r = parse_ok(src);
4050 assert_eq!(r.diagnostics.len(), 0);
4051 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4052 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4053 let Expr::LogicalOp { op, right, .. } = value else {
4054 panic!("expected LogicalOp, got {value:?}");
4055 };
4056 assert_eq!(*op, LogicalOp::Or);
4057 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4058 }
4059
4060 #[test]
4061 fn precedence_comparison_binds_tighter_than_and() {
4062 let src = "entity E { v: a = b and c != d }";
4064 let r = parse_ok(src);
4065 assert_eq!(r.diagnostics.len(), 0);
4066 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4067 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4068 let Expr::LogicalOp { op, left, right, .. } = value else {
4069 panic!("expected LogicalOp, got {value:?}");
4070 };
4071 assert_eq!(*op, LogicalOp::And);
4072 assert!(matches!(left.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4073 assert!(matches!(right.as_ref(), Expr::Comparison { op: ComparisonOp::NotEq, .. }));
4074 }
4075
4076 #[test]
4077 fn precedence_arithmetic_binds_tighter_than_comparison() {
4078 let src = "entity E { v: a + b > c * d }";
4080 let r = parse_ok(src);
4081 assert_eq!(r.diagnostics.len(), 0);
4082 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4083 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4084 let Expr::Comparison { op, left, right, .. } = value else {
4085 panic!("expected Comparison, got {value:?}");
4086 };
4087 assert_eq!(*op, ComparisonOp::Gt);
4088 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
4089 assert!(matches!(right.as_ref(), Expr::BinaryOp { op: BinaryOp::Mul, .. }));
4090 }
4091
4092 #[test]
4093 fn precedence_null_coalesce_binds_tighter_than_comparison() {
4094 let src = "entity E { v: a ?? b = c }";
4096 let r = parse_ok(src);
4097 assert_eq!(r.diagnostics.len(), 0);
4098 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4099 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4100 let Expr::Comparison { op, left, .. } = value else {
4101 panic!("expected Comparison, got {value:?}");
4102 };
4103 assert_eq!(*op, ComparisonOp::Eq);
4104 assert!(matches!(left.as_ref(), Expr::NullCoalesce { .. }));
4105 }
4106
4107 #[test]
4108 fn precedence_not_binds_tighter_than_and() {
4109 let src = r#"rule R {
4111 when: X(a, b)
4112 requires: not a and b
4113 ensures: Done()
4114}"#;
4115 let r = parse_ok(src);
4116 assert_eq!(r.diagnostics.len(), 0);
4117 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4118 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4119 let Expr::LogicalOp { op, left, .. } = value else {
4120 panic!("expected LogicalOp, got {value:?}");
4121 };
4122 assert_eq!(*op, LogicalOp::And);
4123 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4124 }
4125
4126 #[test]
4127 fn precedence_where_captures_full_condition() {
4128 let src = "entity E { v: items where status = active }";
4132 let r = parse_ok(src);
4133 assert_eq!(r.diagnostics.len(), 0);
4134 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4135 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4136 let Expr::Where { condition, .. } = value else {
4137 panic!("expected Where, got {value:?}");
4138 };
4139 assert!(matches!(condition.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4140 }
4141
4142 #[test]
4143 fn precedence_where_captures_and_or_conditions() {
4144 let src = "entity E { v: items where status = active and count > 0 }";
4147 let r = parse_ok(src);
4148 assert_eq!(r.diagnostics.len(), 0);
4149 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4150 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4151 let Expr::Where { condition, .. } = value else {
4152 panic!("expected Where, got {value:?}");
4153 };
4154 assert!(matches!(condition.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4155 }
4156
4157 #[test]
4158 fn precedence_projection_applies_to_where_result() {
4159 let src = "entity E { v: items where status = confirmed -> interviewer }";
4162 let r = parse_ok(src);
4163 assert_eq!(r.diagnostics.len(), 0);
4164 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4165 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4166 let Expr::ProjectionMap { source, field, .. } = value else {
4167 panic!("expected ProjectionMap, got {value:?}");
4168 };
4169 assert_eq!(field.name, "interviewer");
4170 assert!(matches!(source.as_ref(), Expr::Where { .. }));
4171 }
4172
4173 #[test]
4174 fn precedence_lambda_binds_loosest() {
4175 let src = "entity E { v: items.any(i => i.active and i.valid) }";
4177 let r = parse_ok(src);
4178 assert_eq!(r.diagnostics.len(), 0);
4179 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4180 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4181 let Expr::Call { args, .. } = value else { panic!() };
4182 let CallArg::Positional(Expr::Lambda { body, .. }) = &args[0] else { panic!() };
4183 assert!(matches!(body.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4184 }
4185
4186 #[test]
4187 fn precedence_in_binds_at_comparison_level() {
4188 let src = r#"rule R {
4190 when: X(x, y)
4191 requires: x in {a, b} and y not in {c}
4192 ensures: Done()
4193}"#;
4194 let r = parse_ok(src);
4195 assert_eq!(r.diagnostics.len(), 0);
4196 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4197 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4198 let Expr::LogicalOp { op, left, right, .. } = value else {
4199 panic!("expected LogicalOp, got {value:?}");
4200 };
4201 assert_eq!(*op, LogicalOp::And);
4202 assert!(matches!(left.as_ref(), Expr::In { .. }));
4203 assert!(matches!(right.as_ref(), Expr::NotIn { .. }));
4204 }
4205
4206 #[test]
4209 fn multiline_ensures_block() {
4210 let src = r#"rule R {
4211 when: X(doc)
4212 ensures:
4213 doc.status = published
4214 Notification.created(to: doc.author)
4215}"#;
4216 let r = parse_ok(src);
4217 assert_eq!(r.diagnostics.len(), 0);
4218 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4219 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4220 assert_eq!(keyword, "ensures");
4221 let Expr::Block { items, .. } = value else {
4222 panic!("expected Block for multi-line ensures, got {value:?}");
4223 };
4224 assert_eq!(items.len(), 2);
4225 }
4226
4227 #[test]
4228 fn singleline_ensures_value() {
4229 let src = r#"rule R {
4230 when: X(doc)
4231 ensures: doc.status = published
4232}"#;
4233 let r = parse_ok(src);
4234 assert_eq!(r.diagnostics.len(), 0);
4235 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4236 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4237 assert_eq!(keyword, "ensures");
4238 assert!(!matches!(value, Expr::Block { .. }), "single-line ensures should not be Block");
4240 }
4241
4242 #[test]
4243 fn multiline_requires_with_continuation() {
4244 let src = r#"rule R {
4245 when: X(a)
4246 requires:
4247 a.count >= 2
4248 or a.items.any(i => i.can_solo)
4249 ensures: Done()
4250}"#;
4251 let r = parse_ok(src);
4252 assert_eq!(r.diagnostics.len(), 0);
4253 }
4254
4255 #[test]
4258 fn object_literal_single_field() {
4259 let src = r#"rule R {
4260 when: X()
4261 ensures:
4262 let o = {name: "test"}
4263 Done()
4264}"#;
4265 let r = parse_ok(src);
4266 assert_eq!(r.diagnostics.len(), 0);
4267 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4268 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4269 let Expr::Block { items, .. } = value else { panic!() };
4270 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4271 assert!(matches!(let_val.as_ref(), Expr::ObjectLiteral { .. }));
4272 }
4273
4274 #[test]
4275 fn set_literal_single_element() {
4276 let src = r#"rule R {
4277 when: X()
4278 ensures:
4279 let s = {active}
4280 Done()
4281}"#;
4282 let r = parse_ok(src);
4283 assert_eq!(r.diagnostics.len(), 0);
4284 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4285 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4286 let Expr::Block { items, .. } = value else { panic!() };
4287 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4288 assert!(matches!(let_val.as_ref(), Expr::SetLiteral { .. }),
4289 "bare {{ident}} should parse as set literal, got {:?}", let_val);
4290 }
4291
4292 #[test]
4295 fn lambda_with_chained_access() {
4296 let src = "entity E { v: items.all(t => t.item.status = active) }";
4297 let r = parse_ok(src);
4298 assert_eq!(r.diagnostics.len(), 0);
4299 }
4300
4301 #[test]
4302 fn nested_lambda() {
4303 let src = "entity E { v: groups.any(g => g.items.all(i => i.valid)) }";
4304 let r = parse_ok(src);
4305 assert_eq!(r.diagnostics.len(), 0);
4306 }
4307
4308 #[test]
4311 fn qualified_name_with_member_access() {
4312 let src = "entity E { v: shared/Validator.check }";
4313 let r = parse_ok(src);
4314 assert_eq!(r.diagnostics.len(), 0);
4315 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4316 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4317 let Expr::MemberAccess { object, field, .. } = value else {
4318 panic!("expected MemberAccess, got {value:?}");
4319 };
4320 assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
4321 assert_eq!(field.name, "check");
4322 }
4323
4324 #[test]
4325 fn qualified_name_in_call() {
4326 let src = r#"rule R {
4327 when: X(item)
4328 requires: shared/Validator.check(item: item)
4329 ensures: Done()
4330}"#;
4331 let r = parse_ok(src);
4332 assert_eq!(r.diagnostics.len(), 0);
4333 }
4334
4335 #[test]
4338 fn nested_if_inside_for() {
4339 let src = r#"rule R {
4340 when: X()
4341 for user in Users where user.active:
4342 if user.role = admin:
4343 ensures: AdminNotified(user: user)
4344 else:
4345 ensures: UserNotified(user: user)
4346}"#;
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::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4351 assert!(matches!(items[0].kind, BlockItemKind::IfBlock { .. }));
4352 }
4353
4354 #[test]
4355 fn for_with_let_before_ensures() {
4356 let src = r#"rule R {
4357 when: schedule: DigestSchedule.next_run_at <= now
4358 for user in Users where user.active:
4359 let pending = user.tasks where status = pending
4360 ensures: DigestEmail.created(to: user.email, tasks: pending)
4361}"#;
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::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4366 assert_eq!(items.len(), 2, "for body should have let + ensures");
4367 assert!(matches!(items[0].kind, BlockItemKind::Let { .. }));
4368 assert!(matches!(items[1].kind, BlockItemKind::Clause { .. }));
4369 }
4370
4371 #[test]
4374 fn join_lookup_all_unnamed() {
4375 let src = "entity E { match: Other{a, b, c} }";
4376 let r = parse_ok(src);
4377 assert_eq!(r.diagnostics.len(), 0);
4378 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4379 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4380 let Expr::JoinLookup { fields, .. } = value else { panic!() };
4381 assert_eq!(fields.len(), 3);
4382 assert!(fields.iter().all(|f| f.value.is_none()));
4383 }
4384
4385 #[test]
4386 fn join_lookup_all_named() {
4387 let src = "entity E { match: Membership{user: actor, workspace: ws} }";
4388 let r = parse_ok(src);
4389 assert_eq!(r.diagnostics.len(), 0);
4390 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4391 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4392 let Expr::JoinLookup { fields, .. } = value else { panic!() };
4393 assert_eq!(fields.len(), 2);
4394 assert!(fields.iter().all(|f| f.value.is_some()));
4395 }
4396
4397 #[test]
4398 fn join_lookup_in_requires() {
4399 let src = r#"rule R {
4400 when: X(user, workspace)
4401 requires: exists WorkspaceMembership{user: user, workspace: workspace}
4402 ensures: Done()
4403}"#;
4404 let r = parse_ok(src);
4405 assert_eq!(r.diagnostics.len(), 0);
4406 }
4407
4408 #[test]
4409 fn join_lookup_negated_in_requires() {
4410 let src = r#"rule R {
4411 when: X(email)
4412 requires: not exists User{email: email}
4413 ensures: Done()
4414}"#;
4415 let r = parse_ok(src);
4416 assert_eq!(r.diagnostics.len(), 0);
4417 }
4418
4419 #[test]
4424 fn implies_basic() {
4425 let src = "rule R { requires: a implies b }";
4426 let r = parse_ok(src);
4427 assert_eq!(r.diagnostics.len(), 0);
4428 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4429 let BlockItemKind::Clause { value, .. } = &b.items[0].kind else { panic!() };
4430 let Expr::LogicalOp { op, .. } = value else { panic!("expected LogicalOp, got {value:?}") };
4431 assert_eq!(*op, LogicalOp::Implies);
4432 }
4433
4434 #[test]
4435 fn implies_precedence_and_binds_tighter() {
4436 let src = "rule R { v: a and b implies c }";
4438 let r = parse_ok(src);
4439 assert_eq!(r.diagnostics.len(), 0);
4440 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4441 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4442 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4443 assert_eq!(*op, LogicalOp::Implies);
4444 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4445 }
4446
4447 #[test]
4448 fn implies_precedence_or_binds_tighter() {
4449 let src = "rule R { v: a or b implies c }";
4451 let r = parse_ok(src);
4452 assert_eq!(r.diagnostics.len(), 0);
4453 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4454 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4455 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4456 assert_eq!(*op, LogicalOp::Implies);
4457 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4458 }
4459
4460 #[test]
4461 fn implies_precedence_implies_above_or() {
4462 let src = "rule R { v: a implies b or c }";
4464 let r = parse_ok(src);
4465 assert_eq!(r.diagnostics.len(), 0);
4466 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4467 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4468 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4469 assert_eq!(*op, LogicalOp::Implies);
4470 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4471 }
4472
4473 #[test]
4474 fn implies_precedence_not_binds_tighter() {
4475 let src = "rule R { v: not a implies b }";
4477 let r = parse_ok(src);
4478 assert_eq!(r.diagnostics.len(), 0);
4479 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4480 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4481 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4482 assert_eq!(*op, LogicalOp::Implies);
4483 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4484 }
4485
4486 #[test]
4487 fn implies_right_associative() {
4488 let src = "rule R { v: a implies b implies c }";
4490 let r = parse_ok(src);
4491 assert_eq!(r.diagnostics.len(), 0);
4492 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4493 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4494 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4495 assert_eq!(*op, LogicalOp::Implies);
4496 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4497 }
4498
4499 #[test]
4500 fn implies_is_keyword_parsed_as_operator() {
4501 let src = "entity E { v: a implies b }";
4504 let r = parse_ok(src);
4505 assert_eq!(r.diagnostics.len(), 0);
4506 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4507 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4508 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4509 }
4510
4511 #[test]
4512 fn implies_in_ensures() {
4513 let src = r#"rule R {
4514 when: X()
4515 ensures: a implies b
4516}"#;
4517 let r = parse_ok(src);
4518 assert_eq!(r.diagnostics.len(), 0);
4519 }
4520
4521 #[test]
4522 fn implies_in_derived_value() {
4523 let src = "entity E { v: a implies b }";
4524 let r = parse_ok(src);
4525 assert_eq!(r.diagnostics.len(), 0);
4526 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4527 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4528 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4529 }
4530
4531 #[test]
4536 fn guidance_ordering_tests_removed() {
4537 }
4541
4542 #[test]
4547 fn contract_signatures_only() {
4548 let src = r#"contract Auditable {
4549 last_modified_by: Actor
4550 last_modified_at: Timestamp
4551}"#;
4552 let r = parse_ok(src);
4553 assert_eq!(r.diagnostics.len(), 0);
4554 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4555 assert_eq!(b.kind, BlockKind::Contract);
4556 assert_eq!(b.name.as_ref().unwrap().name, "Auditable");
4557 assert_eq!(b.items.len(), 2);
4558 }
4559
4560 #[test]
4561 fn contract_with_annotations() {
4562 let src = r#"contract Versioned {
4563 version: Integer
4564 @invariant Monotonic
4565 -- versions must increase
4566 @guidance
4567 -- use semantic versioning
4568}"#;
4569 let r = parse_ok(src);
4570 assert_eq!(r.diagnostics.len(), 0);
4571 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4572 assert_eq!(b.kind, BlockKind::Contract);
4573 assert_eq!(b.items.len(), 3);
4574 }
4575
4576 #[test]
4577 fn contract_with_any_type() {
4578 let src = r#"contract Identifiable {
4579 id: Any
4580}"#;
4581 let r = parse_ok(src);
4582 assert_eq!(r.diagnostics.len(), 0);
4583 }
4584
4585 #[test]
4586 fn contract_lowercase_name_rejected() {
4587 let src = "-- allium: 1\ncontract bad {}";
4588 let r = parse(src);
4589 assert!(
4590 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4591 "expected uppercase error, got: {:?}",
4592 r.diagnostics
4593 );
4594 }
4595
4596 #[test]
4597 fn contract_colon_body_rejected() {
4598 let src = "-- allium: 1\ncontract Bad: something";
4599 let r = parse(src);
4600 assert!(
4601 r.diagnostics.iter().any(|d| d.message.contains("braces")),
4602 "expected braces error, got: {:?}",
4603 r.diagnostics
4604 );
4605 }
4606
4607 #[test]
4612 fn contracts_clause_single_demands() {
4613 let src = "surface S {\n contracts:\n demands Auditable\n}";
4614 let r = parse_ok(src);
4615 assert_eq!(r.diagnostics.len(), 0);
4616 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4617 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4618 panic!("expected ContractsClause, got {:?}", b.items[0].kind)
4619 };
4620 assert_eq!(entries.len(), 1);
4621 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4622 assert_eq!(entries[0].name.name, "Auditable");
4623 }
4624
4625 #[test]
4626 fn contracts_clause_single_fulfils() {
4627 let src = "surface S {\n contracts:\n fulfils EventSubmitter\n}";
4628 let r = parse_ok(src);
4629 assert_eq!(r.diagnostics.len(), 0);
4630 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4631 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4632 panic!("expected ContractsClause")
4633 };
4634 assert_eq!(entries.len(), 1);
4635 assert!(matches!(entries[0].direction, ContractDirection::Fulfils));
4636 assert_eq!(entries[0].name.name, "EventSubmitter");
4637 }
4638
4639 #[test]
4640 fn contracts_clause_mixed() {
4641 let src = "surface S {\n contracts:\n demands Auditable\n fulfils EventSubmitter\n}";
4642 let r = parse_ok(src);
4643 assert_eq!(r.diagnostics.len(), 0);
4644 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4645 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4646 panic!("expected ContractsClause")
4647 };
4648 assert_eq!(entries.len(), 2);
4649 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4650 assert!(matches!(entries[1].direction, ContractDirection::Fulfils));
4651 }
4652
4653 #[test]
4654 fn contracts_with_other_clauses() {
4655 let src = r#"surface S {
4656 facing user: User
4657 contracts:
4658 demands Auditable
4659 exposes:
4660 user.name
4661}"#;
4662 let r = parse_ok(src);
4663 assert_eq!(r.diagnostics.len(), 0);
4664 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4665 assert_eq!(b.items.len(), 3);
4666 }
4667
4668 #[test]
4669 fn contracts_only_surface() {
4670 let src = "surface S {\n contracts:\n demands Foo\n fulfils Bar\n}";
4671 let r = parse_ok(src);
4672 assert_eq!(r.diagnostics.len(), 0);
4673 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4674 assert_eq!(b.items.len(), 1);
4675 }
4676
4677 #[test]
4678 fn contracts_empty_rejected() {
4679 let src = "-- allium: 1\nsurface S {\n contracts:\n}";
4680 let r = parse(src);
4681 assert!(
4682 r.diagnostics.iter().any(|d| d.message.contains("Empty `contracts:`")),
4683 "expected empty contracts error, got: {:?}",
4684 r.diagnostics
4685 );
4686 }
4687
4688 #[test]
4689 fn contracts_inline_block_rejected() {
4690 let src = "-- allium: 1\nsurface S {\n contracts:\n demands Foo {\n }\n}";
4691 let r = parse(src);
4692 assert!(
4693 r.diagnostics.iter().any(|d| d.message.contains("Inline contract blocks")),
4694 "expected inline block error, got: {:?}",
4695 r.diagnostics
4696 );
4697 }
4698
4699 #[test]
4700 fn contracts_unknown_direction_rejected() {
4701 let src = "-- allium: 1\nsurface S {\n contracts:\n requires Foo\n}";
4702 let r = parse(src);
4703 assert!(
4704 r.diagnostics.iter().any(|d| d.message.contains("Unknown direction")),
4705 "expected unknown direction error, got: {:?}",
4706 r.diagnostics
4707 );
4708 }
4709
4710 #[test]
4715 fn annotation_invariant() {
4716 let src = "contract C {\n @invariant Determinism\n -- all evaluations must be deterministic\n}";
4717 let r = parse_ok(src);
4718 assert_eq!(r.diagnostics.len(), 0);
4719 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4720 let BlockItemKind::Annotation(ann) = &b.items[0].kind else {
4721 panic!("expected Annotation, got {:?}", b.items[0].kind)
4722 };
4723 assert!(matches!(ann.kind, AnnotationKind::Invariant));
4724 assert_eq!(ann.name.as_ref().unwrap().name, "Determinism");
4725 assert_eq!(ann.body.len(), 1);
4726 assert_eq!(ann.body[0], "all evaluations must be deterministic");
4727 }
4728
4729 #[test]
4730 fn annotation_multiple_invariants() {
4731 let src = "contract C {\n @invariant A\n -- first\n @invariant B\n -- second\n}";
4732 let r = parse_ok(src);
4733 assert_eq!(r.diagnostics.len(), 0);
4734 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4735 assert_eq!(b.items.len(), 2);
4736 assert!(matches!(&b.items[0].kind, BlockItemKind::Annotation(_)));
4737 assert!(matches!(&b.items[1].kind, BlockItemKind::Annotation(_)));
4738 }
4739
4740 #[test]
4741 fn annotation_invariant_then_guidance() {
4742 let src = "contract C {\n @invariant Safety\n -- must be safe\n @guidance\n -- implementation notes\n}";
4743 let r = parse_ok(src);
4744 assert_eq!(r.diagnostics.len(), 0);
4745 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4746 assert_eq!(b.items.len(), 2);
4747 }
4748
4749 #[test]
4750 fn annotation_guidance_in_rule() {
4751 let src = "rule R {\n when: Event.created\n ensures: something\n @guidance\n -- do it this way\n}";
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 let last = b.items.last().unwrap();
4756 let BlockItemKind::Annotation(ann) = &last.kind else { panic!() };
4757 assert!(matches!(ann.kind, AnnotationKind::Guidance));
4758 assert!(ann.name.is_none());
4759 }
4760
4761 #[test]
4762 fn annotation_guarantee() {
4763 let src = "surface S {\n @guarantee ResponseTime\n -- must respond within 100ms\n}";
4764 let r = parse_ok(src);
4765 assert_eq!(r.diagnostics.len(), 0);
4766 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4767 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
4768 assert!(matches!(ann.kind, AnnotationKind::Guarantee));
4769 assert_eq!(ann.name.as_ref().unwrap().name, "ResponseTime");
4770 }
4771
4772 #[test]
4773 fn annotation_guarantee_then_guidance() {
4774 let src = "surface S {\n @guarantee Fast\n -- sub-second\n @guidance\n -- cache aggressively\n}";
4775 let r = parse_ok(src);
4776 assert_eq!(r.diagnostics.len(), 0);
4777 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4778 assert_eq!(b.items.len(), 2);
4779 }
4780
4781 #[test]
4782 fn annotation_contracts_guarantee_guidance() {
4783 let src = r#"surface S {
4784 contracts:
4785 demands Auditable
4786 @guarantee ResponseTime
4787 -- fast
4788 @guidance
4789 -- notes
4790}"#;
4791 let r = parse_ok(src);
4792 assert_eq!(r.diagnostics.len(), 0);
4793 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4794 assert_eq!(b.items.len(), 3);
4795 }
4796
4797 #[test]
4798 fn annotation_multiline_body() {
4799 let src = "contract C {\n @invariant Multi\n -- line one\n -- line two\n -- line three\n}";
4800 let r = parse_ok(src);
4801 assert_eq!(r.diagnostics.len(), 0);
4802 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4803 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
4804 assert_eq!(ann.body.len(), 3);
4805 assert_eq!(ann.body[0], "line one");
4806 assert_eq!(ann.body[2], "line three");
4807 }
4808
4809 #[test]
4810 fn annotation_empty_body_rejected() {
4811 let src = "-- allium: 1\ncontract C {\n @invariant NoBody\n}";
4812 let r = parse(src);
4813 assert!(
4814 r.diagnostics.iter().any(|d| d.message.contains("at least one indented comment line")),
4815 "expected empty body error, got: {:?}",
4816 r.diagnostics
4817 );
4818 }
4819
4820 #[test]
4821 fn annotation_unknown_keyword_rejected() {
4822 let src = "-- allium: 1\ncontract C {\n @note Something\n -- text\n}";
4823 let r = parse(src);
4824 assert!(
4825 r.diagnostics.iter().any(|d| d.message.contains("Unknown annotation")),
4826 "expected unknown annotation error, got: {:?}",
4827 r.diagnostics
4828 );
4829 }
4830
4831 #[test]
4832 fn expression_invariant_still_works() {
4833 let src = r#"entity E {
4834 status: pending | active
4835 invariant AllValid {
4836 this.status = active
4837 }
4838}"#;
4839 let r = parse_ok(src);
4840 assert_eq!(r.diagnostics.len(), 0);
4841 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4842 let inv = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. }));
4844 assert!(inv.is_some(), "expression-bearing invariant should still parse");
4845 }
4846
4847 #[test]
4848 fn invariant_colon_form_migration() {
4849 let src = "-- allium: 1\ncontract C {\n invariant: SomeName\n}";
4850 let r = parse(src);
4851 assert!(
4852 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
4853 "expected migration diagnostic, got: {:?}",
4854 r.diagnostics
4855 );
4856 }
4857
4858 #[test]
4859 fn guidance_colon_form_migration() {
4860 let src = "-- allium: 1\nrule R {\n when: Event.created\n ensures: something\n guidance: \"do it\"\n}";
4861 let r = parse(src);
4862 assert!(
4863 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
4864 "expected migration diagnostic, got: {:?}",
4865 r.diagnostics
4866 );
4867 }
4868
4869 #[test]
4870 fn guarantee_colon_form_migration() {
4871 let src = "-- allium: 1\nsurface S {\n guarantee: \"fast\"\n}";
4872 let r = parse(src);
4873 assert!(
4874 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
4875 "expected migration diagnostic, got: {:?}",
4876 r.diagnostics
4877 );
4878 }
4879
4880 #[test]
4881 fn annotation_guidance_with_name_rejected() {
4882 let src = "-- allium: 1\ncontract C {\n @guidance Named\n -- text\n}";
4883 let r = parse(src);
4884 assert!(
4885 r.diagnostics.iter().any(|d| d.message.contains("does not take a name")),
4886 "expected guidance name error, got: {:?}",
4887 r.diagnostics
4888 );
4889 }
4890
4891 #[test]
4896 fn invariant_top_level_simple() {
4897 let src = r#"invariant PositiveBalance {
4898 this.balance > 0
4899}"#;
4900 let r = parse_ok(src);
4901 assert_eq!(r.diagnostics.len(), 0);
4902 let Decl::Invariant(inv) = &r.module.declarations[0] else {
4903 panic!("expected Invariant, got {:?}", r.module.declarations[0])
4904 };
4905 assert_eq!(inv.name.name, "PositiveBalance");
4906 }
4907
4908 #[test]
4909 fn invariant_top_level_for_quantifier() {
4910 let src = r#"invariant AllPositive {
4911 for item in items: item.value > 0
4912}"#;
4913 let r = parse_ok(src);
4914 assert_eq!(r.diagnostics.len(), 0);
4915 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
4916 assert!(matches!(inv.body, Expr::For { .. }));
4917 }
4918
4919 #[test]
4920 fn invariant_top_level_nested_for() {
4921 let src = r#"invariant NestedFor {
4922 for a in items: for b in a.children: b.valid = true
4923}"#;
4924 let r = parse_ok(src);
4925 assert_eq!(r.diagnostics.len(), 0);
4926 }
4927
4928 #[test]
4929 fn invariant_top_level_implies() {
4930 let src = r#"invariant ImpliesTest {
4931 this.active implies this.balance > 0
4932}"#;
4933 let r = parse_ok(src);
4934 assert_eq!(r.diagnostics.len(), 0);
4935 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
4936 assert!(matches!(inv.body, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4937 }
4938
4939 #[test]
4940 fn invariant_top_level_let_binding() {
4941 let src = r#"invariant WithLet {
4942 let total = this.items.count()
4943 total > 0
4944}"#;
4945 let r = parse_ok(src);
4946 assert_eq!(r.diagnostics.len(), 0);
4947 }
4948
4949 #[test]
4950 fn invariant_top_level_collection_ops() {
4951 let src = r#"invariant CollectionOps {
4952 this.items where active = true
4953}"#;
4954 let r = parse_ok(src);
4955 assert_eq!(r.diagnostics.len(), 0);
4956 }
4957
4958 #[test]
4959 fn invariant_top_level_exists() {
4960 let src = r#"invariant ExistsCheck {
4961 exists this.primary_contact
4962}"#;
4963 let r = parse_ok(src);
4964 assert_eq!(r.diagnostics.len(), 0);
4965 }
4966
4967 #[test]
4968 fn invariant_top_level_not_exists() {
4969 let src = r#"invariant NotExistsCheck {
4970 not exists this.deleted_at
4971}"#;
4972 let r = parse_ok(src);
4973 assert_eq!(r.diagnostics.len(), 0);
4974 }
4975
4976 #[test]
4977 fn invariant_top_level_optional_navigation() {
4978 let src = r#"invariant OptionalNav {
4979 this.owner?.email ?? "none" != "none"
4980}"#;
4981 let r = parse_ok(src);
4982 assert_eq!(r.diagnostics.len(), 0);
4983 }
4984
4985 #[test]
4986 fn invariant_top_level_lowercase_rejected() {
4987 let src = "-- allium: 1\ninvariant bad { true }";
4988 let r = parse(src);
4989 assert!(
4990 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4991 "expected uppercase error, got: {:?}",
4992 r.diagnostics
4993 );
4994 }
4995
4996 #[test]
4997 fn invariant_entity_level() {
4998 let src = r#"entity Account {
4999 balance: Decimal
5000 invariant NonNegative { this.balance >= 0 }
5001}"#;
5002 let r = parse_ok(src);
5003 assert_eq!(r.diagnostics.len(), 0);
5004 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5005 let BlockItemKind::InvariantBlock { name, body: _ } = &b.items[1].kind else {
5006 panic!("expected InvariantBlock, got {:?}", b.items[1].kind)
5007 };
5008 assert_eq!(name.name, "NonNegative");
5009 }
5010
5011 #[test]
5012 fn invariant_entity_level_this_ref() {
5013 let src = r#"entity Order {
5014 total: Decimal
5015 invariant PositiveTotal { this.total > 0 }
5016}"#;
5017 let r = parse_ok(src);
5018 assert_eq!(r.diagnostics.len(), 0);
5019 }
5020
5021 #[test]
5022 fn invariant_entity_level_implies() {
5023 let src = r#"entity Subscription {
5024 active: Boolean
5025 balance: Decimal
5026 invariant ActiveMeansPositive { this.active implies this.balance > 0 }
5027}"#;
5028 let r = parse_ok(src);
5029 assert_eq!(r.diagnostics.len(), 0);
5030 }
5031
5032 #[test]
5033 fn invariant_entity_level_lowercase_rejected() {
5034 let src = "-- allium: 1\nentity E { invariant bad { true } }";
5035 let r = parse(src);
5036 assert!(
5037 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
5038 "expected uppercase error, got: {:?}",
5039 r.diagnostics
5040 );
5041 }
5042
5043 #[test]
5044 fn invariant_colon_form_in_entity_migration() {
5045 let src = "-- allium: 1\nentity E {\n invariant: -- must be valid\n}";
5047 let r = parse(src);
5048 assert!(
5049 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
5050 "expected migration diagnostic, got: {:?}",
5051 r.diagnostics
5052 );
5053 }
5054
5055 #[test]
5056 fn invariant_top_level_colon_rejected() {
5057 let src = "-- allium: 1\ninvariant Bad: some text";
5059 let r = parse(src);
5060 assert!(
5061 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5062 "expected error for colon-delimited invariant at top level, got: {:?}",
5063 r.diagnostics
5064 );
5065 }
5066
5067 #[test]
5068 fn invariant_same_name_different_scopes() {
5069 let src = r#"invariant SameName { true }
5071entity E {
5072 invariant SameName { true }
5073}"#;
5074 let r = parse_ok(src);
5075 assert_eq!(r.diagnostics.len(), 0);
5076 }
5077
5078 #[test]
5083 fn config_qualified_reference() {
5084 let src = r#"config {
5086 param: Integer = core/config.max_batch_size
5087}"#;
5088 let r = parse_ok(src);
5089 assert_eq!(r.diagnostics.len(), 0);
5090 }
5091
5092 #[test]
5093 fn config_multiple_qualified_refs() {
5094 let src = r#"config {
5095 param_a: Integer = core/config.max_batch_size
5096 param_b: Duration = core/config.default_delay
5097}"#;
5098 let r = parse_ok(src);
5099 assert_eq!(r.diagnostics.len(), 0);
5100 }
5101
5102 #[test]
5103 fn config_qualified_ref_with_type() {
5104 let src = r#"config {
5105 publish_delay: Duration = core/config.default_delay
5106}"#;
5107 let r = parse_ok(src);
5108 assert_eq!(r.diagnostics.len(), 0);
5109 }
5110
5111 #[test]
5112 fn config_qualified_chain() {
5113 let src = r#"config {
5115 first: Integer = core/config.base
5116 second: Integer = first
5117}"#;
5118 let r = parse_ok(src);
5119 assert_eq!(r.diagnostics.len(), 0);
5120 }
5121
5122 #[test]
5123 fn config_renamed_param_with_qualified_ref() {
5124 let src = r#"config {
5125 my_timeout: Duration = core/config.base_timeout
5126}"#;
5127 let r = parse_ok(src);
5128 assert_eq!(r.diagnostics.len(), 0);
5129 }
5130
5131 #[test]
5136 fn config_default_arithmetic() {
5137 let src = r#"config {
5138 param: Integer = other_param + 1
5139}"#;
5140 let r = parse_ok(src);
5141 assert_eq!(r.diagnostics.len(), 0);
5142 }
5143
5144 #[test]
5145 fn config_default_qualified_arithmetic() {
5146 let src = r#"config {
5147 param: Duration = core/config.timeout * 2
5148}"#;
5149 let r = parse_ok(src);
5150 assert_eq!(r.diagnostics.len(), 0);
5151 }
5152
5153 #[test]
5154 fn config_default_parenthesised() {
5155 let src = r#"config {
5156 param: Integer = (base + 1) * factor
5157}"#;
5158 let r = parse_ok(src);
5159 assert_eq!(r.diagnostics.len(), 0);
5160 }
5161
5162 #[test]
5163 fn config_default_two_qualified_refs() {
5164 let src = r#"config {
5165 param: Duration = core/config.a + core/config.b
5166}"#;
5167 let r = parse_ok(src);
5168 assert_eq!(r.diagnostics.len(), 0);
5169 }
5170
5171 #[test]
5172 fn config_default_literal_only() {
5173 let src = r#"config {
5174 param: Integer = 5
5175}"#;
5176 let r = parse_ok(src);
5177 assert_eq!(r.diagnostics.len(), 0);
5178 }
5179
5180 #[test]
5181 fn config_default_decimal_literal() {
5182 let src = r#"config {
5183 param: Decimal = price * 1.5
5184}"#;
5185 let r = parse_ok(src);
5186 assert_eq!(r.diagnostics.len(), 0);
5187 }
5188
5189 #[test]
5190 fn config_default_mixed_operators() {
5191 let src = r#"config {
5192 param: Duration = timeout * 2 + 1.minute
5193}"#;
5194 let r = parse_ok(src);
5195 assert_eq!(r.diagnostics.len(), 0);
5196 }
5197
5198 #[test]
5199 fn config_default_operator_precedence() {
5200 let src = r#"config {
5202 param: Integer = a + b * c
5203}"#;
5204 let r = parse_ok(src);
5205 assert_eq!(r.diagnostics.len(), 0);
5206 }
5207
5208 #[test]
5213 fn version_2_accepted() {
5214 let r = parse("-- allium: 2\nentity User {}");
5215 assert_eq!(r.module.version, Some(2));
5216 assert_eq!(r.diagnostics.len(), 0);
5217 }
5218
5219 #[test]
5220 fn version_99_still_rejected() {
5221 let r = parse("-- allium: 99\nentity User {}");
5222 assert!(r.diagnostics.iter().any(|d|
5223 d.severity == Severity::Error && d.message.contains("unsupported")
5224 ));
5225 }
5226
5227 #[test]
5228 fn contract_typed_signature() {
5229 let src = r#"contract Codec {
5230 serialize: (value: Any) -> ByteArray
5231}"#;
5232 let r = parse_ok(src);
5233 assert_eq!(r.diagnostics.len(), 0);
5234 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5235 assert_eq!(b.kind, BlockKind::Contract);
5236 let BlockItemKind::Assignment { name, value } = &b.items[0].kind else { panic!() };
5237 assert_eq!(name.name, "serialize");
5238 assert!(matches!(value, Expr::ProjectionMap { .. }));
5239 }
5240
5241 #[test]
5242 fn contract_multi_param_signature() {
5243 let src = r#"contract Codec {
5244 serialize: (value: Any, format: String) -> ByteArray
5245}"#;
5246 let r = parse_ok(src);
5247 assert_eq!(r.diagnostics.len(), 0);
5248 }
5249
5250 #[test]
5251 fn comma_separated_entity_fields() {
5252 let src = "entity Point { x: Decimal, y: Decimal }";
5253 let r = parse_ok(src);
5254 assert_eq!(r.diagnostics.len(), 0);
5255 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5256 assert_eq!(b.items.len(), 2);
5257 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "x"));
5258 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "y"));
5259 }
5260
5261 #[test]
5262 fn comma_separated_value_fields() {
5263 let src = "value Coord { x: Integer, y: Integer }";
5264 let r = parse_ok(src);
5265 assert_eq!(r.diagnostics.len(), 0);
5266 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5267 assert_eq!(b.items.len(), 2);
5268 }
5269
5270 #[test]
5275 fn version_3_accepted() {
5276 let r = parse("-- allium: 3\nentity User {}");
5277 assert_eq!(r.module.version, Some(3));
5278 assert_eq!(r.diagnostics.len(), 0);
5279 }
5280
5281 #[test]
5282 fn transitions_block_basic() {
5283 let src = r#"-- allium: 3
5284entity Order {
5285 status: pending | confirmed | shipped | delivered | cancelled
5286
5287 transitions status {
5288 pending -> confirmed
5289 confirmed -> shipped
5290 shipped -> delivered
5291 pending -> cancelled
5292 confirmed -> cancelled
5293 terminal: delivered, cancelled
5294 }
5295}"#;
5296 let r = parse(src);
5297 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5298 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5299 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5301 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5302 };
5303 assert_eq!(graph.field.name, "status");
5304 assert_eq!(graph.edges.len(), 5);
5305 assert_eq!(graph.edges[0].from.name, "pending");
5306 assert_eq!(graph.edges[0].to.name, "confirmed");
5307 assert_eq!(graph.terminal.len(), 2);
5308 assert_eq!(graph.terminal[0].name, "delivered");
5309 assert_eq!(graph.terminal[1].name, "cancelled");
5310 }
5311
5312 #[test]
5313 fn transitions_block_no_terminal() {
5314 let src = r#"-- allium: 3
5315entity Task {
5316 status: open | closed
5317 transitions status {
5318 open -> closed
5319 }
5320}"#;
5321 let r = parse(src);
5322 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5323 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5324 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5325 panic!("expected TransitionsBlock")
5326 };
5327 assert_eq!(graph.edges.len(), 1);
5328 assert!(graph.terminal.is_empty());
5329 }
5330
5331 #[test]
5332 fn produces_emits_migration_warning() {
5333 let src = r#"-- allium: 3
5334rule ShipOrder {
5335 when: ShipOrder(order, tracking)
5336 requires: order.status = picking
5337 produces: tracking_number, shipped_at
5338 ensures: order.status = shipped
5339}"#;
5340 let r = parse(src);
5341 let warnings: Vec<_> = r.diagnostics.iter()
5342 .filter(|d| d.severity == Severity::Warning)
5343 .collect();
5344 assert!(
5345 warnings.iter().any(|d| d.message.contains("`produces:` clauses are removed")),
5346 "expected migration warning for produces, got: {:?}", warnings
5347 );
5348 }
5349
5350 #[test]
5351 fn consumes_emits_migration_warning() {
5352 let src = r#"-- allium: 3
5353rule ReadOrder {
5354 when: Check(order)
5355 consumes: warehouse_assignment
5356 ensures: order.verified = true
5357}"#;
5358 let r = parse(src);
5359 let warnings: Vec<_> = r.diagnostics.iter()
5360 .filter(|d| d.severity == Severity::Warning)
5361 .collect();
5362 assert!(
5363 warnings.iter().any(|d| d.message.contains("`consumes:` clauses are removed")),
5364 "expected migration warning for consumes, got: {:?}", warnings
5365 );
5366 }
5367
5368 #[test]
5369 fn when_clause_on_field() {
5370 let src = r#"-- allium: 3
5371entity Order {
5372 status: pending | shipped | delivered
5373 tracking_number: String when status = shipped | delivered
5374 transitions status {
5375 pending -> shipped
5376 shipped -> delivered
5377 terminal: delivered
5378 }
5379}"#;
5380 let r = parse(src);
5381 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5382 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5383 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5384 let field_with_when = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }));
5385 assert!(field_with_when.is_some(), "expected FieldWithWhen item");
5386 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &field_with_when.unwrap().kind {
5387 assert_eq!(name.name, "tracking_number");
5388 assert_eq!(when_clause.status_field.name, "status");
5389 assert_eq!(when_clause.qualifying_states.len(), 2);
5390 assert_eq!(when_clause.qualifying_states[0].name, "shipped");
5391 assert_eq!(when_clause.qualifying_states[1].name, "delivered");
5392 }
5393 }
5394
5395 #[test]
5396 fn when_clause_single_state() {
5397 let src = r#"-- allium: 3
5398entity Order {
5399 status: active | cancelled
5400 cancelled_at: Timestamp when status = cancelled
5401 transitions status {
5402 active -> cancelled
5403 terminal: cancelled
5404 }
5405}"#;
5406 let r = parse(src);
5407 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5408 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5409 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5410 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &b.items[1].kind {
5411 assert_eq!(name.name, "cancelled_at");
5412 assert_eq!(when_clause.qualifying_states.len(), 1);
5413 assert_eq!(when_clause.qualifying_states[0].name, "cancelled");
5414 } else {
5415 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5416 }
5417 }
5418
5419 #[test]
5420 fn when_clause_with_optional() {
5421 let src = r#"-- allium: 3
5422entity Order {
5423 status: active | cancelled
5424 notes: String? when status = cancelled
5425 transitions status {
5426 active -> cancelled
5427 terminal: cancelled
5428 }
5429}"#;
5430 let r = parse(src);
5431 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5432 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5433 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5434 if let BlockItemKind::FieldWithWhen { name, value, when_clause } = &b.items[1].kind {
5435 assert_eq!(name.name, "notes");
5436 assert!(matches!(value, Expr::TypeOptional { .. }), "expected TypeOptional");
5437 assert_eq!(when_clause.qualifying_states.len(), 1);
5438 } else {
5439 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5440 }
5441 }
5442
5443 #[test]
5444 fn transitions_in_json_output() {
5445 let src = r#"-- allium: 3
5446entity Order {
5447 status: pending | done
5448 transitions status {
5449 pending -> done
5450 terminal: done
5451 }
5452}"#;
5453 let r = parse(src);
5454 let json = serde_json::to_string(&r.module).unwrap();
5455 assert!(json.contains("TransitionsBlock"), "JSON should contain TransitionsBlock: {}", json);
5456 assert!(json.contains("pending"), "JSON should contain 'pending'");
5457 }
5458
5459 #[test]
5460 fn transitions_block_with_commas() {
5461 let src = r#"-- allium: 3
5462entity Order {
5463 status: a | b | c
5464 transitions status {
5465 a -> b,
5466 b -> c,
5467 terminal: c,
5468 }
5469}"#;
5470 let r = parse(src);
5471 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5472 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5473 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5474 panic!("expected TransitionsBlock")
5475 };
5476 assert_eq!(graph.edges.len(), 2);
5477 assert_eq!(graph.terminal.len(), 1);
5478 }
5479
5480 #[test]
5481 fn v3_full_entity_with_transitions_and_rule() {
5482 let src = r#"-- allium: 3
5483entity Order {
5484 status: pending | shipped | delivered
5485 tracking: String when status = shipped | delivered
5486 shipped_at: Timestamp when status = shipped | delivered
5487
5488 transitions status {
5489 pending -> shipped
5490 shipped -> delivered
5491 terminal: delivered
5492 }
5493}
5494
5495rule ShipOrder {
5496 when: ShipOrder(order, tracking)
5497 requires: order.status = pending
5498 ensures:
5499 order.status = shipped
5500 order.tracking = tracking
5501 order.shipped_at = now
5502}"#;
5503 let r = parse(src);
5504 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5505 assert_eq!(r.module.declarations.len(), 2);
5506 }
5507
5508 #[test]
5513 fn transitions_empty_block() {
5514 let src = "-- allium: 3\nentity E {\n status: a | b\n transitions status {}\n}";
5515 let r = parse(src);
5516 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5517 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5518 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5519 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5520 };
5521 assert!(graph.edges.is_empty());
5522 assert!(graph.terminal.is_empty());
5523 }
5524
5525 #[test]
5526 fn transitions_terminal_only() {
5527 let src = r#"-- allium: 3
5528entity E {
5529 status: done
5530 transitions status {
5531 terminal: done
5532 }
5533}"#;
5534 let r = parse(src);
5535 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5536 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5537 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5538 assert!(graph.edges.is_empty());
5539 assert_eq!(graph.terminal.len(), 1);
5540 assert_eq!(graph.terminal[0].name, "done");
5541 }
5542
5543 #[test]
5544 fn transitions_terminal_before_edges() {
5545 let src = r#"-- allium: 3
5546entity E {
5547 status: a | b | c
5548 transitions status {
5549 terminal: c
5550 a -> b
5551 b -> c
5552 }
5553}"#;
5554 let r = parse(src);
5555 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5556 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5557 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5558 assert_eq!(graph.edges.len(), 2);
5559 assert_eq!(graph.terminal.len(), 1);
5560 assert_eq!(graph.terminal[0].name, "c");
5561 }
5562
5563 #[test]
5564 fn transitions_self_loop() {
5565 let src = r#"-- allium: 3
5566entity E {
5567 status: running | stopped
5568 transitions status {
5569 running -> running
5570 running -> stopped
5571 terminal: stopped
5572 }
5573}"#;
5574 let r = parse(src);
5575 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5576 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5577 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5578 assert_eq!(graph.edges.len(), 2);
5579 assert_eq!(graph.edges[0].from.name, "running");
5580 assert_eq!(graph.edges[0].to.name, "running");
5581 }
5582
5583 #[test]
5584 fn transitions_single_edge() {
5585 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s { a -> b }\n}";
5586 let r = parse(src);
5587 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5588 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5589 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5590 assert_eq!(graph.field.name, "s");
5591 assert_eq!(graph.edges.len(), 1);
5592 }
5593
5594 #[test]
5595 fn transitions_multiple_terminal_values() {
5596 let src = r#"-- allium: 3
5597entity E {
5598 status: a | b | c | d | e
5599 transitions status {
5600 a -> b
5601 b -> c
5602 terminal: c, d, e
5603 }
5604}"#;
5605 let r = parse(src);
5606 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5607 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5608 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5609 assert_eq!(graph.terminal.len(), 3);
5610 }
5611
5612 #[test]
5613 fn transitions_trailing_comma_in_terminal() {
5614 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n terminal: b,\n }\n}";
5615 let r = parse(src);
5616 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5617 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5618 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5619 assert_eq!(graph.terminal.len(), 1);
5620 }
5621
5622 #[test]
5623 fn transitions_among_other_entity_items() {
5624 let src = r#"-- allium: 3
5626entity Order {
5627 status: pending | shipped | delivered
5628 customer: Customer
5629 tracking: String?
5630
5631 transitions status {
5632 pending -> shipped
5633 shipped -> delivered
5634 terminal: delivered
5635 }
5636
5637 active_items: items where status = active
5638 invariant Positive { this.total > 0 }
5639}"#;
5640 let r = parse(src);
5641 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5642 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5643 assert_eq!(b.items.len(), 6);
5645 assert!(matches!(&b.items[3].kind, BlockItemKind::TransitionsBlock(_)));
5646 assert!(matches!(&b.items[5].kind, BlockItemKind::InvariantBlock { .. }));
5647 }
5648
5649 #[test]
5650 fn transitions_error_recovery_missing_arrow() {
5651 let src = r#"-- allium: 3
5652entity E {
5653 status: a | b | c
5654 transitions status {
5655 a b
5656 b -> c
5657 }
5658}"#;
5659 let r = parse(src);
5660 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5662 "expected error for missing arrow, got: {:?}", r.diagnostics);
5663 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5665 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5666 panic!("expected TransitionsBlock")
5667 };
5668 assert_eq!(graph.edges.len(), 1, "should recover and parse second edge");
5669 assert_eq!(graph.edges[0].from.name, "b");
5670 }
5671
5672 #[test]
5673 fn transitions_field_name_preserved() {
5674 let src = "-- allium: 3\nentity E {\n phase: x | y\n transitions phase { x -> y }\n}";
5675 let r = parse(src);
5676 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5677 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5678 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5679 assert_eq!(graph.field.name, "phase");
5680 }
5681
5682 #[test]
5683 fn transitions_diamond_topology() {
5684 let src = r#"-- allium: 3
5686entity E {
5687 status: new | path_a | path_b | done
5688 transitions status {
5689 new -> path_a
5690 new -> path_b
5691 path_a -> done
5692 path_b -> done
5693 terminal: done
5694 }
5695}"#;
5696 let r = parse(src);
5697 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5698 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5699 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5700 assert_eq!(graph.edges.len(), 4);
5701 }
5702
5703 #[test]
5704 fn transitions_edge_span_is_from_to_range() {
5705 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n }\n}";
5706 let r = parse(src);
5707 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5708 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5709 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5710 let edge = &graph.edges[0];
5711 assert!(edge.span.start <= edge.from.span.start);
5713 assert!(edge.span.end >= edge.to.span.end);
5714 }
5715
5716 #[test]
5721 fn when_clause_multiple_fields() {
5722 let src = r#"-- allium: 3
5723entity Order {
5724 status: pending | shipped | delivered
5725 tracking: String when status = shipped | delivered
5726 shipped_at: Timestamp when status = shipped | delivered
5727 delivered_at: Timestamp when status = delivered
5728 transitions status {
5729 pending -> shipped
5730 shipped -> delivered
5731 terminal: delivered
5732 }
5733}"#;
5734 let r = parse(src);
5735 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5736 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5737 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5738 let when_count = b.items.iter()
5739 .filter(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }))
5740 .count();
5741 assert_eq!(when_count, 3);
5742 }
5743
5744 #[test]
5745 fn legacy_produces_consumes_skipped_with_warnings() {
5746 let src = r#"-- allium: 3
5747rule R {
5748 when: Go(x)
5749 produces: field_a
5750 consumes: field_b
5751 ensures: x.done = true
5752}"#;
5753 let r = parse(src);
5754 let warnings: Vec<_> = r.diagnostics.iter()
5755 .filter(|d| d.severity == Severity::Warning)
5756 .collect();
5757 assert!(warnings.len() >= 2, "expected at least 2 migration warnings, got {}", warnings.len());
5758 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5760 assert!(
5761 !b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. })),
5762 "legacy produces/consumes should not become FieldWithWhen"
5763 );
5764 }
5765
5766 #[test]
5771 fn v3_entity_with_transitions_and_invariant() {
5772 let src = r#"-- allium: 3
5773entity Account {
5774 status: open | frozen | closed
5775 balance: Decimal
5776
5777 transitions status {
5778 open -> frozen
5779 frozen -> open
5780 open -> closed
5781 frozen -> closed
5782 terminal: closed
5783 }
5784
5785 invariant NonNegative { this.balance >= 0 }
5786}"#;
5787 let r = parse(src);
5788 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5789 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5790 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::TransitionsBlock(_))));
5791 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. })));
5792 }
5793
5794 #[test]
5795 fn v3_rule_with_multiple_ensures() {
5796 let src = r#"-- allium: 3
5797rule CompleteOrder {
5798 when: Complete(order)
5799 requires: order.status = shipped
5800 ensures: order.status = delivered
5801 ensures: order.completed_at = now
5802 ensures: order.receipt_number = generate_receipt()
5803}"#;
5804 let r = parse(src);
5805 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5806 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5807 let ensures_count = b.items.iter()
5808 .filter(|i| matches!(&i.kind, BlockItemKind::Clause { keyword, .. } if keyword == "ensures"))
5809 .count();
5810 assert_eq!(ensures_count, 3);
5811 }
5812
5813 #[test]
5814 fn v3_rule_with_if_block() {
5815 let src = r#"-- allium: 3
5816rule Cancel {
5817 when: Cancel(order, reason)
5818 requires: order.status != delivered
5819 ensures:
5820 order.status = cancelled
5821 order.cancelled_at = now
5822 if reason = customer_request:
5823 order.cancelled_by = order.customer
5824}"#;
5825 let r = parse(src);
5826 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5827 }
5828
5829 #[test]
5830 fn v3_complete_lifecycle_spec() {
5831 let src = r#"-- allium: 3
5832
5833entity Subscription {
5834 status: trial | active | past_due | cancelled
5835 started_at: Timestamp when status = active | past_due | cancelled
5836 cancelled_at: Timestamp when status = cancelled
5837 balance: Decimal
5838
5839 transitions status {
5840 trial -> active
5841 active -> past_due
5842 past_due -> active
5843 active -> cancelled
5844 past_due -> cancelled
5845 terminal: cancelled
5846 }
5847
5848 invariant NonNegative { this.balance >= 0 }
5849}
5850
5851config {
5852 trial_period: Duration = 14.days
5853}
5854
5855rule ActivateSubscription {
5856 when: Activate(sub)
5857 requires: sub.status = trial
5858 ensures:
5859 sub.status = active
5860 sub.started_at = now
5861}
5862
5863rule CancelSubscription {
5864 when: Cancel(sub)
5865 requires: sub.status != cancelled
5866 ensures:
5867 sub.status = cancelled
5868 sub.cancelled_at = now
5869}
5870
5871invariant AllCancelledHaveTimestamp {
5872 for sub in Subscriptions where status = cancelled:
5873 sub.cancelled_at != null
5874}
5875
5876surface SubscriptionDashboard {
5877 facing user: User
5878 context sub: Subscription where owner = user
5879 exposes:
5880 sub.status
5881 sub.balance
5882 provides:
5883 Cancel(sub) when sub.status != cancelled
5884}
5885"#;
5886 let r = parse(src);
5887 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5888 assert_eq!(r.module.declarations.len(), 6);
5890 }
5891
5892 #[test]
5893 fn v3_produces_consumes_are_field_names_in_entities() {
5894 let src = r#"-- allium: 3
5896entity Factory {
5897 produces: widget_a
5898 consumes: raw_material
5899}"#;
5900 let r = parse(src);
5901 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5902 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5903 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "produces"));
5904 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "consumes"));
5905 }
5906
5907 #[test]
5908 fn v3_legacy_produces_consumes_emit_warnings_in_rules() {
5909 let src = r#"-- allium: 3
5910rule Ship {
5911 when: Ship(order)
5912 produces: tracking_number
5913 consumes: warehouse
5914 ensures: order.status = shipped
5915}"#;
5916 let r = parse(src);
5917 let warnings: Vec<_> = r.diagnostics.iter()
5918 .filter(|d| d.severity == Severity::Warning)
5919 .collect();
5920 assert!(warnings.len() >= 2, "expected migration warnings, got {:?}", warnings);
5921 }
5922
5923 #[test]
5924 fn v3_version_preserved_in_module() {
5925 let src = "-- allium: 3\nentity E {}";
5926 let r = parse(src);
5927 assert_eq!(r.module.version, Some(3));
5928 }
5929
5930 #[test]
5931 fn v3_version_4_still_rejected() {
5932 let src = "-- allium: 4\nentity E {}";
5933 let r = parse(src);
5934 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error
5935 && d.message.contains("unsupported")));
5936 }
5937
5938 #[test]
5943 fn backtick_in_named_enum() {
5944 let src = "-- allium: 3\nenum Locale { en | fr | `de-CH-1996` | `no-cache` }";
5945 let r = parse(src);
5946 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5947 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5948 assert_eq!(b.items.len(), 4);
5949 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[0].kind else { panic!() };
5951 assert_eq!(name.name, "en");
5952 assert!(!backtick_quoted);
5953 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[2].kind else { panic!() };
5955 assert_eq!(name.name, "de-CH-1996");
5956 assert!(backtick_quoted);
5957 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[3].kind else { panic!() };
5959 assert_eq!(name.name, "no-cache");
5960 assert!(backtick_quoted);
5961 }
5962
5963 #[test]
5964 fn backtick_in_inline_enum() {
5965 let src = "-- allium: 3\nentity E { cache: `no-cache` | `no-store` | `public` }";
5966 let r = parse(src);
5967 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5968 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5969 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
5970 assert!(matches!(value, Expr::Pipe { .. }));
5972 }
5973
5974 #[test]
5975 fn backtick_in_comparison() {
5976 let src = r#"-- allium: 3
5977rule R {
5978 when: Check(item)
5979 requires: item.locale = `de-CH-1996`
5980 ensures: Done()
5981}"#;
5982 let r = parse(src);
5983 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5984 }
5985
5986 #[test]
5987 fn backtick_mixed_with_unquoted() {
5988 let src = "-- allium: 3\nenum CacheDirective { `no-cache` | `no-store` | public | private }";
5989 let r = parse(src);
5990 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5991 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5992 assert_eq!(b.items.len(), 4);
5993 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[0].kind else { panic!() };
5994 assert!(backtick_quoted);
5995 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[2].kind else { panic!() };
5996 assert!(!backtick_quoted);
5997 }
5998
5999 #[test]
6004 fn v3_lifecycle_fixture() {
6005 let src = include_str!("../tests/fixtures/v3-lifecycle.allium");
6006 let r = parse(src);
6007 let errors: Vec<_> = r.diagnostics.iter()
6008 .filter(|d| d.severity == Severity::Error)
6009 .collect();
6010 assert_eq!(
6011 errors.len(),
6012 0,
6013 "expected no errors in v3 lifecycle fixture, got: {:?}",
6014 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
6015 );
6016 }
6017}