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