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.at(TokenKind::RParen) {
2543 let end = self.advance().span; return Some(Expr::Block {
2545 span: start.merge(end),
2546 items: Vec::new(),
2547 });
2548 }
2549
2550 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
2552 let mut bindings = Vec::new();
2553 while !self.at(TokenKind::RParen) && !self.at_eof() {
2554 let name = self.parse_ident_in("parameter name")?;
2555 self.expect(TokenKind::Colon)?;
2556 let value = self.parse_expr(0)?;
2557 bindings.push(Expr::Binding {
2558 span: name.span.merge(value.span()),
2559 name,
2560 value: Box::new(value),
2561 });
2562 self.eat(TokenKind::Comma);
2563 }
2564 self.expect(TokenKind::RParen)?;
2565 if bindings.len() == 1 {
2566 return Some(bindings.into_iter().next().unwrap());
2567 }
2568 let span = start.merge(bindings.last().unwrap().span());
2569 return Some(Expr::Block {
2570 span,
2571 items: bindings,
2572 });
2573 }
2574
2575 let expr = self.parse_expr(0)?;
2576 self.expect(TokenKind::RParen)?;
2577 Some(expr)
2578 }
2579}
2580
2581#[cfg(test)]
2586mod tests {
2587 use super::*;
2588 use crate::diagnostic::Severity;
2589
2590 fn parse_ok(src: &str) -> ParseResult {
2591 let owned;
2594 let input = if src.starts_with("-- allium:") {
2595 src
2596 } else {
2597 owned = format!("-- allium: 1\n{src}");
2598 &owned
2599 };
2600 let result = parse(input);
2601 if !result.diagnostics.is_empty() {
2602 for d in &result.diagnostics {
2603 eprintln!(
2604 " [{:?}] {} ({}..{})",
2605 d.severity, d.message, d.span.start, d.span.end
2606 );
2607 }
2608 }
2609 result
2610 }
2611
2612 #[test]
2613 fn version_marker() {
2614 let r = parse_ok("-- allium: 1\n");
2615 assert_eq!(r.module.version, Some(1));
2616 assert_eq!(r.diagnostics.len(), 0);
2617 }
2618
2619 #[test]
2620 fn version_missing_warns() {
2621 let r = parse("entity User {}");
2622 assert_eq!(r.module.version, None);
2623 assert_eq!(r.diagnostics.len(), 1);
2624 assert_eq!(r.diagnostics[0].severity, Severity::Warning);
2625 assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
2626 }
2627
2628 #[test]
2629 fn version_unsupported_errors() {
2630 let r = parse("-- allium: 99\nentity User {}");
2631 assert_eq!(r.module.version, Some(99));
2632 assert!(r.diagnostics.iter().any(|d|
2633 d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
2634 ), "expected unsupported version error, got: {:?}", r.diagnostics);
2635 }
2636
2637 #[test]
2638 fn empty_entity() {
2639 let r = parse_ok("entity User {}");
2640 assert_eq!(r.diagnostics.len(), 0);
2641 assert_eq!(r.module.declarations.len(), 1);
2642 match &r.module.declarations[0] {
2643 Decl::Block(b) => {
2644 assert_eq!(b.kind, BlockKind::Entity);
2645 assert_eq!(b.name.as_ref().unwrap().name, "User");
2646 }
2647 other => panic!("expected Block, got {other:?}"),
2648 }
2649 }
2650
2651 #[test]
2652 fn entity_with_fields() {
2653 let src = r#"entity Order {
2654 customer: Customer
2655 status: pending | active | completed
2656 total: Decimal
2657}"#;
2658 let r = parse_ok(src);
2659 assert_eq!(r.diagnostics.len(), 0);
2660 match &r.module.declarations[0] {
2661 Decl::Block(b) => {
2662 assert_eq!(b.items.len(), 3);
2663 }
2664 other => panic!("expected Block, got {other:?}"),
2665 }
2666 }
2667
2668 #[test]
2669 fn use_declaration() {
2670 let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
2671 assert_eq!(r.diagnostics.len(), 0);
2672 match &r.module.declarations[0] {
2673 Decl::Use(u) => {
2674 assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
2675 }
2676 other => panic!("expected Use, got {other:?}"),
2677 }
2678 }
2679
2680 #[test]
2681 fn enum_declaration() {
2682 let src = "enum OrderStatus { pending | shipped | delivered }";
2683 let r = parse_ok(src);
2684 assert_eq!(r.diagnostics.len(), 0);
2685 }
2686
2687 #[test]
2688 fn config_block() {
2689 let src = r#"config {
2690 max_retries: Integer = 3
2691 timeout: Duration = 24.hours
2692}"#;
2693 let r = parse_ok(src);
2698 assert_eq!(r.diagnostics.len(), 0);
2699 }
2700
2701 #[test]
2702 fn rule_declaration() {
2703 let src = r#"rule PlaceOrder {
2704 when: CustomerPlacesOrder(customer, items, total)
2705 requires: total > 0
2706 ensures: Order.created(customer: customer, status: pending, total: total)
2707}"#;
2708 let r = parse_ok(src);
2709 assert_eq!(r.diagnostics.len(), 0);
2710 match &r.module.declarations[0] {
2711 Decl::Block(b) => {
2712 assert_eq!(b.kind, BlockKind::Rule);
2713 assert_eq!(b.items.len(), 3);
2714 }
2715 other => panic!("expected Block, got {other:?}"),
2716 }
2717 }
2718
2719 #[test]
2720 fn expression_precedence() {
2721 let r = parse_ok("rule T { v: a + b * c }");
2722 match &r.module.declarations[0] {
2724 Decl::Block(b) => match &b.items[0].kind {
2725 BlockItemKind::Assignment { value, .. } => match value {
2726 Expr::BinaryOp { op, right, .. } => {
2727 assert_eq!(*op, BinaryOp::Add);
2728 assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2729 }
2730 other => panic!("expected BinaryOp, got {other:?}"),
2731 },
2732 other => panic!("expected Assignment, got {other:?}"),
2733 },
2734 other => panic!("expected Block, got {other:?}"),
2735 }
2736 }
2737
2738 #[test]
2739 fn default_declaration() {
2740 let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2741 let r = parse_ok(src);
2742 assert_eq!(r.diagnostics.len(), 0);
2743 }
2744
2745 #[test]
2746 fn open_question() {
2747 let src = r#"open question "Should admins be role-specific?""#;
2748 let r = parse_ok(src);
2749 assert_eq!(r.diagnostics.len(), 0);
2750 }
2751
2752 #[test]
2753 fn external_entity() {
2754 let src = "external entity Customer { email: String }";
2755 let r = parse_ok(src);
2756 assert_eq!(r.diagnostics.len(), 0);
2757 match &r.module.declarations[0] {
2758 Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2759 other => panic!("expected Block, got {other:?}"),
2760 }
2761 }
2762
2763 #[test]
2764 fn where_expression() {
2765 let src = "entity E { active: items where status = active }";
2766 let r = parse_ok(src);
2767 assert_eq!(r.diagnostics.len(), 0);
2768 }
2769
2770 #[test]
2771 fn with_expression() {
2772 let src = "entity E { slots: InterviewSlot with candidacy = this }";
2773 let r = parse_ok(src);
2774 assert_eq!(r.diagnostics.len(), 0);
2775 }
2776
2777 #[test]
2778 fn lambda_expression() {
2779 let src = "entity E { v: items.any(i => i.active) }";
2780 let r = parse_ok(src);
2781 assert_eq!(r.diagnostics.len(), 0);
2782 }
2783
2784 #[test]
2785 fn deferred() {
2786 let src = "deferred InterviewerMatching.suggest";
2787 let r = parse_ok(src);
2788 assert_eq!(r.diagnostics.len(), 0);
2789 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2790 assert!(matches!(&d.path, Expr::MemberAccess { .. }));
2791 assert!(d.location_hint.is_none());
2792 }
2793
2794 #[test]
2795 fn deferred_qualified_path() {
2796 let src = "deferred billing/InvoiceWorkflow";
2797 let r = parse_ok(src);
2798 assert_eq!(r.diagnostics.len(), 0);
2799 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2800 let Expr::QualifiedName(q) = &d.path else {
2801 panic!("expected QualifiedName, got {:?}", d.path)
2802 };
2803 assert_eq!(q.qualifier.as_deref(), Some("billing"));
2804 assert_eq!(q.name, "InvoiceWorkflow");
2805 }
2806
2807 #[test]
2808 fn deferred_qualified_path_with_member() {
2809 let src = "deferred billing/InvoiceWorkflow.initiate";
2810 let r = parse_ok(src);
2811 assert_eq!(r.diagnostics.len(), 0);
2812 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2813 let Expr::MemberAccess { object, field, .. } = &d.path else {
2814 panic!("expected MemberAccess, got {:?}", d.path)
2815 };
2816 assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
2817 assert_eq!(field.name, "initiate");
2818 }
2819
2820 #[test]
2821 fn deferred_with_quoted_location_hint() {
2822 let src = "deferred Foo.bar \"detailed/foo.allium\"";
2823 let r = parse_ok(src);
2824 assert_eq!(r.diagnostics.len(), 0);
2825 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2826 let hint = d.location_hint.as_ref().expect("location hint parsed");
2827 assert_eq!(hint.text(), "detailed/foo.allium");
2828 assert_eq!(d.span.end, hint.span.end, "declaration span covers the hint");
2829 }
2830
2831 #[test]
2832 fn deferred_hint_string_must_share_the_line() {
2833 let src = "deferred Foo\n\"detailed/foo.allium\"";
2836 let r = parse_ok(src);
2837 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2838 assert!(d.location_hint.is_none());
2839 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected declaration")));
2840 }
2841
2842 #[test]
2843 fn deferred_path_rejects_expression_shapes() {
2844 for src in ["deferred Foo(\"x\")", "deferred Foo = \"x\""] {
2848 let r = parse_ok(src);
2849 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2850 assert!(matches!(&d.path, Expr::Ident(id) if id.name == "Foo"));
2851 assert!(
2852 r.diagnostics.iter().any(|d| d.message.contains("expected declaration")),
2853 "leftover tokens must error in {src:?}"
2854 );
2855 }
2856 }
2857
2858 #[test]
2859 fn deferred_parenthesised_path_errors() {
2860 let r = parse_ok("deferred (Foo)");
2861 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected deferred name")));
2862 }
2863
2864 #[test]
2865 fn deferred_trailing_dot_keeps_declaration() {
2866 let r = parse_ok("deferred Dangling.");
2869 let Decl::Deferred(d) = &r.module.declarations[0] else { panic!() };
2870 assert!(matches!(&d.path, Expr::Ident(id) if id.name == "Dangling"));
2871 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected declaration")));
2872 }
2873
2874 #[test]
2875 fn deferred_dangling_qualifier_errors() {
2876 let r = parse_ok("deferred billing/");
2877 assert!(r.diagnostics.iter().any(|d| d.message.contains("expected deferred name after '/'")));
2878 }
2879
2880 #[test]
2881 fn deferred_path_does_not_cross_lines() {
2882 let src = "deferred Dangling.\ndeferred Next.step";
2886 let r = parse_ok(src);
2887 let deferreds: Vec<_> = r
2888 .module
2889 .declarations
2890 .iter()
2891 .filter(|d| matches!(d, Decl::Deferred(_)))
2892 .collect();
2893 assert_eq!(deferreds.len(), 2, "both declarations survive");
2894 let Decl::Deferred(d) = deferreds[0] else { panic!() };
2895 assert!(matches!(&d.path, Expr::Ident(id) if id.name == "Dangling"));
2896 }
2897
2898 #[test]
2899 fn variant_declaration() {
2900 let src = "variant Email : Notification { subject: String }";
2901 let r = parse_ok(src);
2902 assert_eq!(r.diagnostics.len(), 0);
2903 }
2904
2905 #[test]
2908 fn projection_arrow() {
2909 let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2910 let r = parse_ok(src);
2911 assert_eq!(r.diagnostics.len(), 0);
2912 }
2913
2914 #[test]
2917 fn transitions_to_trigger() {
2918 let src = "rule R { when: Interview.status transitions_to scheduled\n ensures: Notification.created() }";
2919 let r = parse_ok(src);
2920 assert_eq!(r.diagnostics.len(), 0);
2921 }
2922
2923 #[test]
2924 fn becomes_trigger() {
2925 let src = "rule R { when: Interview.status becomes scheduled\n ensures: Notification.created() }";
2926 let r = parse_ok(src);
2927 assert_eq!(r.diagnostics.len(), 0);
2928 }
2929
2930 #[test]
2933 fn when_binding() {
2934 let src = "rule R {\n when: interview: Interview.status transitions_to scheduled\n ensures: Notification.created()\n}";
2935 let r = parse_ok(src);
2936 assert_eq!(r.diagnostics.len(), 0);
2937 let decl = &r.module.declarations[0];
2939 if let Decl::Block(b) = decl {
2940 if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2941 assert_eq!(keyword, "when");
2942 assert!(matches!(value, Expr::Binding { .. }));
2943 } else {
2944 panic!("expected clause");
2945 }
2946 } else {
2947 panic!("expected block decl");
2948 }
2949 }
2950
2951 #[test]
2952 fn when_binding_temporal() {
2953 let src = "rule R {\n when: invitation: Invitation.expires_at <= now\n ensures: Invitation.expired()\n}";
2954 let r = parse_ok(src);
2955 assert_eq!(r.diagnostics.len(), 0);
2956 }
2957
2958 #[test]
2959 fn when_binding_created() {
2960 let src = "rule R {\n when: batch: DigestBatch.created\n ensures: Email.created()\n}";
2961 let r = parse_ok(src);
2962 assert_eq!(r.diagnostics.len(), 0);
2963 }
2964
2965 #[test]
2966 fn facing_binding() {
2967 let src = "surface S {\n facing viewer: Interviewer\n exposes: InterviewList\n}";
2968 let r = parse_ok(src);
2969 assert_eq!(r.diagnostics.len(), 0);
2970 }
2971
2972 #[test]
2973 fn context_binding() {
2974 let src = "surface S {\n facing viewer: Interviewer\n context assignment: SlotConfirmation where interviewer = viewer\n}";
2975 let r = parse_ok(src);
2976 assert_eq!(r.diagnostics.len(), 0);
2977 }
2978
2979 #[test]
2982 fn rule_level_for() {
2983 let src = r#"rule ProcessDigests {
2984 when: schedule: DigestSchedule.next_run_at <= now
2985 for user in Users where notification_setting.digest_enabled:
2986 ensures: DigestBatch.created(user: user)
2987}"#;
2988 let r = parse_ok(src);
2989 assert_eq!(r.diagnostics.len(), 0);
2990 if let Decl::Block(b) = &r.module.declarations[0] {
2991 assert!(b.items.len() >= 2);
2993 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2994 } else {
2995 panic!("expected block decl");
2996 }
2997 }
2998
2999 #[test]
3002 fn let_in_ensures_block() {
3003 let src = r#"rule R {
3004 when: ScheduleInterview(candidacy, time, interviewers)
3005 ensures:
3006 let slot = InterviewSlot.created(time: time, candidacy: candidacy)
3007 for interviewer in interviewers:
3008 SlotConfirmation.created(slot: slot, interviewer: interviewer)
3009}"#;
3010 let r = parse_ok(src);
3011 assert_eq!(r.diagnostics.len(), 0);
3012 }
3013
3014 #[test]
3017 fn provides_when_guard() {
3018 let src = "surface S {\n facing viewer: Interviewer\n provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
3019 let r = parse_ok(src);
3020 assert_eq!(r.diagnostics.len(), 0);
3021 }
3022
3023 #[test]
3026 fn optional_type_suffix() {
3027 let src = "entity E { locked_until: Timestamp? }";
3028 let r = parse_ok(src);
3029 assert_eq!(r.diagnostics.len(), 0);
3030 }
3031
3032 #[test]
3033 fn optional_trigger_param() {
3034 let src = "rule R { when: Report(interviewer, interview, reason, details?)\n ensures: Done() }";
3035 let r = parse_ok(src);
3036 assert_eq!(r.diagnostics.len(), 0);
3037 }
3038
3039 #[test]
3042 fn qualified_config_access() {
3043 let src = "entity E { duration: oauth/config.session_duration }";
3044 let r = parse_ok(src);
3045 assert_eq!(r.diagnostics.len(), 0);
3046 }
3047
3048 #[test]
3051 fn realistic_spec() {
3052 let src = r#"-- allium: 1
3053
3054enum OrderStatus { pending | shipped | delivered }
3055
3056external entity Customer {
3057 email: String
3058 name: String
3059}
3060
3061entity Order {
3062 customer: Customer
3063 status: OrderStatus
3064 total: Decimal
3065 items: OrderItem with order = this
3066 shipped_items: items where status = shipped
3067 confirmed_items: items where status = confirmed -> item
3068 is_complete: status = delivered
3069 locked_until: Timestamp?
3070}
3071
3072config {
3073 max_retries: Integer = 3
3074 timeout: Duration = 24.hours
3075}
3076
3077rule PlaceOrder {
3078 when: CustomerPlacesOrder(customer, items, total)
3079 requires: total > 0
3080 ensures: Order.created(customer: customer, status: pending, total: total)
3081}
3082
3083rule ShipOrder {
3084 when: order: Order.status transitions_to shipped
3085 ensures: Email.created(to: order.customer.email, template: order_shipped)
3086}
3087
3088open question "How do we handle partial shipments?"
3089"#;
3090 let r = parse_ok(src);
3091 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
3092 assert_eq!(r.module.version, Some(1));
3093 assert_eq!(r.module.declarations.len(), 7);
3094 }
3095
3096 #[test]
3097 fn extension_behaviour_excerpt() {
3098 let src = r#"value Document {
3101 uri: String
3102 text: String
3103}
3104
3105entity Finding {
3106 code: String
3107 severity: error | warning | info
3108 range: FindingRange
3109}
3110
3111entity DiagnosticsMode {
3112 value: strict | relaxed
3113}
3114
3115config {
3116 duplicateKey: String = "allium.config.duplicateKey"
3117}
3118
3119rule RefreshDiagnostics {
3120 when: DocumentOpened(document) or DocumentChanged(document)
3121 requires: document.language_id = "allium"
3122 ensures: FindingsComputed(document)
3123}
3124
3125surface DiagnosticsDashboard {
3126 facing viewer: Developer
3127 context doc: Document where viewer.active_document = doc
3128 provides: RunChecks(viewer) when doc.language_id = "allium"
3129 exposes: FindingList
3130}
3131
3132rule ProcessDigests {
3133 when: schedule: DigestSchedule.next_run_at <= now
3134 for user in Users where notification_setting.digest_enabled:
3135 let settings = user.notification_setting
3136 ensures: DigestBatch.created(user: user)
3137}
3138"#;
3139 let r = parse_ok(src);
3140 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
3141 assert_eq!(r.module.declarations.len(), 7);
3143 }
3144
3145 #[test]
3146 fn exists_as_identifier() {
3147 let src = r#"rule R {
3148 when: X()
3149 ensures: CompletionItemAvailable(label: exists)
3150}"#;
3151 let r = parse_ok(src);
3152 assert_eq!(r.diagnostics.len(), 0);
3153 }
3154
3155 #[test]
3158 fn pipe_binds_tighter_than_or() {
3159 let src = "entity E { v: a or b | c }";
3161 let r = parse_ok(src);
3162 assert_eq!(r.diagnostics.len(), 0);
3163 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3164 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3165 let Expr::LogicalOp { op, right, .. } = value else {
3167 panic!("expected LogicalOp, got {value:?}");
3168 };
3169 assert_eq!(*op, LogicalOp::Or);
3170 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
3172 }
3173
3174 #[test]
3177 fn variant_with_pipe_base() {
3178 let src = "variant Mixed : TypeA | TypeB";
3179 let r = parse_ok(src);
3180 assert_eq!(r.diagnostics.len(), 0);
3181 let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
3182 assert!(matches!(v.base, Expr::Pipe { .. }));
3183 }
3184
3185 #[test]
3188 fn for_block_where_comparison() {
3189 let src = r#"rule R {
3190 when: X()
3191 for item in Items where item.status = active:
3192 ensures: Processed(item: item)
3193}"#;
3194 let r = parse_ok(src);
3195 assert_eq!(r.diagnostics.len(), 0);
3196 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3197 let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
3198 assert!(filter.is_some());
3199 assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
3200 }
3201
3202 #[test]
3205 fn for_expr_where_comparison() {
3206 let src = r#"rule R {
3207 when: X()
3208 ensures:
3209 for item in Items where item.active = true:
3210 Processed(item: item)
3211}"#;
3212 let r = parse_ok(src);
3213 assert_eq!(r.diagnostics.len(), 0);
3214 }
3215
3216 #[test]
3219 fn if_else_if_else() {
3220 let src = r#"rule R {
3221 when: X(v)
3222 ensures:
3223 if v < 10: Small()
3224 else if v < 100: Medium()
3225 else: Large()
3226}"#;
3227 let r = parse_ok(src);
3228 assert_eq!(r.diagnostics.len(), 0);
3229 }
3230
3231 #[test]
3234 fn null_coalesce_and_optional_chain() {
3235 let src = "entity E { v: a?.b ?? fallback }";
3236 let r = parse_ok(src);
3237 assert_eq!(r.diagnostics.len(), 0);
3238 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3239 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3240 assert!(matches!(value, Expr::NullCoalesce { .. }));
3242 }
3243
3244 #[test]
3247 fn generic_type_nested() {
3248 let src = "entity E { v: List<Set<String>> }";
3249 let r = parse_ok(src);
3250 assert_eq!(r.diagnostics.len(), 0);
3251 }
3252
3253 #[test]
3256 fn collection_literals() {
3257 let src = r#"rule R {
3258 when: X()
3259 ensures:
3260 let s = {a, b, c}
3261 let o = {name: "test", count: 42}
3262 Done()
3263}"#;
3264 let r = parse_ok(src);
3265 assert_eq!(r.diagnostics.len(), 0);
3266 }
3267
3268 #[test]
3269 fn spec_list_literal_parses() {
3270 let src = r#"rule R {
3272 when: X()
3273 ensures:
3274 let l = [1, 2, 3]
3275 Done()
3276}"#;
3277 let r = parse_ok(src);
3278 assert_eq!(r.diagnostics.len(), 0, "list literal should parse cleanly");
3279 }
3280
3281 #[test]
3282 fn list_literal_ast_shape() {
3283 let r = parse_ok("default E e = { items: [1, 2, 3] }");
3284 let Decl::Default(def) = &r.module.declarations[0] else { panic!() };
3285 let Expr::ObjectLiteral { fields, .. } = &def.value else { panic!("expected object literal") };
3286 let Expr::ListLiteral { elements, .. } = &fields[0].value else {
3287 panic!("expected ListLiteral, got {:?}", fields[0].value)
3288 };
3289 assert_eq!(elements.len(), 3);
3290 }
3291
3292 #[test]
3293 fn empty_list_literal_parses() {
3294 let r = parse_ok("default E e = { items: [] }");
3295 assert_eq!(r.diagnostics.len(), 0);
3296 }
3297
3298 #[test]
3299 fn qualified_default_type_parses() {
3300 let r = parse_ok("use \"./p.allium\" as gp\n\ndefault gp/Policy my_policy = { id: \"x\" }");
3301 let def = r.module.declarations.iter().find_map(|d| match d {
3302 Decl::Default(def) => Some(def),
3303 _ => None,
3304 }).expect("default decl");
3305 assert_eq!(def.type_alias.as_ref().map(|a| a.name.as_str()), Some("gp"));
3306 assert_eq!(def.type_name.as_ref().map(|t| t.name.as_str()), Some("Policy"));
3307 assert_eq!(def.name.name, "my_policy");
3308 }
3309
3310 #[test]
3313 fn given_block() {
3314 let src = "given { viewer: User\n time: Timestamp }";
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::Given);
3319 assert!(b.name.is_none());
3320 }
3321
3322 #[test]
3325 fn actor_block() {
3326 let src = "actor Admin { identified_by: User where role = admin }";
3327 let r = parse_ok(src);
3328 assert_eq!(r.diagnostics.len(), 0);
3329 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3330 assert_eq!(b.kind, BlockKind::Actor);
3331 }
3332
3333 #[test]
3336 fn join_lookup() {
3337 let src = "entity E { match: Other{field_a, field_b: value} }";
3338 let r = parse_ok(src);
3339 assert_eq!(r.diagnostics.len(), 0);
3340 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3341 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3342 assert!(matches!(value, Expr::JoinLookup { .. }));
3343 }
3344
3345 #[test]
3348 fn in_not_in_set() {
3349 let src = r#"rule R {
3350 when: X(s)
3351 requires: s in {a, b, c}
3352 requires: s not in {d, e}
3353 ensures: Done()
3354}"#;
3355 let r = parse_ok(src);
3356 assert_eq!(r.diagnostics.len(), 0);
3357 }
3358
3359 #[test]
3362 fn comprehensive_fixture() {
3363 let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
3364 let r = parse(src);
3365 assert_eq!(
3366 r.diagnostics.len(),
3367 0,
3368 "expected no errors in comprehensive fixture, got: {:?}",
3369 r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
3370 );
3371 assert!(r.module.declarations.len() > 30, "expected many declarations");
3372 }
3373
3374 #[test]
3377 fn error_expected_declaration() {
3378 let r = parse("-- allium: 1\n+ invalid");
3379 assert!(r.diagnostics.len() >= 1);
3380 let msg = &r.diagnostics[0].message;
3381 assert!(msg.contains("expected declaration"), "got: {msg}");
3382 assert!(msg.contains("entity"), "should list valid options, got: {msg}");
3383 assert!(msg.contains("rule"), "should list valid options, got: {msg}");
3384 }
3385
3386 #[test]
3387 fn error_expected_expression() {
3388 let r = parse("-- allium: 1\nentity E { v: }");
3389 assert!(r.diagnostics.len() >= 1);
3390 let msg = &r.diagnostics[0].message;
3391 assert!(msg.contains("expected expression"), "got: {msg}");
3392 assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
3393 }
3394
3395 #[test]
3396 fn error_expected_block_item() {
3397 let r = parse("-- allium: 1\nentity E { + }");
3398 assert!(r.diagnostics.len() >= 1);
3399 let msg = &r.diagnostics[0].message;
3400 assert!(msg.contains("expected block item"), "got: {msg}");
3401 }
3402
3403 #[test]
3404 fn error_expected_identifier() {
3405 let r = parse("-- allium: 1\nentity 123 {}");
3406 assert!(r.diagnostics.len() >= 1);
3407 let msg = &r.diagnostics[0].message;
3408 assert!(msg.contains("expected entity name"), "got: {msg}");
3410 assert!(msg.contains("number"), "should say what was found, got: {msg}");
3412 }
3413
3414 #[test]
3415 fn error_missing_brace() {
3416 let r = parse("entity E {");
3417 assert!(r.diagnostics.len() >= 1);
3418 let msg = &r.diagnostics[0].message;
3419 assert!(msg.contains("expected"), "got: {msg}");
3420 }
3421
3422 #[test]
3423 fn error_recovery_multiple() {
3424 let r = parse("entity E { + }\nentity F { - }");
3426 assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
3427 }
3428
3429 #[test]
3430 fn error_dedup_same_line() {
3431 let r = parse("-- allium: 1\n+ - * /");
3433 let errors: Vec<_> = r.diagnostics.iter()
3434 .filter(|d| d.severity == crate::diagnostic::Severity::Error)
3435 .collect();
3436 assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
3437 }
3438
3439 #[test]
3440 fn for_block() {
3441 let src = r#"rule R {
3442 when: X()
3443 for user in Users where user.active:
3444 ensures: Notified(user: user)
3445}"#;
3446 let r = parse_ok(src);
3447 assert_eq!(r.diagnostics.len(), 0);
3448 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3449 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
3450 }
3451
3452 #[test]
3453 fn for_expr() {
3454 let src = r#"rule R {
3455 when: X(project)
3456 ensures:
3457 let total = for task in project.tasks: task.effort
3458 Done(total: total)
3459}"#;
3460 let r = parse_ok(src);
3461 assert_eq!(r.diagnostics.len(), 0);
3462 }
3463
3464 #[test]
3465 fn for_where() {
3466 let src = r#"rule R {
3467 when: X()
3468 for item in Items where item.active:
3469 ensures: Processed(item: item)
3470}"#;
3471 let r = parse_ok(src);
3472 assert_eq!(r.diagnostics.len(), 0);
3473 }
3474
3475 #[test]
3476 fn spec_reject_for_with_filter() {
3477 let src = r#"rule R {
3480 when: X()
3481 for slot in Slot with slot.role = reviewer:
3482 ensures: Reviewed(slot: slot)
3483}"#;
3484 let r = parse_ok(src);
3485 assert!(
3486 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3487 "expected error for `for ... with` (spec uses `where`), but parsed without errors"
3488 );
3489 }
3490
3491 #[test]
3492 fn block_level_if() {
3493 let src = r#"rule R {
3494 when: X(task)
3495 if task.priority = high:
3496 ensures: Escalated(task: task)
3497}"#;
3498 let r = parse_ok(src);
3499 assert_eq!(r.diagnostics.len(), 0);
3500 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3501 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3502 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3503 };
3504 assert_eq!(branches.len(), 1);
3505 assert!(else_items.is_none());
3506 }
3507
3508 #[test]
3509 fn block_level_if_else() {
3510 let src = r#"rule R {
3511 when: X(score)
3512 if score > 80:
3513 ensures: High()
3514 else if score > 40:
3515 ensures: Medium()
3516 else:
3517 ensures: Low()
3518}"#;
3519 let r = parse_ok(src);
3520 assert_eq!(r.diagnostics.len(), 0);
3521 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3522 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
3523 panic!("expected IfBlock, got {:?}", b.items[1].kind);
3524 };
3525 assert_eq!(branches.len(), 2);
3526 assert!(else_items.is_some());
3527 }
3528
3529 #[test]
3530 fn wildcard_type_parameter() {
3531 let src = "entity E { codec: Codec<*> }";
3532 let r = parse_ok(src);
3533 assert_eq!(r.diagnostics.len(), 0);
3534 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3535 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3536 if let Expr::GenericType { args, .. } = value {
3537 assert_eq!(args.len(), 1);
3538 if let Expr::Ident(id) = &args[0] {
3539 assert_eq!(id.name, "*");
3540 } else {
3541 panic!("expected wildcard ident, got {:?}", args[0]);
3542 }
3543 } else {
3544 panic!("expected GenericType, got {:?}", value);
3545 }
3546 }
3547
3548 #[test]
3549 fn guidance_clause_comment_only_value_migration() {
3550 let src = "-- allium: 1\nrule R {\n ensures: Done()\n guidance: -- just a comment\n}";
3552 let r = parse(src);
3553 assert!(
3554 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
3555 "expected migration diagnostic, got: {:?}",
3556 r.diagnostics
3557 );
3558 }
3559
3560 #[test]
3561 fn spec_reject_for_expr_with_filter() {
3562 let src = r#"rule R {
3564 when: X(project)
3565 ensures:
3566 let total = for task in project.tasks with task.active: task.effort
3567 Done(total: total)
3568}"#;
3569 let r = parse_ok(src);
3570 assert!(
3571 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3572 "expected error for `for ... with` in expression (spec uses `where`), but parsed without errors"
3573 );
3574 }
3575
3576 #[test]
3577 fn for_destructured_binding() {
3578 let src = r#"rule R {
3579 when: X()
3580 for (key, value) in Pairs where key != null:
3581 ensures: Processed(key: key, value: value)
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::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
3587 assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
3588 }
3589
3590 #[test]
3591 fn dot_path_assignment() {
3592 let src = r#"entity Shard {
3593 ShardGroup.shard_cache: Shard with group = this
3594}"#;
3595 let r = parse_ok(src);
3596 assert_eq!(r.diagnostics.len(), 0);
3597 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3598 let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
3599 panic!("expected PathAssignment, got {:?}", b.items[0].kind);
3600 };
3601 assert!(matches!(path, Expr::MemberAccess { .. }));
3602 }
3603
3604 #[test]
3605 fn language_reference_fixture() {
3606 let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
3607 let r = parse(src);
3608 let errors: Vec<_> = r.diagnostics.iter()
3609 .filter(|d| d.severity == Severity::Error)
3610 .collect();
3611 assert_eq!(
3612 errors.len(),
3613 0,
3614 "expected no errors in language-reference fixture, got: {:?}",
3615 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
3616 );
3617 }
3618
3619 #[test]
3636 fn spec_for_bare_form() {
3637 let src = r#"rule ProcessDigests {
3639 when: schedule: DigestSchedule.next_run_at <= now
3640 for user in Users where notification_setting.digest_enabled:
3641 let settings = user.notification_setting
3642 ensures: DigestBatch.created(user: user)
3643}"#;
3644 let r = parse_ok(src);
3645 assert_eq!(r.diagnostics.len(), 0);
3646 }
3647
3648 #[test]
3649 fn spec_reject_for_each() {
3650 let src = r#"rule R {
3652 when: X()
3653 for each user in Users where user.active:
3654 ensures: Notified(user: user)
3655}"#;
3656 let r = parse_ok(src);
3657 assert!(
3658 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3659 "expected error for `for each` (not in spec), but parsed without errors"
3660 );
3661 }
3662
3663 #[test]
3666 fn spec_reject_double_equals() {
3667 let src = "rule R { when: X(a)\n requires: a.status == active\n ensures: Done() }";
3669 let r = parse_ok(src);
3670 assert!(
3671 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3672 "expected error for `==` (not in spec), but parsed without errors"
3673 );
3674 }
3675
3676 #[test]
3679 fn spec_reject_system_block() {
3680 let src = "system PaymentGateway {\n timeout: 30.seconds\n}";
3682 let r = parse_ok(src);
3683 assert!(
3684 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3685 "expected error for `system` block (not in spec), but parsed without errors"
3686 );
3687 }
3688
3689 #[test]
3692 fn spec_reject_tags_clause() {
3693 let src = r#"rule R {
3695 when: MigrationTriggered()
3696 tags: infrastructure, migration
3697 ensures: MigrationComplete()
3698}"#;
3699 let r = parse_ok(src);
3700 assert!(
3701 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3702 "expected error for `tags:` clause (not in spec), but parsed without errors"
3703 );
3704 }
3705
3706 #[test]
3709 fn spec_reject_includes_operator() {
3710 let src = r#"rule R {
3712 when: X(a, b)
3713 requires: a.items includes b
3714 ensures: Done()
3715}"#;
3716 let r = parse_ok(src);
3717 assert!(
3718 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3719 "expected error for `includes` operator (not in spec), but parsed without errors"
3720 );
3721 }
3722
3723 #[test]
3724 fn spec_reject_excludes_operator() {
3725 let src = r#"rule R {
3727 when: X(a, b)
3728 requires: a.items excludes b
3729 ensures: Done()
3730}"#;
3731 let r = parse_ok(src);
3732 assert!(
3733 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3734 "expected error for `excludes` operator (not in spec), but parsed without errors"
3735 );
3736 }
3737
3738 #[test]
3741 fn spec_reject_range_literal() {
3742 let src = r#"rule R {
3744 when: X(v)
3745 requires: v in [1..100]
3746 ensures: Done()
3747}"#;
3748 let r = parse_ok(src);
3749 assert!(
3750 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3751 "expected error for `..` range (not in spec), but parsed without errors"
3752 );
3753 }
3754
3755 #[test]
3758 fn spec_within_in_actor() {
3759 let src = r#"actor WorkspaceAdmin {
3761 within: Workspace
3762 identified_by: User where role = admin
3763}"#;
3764 let r = parse_ok(src);
3765 assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
3766 }
3767
3768 #[test]
3771 fn spec_reject_module_declaration() {
3772 let src = "module my_spec";
3774 let r = parse_ok(src);
3775 assert!(
3776 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3777 "expected error for `module` declaration (not in spec), but parsed without errors"
3778 );
3779 }
3780
3781 #[test]
3784 fn spec_reject_module_level_guidance() {
3785 let src = r#"guidance: "All rules must be idempotent""#;
3787 let r = parse_ok(src);
3788 assert!(
3789 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3790 "expected error for module-level `guidance:` (not in spec), but parsed without errors"
3791 );
3792 }
3793
3794 #[test]
3797 fn spec_guarantee_in_surface_migration() {
3798 let src = "-- allium: 1\nsurface S {\n facing viewer: User\n guarantee: DataIntegrity\n}";
3800 let r = parse(src);
3801 assert!(
3802 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
3803 "expected migration diagnostic, got: {:?}",
3804 r.diagnostics
3805 );
3806 }
3807
3808 #[test]
3809 fn spec_timeout_in_surface() {
3810 let src = r#"surface InvitationView {
3812 facing recipient: Candidate
3813 context invitation: ResourceInvitation where email = recipient.email
3814 timeout: InvitationExpires
3815}"#;
3816 let r = parse_ok(src);
3817 assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
3818 }
3819
3820 #[test]
3821 fn spec_timeout_in_surface_with_when() {
3822 let src = r#"surface InvitationView {
3824 facing recipient: Candidate
3825 context invitation: ResourceInvitation where email = recipient.email
3826 timeout: InvitationExpires when invitation.expires_at <= now
3827}"#;
3828 let r = parse_ok(src);
3829 assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
3830 }
3831
3832 #[test]
3835 fn spec_reject_suffix_predicate() {
3836 let src = r#"rule R {
3838 when: X()
3839 requires: finding.code starts_with "allium."
3840 ensures: Done()
3841}"#;
3842 let r = parse_ok(src);
3843 assert!(
3844 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
3845 "expected error for suffix predicate (not in spec), but parsed without errors"
3846 );
3847 }
3848
3849 #[test]
3852 fn spec_add_remove_in_ensures() {
3853 let src = r#"rule R {
3856 when: AssignInterviewer(interview, new_interviewer)
3857 ensures:
3858 interview.interviewers.add(new_interviewer)
3859}"#;
3860 let r = parse_ok(src);
3861 assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
3862 }
3863
3864 #[test]
3865 fn spec_remove_in_ensures() {
3866 let src = r#"rule R {
3867 when: RemoveInterviewer(interview, leaving)
3868 ensures:
3869 interview.interviewers.remove(leaving)
3870}"#;
3871 let r = parse_ok(src);
3872 assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3873 }
3874
3875 #[test]
3878 fn spec_first_last_access() {
3879 let src = "entity E { latest: attempts.last\n earliest: attempts.first }";
3881 let r = parse_ok(src);
3882 assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3883 }
3884
3885 #[test]
3888 fn spec_set_arithmetic() {
3889 let src = r#"entity Role {
3891 permissions: Set<String>
3892 inherited: Set<String>
3893 all_permissions: permissions + inherited
3894 removed: old_mentions - new_mentions
3895}"#;
3896 let r = parse_ok(src);
3897 assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3898 }
3899
3900 #[test]
3903 fn spec_discard_binding_in_trigger() {
3904 let src = r#"rule R {
3906 when: _: LogProcessor.last_flush_check <= now
3907 ensures: Flushed()
3908}"#;
3909 let r = parse_ok(src);
3910 assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3911 }
3912
3913 #[test]
3914 fn spec_discard_in_trigger_params() {
3915 let src = r#"rule R {
3917 when: SomeEvent(_, slot)
3918 ensures: Processed(slot: slot)
3919}"#;
3920 let r = parse_ok(src);
3921 assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3922 }
3923
3924 #[test]
3925 fn spec_discard_in_for() {
3926 let src = r#"rule R {
3928 when: X(items)
3929 ensures:
3930 for _ in items: Counted()
3931}"#;
3932 let r = parse_ok(src);
3933 assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3934 }
3935
3936 #[test]
3939 fn spec_default_with_object_literal() {
3940 let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3942 let r = parse_ok(src);
3943 assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3944 }
3945
3946 #[test]
3947 fn spec_default_multiline_object() {
3948 let src = r#"default Role viewer = {
3950 name: "viewer",
3951 permissions: { "documents.read" }
3952}"#;
3953 let r = parse_ok(src);
3954 assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3955 }
3956
3957 #[test]
3960 fn spec_surface_related_clause() {
3961 let src = r#"surface InterviewerDashboard {
3963 facing viewer: Interviewer
3964 context assignment: SlotConfirmation where interviewer = viewer
3965 related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3966}"#;
3967 let r = parse_ok(src);
3968 assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3969 }
3970
3971 #[test]
3972 fn spec_surface_let_binding() {
3973 let src = r#"surface S {
3975 facing viewer: User
3976 let comments = Comments where parent = viewer
3977 exposes: CommentList
3978}"#;
3979 let r = parse_ok(src);
3980 assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3981 }
3982
3983 #[test]
3984 fn spec_surface_multiline_context_where() {
3985 let src = r#"surface InterviewerPendingAssignments {
3987 facing viewer: Interviewer
3988 context assignment: InterviewAssignment
3989 where interviewer = viewer and status = pending
3990 exposes: AssignmentList
3991}"#;
3992 let r = parse_ok(src);
3993 assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3994 }
3995
3996 #[test]
3999 fn spec_for_in_surface_provides() {
4000 let src = r#"surface TaskBoard {
4002 facing viewer: User
4003 for task in Task where task.assignee = viewer:
4004 provides: CompleteTask(viewer, task) when task.status = in_progress
4005 exposes: KanbanBoard
4006}"#;
4007 let r = parse_ok(src);
4008 assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
4009 }
4010
4011 #[test]
4014 fn spec_use_without_alias() {
4015 let src = r#"use "github.com/specs/notifications/def456""#;
4017 let r = parse_ok(src);
4018 assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
4019 }
4020
4021 #[test]
4024 fn spec_empty_external_entity() {
4025 let src = "external entity Commentable {}";
4027 let r = parse_ok(src);
4028 assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
4029 }
4030
4031 #[test]
4034 fn spec_surface_multiline_provides() {
4035 let src = r#"surface ProjectDashboard {
4037 facing viewer: ProjectManager
4038 context project: Project where owner = viewer
4039 provides:
4040 CreateTask(viewer, project) when project.status = active
4041 ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
4042 exposes: TaskList
4043}"#;
4044 let r = parse_ok(src);
4045 assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
4046 }
4047
4048 #[test]
4051 fn spec_surface_multiline_exposes() {
4052 let src = r#"surface InterviewerDashboard {
4054 facing viewer: Interviewer
4055 context assignment: SlotConfirmation where interviewer = viewer
4056 exposes:
4057 assignment.slot.time
4058 assignment.status
4059}"#;
4060 let r = parse_ok(src);
4061 assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
4062 }
4063
4064 #[test]
4074 fn composite_or_trigger() {
4075 let src = r#"rule R {
4076 when: EventA(x) or EventB(x) or EventC(x)
4077 ensures: Done()
4078}"#;
4079 let r = parse_ok(src);
4080 assert_eq!(r.diagnostics.len(), 0);
4081 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4082 let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
4083 assert_eq!(keyword, "when");
4084 let Expr::LogicalOp { op, left, .. } = value else {
4086 panic!("expected LogicalOp, got {value:?}");
4087 };
4088 assert_eq!(*op, LogicalOp::Or);
4089 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4090 }
4091
4092 #[test]
4095 fn value_type_declaration() {
4096 let src = r#"value TimeRange {
4097 start: Timestamp
4098 end: Timestamp
4099 duration: end - start
4100}"#;
4101 let r = parse_ok(src);
4102 assert_eq!(r.diagnostics.len(), 0);
4103 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4104 assert_eq!(b.kind, BlockKind::Value);
4105 assert_eq!(b.name.as_ref().unwrap().name, "TimeRange");
4106 assert_eq!(b.items.len(), 3);
4107 }
4108
4109 #[test]
4112 fn qualified_config_block() {
4113 let src = r#"use "github.com/specs/oauth/abc123" as oauth
4114oauth/config {
4115 session_duration: Duration = 24.hours
4116}"#;
4117 let r = parse_ok(src);
4118 assert_eq!(r.diagnostics.len(), 0);
4119 assert_eq!(r.module.declarations.len(), 2);
4120 }
4121
4122 #[test]
4125 fn string_interpolation_parts() {
4126 let src = r#"rule R {
4127 when: X(name, action)
4128 ensures: Log.created(message: "User {name} did {action}")
4129}"#;
4130 let r = parse_ok(src);
4131 assert_eq!(r.diagnostics.len(), 0);
4132 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4134 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4135 let Expr::Call { args, .. } = value else { panic!() };
4136 let CallArg::Named(arg) = &args[0] else { panic!() };
4137 let Expr::StringLiteral(s) = &arg.value else { panic!() };
4138 assert_eq!(s.parts.len(), 4, "expected 4 string parts: text, interp, text, interp");
4139 assert!(matches!(&s.parts[0], StringPart::Text(t) if t == "User "));
4140 assert!(matches!(&s.parts[1], StringPart::Interpolation(id) if id.name == "name"));
4141 assert!(matches!(&s.parts[2], StringPart::Text(t) if t == " did "));
4142 assert!(matches!(&s.parts[3], StringPart::Interpolation(id) if id.name == "action"));
4143 }
4144
4145 #[test]
4148 fn this_keyword_expression() {
4149 let src = "entity E { items: Item with parent = this }";
4152 let r = parse_ok(src);
4153 assert_eq!(r.diagnostics.len(), 0);
4154 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4155 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4156 let Expr::With { predicate, .. } = value else {
4157 panic!("expected With, got {value:?}");
4158 };
4159 let Expr::Comparison { op, right, .. } = predicate.as_ref() else {
4160 panic!("expected Comparison in with predicate, got {predicate:?}");
4161 };
4162 assert_eq!(*op, ComparisonOp::Eq);
4163 assert!(matches!(right.as_ref(), Expr::This { .. }));
4164 }
4165
4166 #[test]
4169 fn not_prefix_standalone() {
4170 let src = r#"rule R {
4171 when: X(user)
4172 requires: not user.is_locked
4173 ensures: Done()
4174}"#;
4175 let r = parse_ok(src);
4176 assert_eq!(r.diagnostics.len(), 0);
4177 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4178 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4179 assert_eq!(keyword, "requires");
4180 assert!(matches!(value, Expr::Not { .. }));
4181 }
4182
4183 #[test]
4186 fn unary_minus() {
4187 let src = "entity E { offset: -1 }";
4188 let r = parse_ok(src);
4189 assert_eq!(r.diagnostics.len(), 0);
4190 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4191 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4192 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }
4193 | Expr::NumberLiteral { .. }), "expected negation, got {value:?}");
4194 }
4195
4196 #[test]
4199 fn parenthesised_expression() {
4200 let src = "entity E { v: (a + b) * c }";
4201 let r = parse_ok(src);
4202 assert_eq!(r.diagnostics.len(), 0);
4203 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4204 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4205 let Expr::BinaryOp { op, left, .. } = value else {
4207 panic!("expected BinaryOp, got {value:?}");
4208 };
4209 assert_eq!(*op, BinaryOp::Mul);
4210 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
4211 }
4212
4213 #[test]
4216 fn boolean_literals() {
4217 let src = r#"rule R {
4218 when: X(item)
4219 ensures:
4220 item.active = true
4221 item.deleted = false
4222}"#;
4223 let r = parse_ok(src);
4224 assert_eq!(r.diagnostics.len(), 0);
4225 }
4226
4227 #[test]
4230 fn null_literal() {
4231 let src = "entity E { v: parent ?? null }";
4232 let r = parse_ok(src);
4233 assert_eq!(r.diagnostics.len(), 0);
4234 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4235 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4236 let Expr::NullCoalesce { right, .. } = value else { panic!() };
4237 assert!(matches!(right.as_ref(), Expr::Null { .. }));
4238 }
4239
4240 #[test]
4243 fn empty_set_literal() {
4244 let src = "entity E { tags: Set<String>\n default_tags: {} }";
4245 let r = parse_ok(src);
4246 assert_eq!(r.diagnostics.len(), 0);
4247 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4248 let BlockItemKind::Assignment { value, .. } = &b.items[1].kind else { panic!() };
4249 let Expr::SetLiteral { elements, .. } = value else { panic!("expected SetLiteral, got {value:?}") };
4250 assert!(elements.is_empty());
4251 }
4252
4253 #[test]
4265 fn param_assignment_single() {
4266 let src = "entity Plan { can_use(feature): feature in features }";
4267 let r = parse_ok(src);
4268 assert_eq!(r.diagnostics.len(), 0);
4269 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4270 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4271 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4272 };
4273 assert_eq!(name.name, "can_use");
4274 assert_eq!(params.len(), 1);
4275 assert_eq!(params[0].name, "feature");
4276 assert!(matches!(value, Expr::In { .. }));
4277 }
4278
4279 #[test]
4280 fn param_assignment_multiple() {
4281 let src = "entity E { distance(x, y): (x * x + y * y) }";
4282 let r = parse_ok(src);
4283 assert_eq!(r.diagnostics.len(), 0);
4284 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4285 let BlockItemKind::ParamAssignment { name, params, .. } = &b.items[0].kind else {
4286 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4287 };
4288 assert_eq!(name.name, "distance");
4289 assert_eq!(params.len(), 2);
4290 assert_eq!(params[0].name, "x");
4291 assert_eq!(params[1].name, "y");
4292 }
4293
4294 #[test]
4295 fn param_assignment_simple_expression() {
4296 let src = "entity Task { remaining_effort(total): total - effort }";
4297 let r = parse_ok(src);
4298 assert_eq!(r.diagnostics.len(), 0);
4299 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4300 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
4301 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
4302 };
4303 assert_eq!(name.name, "remaining_effort");
4304 assert_eq!(params.len(), 1);
4305 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }));
4306 }
4307
4308 #[test]
4311 fn precedence_logical_and_binds_tighter_than_or() {
4312 let src = "entity E { v: a or b and c }";
4314 let r = parse_ok(src);
4315 assert_eq!(r.diagnostics.len(), 0);
4316 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4317 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4318 let Expr::LogicalOp { op, right, .. } = value else {
4319 panic!("expected LogicalOp, got {value:?}");
4320 };
4321 assert_eq!(*op, LogicalOp::Or);
4322 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4323 }
4324
4325 #[test]
4326 fn precedence_comparison_binds_tighter_than_and() {
4327 let src = "entity E { v: a = b and c != d }";
4329 let r = parse_ok(src);
4330 assert_eq!(r.diagnostics.len(), 0);
4331 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4332 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4333 let Expr::LogicalOp { op, left, right, .. } = value else {
4334 panic!("expected LogicalOp, got {value:?}");
4335 };
4336 assert_eq!(*op, LogicalOp::And);
4337 assert!(matches!(left.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4338 assert!(matches!(right.as_ref(), Expr::Comparison { op: ComparisonOp::NotEq, .. }));
4339 }
4340
4341 #[test]
4342 fn precedence_arithmetic_binds_tighter_than_comparison() {
4343 let src = "entity E { v: a + b > c * d }";
4345 let r = parse_ok(src);
4346 assert_eq!(r.diagnostics.len(), 0);
4347 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4348 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4349 let Expr::Comparison { op, left, right, .. } = value else {
4350 panic!("expected Comparison, got {value:?}");
4351 };
4352 assert_eq!(*op, ComparisonOp::Gt);
4353 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
4354 assert!(matches!(right.as_ref(), Expr::BinaryOp { op: BinaryOp::Mul, .. }));
4355 }
4356
4357 #[test]
4358 fn precedence_null_coalesce_binds_tighter_than_comparison() {
4359 let src = "entity E { v: a ?? b = c }";
4361 let r = parse_ok(src);
4362 assert_eq!(r.diagnostics.len(), 0);
4363 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4364 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4365 let Expr::Comparison { op, left, .. } = value else {
4366 panic!("expected Comparison, got {value:?}");
4367 };
4368 assert_eq!(*op, ComparisonOp::Eq);
4369 assert!(matches!(left.as_ref(), Expr::NullCoalesce { .. }));
4370 }
4371
4372 #[test]
4373 fn precedence_not_binds_tighter_than_and() {
4374 let src = r#"rule R {
4376 when: X(a, b)
4377 requires: not a and b
4378 ensures: Done()
4379}"#;
4380 let r = parse_ok(src);
4381 assert_eq!(r.diagnostics.len(), 0);
4382 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4383 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4384 let Expr::LogicalOp { op, left, .. } = value else {
4385 panic!("expected LogicalOp, got {value:?}");
4386 };
4387 assert_eq!(*op, LogicalOp::And);
4388 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4389 }
4390
4391 #[test]
4392 fn precedence_where_captures_full_condition() {
4393 let src = "entity E { v: items where status = active }";
4397 let r = parse_ok(src);
4398 assert_eq!(r.diagnostics.len(), 0);
4399 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4400 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4401 let Expr::Where { condition, .. } = value else {
4402 panic!("expected Where, got {value:?}");
4403 };
4404 assert!(matches!(condition.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
4405 }
4406
4407 #[test]
4408 fn precedence_where_captures_and_or_conditions() {
4409 let src = "entity E { v: items where status = active and count > 0 }";
4412 let r = parse_ok(src);
4413 assert_eq!(r.diagnostics.len(), 0);
4414 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4415 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4416 let Expr::Where { condition, .. } = value else {
4417 panic!("expected Where, got {value:?}");
4418 };
4419 assert!(matches!(condition.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4420 }
4421
4422 #[test]
4423 fn precedence_projection_applies_to_where_result() {
4424 let src = "entity E { v: items where status = confirmed -> interviewer }";
4427 let r = parse_ok(src);
4428 assert_eq!(r.diagnostics.len(), 0);
4429 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4430 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4431 let Expr::ProjectionMap { source, field, .. } = value else {
4432 panic!("expected ProjectionMap, got {value:?}");
4433 };
4434 assert_eq!(field.name, "interviewer");
4435 assert!(matches!(source.as_ref(), Expr::Where { .. }));
4436 }
4437
4438 #[test]
4439 fn precedence_lambda_binds_loosest() {
4440 let src = "entity E { v: items.any(i => i.active and i.valid) }";
4442 let r = parse_ok(src);
4443 assert_eq!(r.diagnostics.len(), 0);
4444 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4445 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4446 let Expr::Call { args, .. } = value else { panic!() };
4447 let CallArg::Positional(Expr::Lambda { body, .. }) = &args[0] else { panic!() };
4448 assert!(matches!(body.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4449 }
4450
4451 #[test]
4452 fn precedence_in_binds_at_comparison_level() {
4453 let src = r#"rule R {
4455 when: X(x, y)
4456 requires: x in {a, b} and y not in {c}
4457 ensures: Done()
4458}"#;
4459 let r = parse_ok(src);
4460 assert_eq!(r.diagnostics.len(), 0);
4461 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4462 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4463 let Expr::LogicalOp { op, left, right, .. } = value else {
4464 panic!("expected LogicalOp, got {value:?}");
4465 };
4466 assert_eq!(*op, LogicalOp::And);
4467 assert!(matches!(left.as_ref(), Expr::In { .. }));
4468 assert!(matches!(right.as_ref(), Expr::NotIn { .. }));
4469 }
4470
4471 #[test]
4474 fn multiline_ensures_block() {
4475 let src = r#"rule R {
4476 when: X(doc)
4477 ensures:
4478 doc.status = published
4479 Notification.created(to: doc.author)
4480}"#;
4481 let r = parse_ok(src);
4482 assert_eq!(r.diagnostics.len(), 0);
4483 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4484 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4485 assert_eq!(keyword, "ensures");
4486 let Expr::Block { items, .. } = value else {
4487 panic!("expected Block for multi-line ensures, got {value:?}");
4488 };
4489 assert_eq!(items.len(), 2);
4490 }
4491
4492 #[test]
4493 fn singleline_ensures_value() {
4494 let src = r#"rule R {
4495 when: X(doc)
4496 ensures: doc.status = published
4497}"#;
4498 let r = parse_ok(src);
4499 assert_eq!(r.diagnostics.len(), 0);
4500 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4501 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
4502 assert_eq!(keyword, "ensures");
4503 assert!(!matches!(value, Expr::Block { .. }), "single-line ensures should not be Block");
4505 }
4506
4507 #[test]
4508 fn multiline_requires_with_continuation() {
4509 let src = r#"rule R {
4510 when: X(a)
4511 requires:
4512 a.count >= 2
4513 or a.items.any(i => i.can_solo)
4514 ensures: Done()
4515}"#;
4516 let r = parse_ok(src);
4517 assert_eq!(r.diagnostics.len(), 0);
4518 }
4519
4520 #[test]
4523 fn object_literal_single_field() {
4524 let src = r#"rule R {
4525 when: X()
4526 ensures:
4527 let o = {name: "test"}
4528 Done()
4529}"#;
4530 let r = parse_ok(src);
4531 assert_eq!(r.diagnostics.len(), 0);
4532 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4533 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4534 let Expr::Block { items, .. } = value else { panic!() };
4535 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4536 assert!(matches!(let_val.as_ref(), Expr::ObjectLiteral { .. }));
4537 }
4538
4539 #[test]
4540 fn set_literal_single_element() {
4541 let src = r#"rule R {
4542 when: X()
4543 ensures:
4544 let s = {active}
4545 Done()
4546}"#;
4547 let r = parse_ok(src);
4548 assert_eq!(r.diagnostics.len(), 0);
4549 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4550 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
4551 let Expr::Block { items, .. } = value else { panic!() };
4552 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
4553 assert!(matches!(let_val.as_ref(), Expr::SetLiteral { .. }),
4554 "bare {{ident}} should parse as set literal, got {:?}", let_val);
4555 }
4556
4557 #[test]
4560 fn lambda_with_chained_access() {
4561 let src = "entity E { v: items.all(t => t.item.status = active) }";
4562 let r = parse_ok(src);
4563 assert_eq!(r.diagnostics.len(), 0);
4564 }
4565
4566 #[test]
4567 fn nested_lambda() {
4568 let src = "entity E { v: groups.any(g => g.items.all(i => i.valid)) }";
4569 let r = parse_ok(src);
4570 assert_eq!(r.diagnostics.len(), 0);
4571 }
4572
4573 #[test]
4576 fn qualified_name_with_member_access() {
4577 let src = "entity E { v: shared/Validator.check }";
4578 let r = parse_ok(src);
4579 assert_eq!(r.diagnostics.len(), 0);
4580 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4581 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4582 let Expr::MemberAccess { object, field, .. } = value else {
4583 panic!("expected MemberAccess, got {value:?}");
4584 };
4585 assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
4586 assert_eq!(field.name, "check");
4587 }
4588
4589 #[test]
4590 fn qualified_name_in_call() {
4591 let src = r#"rule R {
4592 when: X(item)
4593 requires: shared/Validator.check(item: item)
4594 ensures: Done()
4595}"#;
4596 let r = parse_ok(src);
4597 assert_eq!(r.diagnostics.len(), 0);
4598 }
4599
4600 #[test]
4603 fn nested_if_inside_for() {
4604 let src = r#"rule R {
4605 when: X()
4606 for user in Users where user.active:
4607 if user.role = admin:
4608 ensures: AdminNotified(user: user)
4609 else:
4610 ensures: UserNotified(user: user)
4611}"#;
4612 let r = parse_ok(src);
4613 assert_eq!(r.diagnostics.len(), 0);
4614 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4615 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4616 assert!(matches!(items[0].kind, BlockItemKind::IfBlock { .. }));
4617 }
4618
4619 #[test]
4620 fn for_with_let_before_ensures() {
4621 let src = r#"rule R {
4622 when: schedule: DigestSchedule.next_run_at <= now
4623 for user in Users where user.active:
4624 let pending = user.tasks where status = pending
4625 ensures: DigestEmail.created(to: user.email, tasks: pending)
4626}"#;
4627 let r = parse_ok(src);
4628 assert_eq!(r.diagnostics.len(), 0);
4629 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4630 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
4631 assert_eq!(items.len(), 2, "for body should have let + ensures");
4632 assert!(matches!(items[0].kind, BlockItemKind::Let { .. }));
4633 assert!(matches!(items[1].kind, BlockItemKind::Clause { .. }));
4634 }
4635
4636 #[test]
4639 fn join_lookup_all_unnamed() {
4640 let src = "entity E { match: Other{a, b, c} }";
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(), 3);
4647 assert!(fields.iter().all(|f| f.value.is_none()));
4648 }
4649
4650 #[test]
4651 fn join_lookup_all_named() {
4652 let src = "entity E { match: Membership{user: actor, workspace: ws} }";
4653 let r = parse_ok(src);
4654 assert_eq!(r.diagnostics.len(), 0);
4655 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4656 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4657 let Expr::JoinLookup { fields, .. } = value else { panic!() };
4658 assert_eq!(fields.len(), 2);
4659 assert!(fields.iter().all(|f| f.value.is_some()));
4660 }
4661
4662 #[test]
4663 fn join_lookup_in_requires() {
4664 let src = r#"rule R {
4665 when: X(user, workspace)
4666 requires: exists WorkspaceMembership{user: user, workspace: workspace}
4667 ensures: Done()
4668}"#;
4669 let r = parse_ok(src);
4670 assert_eq!(r.diagnostics.len(), 0);
4671 }
4672
4673 #[test]
4674 fn join_lookup_negated_in_requires() {
4675 let src = r#"rule R {
4676 when: X(email)
4677 requires: not exists User{email: email}
4678 ensures: Done()
4679}"#;
4680 let r = parse_ok(src);
4681 assert_eq!(r.diagnostics.len(), 0);
4682 }
4683
4684 #[test]
4689 fn implies_basic() {
4690 let src = "rule R { requires: a implies b }";
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::Clause { value, .. } = &b.items[0].kind else { panic!() };
4695 let Expr::LogicalOp { op, .. } = value else { panic!("expected LogicalOp, got {value:?}") };
4696 assert_eq!(*op, LogicalOp::Implies);
4697 }
4698
4699 #[test]
4700 fn implies_precedence_and_binds_tighter() {
4701 let src = "rule R { v: a and b implies c }";
4703 let r = parse_ok(src);
4704 assert_eq!(r.diagnostics.len(), 0);
4705 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4706 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4707 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4708 assert_eq!(*op, LogicalOp::Implies);
4709 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
4710 }
4711
4712 #[test]
4713 fn implies_precedence_or_binds_tighter() {
4714 let src = "rule R { v: a or b implies c }";
4716 let r = parse_ok(src);
4717 assert_eq!(r.diagnostics.len(), 0);
4718 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4719 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4720 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4721 assert_eq!(*op, LogicalOp::Implies);
4722 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4723 }
4724
4725 #[test]
4726 fn implies_precedence_implies_above_or() {
4727 let src = "rule R { v: a implies b or c }";
4729 let r = parse_ok(src);
4730 assert_eq!(r.diagnostics.len(), 0);
4731 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4732 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4733 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4734 assert_eq!(*op, LogicalOp::Implies);
4735 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
4736 }
4737
4738 #[test]
4739 fn implies_precedence_not_binds_tighter() {
4740 let src = "rule R { v: not a implies b }";
4742 let r = parse_ok(src);
4743 assert_eq!(r.diagnostics.len(), 0);
4744 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4745 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4746 let Expr::LogicalOp { op, left, .. } = value else { panic!() };
4747 assert_eq!(*op, LogicalOp::Implies);
4748 assert!(matches!(left.as_ref(), Expr::Not { .. }));
4749 }
4750
4751 #[test]
4752 fn implies_right_associative() {
4753 let src = "rule R { v: a implies b implies c }";
4755 let r = parse_ok(src);
4756 assert_eq!(r.diagnostics.len(), 0);
4757 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4758 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4759 let Expr::LogicalOp { op, right, .. } = value else { panic!() };
4760 assert_eq!(*op, LogicalOp::Implies);
4761 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4762 }
4763
4764 #[test]
4765 fn implies_is_keyword_parsed_as_operator() {
4766 let src = "entity E { v: a implies b }";
4769 let r = parse_ok(src);
4770 assert_eq!(r.diagnostics.len(), 0);
4771 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4772 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4773 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4774 }
4775
4776 #[test]
4777 fn implies_in_ensures() {
4778 let src = r#"rule R {
4779 when: X()
4780 ensures: a implies b
4781}"#;
4782 let r = parse_ok(src);
4783 assert_eq!(r.diagnostics.len(), 0);
4784 }
4785
4786 #[test]
4787 fn implies_in_derived_value() {
4788 let src = "entity E { v: a implies b }";
4789 let r = parse_ok(src);
4790 assert_eq!(r.diagnostics.len(), 0);
4791 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4792 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
4793 assert!(matches!(value, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
4794 }
4795
4796 #[test]
4801 fn guidance_ordering_tests_removed() {
4802 }
4806
4807 #[test]
4812 fn contract_signatures_only() {
4813 let src = r#"contract Auditable {
4814 last_modified_by: Actor
4815 last_modified_at: Timestamp
4816}"#;
4817 let r = parse_ok(src);
4818 assert_eq!(r.diagnostics.len(), 0);
4819 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4820 assert_eq!(b.kind, BlockKind::Contract);
4821 assert_eq!(b.name.as_ref().unwrap().name, "Auditable");
4822 assert_eq!(b.items.len(), 2);
4823 }
4824
4825 #[test]
4826 fn contract_with_annotations() {
4827 let src = r#"contract Versioned {
4828 version: Integer
4829 @invariant Monotonic
4830 -- versions must increase
4831 @guidance
4832 -- use semantic versioning
4833}"#;
4834 let r = parse_ok(src);
4835 assert_eq!(r.diagnostics.len(), 0);
4836 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4837 assert_eq!(b.kind, BlockKind::Contract);
4838 assert_eq!(b.items.len(), 3);
4839 }
4840
4841 #[test]
4842 fn contract_with_any_type() {
4843 let src = r#"contract Identifiable {
4844 id: Any
4845}"#;
4846 let r = parse_ok(src);
4847 assert_eq!(r.diagnostics.len(), 0);
4848 }
4849
4850 #[test]
4851 fn contract_lowercase_name_rejected() {
4852 let src = "-- allium: 1\ncontract bad {}";
4853 let r = parse(src);
4854 assert!(
4855 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
4856 "expected uppercase error, got: {:?}",
4857 r.diagnostics
4858 );
4859 }
4860
4861 #[test]
4862 fn contract_colon_body_rejected() {
4863 let src = "-- allium: 1\ncontract Bad: something";
4864 let r = parse(src);
4865 assert!(
4866 r.diagnostics.iter().any(|d| d.message.contains("braces")),
4867 "expected braces error, got: {:?}",
4868 r.diagnostics
4869 );
4870 }
4871
4872 #[test]
4877 fn contracts_clause_single_demands() {
4878 let src = "surface S {\n contracts:\n demands Auditable\n}";
4879 let r = parse_ok(src);
4880 assert_eq!(r.diagnostics.len(), 0);
4881 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4882 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4883 panic!("expected ContractsClause, got {:?}", b.items[0].kind)
4884 };
4885 assert_eq!(entries.len(), 1);
4886 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4887 assert_eq!(entries[0].name.name, "Auditable");
4888 }
4889
4890 #[test]
4891 fn contracts_clause_single_fulfils() {
4892 let src = "surface S {\n contracts:\n fulfils EventSubmitter\n}";
4893 let r = parse_ok(src);
4894 assert_eq!(r.diagnostics.len(), 0);
4895 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4896 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4897 panic!("expected ContractsClause")
4898 };
4899 assert_eq!(entries.len(), 1);
4900 assert!(matches!(entries[0].direction, ContractDirection::Fulfils));
4901 assert_eq!(entries[0].name.name, "EventSubmitter");
4902 }
4903
4904 #[test]
4905 fn contracts_clause_qualified_fulfils() {
4906 let src = "surface S {\n contracts:\n fulfils base/MyContract\n}";
4907 let r = parse_ok(src);
4908 assert_eq!(r.diagnostics.len(), 0);
4909 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4910 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4911 panic!("expected ContractsClause, got {:?}", b.items[0].kind)
4912 };
4913 assert_eq!(entries.len(), 1);
4914 assert!(matches!(entries[0].direction, ContractDirection::Fulfils));
4915 assert_eq!(entries[0].qualifier.as_deref(), Some("base"));
4916 assert_eq!(entries[0].name.name, "MyContract");
4917 }
4918
4919 #[test]
4920 fn contracts_clause_qualified_demands() {
4921 let src = "surface S {\n contracts:\n demands base/MyContract\n fulfils Local\n}";
4922 let r = parse_ok(src);
4923 assert_eq!(r.diagnostics.len(), 0);
4924 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4925 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4926 panic!("expected ContractsClause")
4927 };
4928 assert_eq!(entries.len(), 2);
4929 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4930 assert_eq!(entries[0].qualifier.as_deref(), Some("base"));
4931 assert_eq!(entries[0].name.name, "MyContract");
4932 assert_eq!(entries[1].qualifier, None);
4933 assert_eq!(entries[1].name.name, "Local");
4934 }
4935
4936 #[test]
4937 fn contracts_clause_qualified_missing_name_errors() {
4938 let src = "surface S {\n contracts:\n fulfils base/\n}";
4939 let r = parse(src);
4940 assert!(
4941 r.diagnostics
4942 .iter()
4943 .any(|d| d.message.contains("contract name after '/'")),
4944 "expected an error about the missing name, got {:?}",
4945 r.diagnostics
4946 );
4947 }
4948
4949 #[test]
4950 fn contracts_clause_mixed() {
4951 let src = "surface S {\n contracts:\n demands Auditable\n fulfils EventSubmitter\n}";
4952 let r = parse_ok(src);
4953 assert_eq!(r.diagnostics.len(), 0);
4954 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4955 let BlockItemKind::ContractsClause { entries } = &b.items[0].kind else {
4956 panic!("expected ContractsClause")
4957 };
4958 assert_eq!(entries.len(), 2);
4959 assert!(matches!(entries[0].direction, ContractDirection::Demands));
4960 assert!(matches!(entries[1].direction, ContractDirection::Fulfils));
4961 }
4962
4963 #[test]
4964 fn contracts_with_other_clauses() {
4965 let src = r#"surface S {
4966 facing user: User
4967 contracts:
4968 demands Auditable
4969 exposes:
4970 user.name
4971}"#;
4972 let r = parse_ok(src);
4973 assert_eq!(r.diagnostics.len(), 0);
4974 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4975 assert_eq!(b.items.len(), 3);
4976 }
4977
4978 #[test]
4979 fn contracts_only_surface() {
4980 let src = "surface S {\n contracts:\n demands Foo\n fulfils Bar\n}";
4981 let r = parse_ok(src);
4982 assert_eq!(r.diagnostics.len(), 0);
4983 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
4984 assert_eq!(b.items.len(), 1);
4985 }
4986
4987 #[test]
4988 fn contracts_empty_rejected() {
4989 let src = "-- allium: 1\nsurface S {\n contracts:\n}";
4990 let r = parse(src);
4991 assert!(
4992 r.diagnostics.iter().any(|d| d.message.contains("Empty `contracts:`")),
4993 "expected empty contracts error, got: {:?}",
4994 r.diagnostics
4995 );
4996 }
4997
4998 #[test]
4999 fn contracts_inline_block_rejected() {
5000 let src = "-- allium: 1\nsurface S {\n contracts:\n demands Foo {\n }\n}";
5001 let r = parse(src);
5002 assert!(
5003 r.diagnostics.iter().any(|d| d.message.contains("Inline contract blocks")),
5004 "expected inline block error, got: {:?}",
5005 r.diagnostics
5006 );
5007 }
5008
5009 #[test]
5010 fn contracts_unknown_direction_rejected() {
5011 let src = "-- allium: 1\nsurface S {\n contracts:\n requires Foo\n}";
5012 let r = parse(src);
5013 assert!(
5014 r.diagnostics.iter().any(|d| d.message.contains("Unknown direction")),
5015 "expected unknown direction error, got: {:?}",
5016 r.diagnostics
5017 );
5018 }
5019
5020 #[test]
5025 fn annotation_invariant() {
5026 let src = "contract C {\n @invariant Determinism\n -- all evaluations must be deterministic\n}";
5027 let r = parse_ok(src);
5028 assert_eq!(r.diagnostics.len(), 0);
5029 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5030 let BlockItemKind::Annotation(ann) = &b.items[0].kind else {
5031 panic!("expected Annotation, got {:?}", b.items[0].kind)
5032 };
5033 assert!(matches!(ann.kind, AnnotationKind::Invariant));
5034 assert_eq!(ann.name.as_ref().unwrap().name, "Determinism");
5035 assert_eq!(ann.body.len(), 1);
5036 assert_eq!(ann.body[0], "all evaluations must be deterministic");
5037 }
5038
5039 #[test]
5040 fn annotation_multiple_invariants() {
5041 let src = "contract C {\n @invariant A\n -- first\n @invariant B\n -- second\n}";
5042 let r = parse_ok(src);
5043 assert_eq!(r.diagnostics.len(), 0);
5044 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5045 assert_eq!(b.items.len(), 2);
5046 assert!(matches!(&b.items[0].kind, BlockItemKind::Annotation(_)));
5047 assert!(matches!(&b.items[1].kind, BlockItemKind::Annotation(_)));
5048 }
5049
5050 #[test]
5051 fn annotation_invariant_then_guidance() {
5052 let src = "contract C {\n @invariant Safety\n -- must be safe\n @guidance\n -- implementation notes\n}";
5053 let r = parse_ok(src);
5054 assert_eq!(r.diagnostics.len(), 0);
5055 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5056 assert_eq!(b.items.len(), 2);
5057 }
5058
5059 #[test]
5060 fn annotation_guidance_in_rule() {
5061 let src = "rule R {\n when: Event.created\n ensures: something\n @guidance\n -- do it this way\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 last = b.items.last().unwrap();
5066 let BlockItemKind::Annotation(ann) = &last.kind else { panic!() };
5067 assert!(matches!(ann.kind, AnnotationKind::Guidance));
5068 assert!(ann.name.is_none());
5069 }
5070
5071 #[test]
5072 fn annotation_guarantee() {
5073 let src = "surface S {\n @guarantee ResponseTime\n -- must respond within 100ms\n}";
5074 let r = parse_ok(src);
5075 assert_eq!(r.diagnostics.len(), 0);
5076 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5077 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
5078 assert!(matches!(ann.kind, AnnotationKind::Guarantee));
5079 assert_eq!(ann.name.as_ref().unwrap().name, "ResponseTime");
5080 }
5081
5082 #[test]
5083 fn annotation_guarantee_then_guidance() {
5084 let src = "surface S {\n @guarantee Fast\n -- sub-second\n @guidance\n -- cache aggressively\n}";
5085 let r = parse_ok(src);
5086 assert_eq!(r.diagnostics.len(), 0);
5087 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5088 assert_eq!(b.items.len(), 2);
5089 }
5090
5091 #[test]
5092 fn annotation_contracts_guarantee_guidance() {
5093 let src = r#"surface S {
5094 contracts:
5095 demands Auditable
5096 @guarantee ResponseTime
5097 -- fast
5098 @guidance
5099 -- notes
5100}"#;
5101 let r = parse_ok(src);
5102 assert_eq!(r.diagnostics.len(), 0);
5103 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5104 assert_eq!(b.items.len(), 3);
5105 }
5106
5107 #[test]
5108 fn annotation_multiline_body() {
5109 let src = "contract C {\n @invariant Multi\n -- line one\n -- line two\n -- line three\n}";
5110 let r = parse_ok(src);
5111 assert_eq!(r.diagnostics.len(), 0);
5112 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5113 let BlockItemKind::Annotation(ann) = &b.items[0].kind else { panic!() };
5114 assert_eq!(ann.body.len(), 3);
5115 assert_eq!(ann.body[0], "line one");
5116 assert_eq!(ann.body[2], "line three");
5117 }
5118
5119 #[test]
5120 fn annotation_empty_body_rejected() {
5121 let src = "-- allium: 1\ncontract C {\n @invariant NoBody\n}";
5122 let r = parse(src);
5123 assert!(
5124 r.diagnostics.iter().any(|d| d.message.contains("at least one indented comment line")),
5125 "expected empty body error, got: {:?}",
5126 r.diagnostics
5127 );
5128 }
5129
5130 #[test]
5131 fn annotation_unknown_keyword_rejected() {
5132 let src = "-- allium: 1\ncontract C {\n @note Something\n -- text\n}";
5133 let r = parse(src);
5134 assert!(
5135 r.diagnostics.iter().any(|d| d.message.contains("Unknown annotation")),
5136 "expected unknown annotation error, got: {:?}",
5137 r.diagnostics
5138 );
5139 }
5140
5141 #[test]
5142 fn expression_invariant_still_works() {
5143 let src = r#"entity E {
5144 status: pending | active
5145 invariant AllValid {
5146 this.status = active
5147 }
5148}"#;
5149 let r = parse_ok(src);
5150 assert_eq!(r.diagnostics.len(), 0);
5151 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5152 let inv = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. }));
5154 assert!(inv.is_some(), "expression-bearing invariant should still parse");
5155 }
5156
5157 #[test]
5158 fn invariant_colon_form_migration() {
5159 let src = "-- allium: 1\ncontract C {\n invariant: SomeName\n}";
5160 let r = parse(src);
5161 assert!(
5162 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
5163 "expected migration diagnostic, got: {:?}",
5164 r.diagnostics
5165 );
5166 }
5167
5168 #[test]
5169 fn guidance_colon_form_migration() {
5170 let src = "-- allium: 1\nrule R {\n when: Event.created\n ensures: something\n guidance: \"do it\"\n}";
5171 let r = parse(src);
5172 assert!(
5173 r.diagnostics.iter().any(|d| d.message.contains("`guidance:` syntax was replaced")),
5174 "expected migration diagnostic, got: {:?}",
5175 r.diagnostics
5176 );
5177 }
5178
5179 #[test]
5180 fn guarantee_colon_form_migration() {
5181 let src = "-- allium: 1\nsurface S {\n guarantee: \"fast\"\n}";
5182 let r = parse(src);
5183 assert!(
5184 r.diagnostics.iter().any(|d| d.message.contains("`guarantee:` syntax was replaced")),
5185 "expected migration diagnostic, got: {:?}",
5186 r.diagnostics
5187 );
5188 }
5189
5190 #[test]
5191 fn annotation_guidance_with_name_rejected() {
5192 let src = "-- allium: 1\ncontract C {\n @guidance Named\n -- text\n}";
5193 let r = parse(src);
5194 assert!(
5195 r.diagnostics.iter().any(|d| d.message.contains("does not take a name")),
5196 "expected guidance name error, got: {:?}",
5197 r.diagnostics
5198 );
5199 }
5200
5201 #[test]
5206 fn invariant_top_level_simple() {
5207 let src = r#"invariant PositiveBalance {
5208 this.balance > 0
5209}"#;
5210 let r = parse_ok(src);
5211 assert_eq!(r.diagnostics.len(), 0);
5212 let Decl::Invariant(inv) = &r.module.declarations[0] else {
5213 panic!("expected Invariant, got {:?}", r.module.declarations[0])
5214 };
5215 assert_eq!(inv.name.name, "PositiveBalance");
5216 }
5217
5218 #[test]
5219 fn invariant_top_level_for_quantifier() {
5220 let src = r#"invariant AllPositive {
5221 for item in items: item.value > 0
5222}"#;
5223 let r = parse_ok(src);
5224 assert_eq!(r.diagnostics.len(), 0);
5225 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
5226 assert!(matches!(inv.body, Expr::For { .. }));
5227 }
5228
5229 #[test]
5230 fn invariant_top_level_nested_for() {
5231 let src = r#"invariant NestedFor {
5232 for a in items: for b in a.children: b.valid = true
5233}"#;
5234 let r = parse_ok(src);
5235 assert_eq!(r.diagnostics.len(), 0);
5236 }
5237
5238 #[test]
5239 fn invariant_top_level_implies() {
5240 let src = r#"invariant ImpliesTest {
5241 this.active implies this.balance > 0
5242}"#;
5243 let r = parse_ok(src);
5244 assert_eq!(r.diagnostics.len(), 0);
5245 let Decl::Invariant(inv) = &r.module.declarations[0] else { panic!() };
5246 assert!(matches!(inv.body, Expr::LogicalOp { op: LogicalOp::Implies, .. }));
5247 }
5248
5249 #[test]
5250 fn invariant_top_level_let_binding() {
5251 let src = r#"invariant WithLet {
5252 let total = this.items.count()
5253 total > 0
5254}"#;
5255 let r = parse_ok(src);
5256 assert_eq!(r.diagnostics.len(), 0);
5257 }
5258
5259 #[test]
5260 fn invariant_top_level_collection_ops() {
5261 let src = r#"invariant CollectionOps {
5262 this.items where active = true
5263}"#;
5264 let r = parse_ok(src);
5265 assert_eq!(r.diagnostics.len(), 0);
5266 }
5267
5268 #[test]
5269 fn invariant_top_level_exists() {
5270 let src = r#"invariant ExistsCheck {
5271 exists this.primary_contact
5272}"#;
5273 let r = parse_ok(src);
5274 assert_eq!(r.diagnostics.len(), 0);
5275 }
5276
5277 #[test]
5278 fn invariant_top_level_not_exists() {
5279 let src = r#"invariant NotExistsCheck {
5280 not exists this.deleted_at
5281}"#;
5282 let r = parse_ok(src);
5283 assert_eq!(r.diagnostics.len(), 0);
5284 }
5285
5286 #[test]
5287 fn invariant_top_level_optional_navigation() {
5288 let src = r#"invariant OptionalNav {
5289 this.owner?.email ?? "none" != "none"
5290}"#;
5291 let r = parse_ok(src);
5292 assert_eq!(r.diagnostics.len(), 0);
5293 }
5294
5295 #[test]
5296 fn invariant_top_level_lowercase_rejected() {
5297 let src = "-- allium: 1\ninvariant bad { true }";
5298 let r = parse(src);
5299 assert!(
5300 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
5301 "expected uppercase error, got: {:?}",
5302 r.diagnostics
5303 );
5304 }
5305
5306 #[test]
5307 fn invariant_entity_level() {
5308 let src = r#"entity Account {
5309 balance: Decimal
5310 invariant NonNegative { this.balance >= 0 }
5311}"#;
5312 let r = parse_ok(src);
5313 assert_eq!(r.diagnostics.len(), 0);
5314 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5315 let BlockItemKind::InvariantBlock { name, body: _ } = &b.items[1].kind else {
5316 panic!("expected InvariantBlock, got {:?}", b.items[1].kind)
5317 };
5318 assert_eq!(name.name, "NonNegative");
5319 }
5320
5321 #[test]
5322 fn invariant_entity_level_this_ref() {
5323 let src = r#"entity Order {
5324 total: Decimal
5325 invariant PositiveTotal { this.total > 0 }
5326}"#;
5327 let r = parse_ok(src);
5328 assert_eq!(r.diagnostics.len(), 0);
5329 }
5330
5331 #[test]
5332 fn invariant_entity_level_implies() {
5333 let src = r#"entity Subscription {
5334 active: Boolean
5335 balance: Decimal
5336 invariant ActiveMeansPositive { this.active implies this.balance > 0 }
5337}"#;
5338 let r = parse_ok(src);
5339 assert_eq!(r.diagnostics.len(), 0);
5340 }
5341
5342 #[test]
5343 fn invariant_entity_level_lowercase_rejected() {
5344 let src = "-- allium: 1\nentity E { invariant bad { true } }";
5345 let r = parse(src);
5346 assert!(
5347 r.diagnostics.iter().any(|d| d.message.contains("uppercase")),
5348 "expected uppercase error, got: {:?}",
5349 r.diagnostics
5350 );
5351 }
5352
5353 #[test]
5354 fn invariant_colon_form_in_entity_migration() {
5355 let src = "-- allium: 1\nentity E {\n invariant: -- must be valid\n}";
5357 let r = parse(src);
5358 assert!(
5359 r.diagnostics.iter().any(|d| d.message.contains("`invariant:` syntax was replaced")),
5360 "expected migration diagnostic, got: {:?}",
5361 r.diagnostics
5362 );
5363 }
5364
5365 #[test]
5366 fn invariant_top_level_colon_rejected() {
5367 let src = "-- allium: 1\ninvariant Bad: some text";
5369 let r = parse(src);
5370 assert!(
5371 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5372 "expected error for colon-delimited invariant at top level, got: {:?}",
5373 r.diagnostics
5374 );
5375 }
5376
5377 #[test]
5378 fn invariant_same_name_different_scopes() {
5379 let src = r#"invariant SameName { true }
5381entity E {
5382 invariant SameName { true }
5383}"#;
5384 let r = parse_ok(src);
5385 assert_eq!(r.diagnostics.len(), 0);
5386 }
5387
5388 #[test]
5393 fn config_qualified_reference() {
5394 let src = r#"config {
5396 param: Integer = core/config.max_batch_size
5397}"#;
5398 let r = parse_ok(src);
5399 assert_eq!(r.diagnostics.len(), 0);
5400 }
5401
5402 #[test]
5403 fn config_multiple_qualified_refs() {
5404 let src = r#"config {
5405 param_a: Integer = core/config.max_batch_size
5406 param_b: Duration = core/config.default_delay
5407}"#;
5408 let r = parse_ok(src);
5409 assert_eq!(r.diagnostics.len(), 0);
5410 }
5411
5412 #[test]
5413 fn config_qualified_ref_with_type() {
5414 let src = r#"config {
5415 publish_delay: Duration = core/config.default_delay
5416}"#;
5417 let r = parse_ok(src);
5418 assert_eq!(r.diagnostics.len(), 0);
5419 }
5420
5421 #[test]
5422 fn config_qualified_chain() {
5423 let src = r#"config {
5425 first: Integer = core/config.base
5426 second: Integer = first
5427}"#;
5428 let r = parse_ok(src);
5429 assert_eq!(r.diagnostics.len(), 0);
5430 }
5431
5432 #[test]
5433 fn config_renamed_param_with_qualified_ref() {
5434 let src = r#"config {
5435 my_timeout: Duration = core/config.base_timeout
5436}"#;
5437 let r = parse_ok(src);
5438 assert_eq!(r.diagnostics.len(), 0);
5439 }
5440
5441 #[test]
5446 fn config_default_arithmetic() {
5447 let src = r#"config {
5448 param: Integer = other_param + 1
5449}"#;
5450 let r = parse_ok(src);
5451 assert_eq!(r.diagnostics.len(), 0);
5452 }
5453
5454 #[test]
5455 fn config_default_qualified_arithmetic() {
5456 let src = r#"config {
5457 param: Duration = core/config.timeout * 2
5458}"#;
5459 let r = parse_ok(src);
5460 assert_eq!(r.diagnostics.len(), 0);
5461 }
5462
5463 #[test]
5464 fn config_default_parenthesised() {
5465 let src = r#"config {
5466 param: Integer = (base + 1) * factor
5467}"#;
5468 let r = parse_ok(src);
5469 assert_eq!(r.diagnostics.len(), 0);
5470 }
5471
5472 #[test]
5473 fn config_default_two_qualified_refs() {
5474 let src = r#"config {
5475 param: Duration = core/config.a + core/config.b
5476}"#;
5477 let r = parse_ok(src);
5478 assert_eq!(r.diagnostics.len(), 0);
5479 }
5480
5481 #[test]
5482 fn config_default_literal_only() {
5483 let src = r#"config {
5484 param: Integer = 5
5485}"#;
5486 let r = parse_ok(src);
5487 assert_eq!(r.diagnostics.len(), 0);
5488 }
5489
5490 #[test]
5491 fn config_default_decimal_literal() {
5492 let src = r#"config {
5493 param: Decimal = price * 1.5
5494}"#;
5495 let r = parse_ok(src);
5496 assert_eq!(r.diagnostics.len(), 0);
5497 }
5498
5499 #[test]
5500 fn config_default_mixed_operators() {
5501 let src = r#"config {
5502 param: Duration = timeout * 2 + 1.minute
5503}"#;
5504 let r = parse_ok(src);
5505 assert_eq!(r.diagnostics.len(), 0);
5506 }
5507
5508 #[test]
5509 fn config_default_operator_precedence() {
5510 let src = r#"config {
5512 param: Integer = a + b * c
5513}"#;
5514 let r = parse_ok(src);
5515 assert_eq!(r.diagnostics.len(), 0);
5516 }
5517
5518 #[test]
5523 fn version_2_accepted() {
5524 let r = parse("-- allium: 2\nentity User {}");
5525 assert_eq!(r.module.version, Some(2));
5526 assert_eq!(r.diagnostics.len(), 0);
5527 }
5528
5529 #[test]
5530 fn version_99_still_rejected() {
5531 let r = parse("-- allium: 99\nentity User {}");
5532 assert!(r.diagnostics.iter().any(|d|
5533 d.severity == Severity::Error && d.message.contains("unsupported")
5534 ));
5535 }
5536
5537 #[test]
5538 fn contract_typed_signature() {
5539 let src = r#"contract Codec {
5540 serialize: (value: Any) -> ByteArray
5541}"#;
5542 let r = parse_ok(src);
5543 assert_eq!(r.diagnostics.len(), 0);
5544 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5545 assert_eq!(b.kind, BlockKind::Contract);
5546 let BlockItemKind::Assignment { name, value } = &b.items[0].kind else { panic!() };
5547 assert_eq!(name.name, "serialize");
5548 assert!(matches!(value, Expr::ProjectionMap { .. }));
5549 }
5550
5551 #[test]
5552 fn contract_multi_param_signature() {
5553 let src = r#"contract Codec {
5554 serialize: (value: Any, format: String) -> ByteArray
5555}"#;
5556 let r = parse_ok(src);
5557 assert_eq!(r.diagnostics.len(), 0);
5558 }
5559
5560 #[test]
5561 fn comma_separated_entity_fields() {
5562 let src = "entity Point { x: Decimal, y: Decimal }";
5563 let r = parse_ok(src);
5564 assert_eq!(r.diagnostics.len(), 0);
5565 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5566 assert_eq!(b.items.len(), 2);
5567 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "x"));
5568 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "y"));
5569 }
5570
5571 #[test]
5572 fn comma_separated_value_fields() {
5573 let src = "value Coord { x: Integer, y: Integer }";
5574 let r = parse_ok(src);
5575 assert_eq!(r.diagnostics.len(), 0);
5576 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5577 assert_eq!(b.items.len(), 2);
5578 }
5579
5580 #[test]
5585 fn version_3_accepted() {
5586 let r = parse("-- allium: 3\nentity User {}");
5587 assert_eq!(r.module.version, Some(3));
5588 assert_eq!(r.diagnostics.len(), 0);
5589 }
5590
5591 #[test]
5592 fn transitions_block_basic() {
5593 let src = r#"-- allium: 3
5594entity Order {
5595 status: pending | confirmed | shipped | delivered | cancelled
5596
5597 transitions status {
5598 pending -> confirmed
5599 confirmed -> shipped
5600 shipped -> delivered
5601 pending -> cancelled
5602 confirmed -> cancelled
5603 terminal: delivered, cancelled
5604 }
5605}"#;
5606 let r = parse(src);
5607 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5608 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5609 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5611 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5612 };
5613 assert_eq!(graph.field.name, "status");
5614 assert_eq!(graph.edges.len(), 5);
5615 assert_eq!(graph.edges[0].from.name, "pending");
5616 assert_eq!(graph.edges[0].to.name, "confirmed");
5617 assert_eq!(graph.terminal.len(), 2);
5618 assert_eq!(graph.terminal[0].name, "delivered");
5619 assert_eq!(graph.terminal[1].name, "cancelled");
5620 }
5621
5622 #[test]
5623 fn transitions_block_no_terminal() {
5624 let src = r#"-- allium: 3
5625entity Task {
5626 status: open | closed
5627 transitions status {
5628 open -> closed
5629 }
5630}"#;
5631 let r = parse(src);
5632 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5633 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5634 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5635 panic!("expected TransitionsBlock")
5636 };
5637 assert_eq!(graph.edges.len(), 1);
5638 assert!(graph.terminal.is_empty());
5639 }
5640
5641 #[test]
5642 fn produces_emits_migration_warning() {
5643 let src = r#"-- allium: 3
5644rule ShipOrder {
5645 when: ShipOrder(order, tracking)
5646 requires: order.status = picking
5647 produces: tracking_number, shipped_at
5648 ensures: order.status = shipped
5649}"#;
5650 let r = parse(src);
5651 let warnings: Vec<_> = r.diagnostics.iter()
5652 .filter(|d| d.severity == Severity::Warning)
5653 .collect();
5654 assert!(
5655 warnings.iter().any(|d| d.message.contains("`produces:` clauses are removed")),
5656 "expected migration warning for produces, got: {:?}", warnings
5657 );
5658 }
5659
5660 #[test]
5661 fn consumes_emits_migration_warning() {
5662 let src = r#"-- allium: 3
5663rule ReadOrder {
5664 when: Check(order)
5665 consumes: warehouse_assignment
5666 ensures: order.verified = true
5667}"#;
5668 let r = parse(src);
5669 let warnings: Vec<_> = r.diagnostics.iter()
5670 .filter(|d| d.severity == Severity::Warning)
5671 .collect();
5672 assert!(
5673 warnings.iter().any(|d| d.message.contains("`consumes:` clauses are removed")),
5674 "expected migration warning for consumes, got: {:?}", warnings
5675 );
5676 }
5677
5678 #[test]
5679 fn when_clause_on_field() {
5680 let src = r#"-- allium: 3
5681entity Order {
5682 status: pending | shipped | delivered
5683 tracking_number: String when status = shipped | delivered
5684 transitions status {
5685 pending -> shipped
5686 shipped -> delivered
5687 terminal: delivered
5688 }
5689}"#;
5690 let r = parse(src);
5691 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5692 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5693 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5694 let field_with_when = b.items.iter().find(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }));
5695 assert!(field_with_when.is_some(), "expected FieldWithWhen item");
5696 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &field_with_when.unwrap().kind {
5697 assert_eq!(name.name, "tracking_number");
5698 assert_eq!(when_clause.status_field.name, "status");
5699 assert_eq!(when_clause.qualifying_states.len(), 2);
5700 assert_eq!(when_clause.qualifying_states[0].name, "shipped");
5701 assert_eq!(when_clause.qualifying_states[1].name, "delivered");
5702 }
5703 }
5704
5705 #[test]
5706 fn when_clause_single_state() {
5707 let src = r#"-- allium: 3
5708entity Order {
5709 status: active | cancelled
5710 cancelled_at: Timestamp when status = cancelled
5711 transitions status {
5712 active -> cancelled
5713 terminal: cancelled
5714 }
5715}"#;
5716 let r = parse(src);
5717 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5718 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5719 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5720 if let BlockItemKind::FieldWithWhen { name, when_clause, .. } = &b.items[1].kind {
5721 assert_eq!(name.name, "cancelled_at");
5722 assert_eq!(when_clause.qualifying_states.len(), 1);
5723 assert_eq!(when_clause.qualifying_states[0].name, "cancelled");
5724 } else {
5725 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5726 }
5727 }
5728
5729 #[test]
5730 fn when_clause_with_optional() {
5731 let src = r#"-- allium: 3
5732entity Order {
5733 status: active | cancelled
5734 notes: String? when status = cancelled
5735 transitions status {
5736 active -> cancelled
5737 terminal: cancelled
5738 }
5739}"#;
5740 let r = parse(src);
5741 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
5742 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
5743 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5744 if let BlockItemKind::FieldWithWhen { name, value, when_clause } = &b.items[1].kind {
5745 assert_eq!(name.name, "notes");
5746 assert!(matches!(value, Expr::TypeOptional { .. }), "expected TypeOptional");
5747 assert_eq!(when_clause.qualifying_states.len(), 1);
5748 } else {
5749 panic!("expected FieldWithWhen, got {:?}", b.items[1].kind);
5750 }
5751 }
5752
5753 #[test]
5754 fn transitions_in_json_output() {
5755 let src = r#"-- allium: 3
5756entity Order {
5757 status: pending | done
5758 transitions status {
5759 pending -> done
5760 terminal: done
5761 }
5762}"#;
5763 let r = parse(src);
5764 let json = serde_json::to_string(&r.module).unwrap();
5765 assert!(json.contains("TransitionsBlock"), "JSON should contain TransitionsBlock: {}", json);
5766 assert!(json.contains("pending"), "JSON should contain 'pending'");
5767 }
5768
5769 #[test]
5770 fn transitions_block_with_commas() {
5771 let src = r#"-- allium: 3
5772entity Order {
5773 status: a | b | c
5774 transitions status {
5775 a -> b,
5776 b -> c,
5777 terminal: c,
5778 }
5779}"#;
5780 let r = parse(src);
5781 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5782 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5783 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5784 panic!("expected TransitionsBlock")
5785 };
5786 assert_eq!(graph.edges.len(), 2);
5787 assert_eq!(graph.terminal.len(), 1);
5788 }
5789
5790 #[test]
5791 fn v3_full_entity_with_transitions_and_rule() {
5792 let src = r#"-- allium: 3
5793entity Order {
5794 status: pending | shipped | delivered
5795 tracking: String when status = shipped | delivered
5796 shipped_at: Timestamp when status = shipped | delivered
5797
5798 transitions status {
5799 pending -> shipped
5800 shipped -> delivered
5801 terminal: delivered
5802 }
5803}
5804
5805rule ShipOrder {
5806 when: ShipOrder(order, tracking)
5807 requires: order.status = pending
5808 ensures:
5809 order.status = shipped
5810 order.tracking = tracking
5811 order.shipped_at = now
5812}"#;
5813 let r = parse(src);
5814 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5815 assert_eq!(r.module.declarations.len(), 2);
5816 }
5817
5818 #[test]
5823 fn transitions_empty_block() {
5824 let src = "-- allium: 3\nentity E {\n status: a | b\n transitions status {}\n}";
5825 let r = parse(src);
5826 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5827 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5828 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5829 panic!("expected TransitionsBlock, got {:?}", b.items[1].kind)
5830 };
5831 assert!(graph.edges.is_empty());
5832 assert!(graph.terminal.is_empty());
5833 }
5834
5835 #[test]
5836 fn transitions_terminal_only() {
5837 let src = r#"-- allium: 3
5838entity E {
5839 status: done
5840 transitions status {
5841 terminal: done
5842 }
5843}"#;
5844 let r = parse(src);
5845 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5846 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5847 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5848 assert!(graph.edges.is_empty());
5849 assert_eq!(graph.terminal.len(), 1);
5850 assert_eq!(graph.terminal[0].name, "done");
5851 }
5852
5853 #[test]
5854 fn transitions_terminal_before_edges() {
5855 let src = r#"-- allium: 3
5856entity E {
5857 status: a | b | c
5858 transitions status {
5859 terminal: c
5860 a -> b
5861 b -> c
5862 }
5863}"#;
5864 let r = parse(src);
5865 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5866 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5867 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5868 assert_eq!(graph.edges.len(), 2);
5869 assert_eq!(graph.terminal.len(), 1);
5870 assert_eq!(graph.terminal[0].name, "c");
5871 }
5872
5873 #[test]
5874 fn transitions_self_loop() {
5875 let src = r#"-- allium: 3
5876entity E {
5877 status: running | stopped
5878 transitions status {
5879 running -> running
5880 running -> stopped
5881 terminal: stopped
5882 }
5883}"#;
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.edges.len(), 2);
5889 assert_eq!(graph.edges[0].from.name, "running");
5890 assert_eq!(graph.edges[0].to.name, "running");
5891 }
5892
5893 #[test]
5894 fn transitions_single_edge() {
5895 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s { a -> b }\n}";
5896 let r = parse(src);
5897 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5898 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5899 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5900 assert_eq!(graph.field.name, "s");
5901 assert_eq!(graph.edges.len(), 1);
5902 }
5903
5904 #[test]
5905 fn transitions_multiple_terminal_values() {
5906 let src = r#"-- allium: 3
5907entity E {
5908 status: a | b | c | d | e
5909 transitions status {
5910 a -> b
5911 b -> c
5912 terminal: c, d, e
5913 }
5914}"#;
5915 let r = parse(src);
5916 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5917 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5918 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5919 assert_eq!(graph.terminal.len(), 3);
5920 }
5921
5922 #[test]
5923 fn transitions_trailing_comma_in_terminal() {
5924 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n terminal: b,\n }\n}";
5925 let r = parse(src);
5926 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5927 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5928 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5929 assert_eq!(graph.terminal.len(), 1);
5930 }
5931
5932 #[test]
5933 fn transitions_among_other_entity_items() {
5934 let src = r#"-- allium: 3
5936entity Order {
5937 status: pending | shipped | delivered
5938 customer: Customer
5939 tracking: String?
5940
5941 transitions status {
5942 pending -> shipped
5943 shipped -> delivered
5944 terminal: delivered
5945 }
5946
5947 active_items: items where status = active
5948 invariant Positive { this.total > 0 }
5949}"#;
5950 let r = parse(src);
5951 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5952 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5953 assert_eq!(b.items.len(), 6);
5955 assert!(matches!(&b.items[3].kind, BlockItemKind::TransitionsBlock(_)));
5956 assert!(matches!(&b.items[5].kind, BlockItemKind::InvariantBlock { .. }));
5957 }
5958
5959 #[test]
5960 fn transitions_error_recovery_missing_arrow() {
5961 let src = r#"-- allium: 3
5962entity E {
5963 status: a | b | c
5964 transitions status {
5965 a b
5966 b -> c
5967 }
5968}"#;
5969 let r = parse(src);
5970 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error),
5972 "expected error for missing arrow, got: {:?}", r.diagnostics);
5973 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5975 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else {
5976 panic!("expected TransitionsBlock")
5977 };
5978 assert_eq!(graph.edges.len(), 1, "should recover and parse second edge");
5979 assert_eq!(graph.edges[0].from.name, "b");
5980 }
5981
5982 #[test]
5983 fn transitions_field_name_preserved() {
5984 let src = "-- allium: 3\nentity E {\n phase: x | y\n transitions phase { x -> y }\n}";
5985 let r = parse(src);
5986 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
5987 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
5988 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
5989 assert_eq!(graph.field.name, "phase");
5990 }
5991
5992 #[test]
5993 fn transitions_diamond_topology() {
5994 let src = r#"-- allium: 3
5996entity E {
5997 status: new | path_a | path_b | done
5998 transitions status {
5999 new -> path_a
6000 new -> path_b
6001 path_a -> done
6002 path_b -> done
6003 terminal: done
6004 }
6005}"#;
6006 let r = parse(src);
6007 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6008 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6009 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
6010 assert_eq!(graph.edges.len(), 4);
6011 }
6012
6013 #[test]
6014 fn transitions_edge_span_is_from_to_range() {
6015 let src = "-- allium: 3\nentity E {\n s: a | b\n transitions s {\n a -> b\n }\n}";
6016 let r = parse(src);
6017 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6018 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6019 let BlockItemKind::TransitionsBlock(graph) = &b.items[1].kind else { panic!() };
6020 let edge = &graph.edges[0];
6021 assert!(edge.span.start <= edge.from.span.start);
6023 assert!(edge.span.end >= edge.to.span.end);
6024 }
6025
6026 #[test]
6031 fn when_clause_multiple_fields() {
6032 let src = r#"-- allium: 3
6033entity Order {
6034 status: pending | shipped | delivered
6035 tracking: String when status = shipped | delivered
6036 shipped_at: Timestamp when status = shipped | delivered
6037 delivered_at: Timestamp when status = delivered
6038 transitions status {
6039 pending -> shipped
6040 shipped -> delivered
6041 terminal: delivered
6042 }
6043}"#;
6044 let r = parse(src);
6045 let errors: Vec<_> = r.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect();
6046 assert_eq!(errors.len(), 0, "unexpected errors: {:?}", errors);
6047 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6048 let when_count = b.items.iter()
6049 .filter(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. }))
6050 .count();
6051 assert_eq!(when_count, 3);
6052 }
6053
6054 #[test]
6055 fn legacy_produces_consumes_skipped_with_warnings() {
6056 let src = r#"-- allium: 3
6057rule R {
6058 when: Go(x)
6059 produces: field_a
6060 consumes: field_b
6061 ensures: x.done = true
6062}"#;
6063 let r = parse(src);
6064 let warnings: Vec<_> = r.diagnostics.iter()
6065 .filter(|d| d.severity == Severity::Warning)
6066 .collect();
6067 assert!(warnings.len() >= 2, "expected at least 2 migration warnings, got {}", warnings.len());
6068 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6070 assert!(
6071 !b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::FieldWithWhen { .. })),
6072 "legacy produces/consumes should not become FieldWithWhen"
6073 );
6074 }
6075
6076 #[test]
6081 fn v3_entity_with_transitions_and_invariant() {
6082 let src = r#"-- allium: 3
6083entity Account {
6084 status: open | frozen | closed
6085 balance: Decimal
6086
6087 transitions status {
6088 open -> frozen
6089 frozen -> open
6090 open -> closed
6091 frozen -> closed
6092 terminal: closed
6093 }
6094
6095 invariant NonNegative { this.balance >= 0 }
6096}"#;
6097 let r = parse(src);
6098 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6099 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6100 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::TransitionsBlock(_))));
6101 assert!(b.items.iter().any(|i| matches!(&i.kind, BlockItemKind::InvariantBlock { .. })));
6102 }
6103
6104 #[test]
6105 fn v3_rule_with_multiple_ensures() {
6106 let src = r#"-- allium: 3
6107rule CompleteOrder {
6108 when: Complete(order)
6109 requires: order.status = shipped
6110 ensures: order.status = delivered
6111 ensures: order.completed_at = now
6112 ensures: order.receipt_number = generate_receipt()
6113}"#;
6114 let r = parse(src);
6115 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6116 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6117 let ensures_count = b.items.iter()
6118 .filter(|i| matches!(&i.kind, BlockItemKind::Clause { keyword, .. } if keyword == "ensures"))
6119 .count();
6120 assert_eq!(ensures_count, 3);
6121 }
6122
6123 #[test]
6124 fn v3_rule_with_if_block() {
6125 let src = r#"-- allium: 3
6126rule Cancel {
6127 when: Cancel(order, reason)
6128 requires: order.status != delivered
6129 ensures:
6130 order.status = cancelled
6131 order.cancelled_at = now
6132 if reason = customer_request:
6133 order.cancelled_by = order.customer
6134}"#;
6135 let r = parse(src);
6136 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6137 }
6138
6139 #[test]
6140 fn v3_complete_lifecycle_spec() {
6141 let src = r#"-- allium: 3
6142
6143entity Subscription {
6144 status: trial | active | past_due | cancelled
6145 started_at: Timestamp when status = active | past_due | cancelled
6146 cancelled_at: Timestamp when status = cancelled
6147 balance: Decimal
6148
6149 transitions status {
6150 trial -> active
6151 active -> past_due
6152 past_due -> active
6153 active -> cancelled
6154 past_due -> cancelled
6155 terminal: cancelled
6156 }
6157
6158 invariant NonNegative { this.balance >= 0 }
6159}
6160
6161config {
6162 trial_period: Duration = 14.days
6163}
6164
6165rule ActivateSubscription {
6166 when: Activate(sub)
6167 requires: sub.status = trial
6168 ensures:
6169 sub.status = active
6170 sub.started_at = now
6171}
6172
6173rule CancelSubscription {
6174 when: Cancel(sub)
6175 requires: sub.status != cancelled
6176 ensures:
6177 sub.status = cancelled
6178 sub.cancelled_at = now
6179}
6180
6181invariant AllCancelledHaveTimestamp {
6182 for sub in Subscriptions where status = cancelled:
6183 sub.cancelled_at != null
6184}
6185
6186surface SubscriptionDashboard {
6187 facing user: User
6188 context sub: Subscription where owner = user
6189 exposes:
6190 sub.status
6191 sub.balance
6192 provides:
6193 Cancel(sub) when sub.status != cancelled
6194}
6195"#;
6196 let r = parse(src);
6197 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6198 assert_eq!(r.module.declarations.len(), 6);
6200 }
6201
6202 #[test]
6203 fn v3_produces_consumes_are_field_names_in_entities() {
6204 let src = r#"-- allium: 3
6206entity Factory {
6207 produces: widget_a
6208 consumes: raw_material
6209}"#;
6210 let r = parse(src);
6211 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6212 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6213 assert!(matches!(&b.items[0].kind, BlockItemKind::Assignment { name, .. } if name.name == "produces"));
6214 assert!(matches!(&b.items[1].kind, BlockItemKind::Assignment { name, .. } if name.name == "consumes"));
6215 }
6216
6217 #[test]
6218 fn v3_legacy_produces_consumes_emit_warnings_in_rules() {
6219 let src = r#"-- allium: 3
6220rule Ship {
6221 when: Ship(order)
6222 produces: tracking_number
6223 consumes: warehouse
6224 ensures: order.status = shipped
6225}"#;
6226 let r = parse(src);
6227 let warnings: Vec<_> = r.diagnostics.iter()
6228 .filter(|d| d.severity == Severity::Warning)
6229 .collect();
6230 assert!(warnings.len() >= 2, "expected migration warnings, got {:?}", warnings);
6231 }
6232
6233 #[test]
6234 fn v3_version_preserved_in_module() {
6235 let src = "-- allium: 3\nentity E {}";
6236 let r = parse(src);
6237 assert_eq!(r.module.version, Some(3));
6238 }
6239
6240 #[test]
6241 fn v3_version_4_still_rejected() {
6242 let src = "-- allium: 4\nentity E {}";
6243 let r = parse(src);
6244 assert!(r.diagnostics.iter().any(|d| d.severity == Severity::Error
6245 && d.message.contains("unsupported")));
6246 }
6247
6248 #[test]
6253 fn backtick_in_named_enum() {
6254 let src = "-- allium: 3\nenum Locale { en | fr | `de-CH-1996` | `no-cache` }";
6255 let r = parse(src);
6256 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6257 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6258 assert_eq!(b.items.len(), 4);
6259 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[0].kind else { panic!() };
6261 assert_eq!(name.name, "en");
6262 assert!(!backtick_quoted);
6263 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[2].kind else { panic!() };
6265 assert_eq!(name.name, "de-CH-1996");
6266 assert!(backtick_quoted);
6267 let BlockItemKind::EnumVariant { name, backtick_quoted } = &b.items[3].kind else { panic!() };
6269 assert_eq!(name.name, "no-cache");
6270 assert!(backtick_quoted);
6271 }
6272
6273 #[test]
6274 fn backtick_in_inline_enum() {
6275 let src = "-- allium: 3\nentity E { cache: `no-cache` | `no-store` | `public` }";
6276 let r = parse(src);
6277 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6278 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6279 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
6280 assert!(matches!(value, Expr::Pipe { .. }));
6282 }
6283
6284 #[test]
6285 fn backtick_in_comparison() {
6286 let src = r#"-- allium: 3
6287rule R {
6288 when: Check(item)
6289 requires: item.locale = `de-CH-1996`
6290 ensures: Done()
6291}"#;
6292 let r = parse(src);
6293 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6294 }
6295
6296 #[test]
6297 fn backtick_mixed_with_unquoted() {
6298 let src = "-- allium: 3\nenum CacheDirective { `no-cache` | `no-store` | public | private }";
6299 let r = parse(src);
6300 assert_eq!(r.diagnostics.len(), 0, "unexpected diagnostics: {:?}", r.diagnostics);
6301 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
6302 assert_eq!(b.items.len(), 4);
6303 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[0].kind else { panic!() };
6304 assert!(backtick_quoted);
6305 let BlockItemKind::EnumVariant { backtick_quoted, .. } = &b.items[2].kind else { panic!() };
6306 assert!(!backtick_quoted);
6307 }
6308
6309 #[test]
6314 fn v3_lifecycle_fixture() {
6315 let src = include_str!("../tests/fixtures/v3-lifecycle.allium");
6316 let r = parse(src);
6317 let errors: Vec<_> = r.diagnostics.iter()
6318 .filter(|d| d.severity == Severity::Error)
6319 .collect();
6320 assert_eq!(
6321 errors.len(),
6322 0,
6323 "expected no errors in v3 lifecycle fixture, got: {:?}",
6324 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
6325 );
6326 }
6327
6328 #[test]
6329 fn zero_arg_contract_signature_parens() {
6330 let r = parse_ok(
6332 "-- allium: 3\nvalue Foo { value: String }\n\
6333 contract Demo { list_things: () -> Set<Foo> }",
6334 );
6335 assert!(
6336 r.diagnostics.iter().all(|d| d.severity != Severity::Error),
6337 "zero-arg signature should parse without errors, got: {:?}",
6338 r.diagnostics
6339 .iter()
6340 .map(|d| &d.message)
6341 .collect::<Vec<_>>(),
6342 );
6343
6344 let contract = r
6348 .module
6349 .declarations
6350 .iter()
6351 .find_map(|d| match d {
6352 Decl::Block(b) if b.kind == BlockKind::Contract => Some(b),
6353 _ => None,
6354 })
6355 .expect("contract decl");
6356 let BlockItemKind::Assignment { value, .. } = &contract.items[0].kind else {
6357 panic!("expected signature assignment, got {:?}", contract.items[0].kind);
6358 };
6359 let Expr::GenericType { name, .. } = value else {
6360 panic!("expected GenericType signature value, got {value:?}");
6361 };
6362 let Expr::ProjectionMap { source, .. } = name.as_ref() else {
6363 panic!("expected `params -> Return` projection, got {name:?}");
6364 };
6365 assert!(
6366 matches!(source.as_ref(), Expr::Block { items, .. } if items.is_empty()),
6367 "zero-arg params should be an empty block, got {source:?}",
6368 );
6369 }
6370}