1use crate::ast::*;
8use crate::diagnostic::Diagnostic;
9use crate::lexer::{lex, SourceMap, Token, TokenKind};
10use crate::Span;
11
12#[derive(Debug)]
17pub struct ParseResult {
18 pub module: Module,
19 pub diagnostics: Vec<Diagnostic>,
20}
21
22pub fn parse(source: &str) -> ParseResult {
24 let tokens = lex(source);
25 let source_map = SourceMap::new(source);
26 let mut p = Parser {
27 source,
28 tokens,
29 pos: 0,
30 source_map,
31 diagnostics: Vec::new(),
32 };
33 let module = p.parse_module();
34 ParseResult {
35 module,
36 diagnostics: p.diagnostics,
37 }
38}
39
40struct Parser<'s> {
45 source: &'s str,
46 tokens: Vec<Token>,
47 pos: usize,
48 source_map: SourceMap,
49 diagnostics: Vec<Diagnostic>,
50}
51
52impl<'s> Parser<'s> {
57 fn peek(&self) -> Token {
58 self.tokens[self.pos]
59 }
60
61 fn peek_kind(&self) -> TokenKind {
62 self.tokens[self.pos].kind
63 }
64
65 fn peek_at(&self, offset: usize) -> Token {
66 let idx = (self.pos + offset).min(self.tokens.len() - 1);
67 self.tokens[idx]
68 }
69
70 fn advance(&mut self) -> Token {
71 let tok = self.tokens[self.pos];
72 if tok.kind != TokenKind::Eof {
73 self.pos += 1;
74 }
75 tok
76 }
77
78 fn at(&self, kind: TokenKind) -> bool {
79 self.peek_kind() == kind
80 }
81
82 fn at_eof(&self) -> bool {
83 self.at(TokenKind::Eof)
84 }
85
86 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
87 if self.at(kind) {
88 Some(self.advance())
89 } else {
90 None
91 }
92 }
93
94 fn expect(&mut self, kind: TokenKind) -> Option<Token> {
95 if self.at(kind) {
96 Some(self.advance())
97 } else {
98 self.error(
99 self.peek().span,
100 format!("expected {kind}, found {}", self.peek_kind()),
101 );
102 None
103 }
104 }
105
106 fn text(&self, span: Span) -> &'s str {
107 &self.source[span.start..span.end]
108 }
109
110 fn line_of(&self, span: Span) -> u32 {
111 self.source_map.line_col(span.start).0
112 }
113
114 fn col_of(&self, span: Span) -> u32 {
115 self.source_map.line_col(span.start).1
116 }
117
118 fn error(&mut self, span: Span, msg: impl Into<String>) {
119 let line = self.source_map.line_col(span.start).0;
120 if let Some(last) = self.diagnostics.last() {
121 if last.severity == crate::diagnostic::Severity::Error
122 && self.source_map.line_col(last.span.start).0 == line
123 {
124 return;
125 }
126 }
127 self.diagnostics.push(Diagnostic::error(span, msg));
128 }
129
130 fn parse_ident(&mut self) -> Option<Ident> {
132 self.parse_ident_in("identifier")
133 }
134
135 fn parse_ident_in(&mut self, context: &str) -> Option<Ident> {
137 let tok = self.peek();
138 if tok.kind.is_word() {
139 self.advance();
140 Some(Ident {
141 span: tok.span,
142 name: self.text(tok.span).to_string(),
143 })
144 } else {
145 self.error(
146 tok.span,
147 format!("expected {context}, found {}", tok.kind),
148 );
149 None
150 }
151 }
152
153 fn parse_string(&mut self) -> Option<StringLiteral> {
155 let tok = self.expect(TokenKind::String)?;
156 let raw = self.text(tok.span);
157 let inner = &raw[1..raw.len() - 1];
159 let parts = parse_string_parts(inner, tok.span.start + 1);
160 Some(StringLiteral {
161 span: tok.span,
162 parts,
163 })
164 }
165}
166
167fn parse_string_parts(inner: &str, base_offset: usize) -> Vec<StringPart> {
171 let mut parts = Vec::new();
172 let mut buf = String::new();
173 let bytes = inner.as_bytes();
174 let mut i = 0;
175 while i < bytes.len() {
176 if bytes[i] == b'\\' && i + 1 < bytes.len() {
177 buf.push(bytes[i + 1] as char);
178 i += 2;
179 } else if bytes[i] == b'{' {
180 if !buf.is_empty() {
181 parts.push(StringPart::Text(std::mem::take(&mut buf)));
182 }
183 i += 1; let start = i;
185 while i < bytes.len() && bytes[i] != b'}' {
186 i += 1;
187 }
188 let name = std::str::from_utf8(&bytes[start..i]).unwrap_or("").to_string();
189 let span_start = base_offset + start;
190 let span_end = base_offset + i;
191 parts.push(StringPart::Interpolation(Ident {
192 span: Span::new(span_start, span_end),
193 name,
194 }));
195 if i < bytes.len() {
196 i += 1; }
198 } else {
199 buf.push(bytes[i] as char);
200 i += 1;
201 }
202 }
203 if !buf.is_empty() {
204 parts.push(StringPart::Text(buf));
205 }
206 parts
207}
208
209fn is_clause_keyword(text: &str) -> bool {
216 matches!(
217 text,
218 "when"
219 | "requires"
220 | "ensures"
221 | "facing"
222 | "context"
223 | "exposes"
224 | "provides"
225 | "related"
226 | "timeout"
227 | "guarantee"
228 | "guidance"
229 | "identified_by"
230 | "within"
231 )
232}
233
234fn clause_allows_binding(keyword: &str) -> bool {
236 matches!(keyword, "when")
237}
238
239fn is_binding_clause_keyword(text: &str) -> bool {
242 matches!(text, "facing" | "context")
243}
244
245fn token_is_clause_keyword(kind: TokenKind) -> bool {
247 matches!(
248 kind,
249 TokenKind::When | TokenKind::Requires | TokenKind::Ensures | TokenKind::Within
250 )
251}
252
253impl<'s> Parser<'s> {
258 fn parse_module(&mut self) -> Module {
259 let start = self.peek().span;
260 let version = detect_version(self.source);
263
264 match version {
265 None => {
266 self.diagnostics.push(Diagnostic::warning(
267 start,
268 "missing version marker; expected '-- allium: 1' as the first line",
269 ));
270 }
271 Some(1) => {}
272 Some(v) => {
273 self.diagnostics.push(Diagnostic::error(
274 start,
275 format!("unsupported allium version {v}; this parser supports version 1"),
276 ));
277 }
278 }
279
280 let mut decls = Vec::new();
281 while !self.at_eof() {
282 if let Some(d) = self.parse_decl() {
283 decls.push(d);
284 } else {
285 self.advance();
287 }
288 }
289 let end = self.peek().span;
290 Module {
291 span: start.merge(end),
292 version,
293 declarations: decls,
294 }
295 }
296}
297
298fn detect_version(source: &str) -> Option<u32> {
299 for line in source.lines() {
300 let trimmed = line.trim();
301 if trimmed.is_empty() {
302 continue;
303 }
304 if let Some(rest) = trimmed.strip_prefix("--") {
305 let rest = rest.trim();
306 if let Some(ver) = rest.strip_prefix("allium:") {
307 return ver.trim().parse().ok();
308 }
309 }
310 break; }
312 None
313}
314
315impl<'s> Parser<'s> {
320 fn parse_decl(&mut self) -> Option<Decl> {
321 match self.peek_kind() {
322 TokenKind::Use => self.parse_use_decl().map(Decl::Use),
323 TokenKind::Rule => self.parse_block(BlockKind::Rule).map(Decl::Block),
324 TokenKind::Entity => self.parse_block(BlockKind::Entity).map(Decl::Block),
325 TokenKind::External => {
326 let start = self.advance().span;
327 if self.at(TokenKind::Entity) {
328 self.parse_block_from(start, BlockKind::ExternalEntity)
329 .map(Decl::Block)
330 } else {
331 self.error(self.peek().span, "expected 'entity' after 'external'");
332 None
333 }
334 }
335 TokenKind::Value => self.parse_block(BlockKind::Value).map(Decl::Block),
336 TokenKind::Enum => self.parse_block(BlockKind::Enum).map(Decl::Block),
337 TokenKind::Given => self.parse_anonymous_block(BlockKind::Given).map(Decl::Block),
338 TokenKind::Config => self.parse_anonymous_block(BlockKind::Config).map(Decl::Block),
339 TokenKind::Surface => self.parse_block(BlockKind::Surface).map(Decl::Block),
340 TokenKind::Actor => self.parse_block(BlockKind::Actor).map(Decl::Block),
341 TokenKind::Default => self.parse_default_decl().map(Decl::Default),
342 TokenKind::Variant => self.parse_variant_decl().map(Decl::Variant),
343 TokenKind::Deferred => self.parse_deferred_decl().map(Decl::Deferred),
344 TokenKind::Open => self.parse_open_question_decl().map(Decl::OpenQuestion),
345 TokenKind::Ident
347 if self.peek_at(1).kind == TokenKind::Slash
348 && self.text(self.peek_at(2).span) == "config" =>
349 {
350 self.parse_qualified_config().map(Decl::Block)
351 }
352 _ => {
353 self.error(
354 self.peek().span,
355 format!(
356 "expected declaration (entity, rule, enum, value, config, surface, actor, \
357 given, default, variant, deferred, use, open question), found {}",
358 self.peek_kind(),
359 ),
360 );
361 None
362 }
363 }
364 }
365
366 fn parse_use_decl(&mut self) -> Option<UseDecl> {
371 let start = self.expect(TokenKind::Use)?.span;
372 let path = self.parse_string()?;
373 let alias = if self.eat(TokenKind::As).is_some() {
374 Some(self.parse_ident_in("import alias")?)
375 } else {
376 None
377 };
378 let end = alias
379 .as_ref()
380 .map(|a| a.span)
381 .unwrap_or(path.span);
382 Some(UseDecl {
383 span: start.merge(end),
384 path,
385 alias,
386 })
387 }
388
389 fn parse_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
392 let start = self.advance().span; self.parse_block_from(start, kind)
394 }
395
396 fn parse_block_from(&mut self, start: Span, kind: BlockKind) -> Option<BlockDecl> {
397 if kind == BlockKind::ExternalEntity {
400 self.expect(TokenKind::Entity)?;
401 }
402 let context = match kind {
403 BlockKind::Entity | BlockKind::ExternalEntity => "entity name",
404 BlockKind::Rule => "rule name",
405 BlockKind::Surface => "surface name",
406 BlockKind::Actor => "actor name",
407 BlockKind::Value => "value type name",
408 BlockKind::Enum => "enum name",
409 _ => "block name",
410 };
411 let name = Some(self.parse_ident_in(context)?);
412 self.expect(TokenKind::LBrace)?;
413 let items = if kind == BlockKind::Enum {
414 self.parse_enum_body()
415 } else {
416 self.parse_block_items()
417 };
418 let end = self.expect(TokenKind::RBrace)?.span;
419 Some(BlockDecl {
420 span: start.merge(end),
421 kind,
422 name,
423 items,
424 })
425 }
426
427 fn parse_enum_body(&mut self) -> Vec<BlockItem> {
432 let mut items = Vec::new();
433 while !self.at(TokenKind::RBrace) && !self.at_eof() {
434 if self.eat(TokenKind::Pipe).is_some() {
435 continue;
436 }
437 if let Some(ident) = self.parse_ident_in("enum variant") {
438 items.push(BlockItem {
439 span: ident.span,
440 kind: BlockItemKind::EnumVariant { name: ident },
441 });
442 } else {
443 self.advance(); }
445 }
446 items
447 }
448
449 fn parse_anonymous_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
450 let start = self.advance().span;
451 self.expect(TokenKind::LBrace)?;
452 let items = self.parse_block_items();
453 let end = self.expect(TokenKind::RBrace)?.span;
454 Some(BlockDecl {
455 span: start.merge(end),
456 kind,
457 name: None,
458 items,
459 })
460 }
461
462 fn parse_qualified_config(&mut self) -> Option<BlockDecl> {
465 let alias = self.parse_ident_in("config qualifier")?;
466 let start = alias.span;
467 self.expect(TokenKind::Slash)?;
468 self.advance(); self.expect(TokenKind::LBrace)?;
470 let items = self.parse_block_items();
471 let end = self.expect(TokenKind::RBrace)?.span;
472 Some(BlockDecl {
473 span: start.merge(end),
474 kind: BlockKind::Config,
475 name: Some(alias),
476 items,
477 })
478 }
479
480 fn parse_default_decl(&mut self) -> Option<DefaultDecl> {
483 let start = self.expect(TokenKind::Default)?.span;
484
485 let (type_name, name) = if self.peek_kind().is_word()
489 && self.peek_at(1).kind.is_word()
490 && self.peek_at(2).kind == TokenKind::Eq
491 {
492 let t = self.parse_ident_in("type name")?;
493 let n = self.parse_ident_in("default name")?;
494 (Some(t), n)
495 } else {
496 (None, self.parse_ident_in("default name")?)
497 };
498
499 self.expect(TokenKind::Eq)?;
500 let value = self.parse_expr(0)?;
501 Some(DefaultDecl {
502 span: start.merge(value.span()),
503 type_name,
504 name,
505 value,
506 })
507 }
508
509 fn parse_variant_decl(&mut self) -> Option<VariantDecl> {
512 let start = self.expect(TokenKind::Variant)?.span;
513 let name = self.parse_ident_in("variant name")?;
514 self.expect(TokenKind::Colon)?;
515 let base = self.parse_expr(0)?;
516
517 let items = if self.eat(TokenKind::LBrace).is_some() {
518 let items = self.parse_block_items();
519 self.expect(TokenKind::RBrace)?;
520 items
521 } else {
522 Vec::new()
523 };
524
525 let end = if let Some(last) = items.last() {
526 last.span
527 } else {
528 base.span()
529 };
530 Some(VariantDecl {
531 span: start.merge(end),
532 name,
533 base,
534 items,
535 })
536 }
537
538 fn parse_deferred_decl(&mut self) -> Option<DeferredDecl> {
541 let start = self.expect(TokenKind::Deferred)?.span;
542 let path = self.parse_expr(0)?;
543 Some(DeferredDecl {
544 span: start.merge(path.span()),
545 path,
546 })
547 }
548
549 fn parse_open_question_decl(&mut self) -> Option<OpenQuestionDecl> {
552 let start = self.expect(TokenKind::Open)?.span;
553 self.expect(TokenKind::Question)?;
554 let text = self.parse_string()?;
555 Some(OpenQuestionDecl {
556 span: start.merge(text.span),
557 text,
558 })
559 }
560
561 }
564
565impl<'s> Parser<'s> {
570 fn parse_block_items(&mut self) -> Vec<BlockItem> {
571 let mut items = Vec::new();
572 while !self.at(TokenKind::RBrace) && !self.at_eof() {
573 if let Some(item) = self.parse_block_item() {
574 items.push(item);
575 } else {
576 self.advance();
578 }
579 }
580 items
581 }
582
583 fn parse_block_item(&mut self) -> Option<BlockItem> {
584 let start = self.peek().span;
585
586 if self.at(TokenKind::Let) {
588 return self.parse_let_item(start);
589 }
590
591 if self.at(TokenKind::For) {
593 return self.parse_for_block_item(start);
594 }
595
596 if self.at(TokenKind::If) {
598 return self.parse_if_block_item(start);
599 }
600
601 if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
603 self.advance(); self.advance(); let text = self.parse_string()?;
606 return Some(BlockItem {
607 span: start.merge(text.span),
608 kind: BlockItemKind::OpenQuestion { text },
609 });
610 }
611
612 if self.peek_kind().is_word() {
615 if is_binding_clause_keyword(self.text(self.peek().span))
618 && self.peek_at(1).kind.is_word()
619 && self.peek_at(2).kind == TokenKind::Colon
620 {
621 return self.parse_binding_clause_item(start);
622 }
623
624 if self.peek_at(1).kind == TokenKind::Dot
626 && self.peek_at(2).kind.is_word()
627 && self.peek_at(3).kind == TokenKind::Colon
628 {
629 return self.parse_path_assignment_item(start);
630 }
631
632 if self.peek_at(1).kind == TokenKind::LParen {
634 return self.parse_param_or_clause_item(start);
635 }
636
637 if self.peek_at(1).kind == TokenKind::Colon {
639 return self.parse_assign_or_clause_item(start);
640 }
641 }
642
643 if token_is_clause_keyword(self.peek_kind()) && self.peek_at(1).kind == TokenKind::Colon {
645 return self.parse_assign_or_clause_item(start);
646 }
647
648 self.error(
649 start,
650 format!(
651 "expected block item (name: value, let name = value, when:/requires:/ensures: clause, \
652 for ... in ...:, or open question), found {}",
653 self.peek_kind(),
654 ),
655 );
656 None
657 }
658
659 fn parse_let_item(&mut self, start: Span) -> Option<BlockItem> {
660 self.advance(); let name = self.parse_ident_in("binding name")?;
662 self.expect(TokenKind::Eq)?;
663 let value = self.parse_clause_value(start)?;
664 Some(BlockItem {
665 span: start.merge(value.span()),
666 kind: BlockItemKind::Let { name, value },
667 })
668 }
669
670 fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
673 let keyword_tok = self.advance(); let keyword = self.text(keyword_tok.span).to_string();
675 let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
676 self.advance(); let type_expr = self.parse_clause_value(start)?;
678 let value_span = type_expr.span();
679 let value = Expr::Binding {
680 span: binding_name.span.merge(value_span),
681 name: binding_name,
682 value: Box::new(type_expr),
683 };
684 Some(BlockItem {
685 span: start.merge(value_span),
686 kind: BlockItemKind::Clause { keyword, value },
687 })
688 }
689
690 fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
693 self.advance(); let binding = self.parse_for_binding()?;
695 self.expect(TokenKind::In)?;
696
697 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
698
699 let filter = if self.eat(TokenKind::Where).is_some() {
700 Some(self.parse_expr(0)?)
703 } else {
704 None
705 };
706
707 self.expect(TokenKind::Colon)?;
708
709 let for_line = self.line_of(start);
711 let next_line = self.line_of(self.peek().span);
712
713 let items = if next_line > for_line {
714 let base_col = self.col_of(self.peek().span);
715 self.parse_indented_block_items(base_col)
716 } else {
717 let mut items = Vec::new();
719 if let Some(item) = self.parse_block_item() {
720 items.push(item);
721 }
722 items
723 };
724
725 let end = items
726 .last()
727 .map(|i| i.span)
728 .unwrap_or(start);
729
730 Some(BlockItem {
731 span: start.merge(end),
732 kind: BlockItemKind::ForBlock {
733 binding,
734 collection,
735 filter,
736 items,
737 },
738 })
739 }
740
741 fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
743 let mut items = Vec::new();
744 while !self.at_eof()
745 && !self.at(TokenKind::RBrace)
746 && self.col_of(self.peek().span) >= base_col
747 {
748 if let Some(item) = self.parse_block_item() {
749 items.push(item);
750 } else {
751 self.advance();
752 break;
753 }
754 }
755 items
756 }
757
758 fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
760 self.advance(); let mut branches = Vec::new();
762
763 let condition = self.parse_expr(0)?;
765 self.expect(TokenKind::Colon)?;
766 let if_line = self.line_of(start);
767 let items = self.parse_if_block_body(if_line);
768 branches.push(CondBlockBranch {
769 span: start.merge(items.last().map(|i| i.span).unwrap_or(start)),
770 condition,
771 items,
772 });
773
774 let mut else_items = None;
776 while self.at(TokenKind::Else) {
777 let else_tok = self.advance();
778 if self.at(TokenKind::If) {
779 let if_start = self.advance().span;
780 let cond = self.parse_expr(0)?;
781 self.expect(TokenKind::Colon)?;
782 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
783 branches.push(CondBlockBranch {
784 span: if_start.merge(body_items.last().map(|i| i.span).unwrap_or(if_start)),
785 condition: cond,
786 items: body_items,
787 });
788 } else {
789 self.expect(TokenKind::Colon)?;
790 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
791 else_items = Some(body_items);
792 break;
793 }
794 }
795
796 let end = else_items
797 .as_ref()
798 .and_then(|items| items.last().map(|i| i.span))
799 .or_else(|| branches.last().and_then(|b| b.items.last().map(|i| i.span)))
800 .unwrap_or(start);
801
802 Some(BlockItem {
803 span: start.merge(end),
804 kind: BlockItemKind::IfBlock {
805 branches,
806 else_items,
807 },
808 })
809 }
810
811 fn parse_if_block_body(&mut self, keyword_line: u32) -> Vec<BlockItem> {
813 let next_line = self.line_of(self.peek().span);
814 if next_line > keyword_line {
815 let base_col = self.col_of(self.peek().span);
816 self.parse_indented_block_items(base_col)
817 } else {
818 let mut items = Vec::new();
820 if let Some(item) = self.parse_block_item() {
821 items.push(item);
822 }
823 items
824 }
825 }
826
827 fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
828 let name_tok = self.advance(); let name_text = self.text(name_tok.span).to_string();
830 self.advance(); let allows_binding = clause_allows_binding(&name_text);
833 let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
834 let value_span = value.span();
835
836 let kind = if is_clause_keyword(&name_text) {
837 BlockItemKind::Clause {
838 keyword: name_text,
839 value,
840 }
841 } else {
842 BlockItemKind::Assignment {
843 name: Ident {
844 span: name_tok.span,
845 name: name_text,
846 },
847 value,
848 }
849 };
850
851 Some(BlockItem {
852 span: start.merge(value_span),
853 kind,
854 })
855 }
856
857 fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
859 let obj_tok = self.advance(); self.advance(); let field = self.parse_ident_in("field name")?;
862 self.advance(); let path = Expr::MemberAccess {
865 span: obj_tok.span.merge(field.span),
866 object: Box::new(Expr::Ident(Ident {
867 span: obj_tok.span,
868 name: self.text(obj_tok.span).to_string(),
869 })),
870 field,
871 };
872
873 let value = self.parse_clause_value(start)?;
874 let value_span = value.span();
875 Some(BlockItem {
876 span: start.merge(value_span),
877 kind: BlockItemKind::PathAssignment { path, value },
878 })
879 }
880
881 fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
882 let saved_pos = self.pos;
886 let _name_tok = self.advance();
887 self.advance(); let mut depth = 1u32;
891 while !self.at_eof() && depth > 0 {
892 match self.peek_kind() {
893 TokenKind::LParen => {
894 depth += 1;
895 self.advance();
896 }
897 TokenKind::RParen => {
898 depth -= 1;
899 self.advance();
900 }
901 _ => {
902 self.advance();
903 }
904 }
905 }
906
907 if self.at(TokenKind::Colon) {
908 self.pos = saved_pos;
910 let name = self.parse_ident_in("derived value name")?;
911 self.expect(TokenKind::LParen)?;
912 let params = self.parse_ident_list()?;
913 self.expect(TokenKind::RParen)?;
914 self.expect(TokenKind::Colon)?;
915 let value = self.parse_clause_value(start)?;
916 Some(BlockItem {
917 span: start.merge(value.span()),
918 kind: BlockItemKind::ParamAssignment {
919 name,
920 params,
921 value,
922 },
923 })
924 } else {
925 self.pos = saved_pos;
927 if self.peek_at(1).kind == TokenKind::Colon {
929 }
931 self.parse_assign_or_clause_item(start)
933 }
934 }
935
936 fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
937 let mut params = Vec::new();
938 if !self.at(TokenKind::RParen) {
939 params.push(self.parse_ident_in("parameter name")?);
940 while self.eat(TokenKind::Comma).is_some() {
941 params.push(self.parse_ident_in("parameter name")?);
942 }
943 }
944 Some(params)
945 }
946
947 fn parse_for_binding(&mut self) -> Option<ForBinding> {
949 if self.at(TokenKind::LParen) {
950 let start = self.advance().span; let mut idents = Vec::new();
952 idents.push(self.parse_ident_in("loop variable")?);
953 while self.eat(TokenKind::Comma).is_some() {
954 idents.push(self.parse_ident_in("loop variable")?);
955 }
956 let end = self.expect(TokenKind::RParen)?.span;
957 Some(ForBinding::Destructured(idents, start.merge(end)))
958 } else {
959 let ident = self.parse_ident_in("loop variable")?;
960 Some(ForBinding::Single(ident))
961 }
962 }
963
964 fn parse_clause_value_maybe_binding(
968 &mut self,
969 clause_start: Span,
970 allow_binding: bool,
971 ) -> Option<Expr> {
972 if allow_binding
973 && self.peek_kind().is_word()
974 && self.peek_at(1).kind == TokenKind::Colon
975 {
976 let clause_line = self.line_of(clause_start);
979 let next_line = self.line_of(self.peek().span);
980 let colon_is_block_item = next_line > clause_line
981 && self.peek_at(2).kind != TokenKind::Eof
982 && self.line_of(self.peek_at(2).span) == next_line;
983
984 if next_line == clause_line || colon_is_block_item {
985 let name = self.parse_ident_in("binding name")?;
986 self.advance(); let inner = self.parse_clause_value(clause_start)?;
988 return Some(Expr::Binding {
989 span: name.span.merge(inner.span()),
990 name,
991 value: Box::new(inner),
992 });
993 }
994 }
995 self.parse_clause_value(clause_start)
996 }
997
998 fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1001 let clause_line = self.line_of(clause_start);
1002 let next = self.peek();
1003 let next_line = self.line_of(next.span);
1004
1005 if next_line > clause_line {
1006 let base_col = self.col_of(next.span);
1011 let clause_col = self.col_of(clause_start);
1012 if base_col <= clause_col {
1013 return Some(Expr::Block {
1014 span: clause_start,
1015 items: Vec::new(),
1016 });
1017 }
1018 self.parse_indented_block(base_col)
1019 } else {
1020 self.parse_expr(0)
1022 }
1023 }
1024
1025 fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1028 let start = self.peek().span;
1029 let mut items = Vec::new();
1030
1031 while !self.at_eof()
1032 && !self.at(TokenKind::RBrace)
1033 && self.col_of(self.peek().span) >= base_col
1034 {
1035 if self.at(TokenKind::Let) {
1037 let let_start = self.advance().span;
1038 if let Some(name) = self.parse_ident_in("binding name") {
1039 if self.expect(TokenKind::Eq).is_some() {
1040 if let Some(value) = self.parse_expr(0) {
1041 items.push(Expr::LetExpr {
1042 span: let_start.merge(value.span()),
1043 name,
1044 value: Box::new(value),
1045 });
1046 continue;
1047 }
1048 }
1049 }
1050 break;
1051 }
1052
1053 if let Some(expr) = self.parse_expr(0) {
1054 items.push(expr);
1055 } else {
1056 self.advance();
1057 break;
1058 }
1059 }
1060
1061 if items.len() == 1 {
1062 Some(items.pop().unwrap())
1063 } else {
1064 let end = items.last().map(|e| e.span()).unwrap_or(start);
1065 Some(Expr::Block {
1066 span: start.merge(end),
1067 items,
1068 })
1069 }
1070 }
1071}
1072
1073const BP_LAMBDA: u8 = 4;
1079const BP_WHEN_GUARD: u8 = 5;
1080const BP_PROJECTION: u8 = 6;
1081const BP_WITH_WHERE: u8 = 7;
1082const BP_OR: u8 = 10;
1083const BP_AND: u8 = 20;
1084const BP_COMPARE: u8 = 30;
1085const BP_TRANSITION: u8 = 32;
1086const BP_NULL_COALESCE: u8 = 40;
1087const BP_ADD: u8 = 50;
1088const BP_MUL: u8 = 60;
1089const BP_PIPE: u8 = 65;
1090const BP_PREFIX: u8 = 70;
1091const BP_POSTFIX: u8 = 80;
1092
1093impl<'s> Parser<'s> {
1094 pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1095 let mut lhs = self.parse_prefix()?;
1096
1097 loop {
1098 if let Some((l_bp, r_bp)) = self.infix_bp() {
1099 if l_bp < min_bp {
1100 break;
1101 }
1102 lhs = self.parse_infix(lhs, r_bp)?;
1103 } else if let Some(l_bp) = self.postfix_bp() {
1104 if l_bp < min_bp {
1105 break;
1106 }
1107 lhs = self.parse_postfix(lhs)?;
1108 } else {
1109 break;
1110 }
1111 }
1112
1113 Some(lhs)
1114 }
1115
1116 fn parse_prefix(&mut self) -> Option<Expr> {
1119 match self.peek_kind() {
1120 TokenKind::Not => {
1121 let start = self.advance().span;
1122 if self.at(TokenKind::Exists) {
1123 self.advance();
1124 let operand = self.parse_expr(BP_PREFIX)?;
1125 Some(Expr::NotExists {
1126 span: start.merge(operand.span()),
1127 operand: Box::new(operand),
1128 })
1129 } else {
1130 let operand = self.parse_expr(BP_PREFIX)?;
1131 Some(Expr::Not {
1132 span: start.merge(operand.span()),
1133 operand: Box::new(operand),
1134 })
1135 }
1136 }
1137 TokenKind::Exists => {
1138 let next = self.peek_at(1).kind;
1141 if matches!(
1142 next,
1143 TokenKind::RParen
1144 | TokenKind::RBrace
1145 | TokenKind::RBracket
1146 | TokenKind::Comma
1147 | TokenKind::Eof
1148 ) {
1149 let id = self.parse_ident()?;
1150 return Some(Expr::Ident(id));
1151 }
1152 let start = self.advance().span;
1153 let operand = self.parse_expr(BP_PREFIX)?;
1154 Some(Expr::Exists {
1155 span: start.merge(operand.span()),
1156 operand: Box::new(operand),
1157 })
1158 }
1159 TokenKind::If => self.parse_if_expr(),
1160 TokenKind::For => self.parse_for_expr(),
1161 TokenKind::LBrace => self.parse_brace_expr(),
1162 TokenKind::LBracket => {
1163 let t = self.advance();
1164 self.error(t.span, "list literals `[...]` are not supported; use `Set<T>` type annotation or `{...}` set literal");
1165 None
1166 }
1167 TokenKind::LParen => self.parse_paren_expr(),
1168 TokenKind::Number => {
1169 let t = self.advance();
1170 Some(Expr::NumberLiteral {
1171 span: t.span,
1172 value: self.text(t.span).to_string(),
1173 })
1174 }
1175 TokenKind::Duration => {
1176 let t = self.advance();
1177 Some(Expr::DurationLiteral {
1178 span: t.span,
1179 value: self.text(t.span).to_string(),
1180 })
1181 }
1182 TokenKind::String => {
1183 let sl = self.parse_string()?;
1184 Some(Expr::StringLiteral(sl))
1185 }
1186 TokenKind::True => {
1187 let t = self.advance();
1188 Some(Expr::BoolLiteral {
1189 span: t.span,
1190 value: true,
1191 })
1192 }
1193 TokenKind::False => {
1194 let t = self.advance();
1195 Some(Expr::BoolLiteral {
1196 span: t.span,
1197 value: false,
1198 })
1199 }
1200 TokenKind::Null => {
1201 let t = self.advance();
1202 Some(Expr::Null { span: t.span })
1203 }
1204 TokenKind::Now => {
1205 let t = self.advance();
1206 Some(Expr::Now { span: t.span })
1207 }
1208 TokenKind::This => {
1209 let t = self.advance();
1210 Some(Expr::This { span: t.span })
1211 }
1212 TokenKind::Within => {
1213 let t = self.advance();
1214 Some(Expr::Within { span: t.span })
1215 }
1216 k if k.is_word() => {
1217 let id = self.parse_ident()?;
1218 Some(Expr::Ident(id))
1219 }
1220 TokenKind::Star => {
1221 let t = self.advance();
1223 Some(Expr::Ident(Ident {
1224 span: t.span,
1225 name: "*".into(),
1226 }))
1227 }
1228 TokenKind::Minus => {
1229 let start = self.advance().span;
1231 let operand = self.parse_expr(BP_PREFIX)?;
1232 Some(Expr::BinaryOp {
1233 span: start.merge(operand.span()),
1234 left: Box::new(Expr::NumberLiteral {
1235 span: start,
1236 value: "0".into(),
1237 }),
1238 op: BinaryOp::Sub,
1239 right: Box::new(operand),
1240 })
1241 }
1242 _ => {
1243 self.error(
1244 self.peek().span,
1245 format!(
1246 "expected expression (identifier, number, string, true/false, null, \
1247 if/for/not/exists, '(', '{{', '['), found {}",
1248 self.peek_kind(),
1249 ),
1250 );
1251 None
1252 }
1253 }
1254 }
1255
1256 fn infix_bp(&self) -> Option<(u8, u8)> {
1259 match self.peek_kind() {
1260 TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1263 TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1264 TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1265 TokenKind::And => Some((BP_AND, BP_AND + 1)),
1266 TokenKind::Eq | TokenKind::BangEq => {
1267 Some((BP_COMPARE, BP_COMPARE + 1))
1268 }
1269 TokenKind::Lt => {
1270 if self.pos > 0 {
1273 let prev = self.tokens[self.pos - 1];
1274 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1275 return None;
1276 }
1277 }
1278 Some((BP_COMPARE, BP_COMPARE + 1))
1279 }
1280 TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1281 Some((BP_COMPARE, BP_COMPARE + 1))
1282 }
1283 TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1284 TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1286 Some((BP_COMPARE, BP_COMPARE + 1))
1287 }
1288 TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1289 TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1290 TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1291 TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1292 TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1293 TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1294 TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1295 TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1296 _ => None,
1297 }
1298 }
1299
1300 fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1301 let op_tok = self.advance();
1302 match op_tok.kind {
1303 TokenKind::FatArrow => {
1304 let body = self.parse_expr(r_bp)?;
1305 Some(Expr::Lambda {
1306 span: lhs.span().merge(body.span()),
1307 param: Box::new(lhs),
1308 body: Box::new(body),
1309 })
1310 }
1311 TokenKind::Pipe => {
1312 let rhs = self.parse_expr(r_bp)?;
1313 Some(Expr::Pipe {
1314 span: lhs.span().merge(rhs.span()),
1315 left: Box::new(lhs),
1316 right: Box::new(rhs),
1317 })
1318 }
1319 TokenKind::Or => {
1320 let rhs = self.parse_expr(r_bp)?;
1321 Some(Expr::LogicalOp {
1322 span: lhs.span().merge(rhs.span()),
1323 left: Box::new(lhs),
1324 op: LogicalOp::Or,
1325 right: Box::new(rhs),
1326 })
1327 }
1328 TokenKind::And => {
1329 let rhs = self.parse_expr(r_bp)?;
1330 Some(Expr::LogicalOp {
1331 span: lhs.span().merge(rhs.span()),
1332 left: Box::new(lhs),
1333 op: LogicalOp::And,
1334 right: Box::new(rhs),
1335 })
1336 }
1337 TokenKind::Eq => {
1338 let rhs = self.parse_expr(r_bp)?;
1339 Some(Expr::Comparison {
1340 span: lhs.span().merge(rhs.span()),
1341 left: Box::new(lhs),
1342 op: ComparisonOp::Eq,
1343 right: Box::new(rhs),
1344 })
1345 }
1346 TokenKind::BangEq => {
1347 let rhs = self.parse_expr(r_bp)?;
1348 Some(Expr::Comparison {
1349 span: lhs.span().merge(rhs.span()),
1350 left: Box::new(lhs),
1351 op: ComparisonOp::NotEq,
1352 right: Box::new(rhs),
1353 })
1354 }
1355 TokenKind::Lt => {
1356 let rhs = self.parse_expr(r_bp)?;
1357 Some(Expr::Comparison {
1358 span: lhs.span().merge(rhs.span()),
1359 left: Box::new(lhs),
1360 op: ComparisonOp::Lt,
1361 right: Box::new(rhs),
1362 })
1363 }
1364 TokenKind::LtEq => {
1365 let rhs = self.parse_expr(r_bp)?;
1366 Some(Expr::Comparison {
1367 span: lhs.span().merge(rhs.span()),
1368 left: Box::new(lhs),
1369 op: ComparisonOp::LtEq,
1370 right: Box::new(rhs),
1371 })
1372 }
1373 TokenKind::Gt => {
1374 let rhs = self.parse_expr(r_bp)?;
1375 Some(Expr::Comparison {
1376 span: lhs.span().merge(rhs.span()),
1377 left: Box::new(lhs),
1378 op: ComparisonOp::Gt,
1379 right: Box::new(rhs),
1380 })
1381 }
1382 TokenKind::GtEq => {
1383 let rhs = self.parse_expr(r_bp)?;
1384 Some(Expr::Comparison {
1385 span: lhs.span().merge(rhs.span()),
1386 left: Box::new(lhs),
1387 op: ComparisonOp::GtEq,
1388 right: Box::new(rhs),
1389 })
1390 }
1391 TokenKind::In => {
1392 let rhs = self.parse_expr(r_bp)?;
1393 Some(Expr::In {
1394 span: lhs.span().merge(rhs.span()),
1395 element: Box::new(lhs),
1396 collection: Box::new(rhs),
1397 })
1398 }
1399 TokenKind::Not => {
1400 self.expect(TokenKind::In)?;
1402 let rhs = self.parse_expr(r_bp)?;
1403 Some(Expr::NotIn {
1404 span: lhs.span().merge(rhs.span()),
1405 element: Box::new(lhs),
1406 collection: Box::new(rhs),
1407 })
1408 }
1409 TokenKind::Where => {
1410 let rhs = self.parse_expr(r_bp)?;
1411 Some(Expr::Where {
1412 span: lhs.span().merge(rhs.span()),
1413 source: Box::new(lhs),
1414 condition: Box::new(rhs),
1415 })
1416 }
1417 TokenKind::With => {
1418 let rhs = self.parse_expr(r_bp)?;
1419 Some(Expr::With {
1420 span: lhs.span().merge(rhs.span()),
1421 source: Box::new(lhs),
1422 predicate: Box::new(rhs),
1423 })
1424 }
1425 TokenKind::QuestionQuestion => {
1426 let rhs = self.parse_expr(r_bp)?;
1427 Some(Expr::NullCoalesce {
1428 span: lhs.span().merge(rhs.span()),
1429 left: Box::new(lhs),
1430 right: Box::new(rhs),
1431 })
1432 }
1433 TokenKind::Plus => {
1434 let rhs = self.parse_expr(r_bp)?;
1435 Some(Expr::BinaryOp {
1436 span: lhs.span().merge(rhs.span()),
1437 left: Box::new(lhs),
1438 op: BinaryOp::Add,
1439 right: Box::new(rhs),
1440 })
1441 }
1442 TokenKind::Minus => {
1443 let rhs = self.parse_expr(r_bp)?;
1444 Some(Expr::BinaryOp {
1445 span: lhs.span().merge(rhs.span()),
1446 left: Box::new(lhs),
1447 op: BinaryOp::Sub,
1448 right: Box::new(rhs),
1449 })
1450 }
1451 TokenKind::Star => {
1452 let rhs = self.parse_expr(r_bp)?;
1453 Some(Expr::BinaryOp {
1454 span: lhs.span().merge(rhs.span()),
1455 left: Box::new(lhs),
1456 op: BinaryOp::Mul,
1457 right: Box::new(rhs),
1458 })
1459 }
1460 TokenKind::Slash => {
1461 if let Expr::Ident(ref id) = lhs {
1466 if self.peek_kind().is_word() {
1467 let next_text = self.text(self.peek().span);
1468 let is_qualified = next_text
1469 .chars()
1470 .next()
1471 .is_some_and(|c| c.is_uppercase())
1472 || matches!(
1473 self.peek_kind(),
1474 TokenKind::Config | TokenKind::Entity | TokenKind::Value
1475 );
1476 if is_qualified {
1477 let name_tok = self.advance();
1478 return Some(Expr::QualifiedName(QualifiedName {
1479 span: lhs.span().merge(name_tok.span),
1480 qualifier: Some(id.name.clone()),
1481 name: self.text(name_tok.span).to_string(),
1482 }));
1483 }
1484 }
1485 }
1486 let rhs = self.parse_expr(r_bp)?;
1487 Some(Expr::BinaryOp {
1488 span: lhs.span().merge(rhs.span()),
1489 left: Box::new(lhs),
1490 op: BinaryOp::Div,
1491 right: Box::new(rhs),
1492 })
1493 }
1494 TokenKind::ThinArrow => {
1495 let field = self.parse_ident_in("projection field")?;
1496 Some(Expr::ProjectionMap {
1497 span: lhs.span().merge(field.span),
1498 source: Box::new(lhs),
1499 field,
1500 })
1501 }
1502 TokenKind::TransitionsTo => {
1503 let rhs = self.parse_expr(r_bp)?;
1504 Some(Expr::TransitionsTo {
1505 span: lhs.span().merge(rhs.span()),
1506 subject: Box::new(lhs),
1507 new_state: Box::new(rhs),
1508 })
1509 }
1510 TokenKind::Becomes => {
1511 let rhs = self.parse_expr(r_bp)?;
1512 Some(Expr::Becomes {
1513 span: lhs.span().merge(rhs.span()),
1514 subject: Box::new(lhs),
1515 new_state: Box::new(rhs),
1516 })
1517 }
1518 TokenKind::When => {
1519 let rhs = self.parse_expr(r_bp)?;
1521 Some(Expr::WhenGuard {
1522 span: lhs.span().merge(rhs.span()),
1523 action: Box::new(lhs),
1524 condition: Box::new(rhs),
1525 })
1526 }
1527 _ => {
1528 self.error(
1529 op_tok.span,
1530 format!("unexpected infix operator {}", op_tok.kind),
1531 );
1532 None
1533 }
1534 }
1535 }
1536
1537 fn postfix_bp(&self) -> Option<u8> {
1540 match self.peek_kind() {
1541 TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
1542 TokenKind::QuestionMark => Some(BP_POSTFIX),
1543 TokenKind::Lt => {
1546 if self.pos > 0 {
1547 let prev = self.tokens[self.pos - 1];
1548 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1551 return Some(BP_POSTFIX);
1552 }
1553 }
1554 None
1555 }
1556 TokenKind::LParen => Some(BP_POSTFIX),
1557 TokenKind::LBrace => {
1558 let next = self.peek();
1564 let prev_end = if self.pos > 0 {
1565 self.tokens[self.pos - 1].span.end
1566 } else {
1567 0
1568 };
1569 if self.line_of(Span::new(prev_end, prev_end))
1571 == self.line_of(next.span)
1572 {
1573 Some(BP_POSTFIX)
1574 } else {
1575 None
1576 }
1577 }
1578 _ => None,
1579 }
1580 }
1581
1582 fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
1583 match self.peek_kind() {
1584 TokenKind::QuestionMark => {
1585 let end = self.advance().span;
1586 Some(Expr::TypeOptional {
1587 span: lhs.span().merge(end),
1588 inner: Box::new(lhs),
1589 })
1590 }
1591 TokenKind::Lt => {
1592 self.advance(); let mut args = Vec::new();
1595 while !self.at(TokenKind::Gt) && !self.at_eof() {
1597 args.push(self.parse_expr(BP_COMPARE + 1)?);
1598 self.eat(TokenKind::Comma);
1599 }
1600 let end = self.expect(TokenKind::Gt)?.span;
1601 Some(Expr::GenericType {
1602 span: lhs.span().merge(end),
1603 name: Box::new(lhs),
1604 args,
1605 })
1606 }
1607 TokenKind::Dot => {
1608 self.advance();
1609 let field = self.parse_ident_in("field name")?;
1610 Some(Expr::MemberAccess {
1611 span: lhs.span().merge(field.span),
1612 object: Box::new(lhs),
1613 field,
1614 })
1615 }
1616 TokenKind::QuestionDot => {
1617 self.advance();
1618 let field = self.parse_ident_in("field name")?;
1619 Some(Expr::OptionalAccess {
1620 span: lhs.span().merge(field.span),
1621 object: Box::new(lhs),
1622 field,
1623 })
1624 }
1625 TokenKind::LParen => {
1626 self.advance();
1627 let args = self.parse_call_args()?;
1628 let end = self.expect(TokenKind::RParen)?.span;
1629 Some(Expr::Call {
1630 span: lhs.span().merge(end),
1631 function: Box::new(lhs),
1632 args,
1633 })
1634 }
1635 TokenKind::LBrace => {
1636 self.advance();
1637 let fields = self.parse_join_fields()?;
1638 let end = self.expect(TokenKind::RBrace)?.span;
1639 Some(Expr::JoinLookup {
1640 span: lhs.span().merge(end),
1641 entity: Box::new(lhs),
1642 fields,
1643 })
1644 }
1645 _ => None,
1646 }
1647 }
1648
1649 fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
1652 let mut args = Vec::new();
1653 while !self.at(TokenKind::RParen) && !self.at_eof() {
1654 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1656 let name = self.parse_ident_in("argument name")?;
1657 self.advance(); let value = self.parse_expr(0)?;
1659 args.push(CallArg::Named(NamedArg {
1660 span: name.span.merge(value.span()),
1661 name,
1662 value,
1663 }));
1664 } else {
1665 let expr = self.parse_expr(0)?;
1666 args.push(CallArg::Positional(expr));
1667 }
1668 self.eat(TokenKind::Comma);
1669 }
1670 Some(args)
1671 }
1672
1673 fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
1676 let mut fields = Vec::new();
1677 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1678 let field = self.parse_ident_in("join field name")?;
1679 let value = if self.eat(TokenKind::Colon).is_some() {
1680 Some(self.parse_expr(0)?)
1681 } else {
1682 None
1683 };
1684 fields.push(JoinField {
1685 span: field.span.merge(
1686 value
1687 .as_ref()
1688 .map(|v| v.span())
1689 .unwrap_or(field.span),
1690 ),
1691 field,
1692 value,
1693 });
1694 self.eat(TokenKind::Comma);
1695 }
1696 Some(fields)
1697 }
1698
1699 fn parse_if_expr(&mut self) -> Option<Expr> {
1702 let start = self.advance().span; let mut branches = Vec::new();
1704
1705 let condition = self.parse_expr(0)?;
1707 self.expect(TokenKind::Colon)?;
1708 let body = self.parse_branch_body(start)?;
1709 branches.push(CondBranch {
1710 span: start.merge(body.span()),
1711 condition,
1712 body,
1713 });
1714
1715 let mut else_body = None;
1717 while self.at(TokenKind::Else) {
1718 let else_tok = self.advance();
1719 if self.at(TokenKind::If) {
1720 let if_start = self.advance().span;
1721 let cond = self.parse_expr(0)?;
1722 self.expect(TokenKind::Colon)?;
1723 let body = self.parse_branch_body(else_tok.span)?;
1724 branches.push(CondBranch {
1725 span: if_start.merge(body.span()),
1726 condition: cond,
1727 body,
1728 });
1729 } else {
1730 self.expect(TokenKind::Colon)?;
1731 let body = self.parse_branch_body(else_tok.span)?;
1732 else_body = Some(Box::new(body));
1733 break;
1734 }
1735 }
1736
1737 let end = else_body
1738 .as_ref()
1739 .map(|b| b.span())
1740 .or_else(|| branches.last().map(|b| b.body.span()))
1741 .unwrap_or(start);
1742
1743 Some(Expr::Conditional {
1744 span: start.merge(end),
1745 branches,
1746 else_body,
1747 })
1748 }
1749
1750 fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
1751 let keyword_line = self.line_of(keyword_span);
1752 let next_line = self.line_of(self.peek().span);
1753
1754 if next_line > keyword_line {
1755 let base_col = self.col_of(self.peek().span);
1756 self.parse_indented_block(base_col)
1757 } else {
1758 self.parse_expr(0)
1759 }
1760 }
1761
1762 fn parse_for_expr(&mut self) -> Option<Expr> {
1765 let start = self.advance().span; let binding = self.parse_for_binding()?;
1767 self.expect(TokenKind::In)?;
1768
1769 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1771
1772 let filter = if self.eat(TokenKind::Where).is_some() {
1773 Some(Box::new(self.parse_expr(0)?))
1775 } else {
1776 None
1777 };
1778
1779 self.expect(TokenKind::Colon)?;
1780 let body = self.parse_branch_body(start)?;
1781
1782 Some(Expr::For {
1783 span: start.merge(body.span()),
1784 binding,
1785 collection: Box::new(collection),
1786 filter,
1787 body: Box::new(body),
1788 })
1789 }
1790
1791 fn parse_brace_expr(&mut self) -> Option<Expr> {
1794 let start = self.advance().span; if self.at(TokenKind::RBrace) {
1797 let end = self.advance().span;
1798 return Some(Expr::SetLiteral {
1799 span: start.merge(end),
1800 elements: Vec::new(),
1801 });
1802 }
1803
1804 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1806 return self.parse_object_literal(start);
1807 }
1808
1809 self.parse_set_literal(start)
1811 }
1812
1813
1814 fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
1815 let mut fields = Vec::new();
1816 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1817 let name = self.parse_ident_in("field name")?;
1818 self.expect(TokenKind::Colon)?;
1819 let value = self.parse_expr(0)?;
1820 fields.push(NamedArg {
1821 span: name.span.merge(value.span()),
1822 name,
1823 value,
1824 });
1825 self.eat(TokenKind::Comma);
1826 }
1827 let end = self.expect(TokenKind::RBrace)?.span;
1828 Some(Expr::ObjectLiteral {
1829 span: start.merge(end),
1830 fields,
1831 })
1832 }
1833
1834 fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
1835 let mut elements = Vec::new();
1836 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1837 elements.push(self.parse_expr(0)?);
1838 self.eat(TokenKind::Comma);
1839 }
1840 let end = self.expect(TokenKind::RBrace)?.span;
1841 Some(Expr::SetLiteral {
1842 span: start.merge(end),
1843 elements,
1844 })
1845 }
1846
1847 fn parse_paren_expr(&mut self) -> Option<Expr> {
1850 self.advance(); let expr = self.parse_expr(0)?;
1852 self.expect(TokenKind::RParen)?;
1853 Some(expr)
1854 }
1855}
1856
1857#[cfg(test)]
1862mod tests {
1863 use super::*;
1864 use crate::diagnostic::Severity;
1865
1866 fn parse_ok(src: &str) -> ParseResult {
1867 let owned;
1870 let input = if src.starts_with("-- allium:") {
1871 src
1872 } else {
1873 owned = format!("-- allium: 1\n{src}");
1874 &owned
1875 };
1876 let result = parse(input);
1877 if !result.diagnostics.is_empty() {
1878 for d in &result.diagnostics {
1879 eprintln!(
1880 " [{:?}] {} ({}..{})",
1881 d.severity, d.message, d.span.start, d.span.end
1882 );
1883 }
1884 }
1885 result
1886 }
1887
1888 #[test]
1889 fn version_marker() {
1890 let r = parse_ok("-- allium: 1\n");
1891 assert_eq!(r.module.version, Some(1));
1892 assert_eq!(r.diagnostics.len(), 0);
1893 }
1894
1895 #[test]
1896 fn version_missing_warns() {
1897 let r = parse("entity User {}");
1898 assert_eq!(r.module.version, None);
1899 assert_eq!(r.diagnostics.len(), 1);
1900 assert_eq!(r.diagnostics[0].severity, Severity::Warning);
1901 assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
1902 }
1903
1904 #[test]
1905 fn version_unsupported_errors() {
1906 let r = parse("-- allium: 99\nentity User {}");
1907 assert_eq!(r.module.version, Some(99));
1908 assert!(r.diagnostics.iter().any(|d|
1909 d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
1910 ), "expected unsupported version error, got: {:?}", r.diagnostics);
1911 }
1912
1913 #[test]
1914 fn empty_entity() {
1915 let r = parse_ok("entity User {}");
1916 assert_eq!(r.diagnostics.len(), 0);
1917 assert_eq!(r.module.declarations.len(), 1);
1918 match &r.module.declarations[0] {
1919 Decl::Block(b) => {
1920 assert_eq!(b.kind, BlockKind::Entity);
1921 assert_eq!(b.name.as_ref().unwrap().name, "User");
1922 }
1923 other => panic!("expected Block, got {other:?}"),
1924 }
1925 }
1926
1927 #[test]
1928 fn entity_with_fields() {
1929 let src = r#"entity Order {
1930 customer: Customer
1931 status: pending | active | completed
1932 total: Decimal
1933}"#;
1934 let r = parse_ok(src);
1935 assert_eq!(r.diagnostics.len(), 0);
1936 match &r.module.declarations[0] {
1937 Decl::Block(b) => {
1938 assert_eq!(b.items.len(), 3);
1939 }
1940 other => panic!("expected Block, got {other:?}"),
1941 }
1942 }
1943
1944 #[test]
1945 fn use_declaration() {
1946 let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
1947 assert_eq!(r.diagnostics.len(), 0);
1948 match &r.module.declarations[0] {
1949 Decl::Use(u) => {
1950 assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
1951 }
1952 other => panic!("expected Use, got {other:?}"),
1953 }
1954 }
1955
1956 #[test]
1957 fn enum_declaration() {
1958 let src = "enum OrderStatus { pending | shipped | delivered }";
1959 let r = parse_ok(src);
1960 assert_eq!(r.diagnostics.len(), 0);
1961 }
1962
1963 #[test]
1964 fn config_block() {
1965 let src = r#"config {
1966 max_retries: Integer = 3
1967 timeout: Duration = 24.hours
1968}"#;
1969 let r = parse_ok(src);
1974 assert_eq!(r.diagnostics.len(), 0);
1975 }
1976
1977 #[test]
1978 fn rule_declaration() {
1979 let src = r#"rule PlaceOrder {
1980 when: CustomerPlacesOrder(customer, items, total)
1981 requires: total > 0
1982 ensures: Order.created(customer: customer, status: pending, total: total)
1983}"#;
1984 let r = parse_ok(src);
1985 assert_eq!(r.diagnostics.len(), 0);
1986 match &r.module.declarations[0] {
1987 Decl::Block(b) => {
1988 assert_eq!(b.kind, BlockKind::Rule);
1989 assert_eq!(b.items.len(), 3);
1990 }
1991 other => panic!("expected Block, got {other:?}"),
1992 }
1993 }
1994
1995 #[test]
1996 fn expression_precedence() {
1997 let r = parse_ok("rule T { v: a + b * c }");
1998 match &r.module.declarations[0] {
2000 Decl::Block(b) => match &b.items[0].kind {
2001 BlockItemKind::Assignment { value, .. } => match value {
2002 Expr::BinaryOp { op, right, .. } => {
2003 assert_eq!(*op, BinaryOp::Add);
2004 assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2005 }
2006 other => panic!("expected BinaryOp, got {other:?}"),
2007 },
2008 other => panic!("expected Assignment, got {other:?}"),
2009 },
2010 other => panic!("expected Block, got {other:?}"),
2011 }
2012 }
2013
2014 #[test]
2015 fn default_declaration() {
2016 let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2017 let r = parse_ok(src);
2018 assert_eq!(r.diagnostics.len(), 0);
2019 }
2020
2021 #[test]
2022 fn open_question() {
2023 let src = r#"open question "Should admins be role-specific?""#;
2024 let r = parse_ok(src);
2025 assert_eq!(r.diagnostics.len(), 0);
2026 }
2027
2028 #[test]
2029 fn external_entity() {
2030 let src = "external entity Customer { email: String }";
2031 let r = parse_ok(src);
2032 assert_eq!(r.diagnostics.len(), 0);
2033 match &r.module.declarations[0] {
2034 Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2035 other => panic!("expected Block, got {other:?}"),
2036 }
2037 }
2038
2039 #[test]
2040 fn where_expression() {
2041 let src = "entity E { active: items where status = active }";
2042 let r = parse_ok(src);
2043 assert_eq!(r.diagnostics.len(), 0);
2044 }
2045
2046 #[test]
2047 fn with_expression() {
2048 let src = "entity E { slots: InterviewSlot with candidacy = this }";
2049 let r = parse_ok(src);
2050 assert_eq!(r.diagnostics.len(), 0);
2051 }
2052
2053 #[test]
2054 fn lambda_expression() {
2055 let src = "entity E { v: items.any(i => i.active) }";
2056 let r = parse_ok(src);
2057 assert_eq!(r.diagnostics.len(), 0);
2058 }
2059
2060 #[test]
2061 fn deferred() {
2062 let src = "deferred InterviewerMatching.suggest";
2063 let r = parse_ok(src);
2064 assert_eq!(r.diagnostics.len(), 0);
2065 }
2066
2067 #[test]
2068 fn variant_declaration() {
2069 let src = "variant Email : Notification { subject: String }";
2070 let r = parse_ok(src);
2071 assert_eq!(r.diagnostics.len(), 0);
2072 }
2073
2074 #[test]
2077 fn projection_arrow() {
2078 let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2079 let r = parse_ok(src);
2080 assert_eq!(r.diagnostics.len(), 0);
2081 }
2082
2083 #[test]
2086 fn transitions_to_trigger() {
2087 let src = "rule R { when: Interview.status transitions_to scheduled\n ensures: Notification.created() }";
2088 let r = parse_ok(src);
2089 assert_eq!(r.diagnostics.len(), 0);
2090 }
2091
2092 #[test]
2093 fn becomes_trigger() {
2094 let src = "rule R { when: Interview.status becomes scheduled\n ensures: Notification.created() }";
2095 let r = parse_ok(src);
2096 assert_eq!(r.diagnostics.len(), 0);
2097 }
2098
2099 #[test]
2102 fn when_binding() {
2103 let src = "rule R {\n when: interview: Interview.status transitions_to scheduled\n ensures: Notification.created()\n}";
2104 let r = parse_ok(src);
2105 assert_eq!(r.diagnostics.len(), 0);
2106 let decl = &r.module.declarations[0];
2108 if let Decl::Block(b) = decl {
2109 if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2110 assert_eq!(keyword, "when");
2111 assert!(matches!(value, Expr::Binding { .. }));
2112 } else {
2113 panic!("expected clause");
2114 }
2115 } else {
2116 panic!("expected block decl");
2117 }
2118 }
2119
2120 #[test]
2121 fn when_binding_temporal() {
2122 let src = "rule R {\n when: invitation: Invitation.expires_at <= now\n ensures: Invitation.expired()\n}";
2123 let r = parse_ok(src);
2124 assert_eq!(r.diagnostics.len(), 0);
2125 }
2126
2127 #[test]
2128 fn when_binding_created() {
2129 let src = "rule R {\n when: batch: DigestBatch.created\n ensures: Email.created()\n}";
2130 let r = parse_ok(src);
2131 assert_eq!(r.diagnostics.len(), 0);
2132 }
2133
2134 #[test]
2135 fn facing_binding() {
2136 let src = "surface S {\n facing viewer: Interviewer\n exposes: InterviewList\n}";
2137 let r = parse_ok(src);
2138 assert_eq!(r.diagnostics.len(), 0);
2139 }
2140
2141 #[test]
2142 fn context_binding() {
2143 let src = "surface S {\n facing viewer: Interviewer\n context assignment: SlotConfirmation where interviewer = viewer\n}";
2144 let r = parse_ok(src);
2145 assert_eq!(r.diagnostics.len(), 0);
2146 }
2147
2148 #[test]
2151 fn rule_level_for() {
2152 let src = r#"rule ProcessDigests {
2153 when: schedule: DigestSchedule.next_run_at <= now
2154 for user in Users where notification_setting.digest_enabled:
2155 ensures: DigestBatch.created(user: user)
2156}"#;
2157 let r = parse_ok(src);
2158 assert_eq!(r.diagnostics.len(), 0);
2159 if let Decl::Block(b) = &r.module.declarations[0] {
2160 assert!(b.items.len() >= 2);
2162 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2163 } else {
2164 panic!("expected block decl");
2165 }
2166 }
2167
2168 #[test]
2171 fn let_in_ensures_block() {
2172 let src = r#"rule R {
2173 when: ScheduleInterview(candidacy, time, interviewers)
2174 ensures:
2175 let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2176 for interviewer in interviewers:
2177 SlotConfirmation.created(slot: slot, interviewer: interviewer)
2178}"#;
2179 let r = parse_ok(src);
2180 assert_eq!(r.diagnostics.len(), 0);
2181 }
2182
2183 #[test]
2186 fn provides_when_guard() {
2187 let src = "surface S {\n facing viewer: Interviewer\n provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2188 let r = parse_ok(src);
2189 assert_eq!(r.diagnostics.len(), 0);
2190 }
2191
2192 #[test]
2195 fn optional_type_suffix() {
2196 let src = "entity E { locked_until: Timestamp? }";
2197 let r = parse_ok(src);
2198 assert_eq!(r.diagnostics.len(), 0);
2199 }
2200
2201 #[test]
2202 fn optional_trigger_param() {
2203 let src = "rule R { when: Report(interviewer, interview, reason, details?)\n ensures: Done() }";
2204 let r = parse_ok(src);
2205 assert_eq!(r.diagnostics.len(), 0);
2206 }
2207
2208 #[test]
2211 fn qualified_config_access() {
2212 let src = "entity E { duration: oauth/config.session_duration }";
2213 let r = parse_ok(src);
2214 assert_eq!(r.diagnostics.len(), 0);
2215 }
2216
2217 #[test]
2220 fn realistic_spec() {
2221 let src = r#"-- allium: 1
2222
2223enum OrderStatus { pending | shipped | delivered }
2224
2225external entity Customer {
2226 email: String
2227 name: String
2228}
2229
2230entity Order {
2231 customer: Customer
2232 status: OrderStatus
2233 total: Decimal
2234 items: OrderItem with order = this
2235 shipped_items: items where status = shipped
2236 confirmed_items: items where status = confirmed -> item
2237 is_complete: status = delivered
2238 locked_until: Timestamp?
2239}
2240
2241config {
2242 max_retries: Integer = 3
2243 timeout: Duration = 24.hours
2244}
2245
2246rule PlaceOrder {
2247 when: CustomerPlacesOrder(customer, items, total)
2248 requires: total > 0
2249 ensures: Order.created(customer: customer, status: pending, total: total)
2250}
2251
2252rule ShipOrder {
2253 when: order: Order.status transitions_to shipped
2254 ensures: Email.created(to: order.customer.email, template: order_shipped)
2255}
2256
2257open question "How do we handle partial shipments?"
2258"#;
2259 let r = parse_ok(src);
2260 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2261 assert_eq!(r.module.version, Some(1));
2262 assert_eq!(r.module.declarations.len(), 7);
2263 }
2264
2265 #[test]
2266 fn extension_behaviour_excerpt() {
2267 let src = r#"value Document {
2270 uri: String
2271 text: String
2272}
2273
2274entity Finding {
2275 code: String
2276 severity: error | warning | info
2277 range: FindingRange
2278}
2279
2280entity DiagnosticsMode {
2281 value: strict | relaxed
2282}
2283
2284config {
2285 duplicateKey: String = "allium.config.duplicateKey"
2286}
2287
2288rule RefreshDiagnostics {
2289 when: DocumentOpened(document) or DocumentChanged(document)
2290 requires: document.language_id = "allium"
2291 ensures: FindingsComputed(document)
2292}
2293
2294surface DiagnosticsDashboard {
2295 facing viewer: Developer
2296 context doc: Document where viewer.active_document = doc
2297 provides: RunChecks(viewer) when doc.language_id = "allium"
2298 exposes: FindingList
2299}
2300
2301rule ProcessDigests {
2302 when: schedule: DigestSchedule.next_run_at <= now
2303 for user in Users where notification_setting.digest_enabled:
2304 let settings = user.notification_setting
2305 ensures: DigestBatch.created(user: user)
2306}
2307"#;
2308 let r = parse_ok(src);
2309 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2310 assert_eq!(r.module.declarations.len(), 7);
2312 }
2313
2314 #[test]
2315 fn exists_as_identifier() {
2316 let src = r#"rule R {
2317 when: X()
2318 ensures: CompletionItemAvailable(label: exists)
2319}"#;
2320 let r = parse_ok(src);
2321 assert_eq!(r.diagnostics.len(), 0);
2322 }
2323
2324 #[test]
2327 fn pipe_binds_tighter_than_or() {
2328 let src = "entity E { v: a or b | c }";
2330 let r = parse_ok(src);
2331 assert_eq!(r.diagnostics.len(), 0);
2332 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2333 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2334 let Expr::LogicalOp { op, right, .. } = value else {
2336 panic!("expected LogicalOp, got {value:?}");
2337 };
2338 assert_eq!(*op, LogicalOp::Or);
2339 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2341 }
2342
2343 #[test]
2346 fn variant_with_pipe_base() {
2347 let src = "variant Mixed : TypeA | TypeB";
2348 let r = parse_ok(src);
2349 assert_eq!(r.diagnostics.len(), 0);
2350 let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2351 assert!(matches!(v.base, Expr::Pipe { .. }));
2352 }
2353
2354 #[test]
2357 fn for_block_where_comparison() {
2358 let src = r#"rule R {
2359 when: X()
2360 for item in Items where item.status = active:
2361 ensures: Processed(item: item)
2362}"#;
2363 let r = parse_ok(src);
2364 assert_eq!(r.diagnostics.len(), 0);
2365 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2366 let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2367 assert!(filter.is_some());
2368 assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2369 }
2370
2371 #[test]
2374 fn for_expr_where_comparison() {
2375 let src = r#"rule R {
2376 when: X()
2377 ensures:
2378 for item in Items where item.active = true:
2379 Processed(item: item)
2380}"#;
2381 let r = parse_ok(src);
2382 assert_eq!(r.diagnostics.len(), 0);
2383 }
2384
2385 #[test]
2388 fn if_else_if_else() {
2389 let src = r#"rule R {
2390 when: X(v)
2391 ensures:
2392 if v < 10: Small()
2393 else if v < 100: Medium()
2394 else: Large()
2395}"#;
2396 let r = parse_ok(src);
2397 assert_eq!(r.diagnostics.len(), 0);
2398 }
2399
2400 #[test]
2403 fn null_coalesce_and_optional_chain() {
2404 let src = "entity E { v: a?.b ?? fallback }";
2405 let r = parse_ok(src);
2406 assert_eq!(r.diagnostics.len(), 0);
2407 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2408 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2409 assert!(matches!(value, Expr::NullCoalesce { .. }));
2411 }
2412
2413 #[test]
2416 fn generic_type_nested() {
2417 let src = "entity E { v: List<Set<String>> }";
2418 let r = parse_ok(src);
2419 assert_eq!(r.diagnostics.len(), 0);
2420 }
2421
2422 #[test]
2425 fn collection_literals() {
2426 let src = r#"rule R {
2427 when: X()
2428 ensures:
2429 let s = {a, b, c}
2430 let o = {name: "test", count: 42}
2431 Done()
2432}"#;
2433 let r = parse_ok(src);
2434 assert_eq!(r.diagnostics.len(), 0);
2435 }
2436
2437 #[test]
2438 fn spec_reject_list_literal() {
2439 let src = r#"rule R {
2441 when: X()
2442 ensures:
2443 let l = [1, 2, 3]
2444 Done()
2445}"#;
2446 let r = parse_ok(src);
2447 assert!(
2448 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2449 "expected error for `[...]` list literal (not in spec), but parsed without errors"
2450 );
2451 }
2452
2453 #[test]
2456 fn given_block() {
2457 let src = "given { viewer: User\n time: Timestamp }";
2458 let r = parse_ok(src);
2459 assert_eq!(r.diagnostics.len(), 0);
2460 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2461 assert_eq!(b.kind, BlockKind::Given);
2462 assert!(b.name.is_none());
2463 }
2464
2465 #[test]
2468 fn actor_block() {
2469 let src = "actor Admin { identified_by: User where role = admin }";
2470 let r = parse_ok(src);
2471 assert_eq!(r.diagnostics.len(), 0);
2472 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2473 assert_eq!(b.kind, BlockKind::Actor);
2474 }
2475
2476 #[test]
2479 fn join_lookup() {
2480 let src = "entity E { match: Other{field_a, field_b: value} }";
2481 let r = parse_ok(src);
2482 assert_eq!(r.diagnostics.len(), 0);
2483 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2484 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2485 assert!(matches!(value, Expr::JoinLookup { .. }));
2486 }
2487
2488 #[test]
2491 fn in_not_in_set() {
2492 let src = r#"rule R {
2493 when: X(s)
2494 requires: s in {a, b, c}
2495 requires: s not in {d, e}
2496 ensures: Done()
2497}"#;
2498 let r = parse_ok(src);
2499 assert_eq!(r.diagnostics.len(), 0);
2500 }
2501
2502 #[test]
2505 fn comprehensive_fixture() {
2506 let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
2507 let r = parse(src);
2508 assert_eq!(
2509 r.diagnostics.len(),
2510 0,
2511 "expected no errors in comprehensive fixture, got: {:?}",
2512 r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
2513 );
2514 assert!(r.module.declarations.len() > 30, "expected many declarations");
2515 }
2516
2517 #[test]
2520 fn error_expected_declaration() {
2521 let r = parse("-- allium: 1\n+ invalid");
2522 assert!(r.diagnostics.len() >= 1);
2523 let msg = &r.diagnostics[0].message;
2524 assert!(msg.contains("expected declaration"), "got: {msg}");
2525 assert!(msg.contains("entity"), "should list valid options, got: {msg}");
2526 assert!(msg.contains("rule"), "should list valid options, got: {msg}");
2527 }
2528
2529 #[test]
2530 fn error_expected_expression() {
2531 let r = parse("-- allium: 1\nentity E { v: }");
2532 assert!(r.diagnostics.len() >= 1);
2533 let msg = &r.diagnostics[0].message;
2534 assert!(msg.contains("expected expression"), "got: {msg}");
2535 assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
2536 }
2537
2538 #[test]
2539 fn error_expected_block_item() {
2540 let r = parse("-- allium: 1\nentity E { + }");
2541 assert!(r.diagnostics.len() >= 1);
2542 let msg = &r.diagnostics[0].message;
2543 assert!(msg.contains("expected block item"), "got: {msg}");
2544 }
2545
2546 #[test]
2547 fn error_expected_identifier() {
2548 let r = parse("-- allium: 1\nentity 123 {}");
2549 assert!(r.diagnostics.len() >= 1);
2550 let msg = &r.diagnostics[0].message;
2551 assert!(msg.contains("expected entity name"), "got: {msg}");
2553 assert!(msg.contains("number"), "should say what was found, got: {msg}");
2555 }
2556
2557 #[test]
2558 fn error_missing_brace() {
2559 let r = parse("entity E {");
2560 assert!(r.diagnostics.len() >= 1);
2561 let msg = &r.diagnostics[0].message;
2562 assert!(msg.contains("expected"), "got: {msg}");
2563 }
2564
2565 #[test]
2566 fn error_recovery_multiple() {
2567 let r = parse("entity E { + }\nentity F { - }");
2569 assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
2570 }
2571
2572 #[test]
2573 fn error_dedup_same_line() {
2574 let r = parse("-- allium: 1\n+ - * /");
2576 let errors: Vec<_> = r.diagnostics.iter()
2577 .filter(|d| d.severity == crate::diagnostic::Severity::Error)
2578 .collect();
2579 assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
2580 }
2581
2582 #[test]
2583 fn for_block() {
2584 let src = r#"rule R {
2585 when: X()
2586 for user in Users where user.active:
2587 ensures: Notified(user: user)
2588}"#;
2589 let r = parse_ok(src);
2590 assert_eq!(r.diagnostics.len(), 0);
2591 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2592 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2593 }
2594
2595 #[test]
2596 fn for_expr() {
2597 let src = r#"rule R {
2598 when: X(project)
2599 ensures:
2600 let total = for task in project.tasks: task.effort
2601 Done(total: total)
2602}"#;
2603 let r = parse_ok(src);
2604 assert_eq!(r.diagnostics.len(), 0);
2605 }
2606
2607 #[test]
2608 fn for_where() {
2609 let src = r#"rule R {
2610 when: X()
2611 for item in Items where item.active:
2612 ensures: Processed(item: item)
2613}"#;
2614 let r = parse_ok(src);
2615 assert_eq!(r.diagnostics.len(), 0);
2616 }
2617
2618 #[test]
2619 fn spec_reject_for_with_filter() {
2620 let src = r#"rule R {
2623 when: X()
2624 for slot in Slot with slot.role = reviewer:
2625 ensures: Reviewed(slot: slot)
2626}"#;
2627 let r = parse_ok(src);
2628 assert!(
2629 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2630 "expected error for `for ... with` (spec uses `where`), but parsed without errors"
2631 );
2632 }
2633
2634 #[test]
2635 fn block_level_if() {
2636 let src = r#"rule R {
2637 when: X(task)
2638 if task.priority = high:
2639 ensures: Escalated(task: task)
2640}"#;
2641 let r = parse_ok(src);
2642 assert_eq!(r.diagnostics.len(), 0);
2643 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2644 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2645 panic!("expected IfBlock, got {:?}", b.items[1].kind);
2646 };
2647 assert_eq!(branches.len(), 1);
2648 assert!(else_items.is_none());
2649 }
2650
2651 #[test]
2652 fn block_level_if_else() {
2653 let src = r#"rule R {
2654 when: X(score)
2655 if score > 80:
2656 ensures: High()
2657 else if score > 40:
2658 ensures: Medium()
2659 else:
2660 ensures: Low()
2661}"#;
2662 let r = parse_ok(src);
2663 assert_eq!(r.diagnostics.len(), 0);
2664 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2665 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2666 panic!("expected IfBlock, got {:?}", b.items[1].kind);
2667 };
2668 assert_eq!(branches.len(), 2);
2669 assert!(else_items.is_some());
2670 }
2671
2672 #[test]
2673 fn wildcard_type_parameter() {
2674 let src = "entity E { codec: Codec<*> }";
2675 let r = parse_ok(src);
2676 assert_eq!(r.diagnostics.len(), 0);
2677 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2678 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2679 if let Expr::GenericType { args, .. } = value {
2680 assert_eq!(args.len(), 1);
2681 if let Expr::Ident(id) = &args[0] {
2682 assert_eq!(id.name, "*");
2683 } else {
2684 panic!("expected wildcard ident, got {:?}", args[0]);
2685 }
2686 } else {
2687 panic!("expected GenericType, got {:?}", value);
2688 }
2689 }
2690
2691 #[test]
2692 fn guidance_clause_comment_only_value() {
2693 let src = r#"rule R {
2694 guidance: -- just a comment
2695 ensures: Done()
2696}"#;
2697 let r = parse_ok(src);
2698 assert_eq!(r.diagnostics.len(), 0);
2699 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2700 assert_eq!(b.items.len(), 2);
2701 let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
2703 assert_eq!(keyword, "guidance");
2704 assert!(matches!(value, Expr::Block { items, .. } if items.is_empty()));
2705 }
2706
2707 #[test]
2708 fn spec_reject_for_expr_with_filter() {
2709 let src = r#"rule R {
2711 when: X(project)
2712 ensures:
2713 let total = for task in project.tasks with task.active: task.effort
2714 Done(total: total)
2715}"#;
2716 let r = parse_ok(src);
2717 assert!(
2718 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2719 "expected error for `for ... with` in expression (spec uses `where`), but parsed without errors"
2720 );
2721 }
2722
2723 #[test]
2724 fn for_destructured_binding() {
2725 let src = r#"rule R {
2726 when: X()
2727 for (key, value) in Pairs where key != null:
2728 ensures: Processed(key: key, value: value)
2729}"#;
2730 let r = parse_ok(src);
2731 assert_eq!(r.diagnostics.len(), 0);
2732 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2733 let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
2734 assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
2735 }
2736
2737 #[test]
2738 fn dot_path_assignment() {
2739 let src = r#"entity Shard {
2740 ShardGroup.shard_cache: Shard with group = this
2741}"#;
2742 let r = parse_ok(src);
2743 assert_eq!(r.diagnostics.len(), 0);
2744 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2745 let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
2746 panic!("expected PathAssignment, got {:?}", b.items[0].kind);
2747 };
2748 assert!(matches!(path, Expr::MemberAccess { .. }));
2749 }
2750
2751 #[test]
2752 fn language_reference_fixture() {
2753 let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
2754 let r = parse(src);
2755 let errors: Vec<_> = r.diagnostics.iter()
2756 .filter(|d| d.severity == Severity::Error)
2757 .collect();
2758 assert_eq!(
2759 errors.len(),
2760 0,
2761 "expected no errors in language-reference fixture, got: {:?}",
2762 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
2763 );
2764 }
2765
2766 #[test]
2783 fn spec_for_bare_form() {
2784 let src = r#"rule ProcessDigests {
2786 when: schedule: DigestSchedule.next_run_at <= now
2787 for user in Users where notification_setting.digest_enabled:
2788 let settings = user.notification_setting
2789 ensures: DigestBatch.created(user: user)
2790}"#;
2791 let r = parse_ok(src);
2792 assert_eq!(r.diagnostics.len(), 0);
2793 }
2794
2795 #[test]
2796 fn spec_reject_for_each() {
2797 let src = r#"rule R {
2799 when: X()
2800 for each user in Users where user.active:
2801 ensures: Notified(user: user)
2802}"#;
2803 let r = parse_ok(src);
2804 assert!(
2805 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2806 "expected error for `for each` (not in spec), but parsed without errors"
2807 );
2808 }
2809
2810 #[test]
2813 fn spec_reject_double_equals() {
2814 let src = "rule R { when: X(a)\n requires: a.status == active\n ensures: Done() }";
2816 let r = parse_ok(src);
2817 assert!(
2818 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2819 "expected error for `==` (not in spec), but parsed without errors"
2820 );
2821 }
2822
2823 #[test]
2826 fn spec_reject_system_block() {
2827 let src = "system PaymentGateway {\n timeout: 30.seconds\n}";
2829 let r = parse_ok(src);
2830 assert!(
2831 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2832 "expected error for `system` block (not in spec), but parsed without errors"
2833 );
2834 }
2835
2836 #[test]
2839 fn spec_reject_tags_clause() {
2840 let src = r#"rule R {
2842 when: MigrationTriggered()
2843 tags: infrastructure, migration
2844 ensures: MigrationComplete()
2845}"#;
2846 let r = parse_ok(src);
2847 assert!(
2848 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2849 "expected error for `tags:` clause (not in spec), but parsed without errors"
2850 );
2851 }
2852
2853 #[test]
2856 fn spec_reject_includes_operator() {
2857 let src = r#"rule R {
2859 when: X(a, b)
2860 requires: a.items includes b
2861 ensures: Done()
2862}"#;
2863 let r = parse_ok(src);
2864 assert!(
2865 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2866 "expected error for `includes` operator (not in spec), but parsed without errors"
2867 );
2868 }
2869
2870 #[test]
2871 fn spec_reject_excludes_operator() {
2872 let src = r#"rule R {
2874 when: X(a, b)
2875 requires: a.items excludes b
2876 ensures: Done()
2877}"#;
2878 let r = parse_ok(src);
2879 assert!(
2880 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2881 "expected error for `excludes` operator (not in spec), but parsed without errors"
2882 );
2883 }
2884
2885 #[test]
2888 fn spec_reject_range_literal() {
2889 let src = r#"rule R {
2891 when: X(v)
2892 requires: v in [1..100]
2893 ensures: Done()
2894}"#;
2895 let r = parse_ok(src);
2896 assert!(
2897 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2898 "expected error for `..` range (not in spec), but parsed without errors"
2899 );
2900 }
2901
2902 #[test]
2905 fn spec_within_in_actor() {
2906 let src = r#"actor WorkspaceAdmin {
2908 within: Workspace
2909 identified_by: User where role = admin
2910}"#;
2911 let r = parse_ok(src);
2912 assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
2913 }
2914
2915 #[test]
2918 fn spec_reject_module_declaration() {
2919 let src = "module my_spec";
2921 let r = parse_ok(src);
2922 assert!(
2923 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2924 "expected error for `module` declaration (not in spec), but parsed without errors"
2925 );
2926 }
2927
2928 #[test]
2931 fn spec_reject_module_level_guidance() {
2932 let src = r#"guidance: "All rules must be idempotent""#;
2934 let r = parse_ok(src);
2935 assert!(
2936 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2937 "expected error for module-level `guidance:` (not in spec), but parsed without errors"
2938 );
2939 }
2940
2941 #[test]
2944 fn spec_guarantee_in_surface() {
2945 let src = r#"surface S {
2947 facing viewer: User
2948 guarantee: DataIntegrity
2949}"#;
2950 let r = parse_ok(src);
2951 assert_eq!(r.diagnostics.len(), 0, "guarantee: in surface should parse cleanly");
2952 }
2953
2954 #[test]
2955 fn spec_timeout_in_surface() {
2956 let src = r#"surface InvitationView {
2958 facing recipient: Candidate
2959 context invitation: ResourceInvitation where email = recipient.email
2960 timeout: InvitationExpires
2961}"#;
2962 let r = parse_ok(src);
2963 assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
2964 }
2965
2966 #[test]
2967 fn spec_timeout_in_surface_with_when() {
2968 let src = r#"surface InvitationView {
2970 facing recipient: Candidate
2971 context invitation: ResourceInvitation where email = recipient.email
2972 timeout: InvitationExpires when invitation.expires_at <= now
2973}"#;
2974 let r = parse_ok(src);
2975 assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
2976 }
2977
2978 #[test]
2981 fn spec_reject_suffix_predicate() {
2982 let src = r#"rule R {
2984 when: X()
2985 requires: finding.code starts_with "allium."
2986 ensures: Done()
2987}"#;
2988 let r = parse_ok(src);
2989 assert!(
2990 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2991 "expected error for suffix predicate (not in spec), but parsed without errors"
2992 );
2993 }
2994
2995 #[test]
2998 fn spec_add_remove_in_ensures() {
2999 let src = r#"rule R {
3002 when: AssignInterviewer(interview, new_interviewer)
3003 ensures:
3004 interview.interviewers.add(new_interviewer)
3005}"#;
3006 let r = parse_ok(src);
3007 assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
3008 }
3009
3010 #[test]
3011 fn spec_remove_in_ensures() {
3012 let src = r#"rule R {
3013 when: RemoveInterviewer(interview, leaving)
3014 ensures:
3015 interview.interviewers.remove(leaving)
3016}"#;
3017 let r = parse_ok(src);
3018 assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3019 }
3020
3021 #[test]
3024 fn spec_first_last_access() {
3025 let src = "entity E { latest: attempts.last\n earliest: attempts.first }";
3027 let r = parse_ok(src);
3028 assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3029 }
3030
3031 #[test]
3034 fn spec_set_arithmetic() {
3035 let src = r#"entity Role {
3037 permissions: Set<String>
3038 inherited: Set<String>
3039 all_permissions: permissions + inherited
3040 removed: old_mentions - new_mentions
3041}"#;
3042 let r = parse_ok(src);
3043 assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3044 }
3045
3046 #[test]
3049 fn spec_discard_binding_in_trigger() {
3050 let src = r#"rule R {
3052 when: _: LogProcessor.last_flush_check <= now
3053 ensures: Flushed()
3054}"#;
3055 let r = parse_ok(src);
3056 assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3057 }
3058
3059 #[test]
3060 fn spec_discard_in_trigger_params() {
3061 let src = r#"rule R {
3063 when: SomeEvent(_, slot)
3064 ensures: Processed(slot: slot)
3065}"#;
3066 let r = parse_ok(src);
3067 assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3068 }
3069
3070 #[test]
3071 fn spec_discard_in_for() {
3072 let src = r#"rule R {
3074 when: X(items)
3075 ensures:
3076 for _ in items: Counted()
3077}"#;
3078 let r = parse_ok(src);
3079 assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3080 }
3081
3082 #[test]
3085 fn spec_default_with_object_literal() {
3086 let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3088 let r = parse_ok(src);
3089 assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3090 }
3091
3092 #[test]
3093 fn spec_default_multiline_object() {
3094 let src = r#"default Role viewer = {
3096 name: "viewer",
3097 permissions: { "documents.read" }
3098}"#;
3099 let r = parse_ok(src);
3100 assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3101 }
3102
3103 #[test]
3106 fn spec_surface_related_clause() {
3107 let src = r#"surface InterviewerDashboard {
3109 facing viewer: Interviewer
3110 context assignment: SlotConfirmation where interviewer = viewer
3111 related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3112}"#;
3113 let r = parse_ok(src);
3114 assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3115 }
3116
3117 #[test]
3118 fn spec_surface_let_binding() {
3119 let src = r#"surface S {
3121 facing viewer: User
3122 let comments = Comments where parent = viewer
3123 exposes: CommentList
3124}"#;
3125 let r = parse_ok(src);
3126 assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3127 }
3128
3129 #[test]
3130 fn spec_surface_multiline_context_where() {
3131 let src = r#"surface InterviewerPendingAssignments {
3133 facing viewer: Interviewer
3134 context assignment: InterviewAssignment
3135 where interviewer = viewer and status = pending
3136 exposes: AssignmentList
3137}"#;
3138 let r = parse_ok(src);
3139 assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3140 }
3141
3142 #[test]
3145 fn spec_for_in_surface_provides() {
3146 let src = r#"surface TaskBoard {
3148 facing viewer: User
3149 for task in Task where task.assignee = viewer:
3150 provides: CompleteTask(viewer, task) when task.status = in_progress
3151 exposes: KanbanBoard
3152}"#;
3153 let r = parse_ok(src);
3154 assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3155 }
3156
3157 #[test]
3160 fn spec_use_without_alias() {
3161 let src = r#"use "github.com/specs/notifications/def456""#;
3163 let r = parse_ok(src);
3164 assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3165 }
3166
3167 #[test]
3170 fn spec_empty_external_entity() {
3171 let src = "external entity Commentable {}";
3173 let r = parse_ok(src);
3174 assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3175 }
3176
3177 #[test]
3180 fn spec_surface_multiline_provides() {
3181 let src = r#"surface ProjectDashboard {
3183 facing viewer: ProjectManager
3184 context project: Project where owner = viewer
3185 provides:
3186 CreateTask(viewer, project) when project.status = active
3187 ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3188 exposes: TaskList
3189}"#;
3190 let r = parse_ok(src);
3191 assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3192 }
3193
3194 #[test]
3197 fn spec_surface_multiline_exposes() {
3198 let src = r#"surface InterviewerDashboard {
3200 facing viewer: Interviewer
3201 context assignment: SlotConfirmation where interviewer = viewer
3202 exposes:
3203 assignment.slot.time
3204 assignment.status
3205}"#;
3206 let r = parse_ok(src);
3207 assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3208 }
3209
3210 #[test]
3220 fn composite_or_trigger() {
3221 let src = r#"rule R {
3222 when: EventA(x) or EventB(x) or EventC(x)
3223 ensures: Done()
3224}"#;
3225 let r = parse_ok(src);
3226 assert_eq!(r.diagnostics.len(), 0);
3227 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3228 let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
3229 assert_eq!(keyword, "when");
3230 let Expr::LogicalOp { op, left, .. } = value else {
3232 panic!("expected LogicalOp, got {value:?}");
3233 };
3234 assert_eq!(*op, LogicalOp::Or);
3235 assert!(matches!(left.as_ref(), Expr::LogicalOp { op: LogicalOp::Or, .. }));
3236 }
3237
3238 #[test]
3241 fn value_type_declaration() {
3242 let src = r#"value TimeRange {
3243 start: Timestamp
3244 end: Timestamp
3245 duration: end - start
3246}"#;
3247 let r = parse_ok(src);
3248 assert_eq!(r.diagnostics.len(), 0);
3249 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3250 assert_eq!(b.kind, BlockKind::Value);
3251 assert_eq!(b.name.as_ref().unwrap().name, "TimeRange");
3252 assert_eq!(b.items.len(), 3);
3253 }
3254
3255 #[test]
3258 fn qualified_config_block() {
3259 let src = r#"use "github.com/specs/oauth/abc123" as oauth
3260oauth/config {
3261 session_duration: Duration = 24.hours
3262}"#;
3263 let r = parse_ok(src);
3264 assert_eq!(r.diagnostics.len(), 0);
3265 assert_eq!(r.module.declarations.len(), 2);
3266 }
3267
3268 #[test]
3271 fn string_interpolation_parts() {
3272 let src = r#"rule R {
3273 when: X(name, action)
3274 ensures: Log.created(message: "User {name} did {action}")
3275}"#;
3276 let r = parse_ok(src);
3277 assert_eq!(r.diagnostics.len(), 0);
3278 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3280 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3281 let Expr::Call { args, .. } = value else { panic!() };
3282 let CallArg::Named(arg) = &args[0] else { panic!() };
3283 let Expr::StringLiteral(s) = &arg.value else { panic!() };
3284 assert_eq!(s.parts.len(), 4, "expected 4 string parts: text, interp, text, interp");
3285 assert!(matches!(&s.parts[0], StringPart::Text(t) if t == "User "));
3286 assert!(matches!(&s.parts[1], StringPart::Interpolation(id) if id.name == "name"));
3287 assert!(matches!(&s.parts[2], StringPart::Text(t) if t == " did "));
3288 assert!(matches!(&s.parts[3], StringPart::Interpolation(id) if id.name == "action"));
3289 }
3290
3291 #[test]
3294 fn this_keyword_expression() {
3295 let src = "entity E { items: Item with parent = this }";
3298 let r = parse_ok(src);
3299 assert_eq!(r.diagnostics.len(), 0);
3300 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3301 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3302 let Expr::With { predicate, .. } = value else {
3303 panic!("expected With, got {value:?}");
3304 };
3305 let Expr::Comparison { op, right, .. } = predicate.as_ref() else {
3306 panic!("expected Comparison in with predicate, got {predicate:?}");
3307 };
3308 assert_eq!(*op, ComparisonOp::Eq);
3309 assert!(matches!(right.as_ref(), Expr::This { .. }));
3310 }
3311
3312 #[test]
3315 fn not_prefix_standalone() {
3316 let src = r#"rule R {
3317 when: X(user)
3318 requires: not user.is_locked
3319 ensures: Done()
3320}"#;
3321 let r = parse_ok(src);
3322 assert_eq!(r.diagnostics.len(), 0);
3323 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3324 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3325 assert_eq!(keyword, "requires");
3326 assert!(matches!(value, Expr::Not { .. }));
3327 }
3328
3329 #[test]
3332 fn unary_minus() {
3333 let src = "entity E { offset: -1 }";
3334 let r = parse_ok(src);
3335 assert_eq!(r.diagnostics.len(), 0);
3336 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3337 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3338 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }
3339 | Expr::NumberLiteral { .. }), "expected negation, got {value:?}");
3340 }
3341
3342 #[test]
3345 fn parenthesised_expression() {
3346 let src = "entity E { v: (a + b) * c }";
3347 let r = parse_ok(src);
3348 assert_eq!(r.diagnostics.len(), 0);
3349 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3350 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3351 let Expr::BinaryOp { op, left, .. } = value else {
3353 panic!("expected BinaryOp, got {value:?}");
3354 };
3355 assert_eq!(*op, BinaryOp::Mul);
3356 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3357 }
3358
3359 #[test]
3362 fn boolean_literals() {
3363 let src = r#"rule R {
3364 when: X(item)
3365 ensures:
3366 item.active = true
3367 item.deleted = false
3368}"#;
3369 let r = parse_ok(src);
3370 assert_eq!(r.diagnostics.len(), 0);
3371 }
3372
3373 #[test]
3376 fn null_literal() {
3377 let src = "entity E { v: parent ?? null }";
3378 let r = parse_ok(src);
3379 assert_eq!(r.diagnostics.len(), 0);
3380 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3381 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3382 let Expr::NullCoalesce { right, .. } = value else { panic!() };
3383 assert!(matches!(right.as_ref(), Expr::Null { .. }));
3384 }
3385
3386 #[test]
3389 fn empty_set_literal() {
3390 let src = "entity E { tags: Set<String>\n default_tags: {} }";
3391 let r = parse_ok(src);
3392 assert_eq!(r.diagnostics.len(), 0);
3393 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3394 let BlockItemKind::Assignment { value, .. } = &b.items[1].kind else { panic!() };
3395 let Expr::SetLiteral { elements, .. } = value else { panic!("expected SetLiteral, got {value:?}") };
3396 assert!(elements.is_empty());
3397 }
3398
3399 #[test]
3411 fn param_assignment_single() {
3412 let src = "entity Plan { can_use(feature): feature in features }";
3413 let r = parse_ok(src);
3414 assert_eq!(r.diagnostics.len(), 0);
3415 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3416 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
3417 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3418 };
3419 assert_eq!(name.name, "can_use");
3420 assert_eq!(params.len(), 1);
3421 assert_eq!(params[0].name, "feature");
3422 assert!(matches!(value, Expr::In { .. }));
3423 }
3424
3425 #[test]
3426 fn param_assignment_multiple() {
3427 let src = "entity E { distance(x, y): (x * x + y * y) }";
3428 let r = parse_ok(src);
3429 assert_eq!(r.diagnostics.len(), 0);
3430 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3431 let BlockItemKind::ParamAssignment { name, params, .. } = &b.items[0].kind else {
3432 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3433 };
3434 assert_eq!(name.name, "distance");
3435 assert_eq!(params.len(), 2);
3436 assert_eq!(params[0].name, "x");
3437 assert_eq!(params[1].name, "y");
3438 }
3439
3440 #[test]
3441 fn param_assignment_simple_expression() {
3442 let src = "entity Task { remaining_effort(total): total - effort }";
3443 let r = parse_ok(src);
3444 assert_eq!(r.diagnostics.len(), 0);
3445 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3446 let BlockItemKind::ParamAssignment { name, params, value } = &b.items[0].kind else {
3447 panic!("expected ParamAssignment, got {:?}", b.items[0].kind);
3448 };
3449 assert_eq!(name.name, "remaining_effort");
3450 assert_eq!(params.len(), 1);
3451 assert!(matches!(value, Expr::BinaryOp { op: BinaryOp::Sub, .. }));
3452 }
3453
3454 #[test]
3457 fn precedence_logical_and_binds_tighter_than_or() {
3458 let src = "entity E { v: a or b and c }";
3460 let r = parse_ok(src);
3461 assert_eq!(r.diagnostics.len(), 0);
3462 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3463 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3464 let Expr::LogicalOp { op, right, .. } = value else {
3465 panic!("expected LogicalOp, got {value:?}");
3466 };
3467 assert_eq!(*op, LogicalOp::Or);
3468 assert!(matches!(right.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3469 }
3470
3471 #[test]
3472 fn precedence_comparison_binds_tighter_than_and() {
3473 let src = "entity E { v: a = b and c != d }";
3475 let r = parse_ok(src);
3476 assert_eq!(r.diagnostics.len(), 0);
3477 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3478 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3479 let Expr::LogicalOp { op, left, right, .. } = value else {
3480 panic!("expected LogicalOp, got {value:?}");
3481 };
3482 assert_eq!(*op, LogicalOp::And);
3483 assert!(matches!(left.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
3484 assert!(matches!(right.as_ref(), Expr::Comparison { op: ComparisonOp::NotEq, .. }));
3485 }
3486
3487 #[test]
3488 fn precedence_arithmetic_binds_tighter_than_comparison() {
3489 let src = "entity E { v: a + b > c * d }";
3491 let r = parse_ok(src);
3492 assert_eq!(r.diagnostics.len(), 0);
3493 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3494 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3495 let Expr::Comparison { op, left, right, .. } = value else {
3496 panic!("expected Comparison, got {value:?}");
3497 };
3498 assert_eq!(*op, ComparisonOp::Gt);
3499 assert!(matches!(left.as_ref(), Expr::BinaryOp { op: BinaryOp::Add, .. }));
3500 assert!(matches!(right.as_ref(), Expr::BinaryOp { op: BinaryOp::Mul, .. }));
3501 }
3502
3503 #[test]
3504 fn precedence_null_coalesce_binds_tighter_than_comparison() {
3505 let src = "entity E { v: a ?? b = c }";
3507 let r = parse_ok(src);
3508 assert_eq!(r.diagnostics.len(), 0);
3509 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3510 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3511 let Expr::Comparison { op, left, .. } = value else {
3512 panic!("expected Comparison, got {value:?}");
3513 };
3514 assert_eq!(*op, ComparisonOp::Eq);
3515 assert!(matches!(left.as_ref(), Expr::NullCoalesce { .. }));
3516 }
3517
3518 #[test]
3519 fn precedence_not_binds_tighter_than_and() {
3520 let src = r#"rule R {
3522 when: X(a, b)
3523 requires: not a and b
3524 ensures: Done()
3525}"#;
3526 let r = parse_ok(src);
3527 assert_eq!(r.diagnostics.len(), 0);
3528 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3529 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3530 let Expr::LogicalOp { op, left, .. } = value else {
3531 panic!("expected LogicalOp, got {value:?}");
3532 };
3533 assert_eq!(*op, LogicalOp::And);
3534 assert!(matches!(left.as_ref(), Expr::Not { .. }));
3535 }
3536
3537 #[test]
3538 fn precedence_where_captures_full_condition() {
3539 let src = "entity E { v: items where status = active }";
3543 let r = parse_ok(src);
3544 assert_eq!(r.diagnostics.len(), 0);
3545 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3546 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3547 let Expr::Where { condition, .. } = value else {
3548 panic!("expected Where, got {value:?}");
3549 };
3550 assert!(matches!(condition.as_ref(), Expr::Comparison { op: ComparisonOp::Eq, .. }));
3551 }
3552
3553 #[test]
3554 fn precedence_where_captures_and_or_conditions() {
3555 let src = "entity E { v: items where status = active and count > 0 }";
3558 let r = parse_ok(src);
3559 assert_eq!(r.diagnostics.len(), 0);
3560 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3561 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3562 let Expr::Where { condition, .. } = value else {
3563 panic!("expected Where, got {value:?}");
3564 };
3565 assert!(matches!(condition.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3566 }
3567
3568 #[test]
3569 fn precedence_projection_applies_to_where_result() {
3570 let src = "entity E { v: items where status = confirmed -> interviewer }";
3573 let r = parse_ok(src);
3574 assert_eq!(r.diagnostics.len(), 0);
3575 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3576 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3577 let Expr::ProjectionMap { source, field, .. } = value else {
3578 panic!("expected ProjectionMap, got {value:?}");
3579 };
3580 assert_eq!(field.name, "interviewer");
3581 assert!(matches!(source.as_ref(), Expr::Where { .. }));
3582 }
3583
3584 #[test]
3585 fn precedence_lambda_binds_loosest() {
3586 let src = "entity E { v: items.any(i => i.active and i.valid) }";
3588 let r = parse_ok(src);
3589 assert_eq!(r.diagnostics.len(), 0);
3590 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3591 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3592 let Expr::Call { args, .. } = value else { panic!() };
3593 let CallArg::Positional(Expr::Lambda { body, .. }) = &args[0] else { panic!() };
3594 assert!(matches!(body.as_ref(), Expr::LogicalOp { op: LogicalOp::And, .. }));
3595 }
3596
3597 #[test]
3598 fn precedence_in_binds_at_comparison_level() {
3599 let src = r#"rule R {
3601 when: X(x, y)
3602 requires: x in {a, b} and y not in {c}
3603 ensures: Done()
3604}"#;
3605 let r = parse_ok(src);
3606 assert_eq!(r.diagnostics.len(), 0);
3607 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3608 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3609 let Expr::LogicalOp { op, left, right, .. } = value else {
3610 panic!("expected LogicalOp, got {value:?}");
3611 };
3612 assert_eq!(*op, LogicalOp::And);
3613 assert!(matches!(left.as_ref(), Expr::In { .. }));
3614 assert!(matches!(right.as_ref(), Expr::NotIn { .. }));
3615 }
3616
3617 #[test]
3620 fn multiline_ensures_block() {
3621 let src = r#"rule R {
3622 when: X(doc)
3623 ensures:
3624 doc.status = published
3625 Notification.created(to: doc.author)
3626}"#;
3627 let r = parse_ok(src);
3628 assert_eq!(r.diagnostics.len(), 0);
3629 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3630 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3631 assert_eq!(keyword, "ensures");
3632 let Expr::Block { items, .. } = value else {
3633 panic!("expected Block for multi-line ensures, got {value:?}");
3634 };
3635 assert_eq!(items.len(), 2);
3636 }
3637
3638 #[test]
3639 fn singleline_ensures_value() {
3640 let src = r#"rule R {
3641 when: X(doc)
3642 ensures: doc.status = published
3643}"#;
3644 let r = parse_ok(src);
3645 assert_eq!(r.diagnostics.len(), 0);
3646 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3647 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
3648 assert_eq!(keyword, "ensures");
3649 assert!(!matches!(value, Expr::Block { .. }), "single-line ensures should not be Block");
3651 }
3652
3653 #[test]
3654 fn multiline_requires_with_continuation() {
3655 let src = r#"rule R {
3656 when: X(a)
3657 requires:
3658 a.count >= 2
3659 or a.items.any(i => i.can_solo)
3660 ensures: Done()
3661}"#;
3662 let r = parse_ok(src);
3663 assert_eq!(r.diagnostics.len(), 0);
3664 }
3665
3666 #[test]
3669 fn object_literal_single_field() {
3670 let src = r#"rule R {
3671 when: X()
3672 ensures:
3673 let o = {name: "test"}
3674 Done()
3675}"#;
3676 let r = parse_ok(src);
3677 assert_eq!(r.diagnostics.len(), 0);
3678 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3679 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3680 let Expr::Block { items, .. } = value else { panic!() };
3681 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
3682 assert!(matches!(let_val.as_ref(), Expr::ObjectLiteral { .. }));
3683 }
3684
3685 #[test]
3686 fn set_literal_single_element() {
3687 let src = r#"rule R {
3688 when: X()
3689 ensures:
3690 let s = {active}
3691 Done()
3692}"#;
3693 let r = parse_ok(src);
3694 assert_eq!(r.diagnostics.len(), 0);
3695 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3696 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
3697 let Expr::Block { items, .. } = value else { panic!() };
3698 let Expr::LetExpr { value: let_val, .. } = &items[0] else { panic!() };
3699 assert!(matches!(let_val.as_ref(), Expr::SetLiteral { .. }),
3700 "bare {{ident}} should parse as set literal, got {:?}", let_val);
3701 }
3702
3703 #[test]
3706 fn lambda_with_chained_access() {
3707 let src = "entity E { v: items.all(t => t.item.status = active) }";
3708 let r = parse_ok(src);
3709 assert_eq!(r.diagnostics.len(), 0);
3710 }
3711
3712 #[test]
3713 fn nested_lambda() {
3714 let src = "entity E { v: groups.any(g => g.items.all(i => i.valid)) }";
3715 let r = parse_ok(src);
3716 assert_eq!(r.diagnostics.len(), 0);
3717 }
3718
3719 #[test]
3722 fn qualified_name_with_member_access() {
3723 let src = "entity E { v: shared/Validator.check }";
3724 let r = parse_ok(src);
3725 assert_eq!(r.diagnostics.len(), 0);
3726 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3727 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3728 let Expr::MemberAccess { object, field, .. } = value else {
3729 panic!("expected MemberAccess, got {value:?}");
3730 };
3731 assert!(matches!(object.as_ref(), Expr::QualifiedName(_)));
3732 assert_eq!(field.name, "check");
3733 }
3734
3735 #[test]
3736 fn qualified_name_in_call() {
3737 let src = r#"rule R {
3738 when: X(item)
3739 requires: shared/Validator.check(item: item)
3740 ensures: Done()
3741}"#;
3742 let r = parse_ok(src);
3743 assert_eq!(r.diagnostics.len(), 0);
3744 }
3745
3746 #[test]
3749 fn nested_if_inside_for() {
3750 let src = r#"rule R {
3751 when: X()
3752 for user in Users where user.active:
3753 if user.role = admin:
3754 ensures: AdminNotified(user: user)
3755 else:
3756 ensures: UserNotified(user: user)
3757}"#;
3758 let r = parse_ok(src);
3759 assert_eq!(r.diagnostics.len(), 0);
3760 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3761 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
3762 assert!(matches!(items[0].kind, BlockItemKind::IfBlock { .. }));
3763 }
3764
3765 #[test]
3766 fn for_with_let_before_ensures() {
3767 let src = r#"rule R {
3768 when: schedule: DigestSchedule.next_run_at <= now
3769 for user in Users where user.active:
3770 let pending = user.tasks where status = pending
3771 ensures: DigestEmail.created(to: user.email, tasks: pending)
3772}"#;
3773 let r = parse_ok(src);
3774 assert_eq!(r.diagnostics.len(), 0);
3775 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3776 let BlockItemKind::ForBlock { items, .. } = &b.items[1].kind else { panic!() };
3777 assert_eq!(items.len(), 2, "for body should have let + ensures");
3778 assert!(matches!(items[0].kind, BlockItemKind::Let { .. }));
3779 assert!(matches!(items[1].kind, BlockItemKind::Clause { .. }));
3780 }
3781
3782 #[test]
3785 fn join_lookup_all_unnamed() {
3786 let src = "entity E { match: Other{a, b, c} }";
3787 let r = parse_ok(src);
3788 assert_eq!(r.diagnostics.len(), 0);
3789 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3790 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3791 let Expr::JoinLookup { fields, .. } = value else { panic!() };
3792 assert_eq!(fields.len(), 3);
3793 assert!(fields.iter().all(|f| f.value.is_none()));
3794 }
3795
3796 #[test]
3797 fn join_lookup_all_named() {
3798 let src = "entity E { match: Membership{user: actor, workspace: ws} }";
3799 let r = parse_ok(src);
3800 assert_eq!(r.diagnostics.len(), 0);
3801 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
3802 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
3803 let Expr::JoinLookup { fields, .. } = value else { panic!() };
3804 assert_eq!(fields.len(), 2);
3805 assert!(fields.iter().all(|f| f.value.is_some()));
3806 }
3807
3808 #[test]
3809 fn join_lookup_in_requires() {
3810 let src = r#"rule R {
3811 when: X(user, workspace)
3812 requires: exists WorkspaceMembership{user: user, workspace: workspace}
3813 ensures: Done()
3814}"#;
3815 let r = parse_ok(src);
3816 assert_eq!(r.diagnostics.len(), 0);
3817 }
3818
3819 #[test]
3820 fn join_lookup_negated_in_requires() {
3821 let src = r#"rule R {
3822 when: X(email)
3823 requires: not exists User{email: email}
3824 ensures: Done()
3825}"#;
3826 let r = parse_ok(src);
3827 assert_eq!(r.diagnostics.len(), 0);
3828 }
3829}