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 }
1477 self.parse_assign_or_clause_item(start)
1479 }
1480 }
1481
1482 fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
1483 let mut params = Vec::new();
1484 if !self.at(TokenKind::RParen) {
1485 params.push(self.parse_ident_in("parameter name")?);
1486 while self.eat(TokenKind::Comma).is_some() {
1487 params.push(self.parse_ident_in("parameter name")?);
1488 }
1489 }
1490 Some(params)
1491 }
1492
1493 fn parse_for_binding(&mut self) -> Option<ForBinding> {
1495 if self.at(TokenKind::LParen) {
1496 let start = self.advance().span; let mut idents = Vec::new();
1498 idents.push(self.parse_ident_in("loop variable")?);
1499 while self.eat(TokenKind::Comma).is_some() {
1500 idents.push(self.parse_ident_in("loop variable")?);
1501 }
1502 let end = self.expect(TokenKind::RParen)?.span;
1503 Some(ForBinding::Destructured(idents, start.merge(end)))
1504 } else {
1505 let ident = self.parse_ident_in("loop variable")?;
1506 Some(ForBinding::Single(ident))
1507 }
1508 }
1509
1510 fn parse_clause_value_maybe_binding(
1514 &mut self,
1515 clause_start: Span,
1516 allow_binding: bool,
1517 ) -> Option<Expr> {
1518 if allow_binding
1519 && self.peek_kind().is_word()
1520 && self.peek_at(1).kind == TokenKind::Colon
1521 {
1522 let clause_line = self.line_of(clause_start);
1525 let next_line = self.line_of(self.peek().span);
1526 let colon_is_block_item = next_line > clause_line
1527 && self.peek_at(2).kind != TokenKind::Eof
1528 && self.line_of(self.peek_at(2).span) == next_line;
1529
1530 if next_line == clause_line || colon_is_block_item {
1531 let name = self.parse_ident_in("binding name")?;
1532 self.advance(); let inner = self.parse_clause_value(clause_start)?;
1534 return Some(Expr::Binding {
1535 span: name.span.merge(inner.span()),
1536 name,
1537 value: Box::new(inner),
1538 });
1539 }
1540 }
1541 self.parse_clause_value(clause_start)
1542 }
1543
1544 fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1547 let clause_line = self.line_of(clause_start);
1548 let next = self.peek();
1549 let next_line = self.line_of(next.span);
1550
1551 if next_line > clause_line {
1552 let base_col = self.col_of(next.span);
1557 let clause_col = self.col_of(clause_start);
1558 if base_col <= clause_col {
1559 return Some(Expr::Block {
1560 span: clause_start,
1561 items: Vec::new(),
1562 });
1563 }
1564 self.parse_indented_block(base_col)
1565 } else {
1566 self.parse_expr(0)
1568 }
1569 }
1570
1571 fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1574 let start = self.peek().span;
1575 let mut items = Vec::new();
1576
1577 while !self.at_eof()
1578 && !self.at(TokenKind::RBrace)
1579 && self.col_of(self.peek().span) >= base_col
1580 {
1581 if self.at(TokenKind::Let) {
1583 let let_start = self.advance().span;
1584 if let Some(name) = self.parse_ident_in("binding name") {
1585 if self.expect(TokenKind::Eq).is_some() {
1586 if let Some(value) = self.parse_expr(0) {
1587 items.push(Expr::LetExpr {
1588 span: let_start.merge(value.span()),
1589 name,
1590 value: Box::new(value),
1591 });
1592 continue;
1593 }
1594 }
1595 }
1596 break;
1597 }
1598
1599 if let Some(expr) = self.parse_expr(0) {
1600 items.push(expr);
1601 } else {
1602 self.advance();
1603 break;
1604 }
1605 }
1606
1607 if items.len() == 1 {
1608 Some(items.pop().unwrap())
1609 } else {
1610 let end = items.last().map(|e| e.span()).unwrap_or(start);
1611 Some(Expr::Block {
1612 span: start.merge(end),
1613 items,
1614 })
1615 }
1616 }
1617}
1618
1619const BP_LAMBDA: u8 = 4;
1625const BP_WHEN_GUARD: u8 = 5;
1626const BP_PROJECTION: u8 = 6;
1627const BP_WITH_WHERE: u8 = 7;
1628const BP_IMPLIES: u8 = 8;
1629const BP_OR: u8 = 10;
1630const BP_AND: u8 = 20;
1631const BP_COMPARE: u8 = 30;
1632const BP_TRANSITION: u8 = 32;
1633const BP_NULL_COALESCE: u8 = 40;
1634const BP_ADD: u8 = 50;
1635const BP_MUL: u8 = 60;
1636const BP_PIPE: u8 = 65;
1637const BP_PREFIX: u8 = 70;
1638const BP_POSTFIX: u8 = 80;
1639
1640impl<'s> Parser<'s> {
1641 pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1642 let mut lhs = self.parse_prefix()?;
1643
1644 loop {
1645 if let Some((l_bp, r_bp)) = self.infix_bp() {
1646 if l_bp < min_bp {
1647 break;
1648 }
1649 lhs = self.parse_infix(lhs, r_bp)?;
1650 } else if let Some(l_bp) = self.postfix_bp() {
1651 if l_bp < min_bp {
1652 break;
1653 }
1654 lhs = self.parse_postfix(lhs)?;
1655 } else {
1656 break;
1657 }
1658 }
1659
1660 Some(lhs)
1661 }
1662
1663 fn parse_prefix(&mut self) -> Option<Expr> {
1666 match self.peek_kind() {
1667 TokenKind::Not => {
1668 let start = self.advance().span;
1669 if self.at(TokenKind::Exists) {
1670 self.advance();
1671 let operand = self.parse_expr(BP_PREFIX)?;
1672 Some(Expr::NotExists {
1673 span: start.merge(operand.span()),
1674 operand: Box::new(operand),
1675 })
1676 } else {
1677 let operand = self.parse_expr(BP_PREFIX)?;
1678 Some(Expr::Not {
1679 span: start.merge(operand.span()),
1680 operand: Box::new(operand),
1681 })
1682 }
1683 }
1684 TokenKind::Exists => {
1685 let next = self.peek_at(1).kind;
1688 if matches!(
1689 next,
1690 TokenKind::RParen
1691 | TokenKind::RBrace
1692 | TokenKind::RBracket
1693 | TokenKind::Comma
1694 | TokenKind::Eof
1695 ) {
1696 let id = self.parse_ident()?;
1697 return Some(Expr::Ident(id));
1698 }
1699 let start = self.advance().span;
1700 let operand = self.parse_expr(BP_PREFIX)?;
1701 Some(Expr::Exists {
1702 span: start.merge(operand.span()),
1703 operand: Box::new(operand),
1704 })
1705 }
1706 TokenKind::If => self.parse_if_expr(),
1707 TokenKind::For => self.parse_for_expr(),
1708 TokenKind::LBrace => self.parse_brace_expr(),
1709 TokenKind::LBracket => {
1710 let t = self.advance();
1711 self.error(t.span, "list literals `[...]` are not supported; use `Set<T>` type annotation or `{...}` set literal");
1712 None
1713 }
1714 TokenKind::LParen => self.parse_paren_expr(),
1715 TokenKind::Number => {
1716 let t = self.advance();
1717 Some(Expr::NumberLiteral {
1718 span: t.span,
1719 value: self.text(t.span).to_string(),
1720 })
1721 }
1722 TokenKind::Duration => {
1723 let t = self.advance();
1724 Some(Expr::DurationLiteral {
1725 span: t.span,
1726 value: self.text(t.span).to_string(),
1727 })
1728 }
1729 TokenKind::String => {
1730 let sl = self.parse_string()?;
1731 Some(Expr::StringLiteral(sl))
1732 }
1733 TokenKind::BacktickLiteral => {
1734 let t = self.advance();
1735 let raw = self.text(t.span);
1736 let value = raw[1..raw.len() - 1].to_string();
1738 Some(Expr::BacktickLiteral {
1739 span: t.span,
1740 value,
1741 })
1742 }
1743 TokenKind::True => {
1744 let t = self.advance();
1745 Some(Expr::BoolLiteral {
1746 span: t.span,
1747 value: true,
1748 })
1749 }
1750 TokenKind::False => {
1751 let t = self.advance();
1752 Some(Expr::BoolLiteral {
1753 span: t.span,
1754 value: false,
1755 })
1756 }
1757 TokenKind::Null => {
1758 let t = self.advance();
1759 Some(Expr::Null { span: t.span })
1760 }
1761 TokenKind::Now => {
1762 let t = self.advance();
1763 Some(Expr::Now { span: t.span })
1764 }
1765 TokenKind::This => {
1766 let t = self.advance();
1767 Some(Expr::This { span: t.span })
1768 }
1769 TokenKind::Within => {
1770 let t = self.advance();
1771 Some(Expr::Within { span: t.span })
1772 }
1773 k if k.is_word() => {
1774 let id = self.parse_ident()?;
1775 Some(Expr::Ident(id))
1776 }
1777 TokenKind::Star => {
1778 let t = self.advance();
1780 Some(Expr::Ident(Ident {
1781 span: t.span,
1782 name: "*".into(),
1783 }))
1784 }
1785 TokenKind::Minus => {
1786 let start = self.advance().span;
1788 let operand = self.parse_expr(BP_PREFIX)?;
1789 Some(Expr::BinaryOp {
1790 span: start.merge(operand.span()),
1791 left: Box::new(Expr::NumberLiteral {
1792 span: start,
1793 value: "0".into(),
1794 }),
1795 op: BinaryOp::Sub,
1796 right: Box::new(operand),
1797 })
1798 }
1799 _ => {
1800 self.error(
1801 self.peek().span,
1802 format!(
1803 "expected expression (identifier, number, string, true/false, null, \
1804 if/for/not/exists, '(', '{{', '['), found {}",
1805 self.peek_kind(),
1806 ),
1807 );
1808 None
1809 }
1810 }
1811 }
1812
1813 fn infix_bp(&self) -> Option<(u8, u8)> {
1816 match self.peek_kind() {
1817 TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1820 TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1821 TokenKind::Implies => Some((BP_IMPLIES, BP_IMPLIES - 1)), TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1823 TokenKind::And => Some((BP_AND, BP_AND + 1)),
1824 TokenKind::Eq | TokenKind::BangEq => {
1825 Some((BP_COMPARE, BP_COMPARE + 1))
1826 }
1827 TokenKind::Lt => {
1828 if self.pos > 0 {
1831 let prev = self.tokens[self.pos - 1];
1832 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1833 return None;
1834 }
1835 }
1836 Some((BP_COMPARE, BP_COMPARE + 1))
1837 }
1838 TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1839 Some((BP_COMPARE, BP_COMPARE + 1))
1840 }
1841 TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1842 TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1844 Some((BP_COMPARE, BP_COMPARE + 1))
1845 }
1846 TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1847 TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1848 TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1849 TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1850 TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1851 TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1852 TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1853 TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1854 _ => None,
1855 }
1856 }
1857
1858 fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1859 let op_tok = self.advance();
1860 match op_tok.kind {
1861 TokenKind::FatArrow => {
1862 let body = self.parse_expr(r_bp)?;
1863 Some(Expr::Lambda {
1864 span: lhs.span().merge(body.span()),
1865 param: Box::new(lhs),
1866 body: Box::new(body),
1867 })
1868 }
1869 TokenKind::Pipe => {
1870 let rhs = self.parse_expr(r_bp)?;
1871 Some(Expr::Pipe {
1872 span: lhs.span().merge(rhs.span()),
1873 left: Box::new(lhs),
1874 right: Box::new(rhs),
1875 })
1876 }
1877 TokenKind::Implies => {
1878 let rhs = self.parse_expr(r_bp)?;
1879 Some(Expr::LogicalOp {
1880 span: lhs.span().merge(rhs.span()),
1881 left: Box::new(lhs),
1882 op: LogicalOp::Implies,
1883 right: Box::new(rhs),
1884 })
1885 }
1886 TokenKind::Or => {
1887 let rhs = self.parse_expr(r_bp)?;
1888 Some(Expr::LogicalOp {
1889 span: lhs.span().merge(rhs.span()),
1890 left: Box::new(lhs),
1891 op: LogicalOp::Or,
1892 right: Box::new(rhs),
1893 })
1894 }
1895 TokenKind::And => {
1896 let rhs = self.parse_expr(r_bp)?;
1897 Some(Expr::LogicalOp {
1898 span: lhs.span().merge(rhs.span()),
1899 left: Box::new(lhs),
1900 op: LogicalOp::And,
1901 right: Box::new(rhs),
1902 })
1903 }
1904 TokenKind::Eq => {
1905 let rhs = self.parse_expr(r_bp)?;
1906 Some(Expr::Comparison {
1907 span: lhs.span().merge(rhs.span()),
1908 left: Box::new(lhs),
1909 op: ComparisonOp::Eq,
1910 right: Box::new(rhs),
1911 })
1912 }
1913 TokenKind::BangEq => {
1914 let rhs = self.parse_expr(r_bp)?;
1915 Some(Expr::Comparison {
1916 span: lhs.span().merge(rhs.span()),
1917 left: Box::new(lhs),
1918 op: ComparisonOp::NotEq,
1919 right: Box::new(rhs),
1920 })
1921 }
1922 TokenKind::Lt => {
1923 let rhs = self.parse_expr(r_bp)?;
1924 Some(Expr::Comparison {
1925 span: lhs.span().merge(rhs.span()),
1926 left: Box::new(lhs),
1927 op: ComparisonOp::Lt,
1928 right: Box::new(rhs),
1929 })
1930 }
1931 TokenKind::LtEq => {
1932 let rhs = self.parse_expr(r_bp)?;
1933 Some(Expr::Comparison {
1934 span: lhs.span().merge(rhs.span()),
1935 left: Box::new(lhs),
1936 op: ComparisonOp::LtEq,
1937 right: Box::new(rhs),
1938 })
1939 }
1940 TokenKind::Gt => {
1941 let rhs = self.parse_expr(r_bp)?;
1942 Some(Expr::Comparison {
1943 span: lhs.span().merge(rhs.span()),
1944 left: Box::new(lhs),
1945 op: ComparisonOp::Gt,
1946 right: Box::new(rhs),
1947 })
1948 }
1949 TokenKind::GtEq => {
1950 let rhs = self.parse_expr(r_bp)?;
1951 Some(Expr::Comparison {
1952 span: lhs.span().merge(rhs.span()),
1953 left: Box::new(lhs),
1954 op: ComparisonOp::GtEq,
1955 right: Box::new(rhs),
1956 })
1957 }
1958 TokenKind::In => {
1959 let rhs = self.parse_expr(r_bp)?;
1960 Some(Expr::In {
1961 span: lhs.span().merge(rhs.span()),
1962 element: Box::new(lhs),
1963 collection: Box::new(rhs),
1964 })
1965 }
1966 TokenKind::Not => {
1967 self.expect(TokenKind::In)?;
1969 let rhs = self.parse_expr(r_bp)?;
1970 Some(Expr::NotIn {
1971 span: lhs.span().merge(rhs.span()),
1972 element: Box::new(lhs),
1973 collection: Box::new(rhs),
1974 })
1975 }
1976 TokenKind::Where => {
1977 let rhs = self.parse_expr(r_bp)?;
1978 Some(Expr::Where {
1979 span: lhs.span().merge(rhs.span()),
1980 source: Box::new(lhs),
1981 condition: Box::new(rhs),
1982 })
1983 }
1984 TokenKind::With => {
1985 let rhs = self.parse_expr(r_bp)?;
1986 Some(Expr::With {
1987 span: lhs.span().merge(rhs.span()),
1988 source: Box::new(lhs),
1989 predicate: Box::new(rhs),
1990 })
1991 }
1992 TokenKind::QuestionQuestion => {
1993 let rhs = self.parse_expr(r_bp)?;
1994 Some(Expr::NullCoalesce {
1995 span: lhs.span().merge(rhs.span()),
1996 left: Box::new(lhs),
1997 right: Box::new(rhs),
1998 })
1999 }
2000 TokenKind::Plus => {
2001 let rhs = self.parse_expr(r_bp)?;
2002 Some(Expr::BinaryOp {
2003 span: lhs.span().merge(rhs.span()),
2004 left: Box::new(lhs),
2005 op: BinaryOp::Add,
2006 right: Box::new(rhs),
2007 })
2008 }
2009 TokenKind::Minus => {
2010 let rhs = self.parse_expr(r_bp)?;
2011 Some(Expr::BinaryOp {
2012 span: lhs.span().merge(rhs.span()),
2013 left: Box::new(lhs),
2014 op: BinaryOp::Sub,
2015 right: Box::new(rhs),
2016 })
2017 }
2018 TokenKind::Star => {
2019 let rhs = self.parse_expr(r_bp)?;
2020 Some(Expr::BinaryOp {
2021 span: lhs.span().merge(rhs.span()),
2022 left: Box::new(lhs),
2023 op: BinaryOp::Mul,
2024 right: Box::new(rhs),
2025 })
2026 }
2027 TokenKind::Slash => {
2028 if let Expr::Ident(ref id) = lhs {
2033 if self.peek_kind().is_word() {
2034 let next_text = self.text(self.peek().span);
2035 let is_qualified = next_text
2036 .chars()
2037 .next()
2038 .is_some_and(|c| c.is_uppercase())
2039 || matches!(
2040 self.peek_kind(),
2041 TokenKind::Config | TokenKind::Entity | TokenKind::Value
2042 );
2043 if is_qualified {
2044 let name_tok = self.advance();
2045 return Some(Expr::QualifiedName(QualifiedName {
2046 span: lhs.span().merge(name_tok.span),
2047 qualifier: Some(id.name.clone()),
2048 name: self.text(name_tok.span).to_string(),
2049 }));
2050 }
2051 }
2052 }
2053 let rhs = self.parse_expr(r_bp)?;
2054 Some(Expr::BinaryOp {
2055 span: lhs.span().merge(rhs.span()),
2056 left: Box::new(lhs),
2057 op: BinaryOp::Div,
2058 right: Box::new(rhs),
2059 })
2060 }
2061 TokenKind::ThinArrow => {
2062 let field = self.parse_ident_in("projection field")?;
2063 Some(Expr::ProjectionMap {
2064 span: lhs.span().merge(field.span),
2065 source: Box::new(lhs),
2066 field,
2067 })
2068 }
2069 TokenKind::TransitionsTo => {
2070 let rhs = self.parse_expr(r_bp)?;
2071 Some(Expr::TransitionsTo {
2072 span: lhs.span().merge(rhs.span()),
2073 subject: Box::new(lhs),
2074 new_state: Box::new(rhs),
2075 })
2076 }
2077 TokenKind::Becomes => {
2078 let rhs = self.parse_expr(r_bp)?;
2079 Some(Expr::Becomes {
2080 span: lhs.span().merge(rhs.span()),
2081 subject: Box::new(lhs),
2082 new_state: Box::new(rhs),
2083 })
2084 }
2085 TokenKind::When => {
2086 let rhs = self.parse_expr(r_bp)?;
2088 Some(Expr::WhenGuard {
2089 span: lhs.span().merge(rhs.span()),
2090 action: Box::new(lhs),
2091 condition: Box::new(rhs),
2092 })
2093 }
2094 _ => {
2095 self.error(
2096 op_tok.span,
2097 format!("unexpected infix operator {}", op_tok.kind),
2098 );
2099 None
2100 }
2101 }
2102 }
2103
2104 fn postfix_bp(&self) -> Option<u8> {
2107 match self.peek_kind() {
2108 TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
2109 TokenKind::QuestionMark => Some(BP_POSTFIX),
2110 TokenKind::Lt => {
2113 if self.pos > 0 {
2114 let prev = self.tokens[self.pos - 1];
2115 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
2118 return Some(BP_POSTFIX);
2119 }
2120 }
2121 None
2122 }
2123 TokenKind::LParen => Some(BP_POSTFIX),
2124 TokenKind::LBrace => {
2125 let next = self.peek();
2131 let prev_end = if self.pos > 0 {
2132 self.tokens[self.pos - 1].span.end
2133 } else {
2134 0
2135 };
2136 if self.line_of(Span::new(prev_end, prev_end))
2138 == self.line_of(next.span)
2139 {
2140 Some(BP_POSTFIX)
2141 } else {
2142 None
2143 }
2144 }
2145 _ => None,
2146 }
2147 }
2148
2149 fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
2150 match self.peek_kind() {
2151 TokenKind::QuestionMark => {
2152 let end = self.advance().span;
2153 Some(Expr::TypeOptional {
2154 span: lhs.span().merge(end),
2155 inner: Box::new(lhs),
2156 })
2157 }
2158 TokenKind::Lt => {
2159 self.advance(); let mut args = Vec::new();
2162 while !self.at(TokenKind::Gt) && !self.at_eof() {
2164 args.push(self.parse_expr(BP_COMPARE + 1)?);
2165 self.eat(TokenKind::Comma);
2166 }
2167 let end = self.expect(TokenKind::Gt)?.span;
2168 Some(Expr::GenericType {
2169 span: lhs.span().merge(end),
2170 name: Box::new(lhs),
2171 args,
2172 })
2173 }
2174 TokenKind::Dot => {
2175 self.advance();
2176 let field = self.parse_ident_in("field name")?;
2177 Some(Expr::MemberAccess {
2178 span: lhs.span().merge(field.span),
2179 object: Box::new(lhs),
2180 field,
2181 })
2182 }
2183 TokenKind::QuestionDot => {
2184 self.advance();
2185 let field = self.parse_ident_in("field name")?;
2186 Some(Expr::OptionalAccess {
2187 span: lhs.span().merge(field.span),
2188 object: Box::new(lhs),
2189 field,
2190 })
2191 }
2192 TokenKind::LParen => {
2193 self.advance();
2194 let args = self.parse_call_args()?;
2195 let end = self.expect(TokenKind::RParen)?.span;
2196 Some(Expr::Call {
2197 span: lhs.span().merge(end),
2198 function: Box::new(lhs),
2199 args,
2200 })
2201 }
2202 TokenKind::LBrace => {
2203 self.advance();
2204 let fields = self.parse_join_fields()?;
2205 let end = self.expect(TokenKind::RBrace)?.span;
2206 Some(Expr::JoinLookup {
2207 span: lhs.span().merge(end),
2208 entity: Box::new(lhs),
2209 fields,
2210 })
2211 }
2212 _ => None,
2213 }
2214 }
2215
2216 fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
2219 let mut args = Vec::new();
2220 while !self.at(TokenKind::RParen) && !self.at_eof() {
2221 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2223 let name = self.parse_ident_in("argument name")?;
2224 self.advance(); let value = self.parse_expr(0)?;
2226 args.push(CallArg::Named(NamedArg {
2227 span: name.span.merge(value.span()),
2228 name,
2229 value,
2230 }));
2231 } else {
2232 let expr = self.parse_expr(0)?;
2233 args.push(CallArg::Positional(expr));
2234 }
2235 self.eat(TokenKind::Comma);
2236 }
2237 Some(args)
2238 }
2239
2240 fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
2243 let mut fields = Vec::new();
2244 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2245 let field = self.parse_ident_in("join field name")?;
2246 let value = if self.eat(TokenKind::Colon).is_some() {
2247 Some(self.parse_expr(0)?)
2248 } else {
2249 None
2250 };
2251 fields.push(JoinField {
2252 span: field.span.merge(
2253 value
2254 .as_ref()
2255 .map(|v| v.span())
2256 .unwrap_or(field.span),
2257 ),
2258 field,
2259 value,
2260 });
2261 self.eat(TokenKind::Comma);
2262 }
2263 Some(fields)
2264 }
2265
2266 fn parse_if_expr(&mut self) -> Option<Expr> {
2269 let start = self.advance().span; let mut branches = Vec::new();
2271
2272 let condition = self.parse_expr(0)?;
2274 self.expect(TokenKind::Colon)?;
2275 let body = self.parse_branch_body(start)?;
2276 branches.push(CondBranch {
2277 span: start.merge(body.span()),
2278 condition,
2279 body,
2280 });
2281
2282 let mut else_body = None;
2284 while self.at(TokenKind::Else) {
2285 let else_tok = self.advance();
2286 if self.at(TokenKind::If) {
2287 let if_start = self.advance().span;
2288 let cond = self.parse_expr(0)?;
2289 self.expect(TokenKind::Colon)?;
2290 let body = self.parse_branch_body(else_tok.span)?;
2291 branches.push(CondBranch {
2292 span: if_start.merge(body.span()),
2293 condition: cond,
2294 body,
2295 });
2296 } else {
2297 self.expect(TokenKind::Colon)?;
2298 let body = self.parse_branch_body(else_tok.span)?;
2299 else_body = Some(Box::new(body));
2300 break;
2301 }
2302 }
2303
2304 let end = else_body
2305 .as_ref()
2306 .map(|b| b.span())
2307 .or_else(|| branches.last().map(|b| b.body.span()))
2308 .unwrap_or(start);
2309
2310 Some(Expr::Conditional {
2311 span: start.merge(end),
2312 branches,
2313 else_body,
2314 })
2315 }
2316
2317 fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
2318 let keyword_line = self.line_of(keyword_span);
2319 let next_line = self.line_of(self.peek().span);
2320
2321 if next_line > keyword_line {
2322 let base_col = self.col_of(self.peek().span);
2323 self.parse_indented_block(base_col)
2324 } else {
2325 self.parse_expr(0)
2326 }
2327 }
2328
2329 fn parse_for_expr(&mut self) -> Option<Expr> {
2332 let start = self.advance().span; let binding = self.parse_for_binding()?;
2334 self.expect(TokenKind::In)?;
2335
2336 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
2338
2339 let filter = if self.eat(TokenKind::Where).is_some() {
2340 Some(Box::new(self.parse_expr(0)?))
2342 } else {
2343 None
2344 };
2345
2346 self.expect(TokenKind::Colon)?;
2347 let body = self.parse_branch_body(start)?;
2348
2349 Some(Expr::For {
2350 span: start.merge(body.span()),
2351 binding,
2352 collection: Box::new(collection),
2353 filter,
2354 body: Box::new(body),
2355 })
2356 }
2357
2358 fn parse_brace_expr(&mut self) -> Option<Expr> {
2361 let start = self.advance().span; if self.at(TokenKind::RBrace) {
2364 let end = self.advance().span;
2365 return Some(Expr::SetLiteral {
2366 span: start.merge(end),
2367 elements: Vec::new(),
2368 });
2369 }
2370
2371 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2373 return self.parse_object_literal(start);
2374 }
2375
2376 self.parse_set_literal(start)
2378 }
2379
2380
2381 fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
2382 let mut fields = Vec::new();
2383 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2384 let name = self.parse_ident_in("field name")?;
2385 self.expect(TokenKind::Colon)?;
2386 let value = self.parse_expr(0)?;
2387 fields.push(NamedArg {
2388 span: name.span.merge(value.span()),
2389 name,
2390 value,
2391 });
2392 self.eat(TokenKind::Comma);
2393 }
2394 let end = self.expect(TokenKind::RBrace)?.span;
2395 Some(Expr::ObjectLiteral {
2396 span: start.merge(end),
2397 fields,
2398 })
2399 }
2400
2401 fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
2402 let mut elements = Vec::new();
2403 while !self.at(TokenKind::RBrace) && !self.at_eof() {
2404 elements.push(self.parse_expr(0)?);
2405 self.eat(TokenKind::Comma);
2406 }
2407 let end = self.expect(TokenKind::RBrace)?.span;
2408 Some(Expr::SetLiteral {
2409 span: start.merge(end),
2410 elements,
2411 })
2412 }
2413
2414 fn parse_paren_expr(&mut self) -> Option<Expr> {
2417 let start = self.advance().span; if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2421 let mut bindings = Vec::new();
2422 while !self.at(TokenKind::RParen) && !self.at_eof() {
2423 let name = self.parse_ident_in("parameter name")?;
2424 self.expect(TokenKind::Colon)?;
2425 let value = self.parse_expr(0)?;
2426 bindings.push(Expr::Binding {
2427 span: name.span.merge(value.span()),
2428 name,
2429 value: Box::new(value),
2430 });
2431 self.eat(TokenKind::Comma);
2432 }
2433 self.expect(TokenKind::RParen)?;
2434 if bindings.len() == 1 {
2435 return Some(bindings.into_iter().next().unwrap());
2436 }
2437 let span = start.merge(bindings.last().unwrap().span());
2438 return Some(Expr::Block {
2439 span,
2440 items: bindings,
2441 });
2442 }
2443
2444 let expr = self.parse_expr(0)?;
2445 self.expect(TokenKind::RParen)?;
2446 Some(expr)
2447 }
2448}
2449
2450#[cfg(test)]
2455mod tests {
2456 use super::*;
2457 use crate::diagnostic::Severity;
2458
2459 fn parse_ok(src: &str) -> ParseResult {
2460 let owned;
2463 let input = if src.starts_with("-- allium:") {
2464 src
2465 } else {
2466 owned = format!("-- allium: 1\n{src}");
2467 &owned
2468 };
2469 let result = parse(input);
2470 if !result.diagnostics.is_empty() {
2471 for d in &result.diagnostics {
2472 eprintln!(
2473 " [{:?}] {} ({}..{})",
2474 d.severity, d.message, d.span.start, d.span.end
2475 );
2476 }
2477 }
2478 result
2479 }
2480
2481 #[test]
2482 fn version_marker() {
2483 let r = parse_ok("-- allium: 1\n");
2484 assert_eq!(r.module.version, Some(1));
2485 assert_eq!(r.diagnostics.len(), 0);
2486 }
2487
2488 #[test]
2489 fn version_missing_warns() {
2490 let r = parse("entity User {}");
2491 assert_eq!(r.module.version, None);
2492 assert_eq!(r.diagnostics.len(), 1);
2493 assert_eq!(r.diagnostics[0].severity, Severity::Warning);
2494 assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
2495 }
2496
2497 #[test]
2498 fn version_unsupported_errors() {
2499 let r = parse("-- allium: 99\nentity User {}");
2500 assert_eq!(r.module.version, Some(99));
2501 assert!(r.diagnostics.iter().any(|d|
2502 d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
2503 ), "expected unsupported version error, got: {:?}", r.diagnostics);
2504 }
2505
2506 #[test]
2507 fn empty_entity() {
2508 let r = parse_ok("entity User {}");
2509 assert_eq!(r.diagnostics.len(), 0);
2510 assert_eq!(r.module.declarations.len(), 1);
2511 match &r.module.declarations[0] {
2512 Decl::Block(b) => {
2513 assert_eq!(b.kind, BlockKind::Entity);
2514 assert_eq!(b.name.as_ref().unwrap().name, "User");
2515 }
2516 other => panic!("expected Block, got {other:?}"),
2517 }
2518 }
2519
2520 #[test]
2521 fn entity_with_fields() {
2522 let src = r#"entity Order {
2523 customer: Customer
2524 status: pending | active | completed
2525 total: Decimal
2526}"#;
2527 let r = parse_ok(src);
2528 assert_eq!(r.diagnostics.len(), 0);
2529 match &r.module.declarations[0] {
2530 Decl::Block(b) => {
2531 assert_eq!(b.items.len(), 3);
2532 }
2533 other => panic!("expected Block, got {other:?}"),
2534 }
2535 }
2536
2537 #[test]
2538 fn use_declaration() {
2539 let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
2540 assert_eq!(r.diagnostics.len(), 0);
2541 match &r.module.declarations[0] {
2542 Decl::Use(u) => {
2543 assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
2544 }
2545 other => panic!("expected Use, got {other:?}"),
2546 }
2547 }
2548
2549 #[test]
2550 fn enum_declaration() {
2551 let src = "enum OrderStatus { pending | shipped | delivered }";
2552 let r = parse_ok(src);
2553 assert_eq!(r.diagnostics.len(), 0);
2554 }
2555
2556 #[test]
2557 fn config_block() {
2558 let src = r#"config {
2559 max_retries: Integer = 3
2560 timeout: Duration = 24.hours
2561}"#;
2562 let r = parse_ok(src);
2567 assert_eq!(r.diagnostics.len(), 0);
2568 }
2569
2570 #[test]
2571 fn rule_declaration() {
2572 let src = r#"rule PlaceOrder {
2573 when: CustomerPlacesOrder(customer, items, total)
2574 requires: total > 0
2575 ensures: Order.created(customer: customer, status: pending, total: total)
2576}"#;
2577 let r = parse_ok(src);
2578 assert_eq!(r.diagnostics.len(), 0);
2579 match &r.module.declarations[0] {
2580 Decl::Block(b) => {
2581 assert_eq!(b.kind, BlockKind::Rule);
2582 assert_eq!(b.items.len(), 3);
2583 }
2584 other => panic!("expected Block, got {other:?}"),
2585 }
2586 }
2587
2588 #[test]
2589 fn expression_precedence() {
2590 let r = parse_ok("rule T { v: a + b * c }");
2591 match &r.module.declarations[0] {
2593 Decl::Block(b) => match &b.items[0].kind {
2594 BlockItemKind::Assignment { value, .. } => match value {
2595 Expr::BinaryOp { op, right, .. } => {
2596 assert_eq!(*op, BinaryOp::Add);
2597 assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2598 }
2599 other => panic!("expected BinaryOp, got {other:?}"),
2600 },
2601 other => panic!("expected Assignment, got {other:?}"),
2602 },
2603 other => panic!("expected Block, got {other:?}"),
2604 }
2605 }
2606
2607 #[test]
2608 fn default_declaration() {
2609 let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2610 let r = parse_ok(src);
2611 assert_eq!(r.diagnostics.len(), 0);
2612 }
2613
2614 #[test]
2615 fn open_question() {
2616 let src = r#"open question "Should admins be role-specific?""#;
2617 let r = parse_ok(src);
2618 assert_eq!(r.diagnostics.len(), 0);
2619 }
2620
2621 #[test]
2622 fn external_entity() {
2623 let src = "external entity Customer { email: String }";
2624 let r = parse_ok(src);
2625 assert_eq!(r.diagnostics.len(), 0);
2626 match &r.module.declarations[0] {
2627 Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2628 other => panic!("expected Block, got {other:?}"),
2629 }
2630 }
2631
2632 #[test]
2633 fn where_expression() {
2634 let src = "entity E { active: items where status = active }";
2635 let r = parse_ok(src);
2636 assert_eq!(r.diagnostics.len(), 0);
2637 }
2638
2639 #[test]
2640 fn with_expression() {
2641 let src = "entity E { slots: InterviewSlot with candidacy = this }";
2642 let r = parse_ok(src);
2643 assert_eq!(r.diagnostics.len(), 0);
2644 }
2645
2646 #[test]
2647 fn lambda_expression() {
2648 let src = "entity E { v: items.any(i => i.active) }";
2649 let r = parse_ok(src);
2650 assert_eq!(r.diagnostics.len(), 0);
2651 }
2652
2653 #[test]
2654 fn deferred() {
2655 let src = "deferred InterviewerMatching.suggest";
2656 let r = parse_ok(src);
2657 assert_eq!(r.diagnostics.len(), 0);
2658 }
2659
2660 #[test]
2661 fn variant_declaration() {
2662 let src = "variant Email : Notification { subject: String }";
2663 let r = parse_ok(src);
2664 assert_eq!(r.diagnostics.len(), 0);
2665 }
2666
2667 #[test]
2670 fn projection_arrow() {
2671 let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2672 let r = parse_ok(src);
2673 assert_eq!(r.diagnostics.len(), 0);
2674 }
2675
2676 #[test]
2679 fn transitions_to_trigger() {
2680 let src = "rule R { when: Interview.status transitions_to scheduled\n ensures: Notification.created() }";
2681 let r = parse_ok(src);
2682 assert_eq!(r.diagnostics.len(), 0);
2683 }
2684
2685 #[test]
2686 fn becomes_trigger() {
2687 let src = "rule R { when: Interview.status becomes scheduled\n ensures: Notification.created() }";
2688 let r = parse_ok(src);
2689 assert_eq!(r.diagnostics.len(), 0);
2690 }
2691
2692 #[test]
2695 fn when_binding() {
2696 let src = "rule R {\n when: interview: Interview.status transitions_to scheduled\n ensures: Notification.created()\n}";
2697 let r = parse_ok(src);
2698 assert_eq!(r.diagnostics.len(), 0);
2699 let decl = &r.module.declarations[0];
2701 if let Decl::Block(b) = decl {
2702 if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2703 assert_eq!(keyword, "when");
2704 assert!(matches!(value, Expr::Binding { .. }));
2705 } else {
2706 panic!("expected clause");
2707 }
2708 } else {
2709 panic!("expected block decl");
2710 }
2711 }
2712
2713 #[test]
2714 fn when_binding_temporal() {
2715 let src = "rule R {\n when: invitation: Invitation.expires_at <= now\n ensures: Invitation.expired()\n}";
2716 let r = parse_ok(src);
2717 assert_eq!(r.diagnostics.len(), 0);
2718 }
2719
2720 #[test]
2721 fn when_binding_created() {
2722 let src = "rule R {\n when: batch: DigestBatch.created\n ensures: Email.created()\n}";
2723 let r = parse_ok(src);
2724 assert_eq!(r.diagnostics.len(), 0);
2725 }
2726
2727 #[test]
2728 fn facing_binding() {
2729 let src = "surface S {\n facing viewer: Interviewer\n exposes: InterviewList\n}";
2730 let r = parse_ok(src);
2731 assert_eq!(r.diagnostics.len(), 0);
2732 }
2733
2734 #[test]
2735 fn context_binding() {
2736 let src = "surface S {\n facing viewer: Interviewer\n context assignment: SlotConfirmation where interviewer = viewer\n}";
2737 let r = parse_ok(src);
2738 assert_eq!(r.diagnostics.len(), 0);
2739 }
2740
2741 #[test]
2744 fn rule_level_for() {
2745 let src = r#"rule ProcessDigests {
2746 when: schedule: DigestSchedule.next_run_at <= now
2747 for user in Users where notification_setting.digest_enabled:
2748 ensures: DigestBatch.created(user: user)
2749}"#;
2750 let r = parse_ok(src);
2751 assert_eq!(r.diagnostics.len(), 0);
2752 if let Decl::Block(b) = &r.module.declarations[0] {
2753 assert!(b.items.len() >= 2);
2755 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2756 } else {
2757 panic!("expected block decl");
2758 }
2759 }
2760
2761 #[test]
2764 fn let_in_ensures_block() {
2765 let src = r#"rule R {
2766 when: ScheduleInterview(candidacy, time, interviewers)
2767 ensures:
2768 let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2769 for interviewer in interviewers:
2770 SlotConfirmation.created(slot: slot, interviewer: interviewer)
2771}"#;
2772 let r = parse_ok(src);
2773 assert_eq!(r.diagnostics.len(), 0);
2774 }
2775
2776 #[test]
2779 fn provides_when_guard() {
2780 let src = "surface S {\n facing viewer: Interviewer\n provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2781 let r = parse_ok(src);
2782 assert_eq!(r.diagnostics.len(), 0);
2783 }
2784
2785 #[test]
2788 fn optional_type_suffix() {
2789 let src = "entity E { locked_until: Timestamp? }";
2790 let r = parse_ok(src);
2791 assert_eq!(r.diagnostics.len(), 0);
2792 }
2793
2794 #[test]
2795 fn optional_trigger_param() {
2796 let src = "rule R { when: Report(interviewer, interview, reason, details?)\n ensures: Done() }";
2797 let r = parse_ok(src);
2798 assert_eq!(r.diagnostics.len(), 0);
2799 }
2800
2801 #[test]
2804 fn qualified_config_access() {
2805 let src = "entity E { duration: oauth/config.session_duration }";
2806 let r = parse_ok(src);
2807 assert_eq!(r.diagnostics.len(), 0);
2808 }
2809
2810 #[test]
2813 fn realistic_spec() {
2814 let src = r#"-- allium: 1
2815
2816enum OrderStatus { pending | shipped | delivered }
2817
2818external entity Customer {
2819 email: String
2820 name: String
2821}
2822
2823entity Order {
2824 customer: Customer
2825 status: OrderStatus
2826 total: Decimal
2827 items: OrderItem with order = this
2828 shipped_items: items where status = shipped
2829 confirmed_items: items where status = confirmed -> item
2830 is_complete: status = delivered
2831 locked_until: Timestamp?
2832}
2833
2834config {
2835 max_retries: Integer = 3
2836 timeout: Duration = 24.hours
2837}
2838
2839rule PlaceOrder {
2840 when: CustomerPlacesOrder(customer, items, total)
2841 requires: total > 0
2842 ensures: Order.created(customer: customer, status: pending, total: total)
2843}
2844
2845rule ShipOrder {
2846 when: order: Order.status transitions_to shipped
2847 ensures: Email.created(to: order.customer.email, template: order_shipped)
2848}
2849
2850open question "How do we handle partial shipments?"
2851"#;
2852 let r = parse_ok(src);
2853 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2854 assert_eq!(r.module.version, Some(1));
2855 assert_eq!(r.module.declarations.len(), 7);
2856 }
2857
2858 #[test]
2859 fn extension_behaviour_excerpt() {
2860 let src = r#"value Document {
2863 uri: String
2864 text: String
2865}
2866
2867entity Finding {
2868 code: String
2869 severity: error | warning | info
2870 range: FindingRange
2871}
2872
2873entity DiagnosticsMode {
2874 value: strict | relaxed
2875}
2876
2877config {
2878 duplicateKey: String = "allium.config.duplicateKey"
2879}
2880
2881rule RefreshDiagnostics {
2882 when: DocumentOpened(document) or DocumentChanged(document)
2883 requires: document.language_id = "allium"
2884 ensures: FindingsComputed(document)
2885}
2886
2887surface DiagnosticsDashboard {
2888 facing viewer: Developer
2889 context doc: Document where viewer.active_document = doc
2890 provides: RunChecks(viewer) when doc.language_id = "allium"
2891 exposes: FindingList
2892}
2893
2894rule ProcessDigests {
2895 when: schedule: DigestSchedule.next_run_at <= now
2896 for user in Users where notification_setting.digest_enabled:
2897 let settings = user.notification_setting
2898 ensures: DigestBatch.created(user: user)
2899}
2900"#;
2901 let r = parse_ok(src);
2902 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2903 assert_eq!(r.module.declarations.len(), 7);
2905 }
2906
2907 #[test]
2908 fn exists_as_identifier() {
2909 let src = r#"rule R {
2910 when: X()
2911 ensures: CompletionItemAvailable(label: exists)
2912}"#;
2913 let r = parse_ok(src);
2914 assert_eq!(r.diagnostics.len(), 0);
2915 }
2916
2917 #[test]
2920 fn pipe_binds_tighter_than_or() {
2921 let src = "entity E { v: a or b | c }";
2923 let r = parse_ok(src);
2924 assert_eq!(r.diagnostics.len(), 0);
2925 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2926 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2927 let Expr::LogicalOp { op, right, .. } = value else {
2929 panic!("expected LogicalOp, got {value:?}");
2930 };
2931 assert_eq!(*op, LogicalOp::Or);
2932 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2934 }
2935
2936 #[test]
2939 fn variant_with_pipe_base() {
2940 let src = "variant Mixed : TypeA | TypeB";
2941 let r = parse_ok(src);
2942 assert_eq!(r.diagnostics.len(), 0);
2943 let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2944 assert!(matches!(v.base, Expr::Pipe { .. }));
2945 }
2946
2947 #[test]
2950 fn for_block_where_comparison() {
2951 let src = r#"rule R {
2952 when: X()
2953 for item in Items where item.status = active:
2954 ensures: Processed(item: item)
2955}"#;
2956 let r = parse_ok(src);
2957 assert_eq!(r.diagnostics.len(), 0);
2958 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2959 let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2960 assert!(filter.is_some());
2961 assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2962 }
2963
2964 #[test]
2967 fn for_expr_where_comparison() {
2968 let src = r#"rule R {
2969 when: X()
2970 ensures:
2971 for item in Items where item.active = true:
2972 Processed(item: item)
2973}"#;
2974 let r = parse_ok(src);
2975 assert_eq!(r.diagnostics.len(), 0);
2976 }
2977
2978 #[test]
2981 fn if_else_if_else() {
2982 let src = r#"rule R {
2983 when: X(v)
2984 ensures:
2985 if v < 10: Small()
2986 else if v < 100: Medium()
2987 else: Large()
2988}"#;
2989 let r = parse_ok(src);
2990 assert_eq!(r.diagnostics.len(), 0);
2991 }
2992
2993 #[test]
2996 fn null_coalesce_and_optional_chain() {
2997 let src = "entity E { v: a?.b ?? fallback }";
2998 let r = parse_ok(src);
2999 assert_eq!(r.diagnostics.len(), 0);
3000 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3001 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3002 assert!(matches!(value, Expr::NullCoalesce { .. }));
3004 }
3005
3006 #[test]
3009 fn generic_type_nested() {
3010 let src = "entity E { v: List<Set<String>> }";
3011 let r = parse_ok(src);
3012 assert_eq!(r.diagnostics.len(), 0);
3013 }
3014
3015 #[test]
3018 fn collection_literals() {
3019 let src = r#"rule R {
3020 when: X()
3021 ensures:
3022 let s = {a, b, c}
3023 let o = {name: "test", count: 42}
3024 Done()
3025}"#;
3026 let r = parse_ok(src);
3027 assert_eq!(r.diagnostics.len(), 0);
3028 }
3029
3030 #[test]
3031 fn spec_reject_list_literal() {
3032 let src = r#"rule R {
3034 when: X()
3035 ensures:
3036 let l = [1, 2, 3]
3037 Done()
3038}"#;
3039 let r = parse_ok(src);
3040 assert!(
3041 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3042 "expected error for `[...]` list literal (not in spec), but parsed without errors"
3043 );
3044 }
3045
3046 #[test]
3049 fn given_block() {
3050 let src = "given { viewer: User\n time: Timestamp }";
3051 let r = parse_ok(src);
3052 assert_eq!(r.diagnostics.len(), 0);
3053 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3054 assert_eq!(b.kind, BlockKind::Given);
3055 assert!(b.name.is_none());
3056 }
3057
3058 #[test]
3061 fn actor_block() {
3062 let src = "actor Admin { identified_by: User where role = admin }";
3063 let r = parse_ok(src);
3064 assert_eq!(r.diagnostics.len(), 0);
3065 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3066 assert_eq!(b.kind, BlockKind::Actor);
3067 }
3068
3069 #[test]
3072 fn join_lookup() {
3073 let src = "entity E { match: Other{field_a, field_b: value} }";
3074 let r = parse_ok(src);
3075 assert_eq!(r.diagnostics.len(), 0);
3076 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3077 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3078 assert!(matches!(value, Expr::JoinLookup { .. }));
3079 }
3080
3081 #[test]
3084 fn in_not_in_set() {
3085 let src = r#"rule R {
3086 when: X(s)
3087 requires: s in {a, b, c}
3088 requires: s not in {d, e}
3089 ensures: Done()
3090}"#;
3091 let r = parse_ok(src);
3092 assert_eq!(r.diagnostics.len(), 0);
3093 }
3094
3095 #[test]
3098 fn comprehensive_fixture() {
3099 let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
3100 let r = parse(src);
3101 assert_eq!(
3102 r.diagnostics.len(),
3103 0,
3104 "expected no errors in comprehensive fixture, got: {:?}",
3105 r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
3106 );
3107 assert!(r.module.declarations.len() > 30, "expected many declarations");
3108 }
3109
3110 #[test]
3113 fn error_expected_declaration() {
3114 let r = parse("-- allium: 1\n+ invalid");
3115 assert!(r.diagnostics.len() >= 1);
3116 let msg = &r.diagnostics[0].message;
3117 assert!(msg.contains("expected declaration"), "got: {msg}");
3118 assert!(msg.contains("entity"), "should list valid options, got: {msg}");
3119 assert!(msg.contains("rule"), "should list valid options, got: {msg}");
3120 }
3121
3122 #[test]
3123 fn error_expected_expression() {
3124 let r = parse("-- allium: 1\nentity E { v: }");
3125 assert!(r.diagnostics.len() >= 1);
3126 let msg = &r.diagnostics[0].message;
3127 assert!(msg.contains("expected expression"), "got: {msg}");
3128 assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
3129 }
3130
3131 #[test]
3132 fn error_expected_block_item() {
3133 let r = parse("-- allium: 1\nentity E { + }");
3134 assert!(r.diagnostics.len() >= 1);
3135 let msg = &r.diagnostics[0].message;
3136 assert!(msg.contains("expected block item"), "got: {msg}");
3137 }
3138
3139 #[test]
3140 fn error_expected_identifier() {
3141 let r = parse("-- allium: 1\nentity 123 {}");
3142 assert!(r.diagnostics.len() >= 1);
3143 let msg = &r.diagnostics[0].message;
3144 assert!(msg.contains("expected entity name"), "got: {msg}");
3146 assert!(msg.contains("number"), "should say what was found, got: {msg}");
3148 }
3149
3150 #[test]
3151 fn error_missing_brace() {
3152 let r = parse("entity E {");
3153 assert!(r.diagnostics.len() >= 1);
3154 let msg = &r.diagnostics[0].message;
3155 assert!(msg.contains("expected"), "got: {msg}");
3156 }
3157
3158 #[test]
3159 fn error_recovery_multiple() {
3160 let r = parse("entity E { + }\nentity F { - }");
3162 assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
3163 }
3164
3165 #[test]
3166 fn error_dedup_same_line() {
3167 let r = parse("-- allium: 1\n+ - * /");
3169 let errors: Vec<_> = r.diagnostics.iter()
3170 .filter(|d| d.severity == crate::diagnostic::Severity::Error)
3171 .collect();
3172 assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
3173 }
3174
3175 #[test]
3176 fn for_block() {
3177 let src = r#"rule R {
3178 when: X()
3179 for user in Users where user.active:
3180 ensures: Notified(user: user)
3181}"#;
3182 let r = parse_ok(src);
3183 assert_eq!(r.diagnostics.len(), 0);
3184 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3185 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
3186 }
3187
3188 #[test]
3189 fn for_expr() {
3190 let src = r#"rule R {
3191 when: X(project)
3192 ensures:
3193 let total = for task in project.tasks: task.effort
3194 Done(total: total)
3195}"#;
3196 let r = parse_ok(src);
3197 assert_eq!(r.diagnostics.len(), 0);
3198 }
3199
3200 #[test]
3201 fn for_where() {
3202 let src = r#"rule R {
3203 when: X()
3204 for item in Items where item.active:
3205 ensures: Processed(item: item)
3206}"#;
3207 let r = parse_ok(src);
3208 assert_eq!(r.diagnostics.len(), 0);
3209 }
3210
3211 #[test]
3212 fn spec_reject_for_with_filter() {
3213 let src = r#"rule R {
3216 when: X()
3217 for slot in Slot with slot.role = reviewer:
3218 ensures: Reviewed(slot: slot)
3219}"#;
3220 let r = parse_ok(src);
3221 assert!(
3222 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3223 "expected error for `for ... with` (spec uses `where`), but parsed without errors"
3224 );
3225 }
3226
3227 #[test]
3228 fn block_level_if() {
3229 let src = r#"rule R {
3230 when: X(task)
3231 if task.priority = high:
3232 ensures: Escalated(task: task)
3233}"#;
3234 let r = parse_ok(src);
3235 assert_eq!(r.diagnostics.len(), 0);
3236 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3237 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3238 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3239 };
3240 assert_eq!(branches.len(), 1);
3241 assert!(else_items.is_none());
3242 }
3243
3244 #[test]
3245 fn block_level_if_else() {
3246 let src = r#"rule R {
3247 when: X(score)
3248 if score > 80:
3249 ensures: High()
3250 else if score > 40:
3251 ensures: Medium()
3252 else:
3253 ensures: Low()
3254}"#;
3255 let r = parse_ok(src);
3256 assert_eq!(r.diagnostics.len(), 0);
3257 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3258 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3259 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3260 };
3261 assert_eq!(branches.len(), 2);
3262 assert!(else_items.is_some());
3263 }
3264
3265 #[test]
3266 fn wildcard_type_parameter() {
3267 let src = "entity E { codec: Codec<*> }";
3268 let r = parse_ok(src);
3269 assert_eq!(r.diagnostics.len(), 0);
3270 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3271 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3272 if let Expr::GenericType { args, .. } = value {
3273 assert_eq!(args.len(), 1);
3274 if let Expr::Ident(id) = &args[0] {
3275 assert_eq!(id.name, "*");
3276 } else {
3277 panic!("expected wildcard ident, got {:?}", args[0]);
3278 }
3279 } else {
3280 panic!("expected GenericType, got {:?}", value);
3281 }
3282 }
3283
3284 #[test]
3285 fn guidance_clause_comment_only_value_migration() {
3286 let src = "-- allium: 1\nrule R {\n ensures: Done()\n guidance: -- just a comment\n}";
3288 let r = parse(src);
3289 assert!(
3290 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
3291 "expected migration diagnostic, got: {:?}",
3292 r.diagnostics
3293 );
3294 }
3295
3296 #[test]
3297 fn spec_reject_for_expr_with_filter() {
3298 let src = r#"rule R {
3300 when: X(project)
3301 ensures:
3302 let total = for task in project.tasks with task.active: task.effort
3303 Done(total: total)
3304}"#;
3305 let r = parse_ok(src);
3306 assert!(
3307 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3308 "expected error for `for ... with` in expression (spec uses `where`), but parsed without errors"
3309 );
3310 }
3311
3312 #[test]
3313 fn for_destructured_binding() {
3314 let src = r#"rule R {
3315 when: X()
3316 for (key, value) in Pairs where key != null:
3317 ensures: Processed(key: key, value: value)
3318}"#;
3319 let r = parse_ok(src);
3320 assert_eq!(r.diagnostics.len(), 0);
3321 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3322 let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
3323 assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
3324 }
3325
3326 #[test]
3327 fn dot_path_assignment() {
3328 let src = r#"entity Shard {
3329 ShardGroup.shard_cache: Shard with group = this
3330}"#;
3331 let r = parse_ok(src);
3332 assert_eq!(r.diagnostics.len(), 0);
3333 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3334 let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
3335 panic!("expected PathAssignment, got {:?}", b.items[0].kind);
3336 };
3337 assert!(matches!(path, Expr::MemberAccess { .. }));
3338 }
3339
3340 #[test]
3341 fn language_reference_fixture() {
3342 let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
3343 let r = parse(src);
3344 let errors: Vec<_> = r.diagnostics.iter()
3345 .filter(|d| d.severity == Severity::Error)
3346 .collect();
3347 assert_eq!(
3348 errors.len(),
3349 0,
3350 "expected no errors in language-reference fixture, got: {:?}",
3351 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
3352 );
3353 }
3354
3355 #[test]
3372 fn spec_for_bare_form() {
3373 let src = r#"rule ProcessDigests {
3375 when: schedule: DigestSchedule.next_run_at <= now
3376 for user in Users where notification_setting.digest_enabled:
3377 let settings = user.notification_setting
3378 ensures: DigestBatch.created(user: user)
3379}"#;
3380 let r = parse_ok(src);
3381 assert_eq!(r.diagnostics.len(), 0);
3382 }
3383
3384 #[test]
3385 fn spec_reject_for_each() {
3386 let src = r#"rule R {
3388 when: X()
3389 for each user in Users where user.active:
3390 ensures: Notified(user: user)
3391}"#;
3392 let r = parse_ok(src);
3393 assert!(
3394 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3395 "expected error for `for each` (not in spec), but parsed without errors"
3396 );
3397 }
3398
3399 #[test]
3402 fn spec_reject_double_equals() {
3403 let src = "rule R { when: X(a)\n requires: a.status == active\n ensures: Done() }";
3405 let r = parse_ok(src);
3406 assert!(
3407 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3408 "expected error for `==` (not in spec), but parsed without errors"
3409 );
3410 }
3411
3412 #[test]
3415 fn spec_reject_system_block() {
3416 let src = "system PaymentGateway {\n timeout: 30.seconds\n}";
3418 let r = parse_ok(src);
3419 assert!(
3420 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3421 "expected error for `system` block (not in spec), but parsed without errors"
3422 );
3423 }
3424
3425 #[test]
3428 fn spec_reject_tags_clause() {
3429 let src = r#"rule R {
3431 when: MigrationTriggered()
3432 tags: infrastructure, migration
3433 ensures: MigrationComplete()
3434}"#;
3435 let r = parse_ok(src);
3436 assert!(
3437 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3438 "expected error for `tags:` clause (not in spec), but parsed without errors"
3439 );
3440 }
3441
3442 #[test]
3445 fn spec_reject_includes_operator() {
3446 let src = r#"rule R {
3448 when: X(a, b)
3449 requires: a.items includes b
3450 ensures: Done()
3451}"#;
3452 let r = parse_ok(src);
3453 assert!(
3454 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3455 "expected error for `includes` operator (not in spec), but parsed without errors"
3456 );
3457 }
3458
3459 #[test]
3460 fn spec_reject_excludes_operator() {
3461 let src = r#"rule R {
3463 when: X(a, b)
3464 requires: a.items excludes b
3465 ensures: Done()
3466}"#;
3467 let r = parse_ok(src);
3468 assert!(
3469 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3470 "expected error for `excludes` operator (not in spec), but parsed without errors"
3471 );
3472 }
3473
3474 #[test]
3477 fn spec_reject_range_literal() {
3478 let src = r#"rule R {
3480 when: X(v)
3481 requires: v in [1..100]
3482 ensures: Done()
3483}"#;
3484 let r = parse_ok(src);
3485 assert!(
3486 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3487 "expected error for `..` range (not in spec), but parsed without errors"
3488 );
3489 }
3490
3491 #[test]
3494 fn spec_within_in_actor() {
3495 let src = r#"actor WorkspaceAdmin {
3497 within: Workspace
3498 identified_by: User where role = admin
3499}"#;
3500 let r = parse_ok(src);
3501 assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
3502 }
3503
3504 #[test]
3507 fn spec_reject_module_declaration() {
3508 let src = "module my_spec";
3510 let r = parse_ok(src);
3511 assert!(
3512 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3513 "expected error for `module` declaration (not in spec), but parsed without errors"
3514 );
3515 }
3516
3517 #[test]
3520 fn spec_reject_module_level_guidance() {
3521 let src = r#"guidance: "All rules must be idempotent""#;
3523 let r = parse_ok(src);
3524 assert!(
3525 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3526 "expected error for module-level `guidance:` (not in spec), but parsed without errors"
3527 );
3528 }
3529
3530 #[test]
3533 fn spec_guarantee_in_surface_migration() {
3534 let src = "-- allium: 1\nsurface S {\n facing viewer: User\n guarantee: DataIntegrity\n}";
3536 let r = parse(src);
3537 assert!(
3538 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
3539 "expected migration diagnostic, got: {:?}",
3540 r.diagnostics
3541 );
3542 }
3543
3544 #[test]
3545 fn spec_timeout_in_surface() {
3546 let src = r#"surface InvitationView {
3548 facing recipient: Candidate
3549 context invitation: ResourceInvitation where email = recipient.email
3550 timeout: InvitationExpires
3551}"#;
3552 let r = parse_ok(src);
3553 assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
3554 }
3555
3556 #[test]
3557 fn spec_timeout_in_surface_with_when() {
3558 let src = r#"surface InvitationView {
3560 facing recipient: Candidate
3561 context invitation: ResourceInvitation where email = recipient.email
3562 timeout: InvitationExpires when invitation.expires_at <= now
3563}"#;
3564 let r = parse_ok(src);
3565 assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
3566 }
3567
3568 #[test]
3571 fn spec_reject_suffix_predicate() {
3572 let src = r#"rule R {
3574 when: X()
3575 requires: finding.code starts_with "allium."
3576 ensures: Done()
3577}"#;
3578 let r = parse_ok(src);
3579 assert!(
3580 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3581 "expected error for suffix predicate (not in spec), but parsed without errors"
3582 );
3583 }
3584
3585 #[test]
3588 fn spec_add_remove_in_ensures() {
3589 let src = r#"rule R {
3592 when: AssignInterviewer(interview, new_interviewer)
3593 ensures:
3594 interview.interviewers.add(new_interviewer)
3595}"#;
3596 let r = parse_ok(src);
3597 assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
3598 }
3599
3600 #[test]
3601 fn spec_remove_in_ensures() {
3602 let src = r#"rule R {
3603 when: RemoveInterviewer(interview, leaving)
3604 ensures:
3605 interview.interviewers.remove(leaving)
3606}"#;
3607 let r = parse_ok(src);
3608 assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3609 }
3610
3611 #[test]
3614 fn spec_first_last_access() {
3615 let src = "entity E { latest: attempts.last\n earliest: attempts.first }";
3617 let r = parse_ok(src);
3618 assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3619 }
3620
3621 #[test]
3624 fn spec_set_arithmetic() {
3625 let src = r#"entity Role {
3627 permissions: Set<String>
3628 inherited: Set<String>
3629 all_permissions: permissions + inherited
3630 removed: old_mentions - new_mentions
3631}"#;
3632 let r = parse_ok(src);
3633 assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3634 }
3635
3636 #[test]
3639 fn spec_discard_binding_in_trigger() {
3640 let src = r#"rule R {
3642 when: _: LogProcessor.last_flush_check <= now
3643 ensures: Flushed()
3644}"#;
3645 let r = parse_ok(src);
3646 assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3647 }
3648
3649 #[test]
3650 fn spec_discard_in_trigger_params() {
3651 let src = r#"rule R {
3653 when: SomeEvent(_, slot)
3654 ensures: Processed(slot: slot)
3655}"#;
3656 let r = parse_ok(src);
3657 assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3658 }
3659
3660 #[test]
3661 fn spec_discard_in_for() {
3662 let src = r#"rule R {
3664 when: X(items)
3665 ensures:
3666 for _ in items: Counted()
3667}"#;
3668 let r = parse_ok(src);
3669 assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3670 }
3671
3672 #[test]
3675 fn spec_default_with_object_literal() {
3676 let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3678 let r = parse_ok(src);
3679 assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3680 }
3681
3682 #[test]
3683 fn spec_default_multiline_object() {
3684 let src = r#"default Role viewer = {
3686 name: "viewer",
3687 permissions: { "documents.read" }
3688}"#;
3689 let r = parse_ok(src);
3690 assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3691 }
3692
3693 #[test]
3696 fn spec_surface_related_clause() {
3697 let src = r#"surface InterviewerDashboard {
3699 facing viewer: Interviewer
3700 context assignment: SlotConfirmation where interviewer = viewer
3701 related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3702}"#;
3703 let r = parse_ok(src);
3704 assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3705 }
3706
3707 #[test]
3708 fn spec_surface_let_binding() {
3709 let src = r#"surface S {
3711 facing viewer: User
3712 let comments = Comments where parent = viewer
3713 exposes: CommentList
3714}"#;
3715 let r = parse_ok(src);
3716 assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3717 }
3718
3719 #[test]
3720 fn spec_surface_multiline_context_where() {
3721 let src = r#"surface InterviewerPendingAssignments {
3723 facing viewer: Interviewer
3724 context assignment: InterviewAssignment
3725 where interviewer = viewer and status = pending
3726 exposes: AssignmentList
3727}"#;
3728 let r = parse_ok(src);
3729 assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3730 }
3731
3732 #[test]
3735 fn spec_for_in_surface_provides() {
3736 let src = r#"surface TaskBoard {
3738 facing viewer: User
3739 for task in Task where task.assignee = viewer:
3740 provides: CompleteTask(viewer, task) when task.status = in_progress
3741 exposes: KanbanBoard
3742}"#;
3743 let r = parse_ok(src);
3744 assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3745 }
3746
3747 #[test]
3750 fn spec_use_without_alias() {
3751 let src = r#"use "github.com/specs/notifications/def456""#;
3753 let r = parse_ok(src);
3754 assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3755 }
3756
3757 #[test]
3760 fn spec_empty_external_entity() {
3761 let src = "external entity Commentable {}";
3763 let r = parse_ok(src);
3764 assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3765 }
3766
3767 #[test]
3770 fn spec_surface_multiline_provides() {
3771 let src = r#"surface ProjectDashboard {
3773 facing viewer: ProjectManager
3774 context project: Project where owner = viewer
3775 provides:
3776 CreateTask(viewer, project) when project.status = active
3777 ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3778 exposes: TaskList
3779}"#;
3780 let r = parse_ok(src);
3781 assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3782 }
3783
3784 #[test]
3787 fn spec_surface_multiline_exposes() {
3788 let src = r#"surface InterviewerDashboard {
3790 facing viewer: Interviewer
3791 context assignment: SlotConfirmation where interviewer = viewer
3792 exposes:
3793 assignment.slot.time
3794 assignment.status
3795}"#;
3796 let r = parse_ok(src);
3797 assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3798 }
3799
3800 #[test]
3810 fn composite_or_trigger() {
3811 let src = r#"rule R {
3812 when: EventA(x) or EventB(x) or EventC(x)
3813 ensures: Done()
3814}"#;
3815 let r = parse_ok(src);
3816 assert_eq!(r.diagnostics.len(), 0);
3817 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3818 let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
3819 assert_eq!(keyword, "when");
3820 let Expr::LogicalOp { op, left, .. } = value else {
3822 panic!("expected LogicalOp, got {value:?}");
3823 };
3824 assert_eq!(*op, LogicalOp::Or);
3825 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
3826 }
3827
3828 #[test]
3831 fn value_type_declaration() {
3832 let src = r#"value TimeRange {
3833 start: Timestamp
3834 end: Timestamp
3835 duration: end - start
3836}"#;
3837 let r = parse_ok(src);
3838 assert_eq!(r.diagnostics.len(), 0);
3839 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3840 assert_eq!(b.kind, BlockKind::Value);
3841 assert_eq!(b.name.as_ref().unwrap().name, "TimeRange");
3842 assert_eq!(b.items.len(), 3);
3843 }
3844
3845 #[test]
3848 fn qualified_config_block() {
3849 let src = r#"use "github.com/specs/oauth/abc123" as oauth
3850oauth/config {
3851 session_duration: Duration = 24.hours
3852}"#;
3853 let r = parse_ok(src);
3854 assert_eq!(r.diagnostics.len(), 0);
3855 assert_eq!(r.module.declarations.len(), 2);
3856 }
3857
3858 #[test]
3861 fn string_interpolation_parts() {
3862 let src = r#"rule R {
3863 when: X(name, action)
3864 ensures: Log.created(message: "User {name} did {action}")
3865}"#;
3866 let r = parse_ok(src);
3867 assert_eq!(r.diagnostics.len(), 0);
3868 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3870 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3871 let Expr::Call { args, .. } = value else { panic!() };
3872 let CallArg::Named(arg) = &args[0] else { panic!() };
3873 let Expr::StringLiteral(s) = &arg.value else { panic!() };
3874 assert_eq!(s.parts.len(), 4, "expected 4 string parts: text, interp, text, interp");
3875 assert!(matches!(&s.parts[0], StringPart::Text(t) if t == "User "));
3876 assert!(matches!(&s.parts[1], StringPart::Interpolation(id) if id.name == "name"));
3877 assert!(matches!(&s.parts[2], StringPart::Text(t) if t == " did "));
3878 assert!(matches!(&s.parts[3], StringPart::Interpolation(id) if id.name == "action"));
3879 }
3880
3881 #[test]
3884 fn this_keyword_expression() {
3885 let src = "entity E { items: Item with parent = this }";
3888 let r = parse_ok(src);
3889 assert_eq!(r.diagnostics.len(), 0);
3890 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3891 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3892 let Expr::With { predicate, .. } = value else {
3893 panic!("expected With, got {value:?}");
3894 };
3895 let Expr::Comparison { op, right, .. } = predicate.as_ref() else {
3896 panic!("expected Comparison in with predicate, got {predicate:?}");
3897 };
3898 assert_eq!(*op, ComparisonOp::Eq);
3899 assert!(matches!(right.as_ref(), Expr::This { .. }));
3900 }
3901
3902 #[test]
3905 fn not_prefix_standalone() {
3906 let src = r#"rule R {
3907 when: X(user)
3908 requires: not user.is_locked
3909 ensures: Done()
3910}"#;
3911 let r = parse_ok(src);
3912 assert_eq!(r.diagnostics.len(), 0);
3913 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3914 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3915 assert_eq!(keyword, "requires");
3916 assert!(matches!(value, Expr::Not { .. }));
3917 }
3918
3919 #[test]
3922 fn unary_minus() {
3923 let src = "entity E { offset: -1 }";
3924 let r = parse_ok(src);
3925 assert_eq!(r.diagnostics.len(), 0);
3926 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3927 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3928 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }
3929 | Expr::NumberLiteral { .. }), "expected negation, got {value:?}");
3930 }
3931
3932 #[test]
3935 fn parenthesised_expression() {
3936 let src = "entity E { v: (a + b) * c }";
3937 let r = parse_ok(src);
3938 assert_eq!(r.diagnostics.len(), 0);
3939 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3940 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3941 let Expr::BinaryOp { op, left, .. } = value else {
3943 panic!("expected BinaryOp, got {value:?}");
3944 };
3945 assert_eq!(*op, BinaryOp::Mul);
3946 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3947 }
3948
3949 #[test]
3952 fn boolean_literals() {
3953 let src = r#"rule R {
3954 when: X(item)
3955 ensures:
3956 item.active = true
3957 item.deleted = false
3958}"#;
3959 let r = parse_ok(src);
3960 assert_eq!(r.diagnostics.len(), 0);
3961 }
3962
3963 #[test]
3966 fn null_literal() {
3967 let src = "entity E { v: parent ?? null }";
3968 let r = parse_ok(src);
3969 assert_eq!(r.diagnostics.len(), 0);
3970 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3971 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3972 let Expr::NullCoalesce { right, .. } = value else { panic!() };
3973 assert!(matches!(right.as_ref(), Expr::Null { .. }));
3974 }
3975
3976 #[test]
3979 fn empty_set_literal() {
3980 let src = "entity E { tags: Set<String>\n default_tags: {} }";
3981 let r = parse_ok(src);
3982 assert_eq!(r.diagnostics.len(), 0);
3983 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3984 let BlockItemKind::Assignment { value, .. } = &b.items[1].kind else { panic!() };
3985 let Expr::SetLiteral { elements, .. } = value else { panic!("expected SetLiteral, got {value:?}") };
3986 assert!(elements.is_empty());
3987 }
3988
3989 #[test]
4001 fn param_assignment_single() {
4002 let src = "entity Plan { can_use(feature): feature in features }";
4003 let r = parse_ok(src);
4004 assert_eq!(r.diagnostics.len(), 0);
4005 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4006 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4007 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4008 };
4009 assert_eq!(name.name, "can_use");
4010 assert_eq!(params.len(), 1);
4011 assert_eq!(params[0].name, "feature");
4012 assert!(matches!(value, Expr::In { .. }));
4013 }
4014
4015 #[test]
4016 fn param_assignment_multiple() {
4017 let src = "entity E { distance(x, y): (x * x + y * y) }";
4018 let r = parse_ok(src);
4019 assert_eq!(r.diagnostics.len(), 0);
4020 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4021 let BlockItemKind::ParamAssignment { name, params, .. } = &b.items[0].kind else {
4022 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4023 };
4024 assert_eq!(name.name, "distance");
4025 assert_eq!(params.len(), 2);
4026 assert_eq!(params[0].name, "x");
4027 assert_eq!(params[1].name, "y");
4028 }
4029
4030 #[test]
4031 fn param_assignment_simple_expression() {
4032 let src = "entity Task { remaining_effort(total): total - effort }";
4033 let r = parse_ok(src);
4034 assert_eq!(r.diagnostics.len(), 0);
4035 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4036 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4037 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4038 };
4039 assert_eq!(name.name, "remaining_effort");
4040 assert_eq!(params.len(), 1);
4041 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }));
4042 }
4043
4044 #[test]
4047 fn precedence_logical_and_binds_tighter_than_or() {
4048 let src = "entity E { v: a or b and c }";
4050 let r = parse_ok(src);
4051 assert_eq!(r.diagnostics.len(), 0);
4052 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4053 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4054 let Expr::LogicalOp { op, right, .. } = value else {
4055 panic!("expected LogicalOp, got {value:?}");
4056 };
4057 assert_eq!(*op, LogicalOp::Or);
4058 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4059 }
4060
4061 #[test]
4062 fn precedence_comparison_binds_tighter_than_and() {
4063 let src = "entity E { v: a = b and c != d }";
4065 let r = parse_ok(src);
4066 assert_eq!(r.diagnostics.len(), 0);
4067 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4068 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4069 let Expr::LogicalOp { op, left, right, .. } = value else {
4070 panic!("expected LogicalOp, got {value:?}");
4071 };
4072 assert_eq!(*op, LogicalOp::And);
4073 assert!(matches!(left.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4074 assert!(matches!(right.as_ref(), Expr::Comparison { op: ComparisonOp::NotEq, .. }));
4075 }
4076
4077 #[test]
4078 fn precedence_arithmetic_binds_tighter_than_comparison() {
4079 let src = "entity E { v: a + b > c * d }";
4081 let r = parse_ok(src);
4082 assert_eq!(r.diagnostics.len(), 0);
4083 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4084 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4085 let Expr::Comparison { op, left, right, .. } = value else {
4086 panic!("expected Comparison, got {value:?}");
4087 };
4088 assert_eq!(*op, ComparisonOp::Gt);
4089 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
4090 assert!(matches!(right.as_ref(), Expr::BinaryOp { op: BinaryOp::Mul, .. }));
4091 }
4092
4093 #[test]
4094 fn precedence_null_coalesce_binds_tighter_than_comparison() {
4095 let src = "entity E { v: a ?? b = c }";
4097 let r = parse_ok(src);
4098 assert_eq!(r.diagnostics.len(), 0);
4099 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4100 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4101 let Expr::Comparison { op, left, .. } = value else {
4102 panic!("expected Comparison, got {value:?}");
4103 };
4104 assert_eq!(*op, ComparisonOp::Eq);
4105 assert!(matches!(left.as_ref(), Expr::NullCoalesce { .. }));
4106 }
4107
4108 #[test]
4109 fn precedence_not_binds_tighter_than_and() {
4110 let src = r#"rule R {
4112 when: X(a, b)
4113 requires: not a and b
4114 ensures: Done()
4115}"#;
4116 let r = parse_ok(src);
4117 assert_eq!(r.diagnostics.len(), 0);
4118 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4119 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4120 let Expr::LogicalOp { op, left, .. } = value else {
4121 panic!("expected LogicalOp, got {value:?}");
4122 };
4123 assert_eq!(*op, LogicalOp::And);
4124 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4125 }
4126
4127 #[test]
4128 fn precedence_where_captures_full_condition() {
4129 let src = "entity E { v: items where status = active }";
4133 let r = parse_ok(src);
4134 assert_eq!(r.diagnostics.len(), 0);
4135 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4136 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4137 let Expr::Where { condition, .. } = value else {
4138 panic!("expected Where, got {value:?}");
4139 };
4140 assert!(matches!(condition.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4141 }
4142
4143 #[test]
4144 fn precedence_where_captures_and_or_conditions() {
4145 let src = "entity E { v: items where status = active and count > 0 }";
4148 let r = parse_ok(src);
4149 assert_eq!(r.diagnostics.len(), 0);
4150 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4151 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4152 let Expr::Where { condition, .. } = value else {
4153 panic!("expected Where, got {value:?}");
4154 };
4155 assert!(matches!(condition.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4156 }
4157
4158 #[test]
4159 fn precedence_projection_applies_to_where_result() {
4160 let src = "entity E { v: items where status = confirmed -> interviewer }";
4163 let r = parse_ok(src);
4164 assert_eq!(r.diagnostics.len(), 0);
4165 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4166 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4167 let Expr::ProjectionMap { source, field, .. } = value else {
4168 panic!("expected ProjectionMap, got {value:?}");
4169 };
4170 assert_eq!(field.name, "interviewer");
4171 assert!(matches!(source.as_ref(), Expr::Where { .. }));
4172 }
4173
4174 #[test]
4175 fn precedence_lambda_binds_loosest() {
4176 let src = "entity E { v: items.any(i => i.active and i.valid) }";
4178 let r = parse_ok(src);
4179 assert_eq!(r.diagnostics.len(), 0);
4180 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4181 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4182 let Expr::Call { args, .. } = value else { panic!() };
4183 let CallArg::Positional(Expr::Lambda { body, .. }) = &args[0] else { panic!() };
4184 assert!(matches!(body.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4185 }
4186
4187 #[test]
4188 fn precedence_in_binds_at_comparison_level() {
4189 let src = r#"rule R {
4191 when: X(x, y)
4192 requires: x in {a, b} and y not in {c}
4193 ensures: Done()
4194}"#;
4195 let r = parse_ok(src);
4196 assert_eq!(r.diagnostics.len(), 0);
4197 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4198 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4199 let Expr::LogicalOp { op, left, right, .. } = value else {
4200 panic!("expected LogicalOp, got {value:?}");
4201 };
4202 assert_eq!(*op, LogicalOp::And);
4203 assert!(matches!(left.as_ref(), Expr::In { .. }));
4204 assert!(matches!(right.as_ref(), Expr::NotIn { .. }));
4205 }
4206
4207 #[test]
4210 fn multiline_ensures_block() {
4211 let src = r#"rule R {
4212 when: X(doc)
4213 ensures:
4214 doc.status = published
4215 Notification.created(to: doc.author)
4216}"#;
4217 let r = parse_ok(src);
4218 assert_eq!(r.diagnostics.len(), 0);
4219 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4220 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4221 assert_eq!(keyword, "ensures");
4222 let Expr::Block { items, .. } = value else {
4223 panic!("expected Block for multi-line ensures, got {value:?}");
4224 };
4225 assert_eq!(items.len(), 2);
4226 }
4227
4228 #[test]
4229 fn singleline_ensures_value() {
4230 let src = r#"rule R {
4231 when: X(doc)
4232 ensures: doc.status = published
4233}"#;
4234 let r = parse_ok(src);
4235 assert_eq!(r.diagnostics.len(), 0);
4236 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4237 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4238 assert_eq!(keyword, "ensures");
4239 assert!(!matches!(value, Expr::Block { .. }), "single-line ensures should not be Block");
4241 }
4242
4243 #[test]
4244 fn multiline_requires_with_continuation() {
4245 let src = r#"rule R {
4246 when: X(a)
4247 requires:
4248 a.count >= 2
4249 or a.items.any(i => i.can_solo)
4250 ensures: Done()
4251}"#;
4252 let r = parse_ok(src);
4253 assert_eq!(r.diagnostics.len(), 0);
4254 }
4255
4256 #[test]
4259 fn object_literal_single_field() {
4260 let src = r#"rule R {
4261 when: X()
4262 ensures:
4263 let o = {name: "test"}
4264 Done()
4265}"#;
4266 let r = parse_ok(src);
4267 assert_eq!(r.diagnostics.len(), 0);
4268 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4269 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4270 let Expr::Block { items, .. } = value else { panic!() };
4271 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4272 assert!(matches!(let_val.as_ref(), Expr::ObjectLiteral { .. }));
4273 }
4274
4275 #[test]
4276 fn set_literal_single_element() {
4277 let src = r#"rule R {
4278 when: X()
4279 ensures:
4280 let s = {active}
4281 Done()
4282}"#;
4283 let r = parse_ok(src);
4284 assert_eq!(r.diagnostics.len(), 0);
4285 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4286 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4287 let Expr::Block { items, .. } = value else { panic!() };
4288 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4289 assert!(matches!(let_val.as_ref(), Expr::SetLiteral { .. }),
4290 "bare {{ident}} should parse as set literal, got {:?}", let_val);
4291 }
4292
4293 #[test]
4296 fn lambda_with_chained_access() {
4297 let src = "entity E { v: items.all(t => t.item.status = active) }";
4298 let r = parse_ok(src);
4299 assert_eq!(r.diagnostics.len(), 0);
4300 }
4301
4302 #[test]
4303 fn nested_lambda() {
4304 let src = "entity E { v: groups.any(g => g.items.all(i => i.valid)) }";
4305 let r = parse_ok(src);
4306 assert_eq!(r.diagnostics.len(), 0);
4307 }
4308
4309 #[test]
4312 fn qualified_name_with_member_access() {
4313 let src = "entity E { v: shared/Validator.check }";
4314 let r = parse_ok(src);
4315 assert_eq!(r.diagnostics.len(), 0);
4316 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4317 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4318 let Expr::MemberAccess { object, field, .. } = value else {
4319 panic!("expected MemberAccess, got {value:?}");
4320 };
4321 assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
4322 assert_eq!(field.name, "check");
4323 }
4324
4325 #[test]
4326 fn qualified_name_in_call() {
4327 let src = r#"rule R {
4328 when: X(item)
4329 requires: shared/Validator.check(item: item)
4330 ensures: Done()
4331}"#;
4332 let r = parse_ok(src);
4333 assert_eq!(r.diagnostics.len(), 0);
4334 }
4335
4336 #[test]
4339 fn nested_if_inside_for() {
4340 let src = r#"rule R {
4341 when: X()
4342 for user in Users where user.active:
4343 if user.role = admin:
4344 ensures: AdminNotified(user: user)
4345 else:
4346 ensures: UserNotified(user: user)
4347}"#;
4348 let r = parse_ok(src);
4349 assert_eq!(r.diagnostics.len(), 0);
4350 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4351 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4352 assert!(matches!(items[0].kind, BlockItemKind::IfBlock { .. }));
4353 }
4354
4355 #[test]
4356 fn for_with_let_before_ensures() {
4357 let src = r#"rule R {
4358 when: schedule: DigestSchedule.next_run_at <= now
4359 for user in Users where user.active:
4360 let pending = user.tasks where status = pending
4361 ensures: DigestEmail.created(to: user.email, tasks: pending)
4362}"#;
4363 let r = parse_ok(src);
4364 assert_eq!(r.diagnostics.len(), 0);
4365 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4366 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4367 assert_eq!(items.len(), 2, "for body should have let + ensures");
4368 assert!(matches!(items[0].kind, BlockItemKind::Let { .. }));
4369 assert!(matches!(items[1].kind, BlockItemKind::Clause { .. }));
4370 }
4371
4372 #[test]
4375 fn join_lookup_all_unnamed() {
4376 let src = "entity E { match: Other{a, b, c} }";
4377 let r = parse_ok(src);
4378 assert_eq!(r.diagnostics.len(), 0);
4379 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4380 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4381 let Expr::JoinLookup { fields, .. } = value else { panic!() };
4382 assert_eq!(fields.len(), 3);
4383 assert!(fields.iter().all(|f| f.value.is_none()));
4384 }
4385
4386 #[test]
4387 fn join_lookup_all_named() {
4388 let src = "entity E { match: Membership{user: actor, workspace: ws} }";
4389 let r = parse_ok(src);
4390 assert_eq!(r.diagnostics.len(), 0);
4391 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4392 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4393 let Expr::JoinLookup { fields, .. } = value else { panic!() };
4394 assert_eq!(fields.len(), 2);
4395 assert!(fields.iter().all(|f| f.value.is_some()));
4396 }
4397
4398 #[test]
4399 fn join_lookup_in_requires() {
4400 let src = r#"rule R {
4401 when: X(user, workspace)
4402 requires: exists WorkspaceMembership{user: user, workspace: workspace}
4403 ensures: Done()
4404}"#;
4405 let r = parse_ok(src);
4406 assert_eq!(r.diagnostics.len(), 0);
4407 }
4408
4409 #[test]
4410 fn join_lookup_negated_in_requires() {
4411 let src = r#"rule R {
4412 when: X(email)
4413 requires: not exists User{email: email}
4414 ensures: Done()
4415}"#;
4416 let r = parse_ok(src);
4417 assert_eq!(r.diagnostics.len(), 0);
4418 }
4419
4420 #[test]
4425 fn implies_basic() {
4426 let src = "rule R { requires: a implies b }";
4427 let r = parse_ok(src);
4428 assert_eq!(r.diagnostics.len(), 0);
4429 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4430 let BlockItemKind::Clause { value, .. } = &b.items[0].kind else { panic!() };
4431 let Expr::LogicalOp { op, .. } = value else { panic!("expected LogicalOp, got {value:?}") };
4432 assert_eq!(*op, LogicalOp::Implies);
4433 }
4434
4435 #[test]
4436 fn implies_precedence_and_binds_tighter() {
4437 let src = "rule R { v: a and b implies c }";
4439 let r = parse_ok(src);
4440 assert_eq!(r.diagnostics.len(), 0);
4441 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4442 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4443 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4444 assert_eq!(*op, LogicalOp::Implies);
4445 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4446 }
4447
4448 #[test]
4449 fn implies_precedence_or_binds_tighter() {
4450 let src = "rule R { v: a or b implies c }";
4452 let r = parse_ok(src);
4453 assert_eq!(r.diagnostics.len(), 0);
4454 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4455 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4456 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4457 assert_eq!(*op, LogicalOp::Implies);
4458 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4459 }
4460
4461 #[test]
4462 fn implies_precedence_implies_above_or() {
4463 let src = "rule R { v: a implies b or c }";
4465 let r = parse_ok(src);
4466 assert_eq!(r.diagnostics.len(), 0);
4467 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4468 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4469 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4470 assert_eq!(*op, LogicalOp::Implies);
4471 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4472 }
4473
4474 #[test]
4475 fn implies_precedence_not_binds_tighter() {
4476 let src = "rule R { v: not a implies b }";
4478 let r = parse_ok(src);
4479 assert_eq!(r.diagnostics.len(), 0);
4480 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4481 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4482 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4483 assert_eq!(*op, LogicalOp::Implies);
4484 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4485 }
4486
4487 #[test]
4488 fn implies_right_associative() {
4489 let src = "rule R { v: a implies b implies c }";
4491 let r = parse_ok(src);
4492 assert_eq!(r.diagnostics.len(), 0);
4493 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4494 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4495 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4496 assert_eq!(*op, LogicalOp::Implies);
4497 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4498 }
4499
4500 #[test]
4501 fn implies_is_keyword_parsed_as_operator() {
4502 let src = "entity E { v: a implies b }";
4505 let r = parse_ok(src);
4506 assert_eq!(r.diagnostics.len(), 0);
4507 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4508 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4509 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4510 }
4511
4512 #[test]
4513 fn implies_in_ensures() {
4514 let src = r#"rule R {
4515 when: X()
4516 ensures: a implies b
4517}"#;
4518 let r = parse_ok(src);
4519 assert_eq!(r.diagnostics.len(), 0);
4520 }
4521
4522 #[test]
4523 fn implies_in_derived_value() {
4524 let src = "entity E { v: a implies b }";
4525 let r = parse_ok(src);
4526 assert_eq!(r.diagnostics.len(), 0);
4527 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4528 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4529 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4530 }
4531
4532 #[test]
4537 fn guidance_ordering_tests_removed() {
4538 }
4542
4543 #[test]
4548 fn contract_signatures_only() {
4549 let src = r#"contract Auditable {
4550 last_modified_by: Actor
4551 last_modified_at: Timestamp
4552}"#;
4553 let r = parse_ok(src);
4554 assert_eq!(r.diagnostics.len(), 0);
4555 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4556 assert_eq!(b.kind, BlockKind::Contract);
4557 assert_eq!(b.name.as_ref().unwrap().name, "Auditable");
4558 assert_eq!(b.items.len(), 2);
4559 }
4560
4561 #[test]
4562 fn contract_with_annotations() {
4563 let src = r#"contract Versioned {
4564 version: Integer
4565 @invariant Monotonic
4566 -- versions must increase
4567 @guidance
4568 -- use semantic versioning
4569}"#;
4570 let r = parse_ok(src);
4571 assert_eq!(r.diagnostics.len(), 0);
4572 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4573 assert_eq!(b.kind, BlockKind::Contract);
4574 assert_eq!(b.items.len(), 3);
4575 }
4576
4577 #[test]
4578 fn contract_with_any_type() {
4579 let src = r#"contract Identifiable {
4580 id: Any
4581}"#;
4582 let r = parse_ok(src);
4583 assert_eq!(r.diagnostics.len(), 0);
4584 }
4585
4586 #[test]
4587 fn contract_lowercase_name_rejected() {
4588 let src = "-- allium: 1\ncontract bad {}";
4589 let r = parse(src);
4590 assert!(
4591 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4592 "expected uppercase error, got: {:?}",
4593 r.diagnostics
4594 );
4595 }
4596
4597 #[test]
4598 fn contract_colon_body_rejected() {
4599 let src = "-- allium: 1\ncontract Bad: something";
4600 let r = parse(src);
4601 assert!(
4602 r.diagnostics.iter().any(|d| d.message.contains("braces")),
4603 "expected braces error, got: {:?}",
4604 r.diagnostics
4605 );
4606 }
4607
4608 #[test]
4613 fn contracts_clause_single_demands() {
4614 let src = "surface S {\n contracts:\n demands Auditable\n}";
4615 let r = parse_ok(src);
4616 assert_eq!(r.diagnostics.len(), 0);
4617 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4618 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4619 panic!("expected ContractsClause, got {:?}", b.items[0].kind)
4620 };
4621 assert_eq!(entries.len(), 1);
4622 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4623 assert_eq!(entries[0].name.name, "Auditable");
4624 }
4625
4626 #[test]
4627 fn contracts_clause_single_fulfils() {
4628 let src = "surface S {\n contracts:\n fulfils EventSubmitter\n}";
4629 let r = parse_ok(src);
4630 assert_eq!(r.diagnostics.len(), 0);
4631 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4632 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4633 panic!("expected ContractsClause")
4634 };
4635 assert_eq!(entries.len(), 1);
4636 assert!(matches!(entries[0].direction, ContractDirection::Fulfils));
4637 assert_eq!(entries[0].name.name, "EventSubmitter");
4638 }
4639
4640 #[test]
4641 fn contracts_clause_mixed() {
4642 let src = "surface S {\n contracts:\n demands Auditable\n fulfils EventSubmitter\n}";
4643 let r = parse_ok(src);
4644 assert_eq!(r.diagnostics.len(), 0);
4645 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4646 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4647 panic!("expected ContractsClause")
4648 };
4649 assert_eq!(entries.len(), 2);
4650 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4651 assert!(matches!(entries[1].direction, ContractDirection::Fulfils));
4652 }
4653
4654 #[test]
4655 fn contracts_with_other_clauses() {
4656 let src = r#"surface S {
4657 facing user: User
4658 contracts:
4659 demands Auditable
4660 exposes:
4661 user.name
4662}"#;
4663 let r = parse_ok(src);
4664 assert_eq!(r.diagnostics.len(), 0);
4665 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4666 assert_eq!(b.items.len(), 3);
4667 }
4668
4669 #[test]
4670 fn contracts_only_surface() {
4671 let src = "surface S {\n contracts:\n demands Foo\n fulfils Bar\n}";
4672 let r = parse_ok(src);
4673 assert_eq!(r.diagnostics.len(), 0);
4674 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4675 assert_eq!(b.items.len(), 1);
4676 }
4677
4678 #[test]
4679 fn contracts_empty_rejected() {
4680 let src = "-- allium: 1\nsurface S {\n contracts:\n}";
4681 let r = parse(src);
4682 assert!(
4683 r.diagnostics.iter().any(|d| d.message.contains("Empty `contracts:`")),
4684 "expected empty contracts error, got: {:?}",
4685 r.diagnostics
4686 );
4687 }
4688
4689 #[test]
4690 fn contracts_inline_block_rejected() {
4691 let src = "-- allium: 1\nsurface S {\n contracts:\n demands Foo {\n }\n}";
4692 let r = parse(src);
4693 assert!(
4694 r.diagnostics.iter().any(|d| d.message.contains("Inline contract blocks")),
4695 "expected inline block error, got: {:?}",
4696 r.diagnostics
4697 );
4698 }
4699
4700 #[test]
4701 fn contracts_unknown_direction_rejected() {
4702 let src = "-- allium: 1\nsurface S {\n contracts:\n requires Foo\n}";
4703 let r = parse(src);
4704 assert!(
4705 r.diagnostics.iter().any(|d| d.message.contains("Unknown direction")),
4706 "expected unknown direction error, got: {:?}",
4707 r.diagnostics
4708 );
4709 }
4710
4711 #[test]
4716 fn annotation_invariant() {
4717 let src = "contract C {\n @invariant Determinism\n -- all evaluations must be deterministic\n}";
4718 let r = parse_ok(src);
4719 assert_eq!(r.diagnostics.len(), 0);
4720 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4721 let BlockItemKind::Annotation(ann) = &b.items[0].kind else {
4722 panic!("expected Annotation, got {:?}", b.items[0].kind)
4723 };
4724 assert!(matches!(ann.kind, AnnotationKind::Invariant));
4725 assert_eq!(ann.name.as_ref().unwrap().name, "Determinism");
4726 assert_eq!(ann.body.len(), 1);
4727 assert_eq!(ann.body[0], "all evaluations must be deterministic");
4728 }
4729
4730 #[test]
4731 fn annotation_multiple_invariants() {
4732 let src = "contract C {\n @invariant A\n -- first\n @invariant B\n -- second\n}";
4733 let r = parse_ok(src);
4734 assert_eq!(r.diagnostics.len(), 0);
4735 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4736 assert_eq!(b.items.len(), 2);
4737 assert!(matches!(&b.items[0].kind, BlockItemKind::Annotation(_)));
4738 assert!(matches!(&b.items[1].kind, BlockItemKind::Annotation(_)));
4739 }
4740
4741 #[test]
4742 fn annotation_invariant_then_guidance() {
4743 let src = "contract C {\n @invariant Safety\n -- must be safe\n @guidance\n -- implementation notes\n}";
4744 let r = parse_ok(src);
4745 assert_eq!(r.diagnostics.len(), 0);
4746 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4747 assert_eq!(b.items.len(), 2);
4748 }
4749
4750 #[test]
4751 fn annotation_guidance_in_rule() {
4752 let src = "rule R {\n when: Event.created\n ensures: something\n @guidance\n -- do it this way\n}";
4753 let r = parse_ok(src);
4754 assert_eq!(r.diagnostics.len(), 0);
4755 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4756 let last = b.items.last().unwrap();
4757 let BlockItemKind::Annotation(ann) = &last.kind else { panic!() };
4758 assert!(matches!(ann.kind, AnnotationKind::Guidance));
4759 assert!(ann.name.is_none());
4760 }
4761
4762 #[test]
4763 fn annotation_guarantee() {
4764 let src = "surface S {\n @guarantee ResponseTime\n -- must respond within 100ms\n}";
4765 let r = parse_ok(src);
4766 assert_eq!(r.diagnostics.len(), 0);
4767 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4768 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
4769 assert!(matches!(ann.kind, AnnotationKind::Guarantee));
4770 assert_eq!(ann.name.as_ref().unwrap().name, "ResponseTime");
4771 }
4772
4773 #[test]
4774 fn annotation_guarantee_then_guidance() {
4775 let src = "surface S {\n @guarantee Fast\n -- sub-second\n @guidance\n -- cache aggressively\n}";
4776 let r = parse_ok(src);
4777 assert_eq!(r.diagnostics.len(), 0);
4778 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4779 assert_eq!(b.items.len(), 2);
4780 }
4781
4782 #[test]
4783 fn annotation_contracts_guarantee_guidance() {
4784 let src = r#"surface S {
4785 contracts:
4786 demands Auditable
4787 @guarantee ResponseTime
4788 -- fast
4789 @guidance
4790 -- notes
4791}"#;
4792 let r = parse_ok(src);
4793 assert_eq!(r.diagnostics.len(), 0);
4794 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4795 assert_eq!(b.items.len(), 3);
4796 }
4797
4798 #[test]
4799 fn annotation_multiline_body() {
4800 let src = "contract C {\n @invariant Multi\n -- line one\n -- line two\n -- line three\n}";
4801 let r = parse_ok(src);
4802 assert_eq!(r.diagnostics.len(), 0);
4803 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4804 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
4805 assert_eq!(ann.body.len(), 3);
4806 assert_eq!(ann.body[0], "line one");
4807 assert_eq!(ann.body[2], "line three");
4808 }
4809
4810 #[test]
4811 fn annotation_empty_body_rejected() {
4812 let src = "-- allium: 1\ncontract C {\n @invariant NoBody\n}";
4813 let r = parse(src);
4814 assert!(
4815 r.diagnostics.iter().any(|d| d.message.contains("at least one indented comment line")),
4816 "expected empty body error, got: {:?}",
4817 r.diagnostics
4818 );
4819 }
4820
4821 #[test]
4822 fn annotation_unknown_keyword_rejected() {
4823 let src = "-- allium: 1\ncontract C {\n @note Something\n -- text\n}";
4824 let r = parse(src);
4825 assert!(
4826 r.diagnostics.iter().any(|d| d.message.contains("Unknown annotation")),
4827 "expected unknown annotation error, got: {:?}",
4828 r.diagnostics
4829 );
4830 }
4831
4832 #[test]
4833 fn expression_invariant_still_works() {
4834 let src = r#"entity E {
4835 status: pending | active
4836 invariant AllValid {
4837 this.status = active
4838 }
4839}"#;
4840 let r = parse_ok(src);
4841 assert_eq!(r.diagnostics.len(), 0);
4842 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4843 let inv = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. }));
4845 assert!(inv.is_some(), "expression-bearing invariant should still parse");
4846 }
4847
4848 #[test]
4849 fn invariant_colon_form_migration() {
4850 let src = "-- allium: 1\ncontract C {\n invariant: SomeName\n}";
4851 let r = parse(src);
4852 assert!(
4853 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
4854 "expected migration diagnostic, got: {:?}",
4855 r.diagnostics
4856 );
4857 }
4858
4859 #[test]
4860 fn guidance_colon_form_migration() {
4861 let src = "-- allium: 1\nrule R {\n when: Event.created\n ensures: something\n guidance: \"do it\"\n}";
4862 let r = parse(src);
4863 assert!(
4864 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
4865 "expected migration diagnostic, got: {:?}",
4866 r.diagnostics
4867 );
4868 }
4869
4870 #[test]
4871 fn guarantee_colon_form_migration() {
4872 let src = "-- allium: 1\nsurface S {\n guarantee: \"fast\"\n}";
4873 let r = parse(src);
4874 assert!(
4875 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
4876 "expected migration diagnostic, got: {:?}",
4877 r.diagnostics
4878 );
4879 }
4880
4881 #[test]
4882 fn annotation_guidance_with_name_rejected() {
4883 let src = "-- allium: 1\ncontract C {\n @guidance Named\n -- text\n}";
4884 let r = parse(src);
4885 assert!(
4886 r.diagnostics.iter().any(|d| d.message.contains("does not take a name")),
4887 "expected guidance name error, got: {:?}",
4888 r.diagnostics
4889 );
4890 }
4891
4892 #[test]
4897 fn invariant_top_level_simple() {
4898 let src = r#"invariant PositiveBalance {
4899 this.balance > 0
4900}"#;
4901 let r = parse_ok(src);
4902 assert_eq!(r.diagnostics.len(), 0);
4903 let Decl::Invariant(inv) = &r.module.declarations[0] else {
4904 panic!("expected Invariant, got {:?}", r.module.declarations[0])
4905 };
4906 assert_eq!(inv.name.name, "PositiveBalance");
4907 }
4908
4909 #[test]
4910 fn invariant_top_level_for_quantifier() {
4911 let src = r#"invariant AllPositive {
4912 for item in items: item.value > 0
4913}"#;
4914 let r = parse_ok(src);
4915 assert_eq!(r.diagnostics.len(), 0);
4916 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
4917 assert!(matches!(inv.body, Expr::For { .. }));
4918 }
4919
4920 #[test]
4921 fn invariant_top_level_nested_for() {
4922 let src = r#"invariant NestedFor {
4923 for a in items: for b in a.children: b.valid = true
4924}"#;
4925 let r = parse_ok(src);
4926 assert_eq!(r.diagnostics.len(), 0);
4927 }
4928
4929 #[test]
4930 fn invariant_top_level_implies() {
4931 let src = r#"invariant ImpliesTest {
4932 this.active implies this.balance > 0
4933}"#;
4934 let r = parse_ok(src);
4935 assert_eq!(r.diagnostics.len(), 0);
4936 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
4937 assert!(matches!(inv.body, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4938 }
4939
4940 #[test]
4941 fn invariant_top_level_let_binding() {
4942 let src = r#"invariant WithLet {
4943 let total = this.items.count()
4944 total > 0
4945}"#;
4946 let r = parse_ok(src);
4947 assert_eq!(r.diagnostics.len(), 0);
4948 }
4949
4950 #[test]
4951 fn invariant_top_level_collection_ops() {
4952 let src = r#"invariant CollectionOps {
4953 this.items where active = true
4954}"#;
4955 let r = parse_ok(src);
4956 assert_eq!(r.diagnostics.len(), 0);
4957 }
4958
4959 #[test]
4960 fn invariant_top_level_exists() {
4961 let src = r#"invariant ExistsCheck {
4962 exists this.primary_contact
4963}"#;
4964 let r = parse_ok(src);
4965 assert_eq!(r.diagnostics.len(), 0);
4966 }
4967
4968 #[test]
4969 fn invariant_top_level_not_exists() {
4970 let src = r#"invariant NotExistsCheck {
4971 not exists this.deleted_at
4972}"#;
4973 let r = parse_ok(src);
4974 assert_eq!(r.diagnostics.len(), 0);
4975 }
4976
4977 #[test]
4978 fn invariant_top_level_optional_navigation() {
4979 let src = r#"invariant OptionalNav {
4980 this.owner?.email ?? "none" != "none"
4981}"#;
4982 let r = parse_ok(src);
4983 assert_eq!(r.diagnostics.len(), 0);
4984 }
4985
4986 #[test]
4987 fn invariant_top_level_lowercase_rejected() {
4988 let src = "-- allium: 1\ninvariant bad { true }";
4989 let r = parse(src);
4990 assert!(
4991 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4992 "expected uppercase error, got: {:?}",
4993 r.diagnostics
4994 );
4995 }
4996
4997 #[test]
4998 fn invariant_entity_level() {
4999 let src = r#"entity Account {
5000 balance: Decimal
5001 invariant NonNegative { this.balance >= 0 }
5002}"#;
5003 let r = parse_ok(src);
5004 assert_eq!(r.diagnostics.len(), 0);
5005 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5006 let BlockItemKind::InvariantBlock { name, body: _ } = &b.items[1].kind else {
5007 panic!("expected InvariantBlock, got {:?}", b.items[1].kind)
5008 };
5009 assert_eq!(name.name, "NonNegative");
5010 }
5011
5012 #[test]
5013 fn invariant_entity_level_this_ref() {
5014 let src = r#"entity Order {
5015 total: Decimal
5016 invariant PositiveTotal { this.total > 0 }
5017}"#;
5018 let r = parse_ok(src);
5019 assert_eq!(r.diagnostics.len(), 0);
5020 }
5021
5022 #[test]
5023 fn invariant_entity_level_implies() {
5024 let src = r#"entity Subscription {
5025 active: Boolean
5026 balance: Decimal
5027 invariant ActiveMeansPositive { this.active implies this.balance > 0 }
5028}"#;
5029 let r = parse_ok(src);
5030 assert_eq!(r.diagnostics.len(), 0);
5031 }
5032
5033 #[test]
5034 fn invariant_entity_level_lowercase_rejected() {
5035 let src = "-- allium: 1\nentity E { invariant bad { true } }";
5036 let r = parse(src);
5037 assert!(
5038 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
5039 "expected uppercase error, got: {:?}",
5040 r.diagnostics
5041 );
5042 }
5043
5044 #[test]
5045 fn invariant_colon_form_in_entity_migration() {
5046 let src = "-- allium: 1\nentity E {\n invariant: -- must be valid\n}";
5048 let r = parse(src);
5049 assert!(
5050 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
5051 "expected migration diagnostic, got: {:?}",
5052 r.diagnostics
5053 );
5054 }
5055
5056 #[test]
5057 fn invariant_top_level_colon_rejected() {
5058 let src = "-- allium: 1\ninvariant Bad: some text";
5060 let r = parse(src);
5061 assert!(
5062 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5063 "expected error for colon-delimited invariant at top level, got: {:?}",
5064 r.diagnostics
5065 );
5066 }
5067
5068 #[test]
5069 fn invariant_same_name_different_scopes() {
5070 let src = r#"invariant SameName { true }
5072entity E {
5073 invariant SameName { true }
5074}"#;
5075 let r = parse_ok(src);
5076 assert_eq!(r.diagnostics.len(), 0);
5077 }
5078
5079 #[test]
5084 fn config_qualified_reference() {
5085 let src = r#"config {
5087 param: Integer = core/config.max_batch_size
5088}"#;
5089 let r = parse_ok(src);
5090 assert_eq!(r.diagnostics.len(), 0);
5091 }
5092
5093 #[test]
5094 fn config_multiple_qualified_refs() {
5095 let src = r#"config {
5096 param_a: Integer = core/config.max_batch_size
5097 param_b: Duration = core/config.default_delay
5098}"#;
5099 let r = parse_ok(src);
5100 assert_eq!(r.diagnostics.len(), 0);
5101 }
5102
5103 #[test]
5104 fn config_qualified_ref_with_type() {
5105 let src = r#"config {
5106 publish_delay: Duration = core/config.default_delay
5107}"#;
5108 let r = parse_ok(src);
5109 assert_eq!(r.diagnostics.len(), 0);
5110 }
5111
5112 #[test]
5113 fn config_qualified_chain() {
5114 let src = r#"config {
5116 first: Integer = core/config.base
5117 second: Integer = first
5118}"#;
5119 let r = parse_ok(src);
5120 assert_eq!(r.diagnostics.len(), 0);
5121 }
5122
5123 #[test]
5124 fn config_renamed_param_with_qualified_ref() {
5125 let src = r#"config {
5126 my_timeout: Duration = core/config.base_timeout
5127}"#;
5128 let r = parse_ok(src);
5129 assert_eq!(r.diagnostics.len(), 0);
5130 }
5131
5132 #[test]
5137 fn config_default_arithmetic() {
5138 let src = r#"config {
5139 param: Integer = other_param + 1
5140}"#;
5141 let r = parse_ok(src);
5142 assert_eq!(r.diagnostics.len(), 0);
5143 }
5144
5145 #[test]
5146 fn config_default_qualified_arithmetic() {
5147 let src = r#"config {
5148 param: Duration = core/config.timeout * 2
5149}"#;
5150 let r = parse_ok(src);
5151 assert_eq!(r.diagnostics.len(), 0);
5152 }
5153
5154 #[test]
5155 fn config_default_parenthesised() {
5156 let src = r#"config {
5157 param: Integer = (base + 1) * factor
5158}"#;
5159 let r = parse_ok(src);
5160 assert_eq!(r.diagnostics.len(), 0);
5161 }
5162
5163 #[test]
5164 fn config_default_two_qualified_refs() {
5165 let src = r#"config {
5166 param: Duration = core/config.a + core/config.b
5167}"#;
5168 let r = parse_ok(src);
5169 assert_eq!(r.diagnostics.len(), 0);
5170 }
5171
5172 #[test]
5173 fn config_default_literal_only() {
5174 let src = r#"config {
5175 param: Integer = 5
5176}"#;
5177 let r = parse_ok(src);
5178 assert_eq!(r.diagnostics.len(), 0);
5179 }
5180
5181 #[test]
5182 fn config_default_decimal_literal() {
5183 let src = r#"config {
5184 param: Decimal = price * 1.5
5185}"#;
5186 let r = parse_ok(src);
5187 assert_eq!(r.diagnostics.len(), 0);
5188 }
5189
5190 #[test]
5191 fn config_default_mixed_operators() {
5192 let src = r#"config {
5193 param: Duration = timeout * 2 + 1.minute
5194}"#;
5195 let r = parse_ok(src);
5196 assert_eq!(r.diagnostics.len(), 0);
5197 }
5198
5199 #[test]
5200 fn config_default_operator_precedence() {
5201 let src = r#"config {
5203 param: Integer = a + b * c
5204}"#;
5205 let r = parse_ok(src);
5206 assert_eq!(r.diagnostics.len(), 0);
5207 }
5208
5209 #[test]
5214 fn version_2_accepted() {
5215 let r = parse("-- allium: 2\nentity User {}");
5216 assert_eq!(r.module.version, Some(2));
5217 assert_eq!(r.diagnostics.len(), 0);
5218 }
5219
5220 #[test]
5221 fn version_99_still_rejected() {
5222 let r = parse("-- allium: 99\nentity User {}");
5223 assert!(r.diagnostics.iter().any(|d|
5224 d.severity == Severity::Error && d.message.contains("unsupported")
5225 ));
5226 }
5227
5228 #[test]
5229 fn contract_typed_signature() {
5230 let src = r#"contract Codec {
5231 serialize: (value: Any) -> ByteArray
5232}"#;
5233 let r = parse_ok(src);
5234 assert_eq!(r.diagnostics.len(), 0);
5235 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5236 assert_eq!(b.kind, BlockKind::Contract);
5237 let BlockItemKind::Assignment { name, value } = &b.items[0].kind else { panic!() };
5238 assert_eq!(name.name, "serialize");
5239 assert!(matches!(value, Expr::ProjectionMap { .. }));
5240 }
5241
5242 #[test]
5243 fn contract_multi_param_signature() {
5244 let src = r#"contract Codec {
5245 serialize: (value: Any, format: String) -> ByteArray
5246}"#;
5247 let r = parse_ok(src);
5248 assert_eq!(r.diagnostics.len(), 0);
5249 }
5250
5251 #[test]
5252 fn comma_separated_entity_fields() {
5253 let src = "entity Point { x: Decimal, y: Decimal }";
5254 let r = parse_ok(src);
5255 assert_eq!(r.diagnostics.len(), 0);
5256 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5257 assert_eq!(b.items.len(), 2);
5258 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "x"));
5259 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "y"));
5260 }
5261
5262 #[test]
5263 fn comma_separated_value_fields() {
5264 let src = "value Coord { x: Integer, y: Integer }";
5265 let r = parse_ok(src);
5266 assert_eq!(r.diagnostics.len(), 0);
5267 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5268 assert_eq!(b.items.len(), 2);
5269 }
5270
5271 #[test]
5276 fn version_3_accepted() {
5277 let r = parse("-- allium: 3\nentity User {}");
5278 assert_eq!(r.module.version, Some(3));
5279 assert_eq!(r.diagnostics.len(), 0);
5280 }
5281
5282 #[test]
5283 fn transitions_block_basic() {
5284 let src = r#"-- allium: 3
5285entity Order {
5286 status: pending | confirmed | shipped | delivered | cancelled
5287
5288 transitions status {
5289 pending -> confirmed
5290 confirmed -> shipped
5291 shipped -> delivered
5292 pending -> cancelled
5293 confirmed -> cancelled
5294 terminal: delivered, cancelled
5295 }
5296}"#;
5297 let r = parse(src);
5298 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5299 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5300 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5302 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5303 };
5304 assert_eq!(graph.field.name, "status");
5305 assert_eq!(graph.edges.len(), 5);
5306 assert_eq!(graph.edges[0].from.name, "pending");
5307 assert_eq!(graph.edges[0].to.name, "confirmed");
5308 assert_eq!(graph.terminal.len(), 2);
5309 assert_eq!(graph.terminal[0].name, "delivered");
5310 assert_eq!(graph.terminal[1].name, "cancelled");
5311 }
5312
5313 #[test]
5314 fn transitions_block_no_terminal() {
5315 let src = r#"-- allium: 3
5316entity Task {
5317 status: open | closed
5318 transitions status {
5319 open -> closed
5320 }
5321}"#;
5322 let r = parse(src);
5323 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5324 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5325 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5326 panic!("expected TransitionsBlock")
5327 };
5328 assert_eq!(graph.edges.len(), 1);
5329 assert!(graph.terminal.is_empty());
5330 }
5331
5332 #[test]
5333 fn produces_emits_migration_warning() {
5334 let src = r#"-- allium: 3
5335rule ShipOrder {
5336 when: ShipOrder(order, tracking)
5337 requires: order.status = picking
5338 produces: tracking_number, shipped_at
5339 ensures: order.status = shipped
5340}"#;
5341 let r = parse(src);
5342 let warnings: Vec<_> = r.diagnostics.iter()
5343 .filter(|d| d.severity == Severity::Warning)
5344 .collect();
5345 assert!(
5346 warnings.iter().any(|d| d.message.contains("`produces:` clauses are removed")),
5347 "expected migration warning for produces, got: {:?}", warnings
5348 );
5349 }
5350
5351 #[test]
5352 fn consumes_emits_migration_warning() {
5353 let src = r#"-- allium: 3
5354rule ReadOrder {
5355 when: Check(order)
5356 consumes: warehouse_assignment
5357 ensures: order.verified = true
5358}"#;
5359 let r = parse(src);
5360 let warnings: Vec<_> = r.diagnostics.iter()
5361 .filter(|d| d.severity == Severity::Warning)
5362 .collect();
5363 assert!(
5364 warnings.iter().any(|d| d.message.contains("`consumes:` clauses are removed")),
5365 "expected migration warning for consumes, got: {:?}", warnings
5366 );
5367 }
5368
5369 #[test]
5370 fn when_clause_on_field() {
5371 let src = r#"-- allium: 3
5372entity Order {
5373 status: pending | shipped | delivered
5374 tracking_number: String when status = shipped | delivered
5375 transitions status {
5376 pending -> shipped
5377 shipped -> delivered
5378 terminal: delivered
5379 }
5380}"#;
5381 let r = parse(src);
5382 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5383 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5384 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5385 let field_with_when = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }));
5386 assert!(field_with_when.is_some(), "expected FieldWithWhen item");
5387 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &field_with_when.unwrap().kind {
5388 assert_eq!(name.name, "tracking_number");
5389 assert_eq!(when_clause.status_field.name, "status");
5390 assert_eq!(when_clause.qualifying_states.len(), 2);
5391 assert_eq!(when_clause.qualifying_states[0].name, "shipped");
5392 assert_eq!(when_clause.qualifying_states[1].name, "delivered");
5393 }
5394 }
5395
5396 #[test]
5397 fn when_clause_single_state() {
5398 let src = r#"-- allium: 3
5399entity Order {
5400 status: active | cancelled
5401 cancelled_at: Timestamp when status = cancelled
5402 transitions status {
5403 active -> cancelled
5404 terminal: cancelled
5405 }
5406}"#;
5407 let r = parse(src);
5408 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5409 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5410 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5411 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &b.items[1].kind {
5412 assert_eq!(name.name, "cancelled_at");
5413 assert_eq!(when_clause.qualifying_states.len(), 1);
5414 assert_eq!(when_clause.qualifying_states[0].name, "cancelled");
5415 } else {
5416 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5417 }
5418 }
5419
5420 #[test]
5421 fn when_clause_with_optional() {
5422 let src = r#"-- allium: 3
5423entity Order {
5424 status: active | cancelled
5425 notes: String? when status = cancelled
5426 transitions status {
5427 active -> cancelled
5428 terminal: cancelled
5429 }
5430}"#;
5431 let r = parse(src);
5432 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5433 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5434 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5435 if let BlockItemKind::FieldWithWhen { name, value, when_clause } = &b.items[1].kind {
5436 assert_eq!(name.name, "notes");
5437 assert!(matches!(value, Expr::TypeOptional { .. }), "expected TypeOptional");
5438 assert_eq!(when_clause.qualifying_states.len(), 1);
5439 } else {
5440 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5441 }
5442 }
5443
5444 #[test]
5445 fn transitions_in_json_output() {
5446 let src = r#"-- allium: 3
5447entity Order {
5448 status: pending | done
5449 transitions status {
5450 pending -> done
5451 terminal: done
5452 }
5453}"#;
5454 let r = parse(src);
5455 let json = serde_json::to_string(&r.module).unwrap();
5456 assert!(json.contains("TransitionsBlock"), "JSON should contain TransitionsBlock: {}", json);
5457 assert!(json.contains("pending"), "JSON should contain 'pending'");
5458 }
5459
5460 #[test]
5461 fn transitions_block_with_commas() {
5462 let src = r#"-- allium: 3
5463entity Order {
5464 status: a | b | c
5465 transitions status {
5466 a -> b,
5467 b -> c,
5468 terminal: c,
5469 }
5470}"#;
5471 let r = parse(src);
5472 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5473 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5474 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5475 panic!("expected TransitionsBlock")
5476 };
5477 assert_eq!(graph.edges.len(), 2);
5478 assert_eq!(graph.terminal.len(), 1);
5479 }
5480
5481 #[test]
5482 fn v3_full_entity_with_transitions_and_rule() {
5483 let src = r#"-- allium: 3
5484entity Order {
5485 status: pending | shipped | delivered
5486 tracking: String when status = shipped | delivered
5487 shipped_at: Timestamp when status = shipped | delivered
5488
5489 transitions status {
5490 pending -> shipped
5491 shipped -> delivered
5492 terminal: delivered
5493 }
5494}
5495
5496rule ShipOrder {
5497 when: ShipOrder(order, tracking)
5498 requires: order.status = pending
5499 ensures:
5500 order.status = shipped
5501 order.tracking = tracking
5502 order.shipped_at = now
5503}"#;
5504 let r = parse(src);
5505 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5506 assert_eq!(r.module.declarations.len(), 2);
5507 }
5508
5509 #[test]
5514 fn transitions_empty_block() {
5515 let src = "-- allium: 3\nentity E {\n status: a | b\n transitions status {}\n}";
5516 let r = parse(src);
5517 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5518 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5519 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5520 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5521 };
5522 assert!(graph.edges.is_empty());
5523 assert!(graph.terminal.is_empty());
5524 }
5525
5526 #[test]
5527 fn transitions_terminal_only() {
5528 let src = r#"-- allium: 3
5529entity E {
5530 status: done
5531 transitions status {
5532 terminal: done
5533 }
5534}"#;
5535 let r = parse(src);
5536 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5537 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5538 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5539 assert!(graph.edges.is_empty());
5540 assert_eq!(graph.terminal.len(), 1);
5541 assert_eq!(graph.terminal[0].name, "done");
5542 }
5543
5544 #[test]
5545 fn transitions_terminal_before_edges() {
5546 let src = r#"-- allium: 3
5547entity E {
5548 status: a | b | c
5549 transitions status {
5550 terminal: c
5551 a -> b
5552 b -> c
5553 }
5554}"#;
5555 let r = parse(src);
5556 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5557 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5558 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5559 assert_eq!(graph.edges.len(), 2);
5560 assert_eq!(graph.terminal.len(), 1);
5561 assert_eq!(graph.terminal[0].name, "c");
5562 }
5563
5564 #[test]
5565 fn transitions_self_loop() {
5566 let src = r#"-- allium: 3
5567entity E {
5568 status: running | stopped
5569 transitions status {
5570 running -> running
5571 running -> stopped
5572 terminal: stopped
5573 }
5574}"#;
5575 let r = parse(src);
5576 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5577 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5578 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5579 assert_eq!(graph.edges.len(), 2);
5580 assert_eq!(graph.edges[0].from.name, "running");
5581 assert_eq!(graph.edges[0].to.name, "running");
5582 }
5583
5584 #[test]
5585 fn transitions_single_edge() {
5586 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s { a -> b }\n}";
5587 let r = parse(src);
5588 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5589 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5590 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5591 assert_eq!(graph.field.name, "s");
5592 assert_eq!(graph.edges.len(), 1);
5593 }
5594
5595 #[test]
5596 fn transitions_multiple_terminal_values() {
5597 let src = r#"-- allium: 3
5598entity E {
5599 status: a | b | c | d | e
5600 transitions status {
5601 a -> b
5602 b -> c
5603 terminal: c, d, e
5604 }
5605}"#;
5606 let r = parse(src);
5607 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5608 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5609 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5610 assert_eq!(graph.terminal.len(), 3);
5611 }
5612
5613 #[test]
5614 fn transitions_trailing_comma_in_terminal() {
5615 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n terminal: b,\n }\n}";
5616 let r = parse(src);
5617 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5618 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5619 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5620 assert_eq!(graph.terminal.len(), 1);
5621 }
5622
5623 #[test]
5624 fn transitions_among_other_entity_items() {
5625 let src = r#"-- allium: 3
5627entity Order {
5628 status: pending | shipped | delivered
5629 customer: Customer
5630 tracking: String?
5631
5632 transitions status {
5633 pending -> shipped
5634 shipped -> delivered
5635 terminal: delivered
5636 }
5637
5638 active_items: items where status = active
5639 invariant Positive { this.total > 0 }
5640}"#;
5641 let r = parse(src);
5642 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5643 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5644 assert_eq!(b.items.len(), 6);
5646 assert!(matches!(&b.items[3].kind, BlockItemKind::TransitionsBlock(_)));
5647 assert!(matches!(&b.items[5].kind, BlockItemKind::InvariantBlock { .. }));
5648 }
5649
5650 #[test]
5651 fn transitions_error_recovery_missing_arrow() {
5652 let src = r#"-- allium: 3
5653entity E {
5654 status: a | b | c
5655 transitions status {
5656 a b
5657 b -> c
5658 }
5659}"#;
5660 let r = parse(src);
5661 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5663 "expected error for missing arrow, got: {:?}", r.diagnostics);
5664 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5666 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5667 panic!("expected TransitionsBlock")
5668 };
5669 assert_eq!(graph.edges.len(), 1, "should recover and parse second edge");
5670 assert_eq!(graph.edges[0].from.name, "b");
5671 }
5672
5673 #[test]
5674 fn transitions_field_name_preserved() {
5675 let src = "-- allium: 3\nentity E {\n phase: x | y\n transitions phase { x -> y }\n}";
5676 let r = parse(src);
5677 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5678 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5679 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5680 assert_eq!(graph.field.name, "phase");
5681 }
5682
5683 #[test]
5684 fn transitions_diamond_topology() {
5685 let src = r#"-- allium: 3
5687entity E {
5688 status: new | path_a | path_b | done
5689 transitions status {
5690 new -> path_a
5691 new -> path_b
5692 path_a -> done
5693 path_b -> done
5694 terminal: done
5695 }
5696}"#;
5697 let r = parse(src);
5698 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5699 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5700 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5701 assert_eq!(graph.edges.len(), 4);
5702 }
5703
5704 #[test]
5705 fn transitions_edge_span_is_from_to_range() {
5706 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n }\n}";
5707 let r = parse(src);
5708 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5709 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5710 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5711 let edge = &graph.edges[0];
5712 assert!(edge.span.start <= edge.from.span.start);
5714 assert!(edge.span.end >= edge.to.span.end);
5715 }
5716
5717 #[test]
5722 fn when_clause_multiple_fields() {
5723 let src = r#"-- allium: 3
5724entity Order {
5725 status: pending | shipped | delivered
5726 tracking: String when status = shipped | delivered
5727 shipped_at: Timestamp when status = shipped | delivered
5728 delivered_at: Timestamp when status = delivered
5729 transitions status {
5730 pending -> shipped
5731 shipped -> delivered
5732 terminal: delivered
5733 }
5734}"#;
5735 let r = parse(src);
5736 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5737 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5738 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5739 let when_count = b.items.iter()
5740 .filter(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }))
5741 .count();
5742 assert_eq!(when_count, 3);
5743 }
5744
5745 #[test]
5746 fn legacy_produces_consumes_skipped_with_warnings() {
5747 let src = r#"-- allium: 3
5748rule R {
5749 when: Go(x)
5750 produces: field_a
5751 consumes: field_b
5752 ensures: x.done = true
5753}"#;
5754 let r = parse(src);
5755 let warnings: Vec<_> = r.diagnostics.iter()
5756 .filter(|d| d.severity == Severity::Warning)
5757 .collect();
5758 assert!(warnings.len() >= 2, "expected at least 2 migration warnings, got {}", warnings.len());
5759 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5761 assert!(
5762 !b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. })),
5763 "legacy produces/consumes should not become FieldWithWhen"
5764 );
5765 }
5766
5767 #[test]
5772 fn v3_entity_with_transitions_and_invariant() {
5773 let src = r#"-- allium: 3
5774entity Account {
5775 status: open | frozen | closed
5776 balance: Decimal
5777
5778 transitions status {
5779 open -> frozen
5780 frozen -> open
5781 open -> closed
5782 frozen -> closed
5783 terminal: closed
5784 }
5785
5786 invariant NonNegative { this.balance >= 0 }
5787}"#;
5788 let r = parse(src);
5789 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5790 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5791 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::TransitionsBlock(_))));
5792 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. })));
5793 }
5794
5795 #[test]
5796 fn v3_rule_with_multiple_ensures() {
5797 let src = r#"-- allium: 3
5798rule CompleteOrder {
5799 when: Complete(order)
5800 requires: order.status = shipped
5801 ensures: order.status = delivered
5802 ensures: order.completed_at = now
5803 ensures: order.receipt_number = generate_receipt()
5804}"#;
5805 let r = parse(src);
5806 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5807 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5808 let ensures_count = b.items.iter()
5809 .filter(|i| matches!(&i.kind, BlockItemKind::Clause { keyword, .. } if keyword == "ensures"))
5810 .count();
5811 assert_eq!(ensures_count, 3);
5812 }
5813
5814 #[test]
5815 fn v3_rule_with_if_block() {
5816 let src = r#"-- allium: 3
5817rule Cancel {
5818 when: Cancel(order, reason)
5819 requires: order.status != delivered
5820 ensures:
5821 order.status = cancelled
5822 order.cancelled_at = now
5823 if reason = customer_request:
5824 order.cancelled_by = order.customer
5825}"#;
5826 let r = parse(src);
5827 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5828 }
5829
5830 #[test]
5831 fn v3_complete_lifecycle_spec() {
5832 let src = r#"-- allium: 3
5833
5834entity Subscription {
5835 status: trial | active | past_due | cancelled
5836 started_at: Timestamp when status = active | past_due | cancelled
5837 cancelled_at: Timestamp when status = cancelled
5838 balance: Decimal
5839
5840 transitions status {
5841 trial -> active
5842 active -> past_due
5843 past_due -> active
5844 active -> cancelled
5845 past_due -> cancelled
5846 terminal: cancelled
5847 }
5848
5849 invariant NonNegative { this.balance >= 0 }
5850}
5851
5852config {
5853 trial_period: Duration = 14.days
5854}
5855
5856rule ActivateSubscription {
5857 when: Activate(sub)
5858 requires: sub.status = trial
5859 ensures:
5860 sub.status = active
5861 sub.started_at = now
5862}
5863
5864rule CancelSubscription {
5865 when: Cancel(sub)
5866 requires: sub.status != cancelled
5867 ensures:
5868 sub.status = cancelled
5869 sub.cancelled_at = now
5870}
5871
5872invariant AllCancelledHaveTimestamp {
5873 for sub in Subscriptions where status = cancelled:
5874 sub.cancelled_at != null
5875}
5876
5877surface SubscriptionDashboard {
5878 facing user: User
5879 context sub: Subscription where owner = user
5880 exposes:
5881 sub.status
5882 sub.balance
5883 provides:
5884 Cancel(sub) when sub.status != cancelled
5885}
5886"#;
5887 let r = parse(src);
5888 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5889 assert_eq!(r.module.declarations.len(), 6);
5891 }
5892
5893 #[test]
5894 fn v3_produces_consumes_are_field_names_in_entities() {
5895 let src = r#"-- allium: 3
5897entity Factory {
5898 produces: widget_a
5899 consumes: raw_material
5900}"#;
5901 let r = parse(src);
5902 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5903 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5904 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "produces"));
5905 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "consumes"));
5906 }
5907
5908 #[test]
5909 fn v3_legacy_produces_consumes_emit_warnings_in_rules() {
5910 let src = r#"-- allium: 3
5911rule Ship {
5912 when: Ship(order)
5913 produces: tracking_number
5914 consumes: warehouse
5915 ensures: order.status = shipped
5916}"#;
5917 let r = parse(src);
5918 let warnings: Vec<_> = r.diagnostics.iter()
5919 .filter(|d| d.severity == Severity::Warning)
5920 .collect();
5921 assert!(warnings.len() >= 2, "expected migration warnings, got {:?}", warnings);
5922 }
5923
5924 #[test]
5925 fn v3_version_preserved_in_module() {
5926 let src = "-- allium: 3\nentity E {}";
5927 let r = parse(src);
5928 assert_eq!(r.module.version, Some(3));
5929 }
5930
5931 #[test]
5932 fn v3_version_4_still_rejected() {
5933 let src = "-- allium: 4\nentity E {}";
5934 let r = parse(src);
5935 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error
5936 && d.message.contains("unsupported")));
5937 }
5938
5939 #[test]
5944 fn backtick_in_named_enum() {
5945 let src = "-- allium: 3\nenum Locale { en | fr | `de-CH-1996` | `no-cache` }";
5946 let r = parse(src);
5947 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5948 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5949 assert_eq!(b.items.len(), 4);
5950 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[0].kind else { panic!() };
5952 assert_eq!(name.name, "en");
5953 assert!(!backtick_quoted);
5954 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[2].kind else { panic!() };
5956 assert_eq!(name.name, "de-CH-1996");
5957 assert!(backtick_quoted);
5958 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[3].kind else { panic!() };
5960 assert_eq!(name.name, "no-cache");
5961 assert!(backtick_quoted);
5962 }
5963
5964 #[test]
5965 fn backtick_in_inline_enum() {
5966 let src = "-- allium: 3\nentity E { cache: `no-cache` | `no-store` | `public` }";
5967 let r = parse(src);
5968 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5969 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5970 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
5971 assert!(matches!(value, Expr::Pipe { .. }));
5973 }
5974
5975 #[test]
5976 fn backtick_in_comparison() {
5977 let src = r#"-- allium: 3
5978rule R {
5979 when: Check(item)
5980 requires: item.locale = `de-CH-1996`
5981 ensures: Done()
5982}"#;
5983 let r = parse(src);
5984 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5985 }
5986
5987 #[test]
5988 fn backtick_mixed_with_unquoted() {
5989 let src = "-- allium: 3\nenum CacheDirective { `no-cache` | `no-store` | public | private }";
5990 let r = parse(src);
5991 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5992 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5993 assert_eq!(b.items.len(), 4);
5994 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[0].kind else { panic!() };
5995 assert!(backtick_quoted);
5996 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[2].kind else { panic!() };
5997 assert!(!backtick_quoted);
5998 }
5999
6000 #[test]
6005 fn v3_lifecycle_fixture() {
6006 let src = include_str!("../tests/fixtures/v3-lifecycle.allium");
6007 let r = parse(src);
6008 let errors: Vec<_> = r.diagnostics.iter()
6009 .filter(|d| d.severity == Severity::Error)
6010 .collect();
6011 assert_eq!(
6012 errors.len(),
6013 0,
6014 "expected no errors in v3 lifecycle fixture, got: {:?}",
6015 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
6016 );
6017 }
6018}