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 self.diagnostics.push(Diagnostic::error(span, msg));
120 }
121
122 fn parse_ident(&mut self) -> Option<Ident> {
124 self.parse_ident_in("identifier")
125 }
126
127 fn parse_ident_in(&mut self, context: &str) -> Option<Ident> {
129 let tok = self.peek();
130 if tok.kind.is_word() {
131 self.advance();
132 Some(Ident {
133 span: tok.span,
134 name: self.text(tok.span).to_string(),
135 })
136 } else {
137 self.error(
138 tok.span,
139 format!("expected {context}, found {}", tok.kind),
140 );
141 None
142 }
143 }
144
145 fn parse_string(&mut self) -> Option<StringLiteral> {
147 let tok = self.expect(TokenKind::String)?;
148 let raw = self.text(tok.span);
149 let inner = &raw[1..raw.len() - 1];
151 let parts = parse_string_parts(inner, tok.span.start + 1);
152 Some(StringLiteral {
153 span: tok.span,
154 parts,
155 })
156 }
157}
158
159fn parse_string_parts(inner: &str, base_offset: usize) -> Vec<StringPart> {
163 let mut parts = Vec::new();
164 let mut buf = String::new();
165 let bytes = inner.as_bytes();
166 let mut i = 0;
167 while i < bytes.len() {
168 if bytes[i] == b'\\' && i + 1 < bytes.len() {
169 buf.push(bytes[i + 1] as char);
170 i += 2;
171 } else if bytes[i] == b'{' {
172 if !buf.is_empty() {
173 parts.push(StringPart::Text(std::mem::take(&mut buf)));
174 }
175 i += 1; let start = i;
177 while i < bytes.len() && bytes[i] != b'}' {
178 i += 1;
179 }
180 let name = std::str::from_utf8(&bytes[start..i]).unwrap_or("").to_string();
181 let span_start = base_offset + start;
182 let span_end = base_offset + i;
183 parts.push(StringPart::Interpolation(Ident {
184 span: Span::new(span_start, span_end),
185 name,
186 }));
187 if i < bytes.len() {
188 i += 1; }
190 } else {
191 buf.push(bytes[i] as char);
192 i += 1;
193 }
194 }
195 if !buf.is_empty() {
196 parts.push(StringPart::Text(buf));
197 }
198 parts
199}
200
201fn is_clause_keyword(text: &str) -> bool {
208 matches!(
209 text,
210 "when"
211 | "requires"
212 | "ensures"
213 | "trigger"
214 | "facing"
215 | "context"
216 | "exposes"
217 | "provides"
218 | "related"
219 | "timeout"
220 | "guarantee"
221 | "guidance"
222 | "identified_by"
223 | "within"
224 | "tags"
225 | "invariant"
226 )
227}
228
229fn clause_allows_binding(keyword: &str) -> bool {
231 matches!(keyword, "when")
232}
233
234fn is_binding_clause_keyword(text: &str) -> bool {
237 matches!(text, "facing" | "context")
238}
239
240fn token_is_clause_keyword(kind: TokenKind) -> bool {
242 matches!(
243 kind,
244 TokenKind::When | TokenKind::Requires | TokenKind::Ensures | TokenKind::Within
245 )
246}
247
248impl<'s> Parser<'s> {
253 fn parse_module(&mut self) -> Module {
254 let start = self.peek().span;
255 let version = detect_version(self.source);
258
259 match version {
260 None => {
261 self.diagnostics.push(Diagnostic::warning(
262 start,
263 "missing version marker; expected '-- allium: 1' as the first line",
264 ));
265 }
266 Some(1) => {}
267 Some(v) => {
268 self.diagnostics.push(Diagnostic::error(
269 start,
270 format!("unsupported allium version {v}; this parser supports version 1"),
271 ));
272 }
273 }
274
275 let mut decls = Vec::new();
276 while !self.at_eof() {
277 if let Some(d) = self.parse_decl() {
278 decls.push(d);
279 } else {
280 self.advance();
282 }
283 }
284 let end = self.peek().span;
285 Module {
286 span: start.merge(end),
287 version,
288 declarations: decls,
289 }
290 }
291}
292
293fn detect_version(source: &str) -> Option<u32> {
294 for line in source.lines() {
295 let trimmed = line.trim();
296 if trimmed.is_empty() {
297 continue;
298 }
299 if let Some(rest) = trimmed.strip_prefix("--") {
300 let rest = rest.trim();
301 if let Some(ver) = rest.strip_prefix("allium:") {
302 return ver.trim().parse().ok();
303 }
304 }
305 break; }
307 None
308}
309
310impl<'s> Parser<'s> {
315 fn parse_decl(&mut self) -> Option<Decl> {
316 match self.peek_kind() {
317 TokenKind::Use => self.parse_use_decl().map(Decl::Use),
318 TokenKind::Rule => self.parse_block(BlockKind::Rule).map(Decl::Block),
319 TokenKind::Entity => self.parse_block(BlockKind::Entity).map(Decl::Block),
320 TokenKind::External => {
321 let start = self.advance().span;
322 if self.at(TokenKind::Entity) {
323 self.parse_block_from(start, BlockKind::ExternalEntity)
324 .map(Decl::Block)
325 } else {
326 self.error(self.peek().span, "expected 'entity' after 'external'");
327 None
328 }
329 }
330 TokenKind::Value => self.parse_block(BlockKind::Value).map(Decl::Block),
331 TokenKind::Enum => self.parse_block(BlockKind::Enum).map(Decl::Block),
332 TokenKind::Given => self.parse_anonymous_block(BlockKind::Given).map(Decl::Block),
333 TokenKind::Config => self.parse_anonymous_block(BlockKind::Config).map(Decl::Block),
334 TokenKind::Surface => self.parse_block(BlockKind::Surface).map(Decl::Block),
335 TokenKind::Actor => self.parse_block(BlockKind::Actor).map(Decl::Block),
336 TokenKind::Default => self.parse_default_decl().map(Decl::Default),
337 TokenKind::Variant => self.parse_variant_decl().map(Decl::Variant),
338 TokenKind::Deferred => self.parse_deferred_decl().map(Decl::Deferred),
339 TokenKind::Open => self.parse_open_question_decl().map(Decl::OpenQuestion),
340 TokenKind::Module => self.parse_module_decl(),
341 TokenKind::Ident
343 if self.peek_at(1).kind == TokenKind::Slash
344 && self.text(self.peek_at(2).span) == "config" =>
345 {
346 self.parse_qualified_config().map(Decl::Block)
347 }
348 _ => {
349 self.error(
350 self.peek().span,
351 format!(
352 "expected declaration (entity, rule, enum, value, config, surface, actor, \
353 given, default, variant, deferred, use, open question), found {}",
354 self.peek_kind(),
355 ),
356 );
357 None
358 }
359 }
360 }
361
362 fn parse_module_decl(&mut self) -> Option<Decl> {
365 let start = self.expect(TokenKind::Module)?.span;
366 let name = self.parse_ident_in("module name")?;
367 Some(Decl::ModuleDecl(ModuleDecl {
368 span: start.merge(name.span),
369 name,
370 }))
371 }
372
373 fn parse_use_decl(&mut self) -> Option<UseDecl> {
376 let start = self.expect(TokenKind::Use)?.span;
377 let path = self.parse_string()?;
378 let alias = if self.eat(TokenKind::As).is_some() {
379 Some(self.parse_ident_in("import alias")?)
380 } else {
381 None
382 };
383 let end = alias
384 .as_ref()
385 .map(|a| a.span)
386 .unwrap_or(path.span);
387 Some(UseDecl {
388 span: start.merge(end),
389 path,
390 alias,
391 })
392 }
393
394 fn parse_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
397 let start = self.advance().span; self.parse_block_from(start, kind)
399 }
400
401 fn parse_block_from(&mut self, start: Span, kind: BlockKind) -> Option<BlockDecl> {
402 if kind == BlockKind::ExternalEntity {
405 self.expect(TokenKind::Entity)?;
406 }
407 let context = match kind {
408 BlockKind::Entity | BlockKind::ExternalEntity => "entity name",
409 BlockKind::Rule => "rule name",
410 BlockKind::Surface => "surface name",
411 BlockKind::Actor => "actor name",
412 BlockKind::Value => "value type name",
413 BlockKind::Enum => "enum name",
414 _ => "block name",
415 };
416 let name = Some(self.parse_ident_in(context)?);
417 self.expect(TokenKind::LBrace)?;
418 let items = if kind == BlockKind::Enum {
419 self.parse_enum_body()
420 } else {
421 self.parse_block_items()
422 };
423 let end = self.expect(TokenKind::RBrace)?.span;
424 Some(BlockDecl {
425 span: start.merge(end),
426 kind,
427 name,
428 items,
429 })
430 }
431
432 fn parse_enum_body(&mut self) -> Vec<BlockItem> {
437 let mut items = Vec::new();
438 while !self.at(TokenKind::RBrace) && !self.at_eof() {
439 if self.eat(TokenKind::Pipe).is_some() {
440 continue;
441 }
442 if let Some(ident) = self.parse_ident_in("enum variant") {
443 items.push(BlockItem {
444 span: ident.span,
445 kind: BlockItemKind::EnumVariant { name: ident },
446 });
447 } else {
448 self.advance(); }
450 }
451 items
452 }
453
454 fn parse_anonymous_block(&mut self, kind: BlockKind) -> Option<BlockDecl> {
455 let start = self.advance().span;
456 self.expect(TokenKind::LBrace)?;
457 let items = self.parse_block_items();
458 let end = self.expect(TokenKind::RBrace)?.span;
459 Some(BlockDecl {
460 span: start.merge(end),
461 kind,
462 name: None,
463 items,
464 })
465 }
466
467 fn parse_qualified_config(&mut self) -> Option<BlockDecl> {
470 let alias = self.parse_ident_in("config qualifier")?;
471 let start = alias.span;
472 self.expect(TokenKind::Slash)?;
473 self.advance(); self.expect(TokenKind::LBrace)?;
475 let items = self.parse_block_items();
476 let end = self.expect(TokenKind::RBrace)?.span;
477 Some(BlockDecl {
478 span: start.merge(end),
479 kind: BlockKind::Config,
480 name: Some(alias),
481 items,
482 })
483 }
484
485 fn parse_default_decl(&mut self) -> Option<DefaultDecl> {
488 let start = self.expect(TokenKind::Default)?.span;
489
490 let (type_name, name) = if self.peek_kind().is_word()
494 && self.peek_at(1).kind.is_word()
495 && self.peek_at(2).kind == TokenKind::Eq
496 {
497 let t = self.parse_ident_in("type name")?;
498 let n = self.parse_ident_in("default name")?;
499 (Some(t), n)
500 } else {
501 (None, self.parse_ident_in("default name")?)
502 };
503
504 self.expect(TokenKind::Eq)?;
505 let value = self.parse_expr(0)?;
506 Some(DefaultDecl {
507 span: start.merge(value.span()),
508 type_name,
509 name,
510 value,
511 })
512 }
513
514 fn parse_variant_decl(&mut self) -> Option<VariantDecl> {
517 let start = self.expect(TokenKind::Variant)?.span;
518 let name = self.parse_ident_in("variant name")?;
519 self.expect(TokenKind::Colon)?;
520 let base = self.parse_expr(0)?;
521
522 let items = if self.eat(TokenKind::LBrace).is_some() {
523 let items = self.parse_block_items();
524 self.expect(TokenKind::RBrace)?;
525 items
526 } else {
527 Vec::new()
528 };
529
530 let end = if let Some(last) = items.last() {
531 last.span
532 } else {
533 base.span()
534 };
535 Some(VariantDecl {
536 span: start.merge(end),
537 name,
538 base,
539 items,
540 })
541 }
542
543 fn parse_deferred_decl(&mut self) -> Option<DeferredDecl> {
546 let start = self.expect(TokenKind::Deferred)?.span;
547 let path = self.parse_expr(0)?;
548 Some(DeferredDecl {
549 span: start.merge(path.span()),
550 path,
551 })
552 }
553
554 fn parse_open_question_decl(&mut self) -> Option<OpenQuestionDecl> {
557 let start = self.expect(TokenKind::Open)?.span;
558 self.expect(TokenKind::Question)?;
559 let text = self.parse_string()?;
560 Some(OpenQuestionDecl {
561 span: start.merge(text.span),
562 text,
563 })
564 }
565}
566
567impl<'s> Parser<'s> {
572 fn parse_block_items(&mut self) -> Vec<BlockItem> {
573 let mut items = Vec::new();
574 while !self.at(TokenKind::RBrace) && !self.at_eof() {
575 if let Some(item) = self.parse_block_item() {
576 items.push(item);
577 } else {
578 self.advance();
580 }
581 }
582 items
583 }
584
585 fn parse_block_item(&mut self) -> Option<BlockItem> {
586 let start = self.peek().span;
587
588 if self.at(TokenKind::Let) {
590 return self.parse_let_item(start);
591 }
592
593 if self.at(TokenKind::For) {
595 return self.parse_for_block_item(start);
596 }
597
598 if self.at(TokenKind::Open) && self.peek_at(1).kind == TokenKind::Question {
600 self.advance(); self.advance(); let text = self.parse_string()?;
603 return Some(BlockItem {
604 span: start.merge(text.span),
605 kind: BlockItemKind::OpenQuestion { text },
606 });
607 }
608
609 if self.peek_kind().is_word() {
612 if is_binding_clause_keyword(self.text(self.peek().span))
615 && self.peek_at(1).kind.is_word()
616 && self.peek_at(2).kind == TokenKind::Colon
617 {
618 return self.parse_binding_clause_item(start);
619 }
620
621 if self.peek_at(1).kind == TokenKind::LParen {
623 return self.parse_param_or_clause_item(start);
624 }
625
626 if self.peek_at(1).kind == TokenKind::Colon {
628 return self.parse_assign_or_clause_item(start);
629 }
630 }
631
632 if token_is_clause_keyword(self.peek_kind()) && self.peek_at(1).kind == TokenKind::Colon {
634 return self.parse_assign_or_clause_item(start);
635 }
636
637 self.error(
638 start,
639 format!(
640 "expected block item (name: value, let name = value, when:/requires:/ensures: clause, \
641 for ... in ...:, or open question), found {}",
642 self.peek_kind(),
643 ),
644 );
645 None
646 }
647
648 fn parse_let_item(&mut self, start: Span) -> Option<BlockItem> {
649 self.advance(); let name = self.parse_ident_in("binding name")?;
651 self.expect(TokenKind::Eq)?;
652 let value = self.parse_clause_value(start)?;
653 Some(BlockItem {
654 span: start.merge(value.span()),
655 kind: BlockItemKind::Let { name, value },
656 })
657 }
658
659 fn parse_binding_clause_item(&mut self, start: Span) -> Option<BlockItem> {
662 let keyword_tok = self.advance(); let keyword = self.text(keyword_tok.span).to_string();
664 let binding_name = self.parse_ident_in(&format!("{keyword} binding name"))?;
665 self.advance(); let type_expr = self.parse_clause_value(start)?;
667 let value_span = type_expr.span();
668 let value = Expr::Binding {
669 span: binding_name.span.merge(value_span),
670 name: binding_name,
671 value: Box::new(type_expr),
672 };
673 Some(BlockItem {
674 span: start.merge(value_span),
675 kind: BlockItemKind::Clause { keyword, value },
676 })
677 }
678
679 fn parse_for_block_item(&mut self, start: Span) -> Option<BlockItem> {
682 self.advance(); let binding = self.parse_ident_in("loop variable")?;
684 self.expect(TokenKind::In)?;
685
686 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
687
688 let filter = if self.eat(TokenKind::Where).is_some() {
689 Some(self.parse_expr(0)?)
692 } else {
693 None
694 };
695
696 self.expect(TokenKind::Colon)?;
697
698 let for_line = self.line_of(start);
700 let next_line = self.line_of(self.peek().span);
701
702 let items = if next_line > for_line {
703 let base_col = self.col_of(self.peek().span);
704 self.parse_indented_block_items(base_col)
705 } else {
706 let mut items = Vec::new();
708 if let Some(item) = self.parse_block_item() {
709 items.push(item);
710 }
711 items
712 };
713
714 let end = items
715 .last()
716 .map(|i| i.span)
717 .unwrap_or(start);
718
719 Some(BlockItem {
720 span: start.merge(end),
721 kind: BlockItemKind::ForBlock {
722 binding,
723 collection,
724 filter,
725 items,
726 },
727 })
728 }
729
730 fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
732 let mut items = Vec::new();
733 while !self.at_eof()
734 && !self.at(TokenKind::RBrace)
735 && self.col_of(self.peek().span) >= base_col
736 {
737 if let Some(item) = self.parse_block_item() {
738 items.push(item);
739 } else {
740 self.advance();
741 break;
742 }
743 }
744 items
745 }
746
747 fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
748 let name_tok = self.advance(); let name_text = self.text(name_tok.span).to_string();
750 self.advance(); let allows_binding = clause_allows_binding(&name_text);
753 let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
754 let value_span = value.span();
755
756 let kind = if is_clause_keyword(&name_text) {
757 BlockItemKind::Clause {
758 keyword: name_text,
759 value,
760 }
761 } else {
762 BlockItemKind::Assignment {
763 name: Ident {
764 span: name_tok.span,
765 name: name_text,
766 },
767 value,
768 }
769 };
770
771 Some(BlockItem {
772 span: start.merge(value_span),
773 kind,
774 })
775 }
776
777 fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
778 let saved_pos = self.pos;
782 let _name_tok = self.advance();
783 self.advance(); let mut depth = 1u32;
787 while !self.at_eof() && depth > 0 {
788 match self.peek_kind() {
789 TokenKind::LParen => {
790 depth += 1;
791 self.advance();
792 }
793 TokenKind::RParen => {
794 depth -= 1;
795 self.advance();
796 }
797 _ => {
798 self.advance();
799 }
800 }
801 }
802
803 if self.at(TokenKind::Colon) {
804 self.pos = saved_pos;
806 let name = self.parse_ident_in("derived value name")?;
807 self.expect(TokenKind::LParen)?;
808 let params = self.parse_ident_list()?;
809 self.expect(TokenKind::RParen)?;
810 self.expect(TokenKind::Colon)?;
811 let value = self.parse_clause_value(start)?;
812 Some(BlockItem {
813 span: start.merge(value.span()),
814 kind: BlockItemKind::ParamAssignment {
815 name,
816 params,
817 value,
818 },
819 })
820 } else {
821 self.pos = saved_pos;
823 if self.peek_at(1).kind == TokenKind::Colon {
825 }
827 self.parse_assign_or_clause_item(start)
829 }
830 }
831
832 fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
833 let mut params = Vec::new();
834 if !self.at(TokenKind::RParen) {
835 params.push(self.parse_ident_in("parameter name")?);
836 while self.eat(TokenKind::Comma).is_some() {
837 params.push(self.parse_ident_in("parameter name")?);
838 }
839 }
840 Some(params)
841 }
842
843 fn parse_clause_value_maybe_binding(
847 &mut self,
848 clause_start: Span,
849 allow_binding: bool,
850 ) -> Option<Expr> {
851 if allow_binding
852 && self.peek_kind().is_word()
853 && self.peek_at(1).kind == TokenKind::Colon
854 {
855 let clause_line = self.line_of(clause_start);
858 let next_line = self.line_of(self.peek().span);
859 let colon_is_block_item = next_line > clause_line
860 && self.peek_at(2).kind != TokenKind::Eof
861 && self.line_of(self.peek_at(2).span) == next_line;
862
863 if next_line == clause_line || colon_is_block_item {
864 let name = self.parse_ident_in("binding name")?;
865 self.advance(); let inner = self.parse_clause_value(clause_start)?;
867 return Some(Expr::Binding {
868 span: name.span.merge(inner.span()),
869 name,
870 value: Box::new(inner),
871 });
872 }
873 }
874 self.parse_clause_value(clause_start)
875 }
876
877 fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
880 let clause_line = self.line_of(clause_start);
881 let next = self.peek();
882 let next_line = self.line_of(next.span);
883
884 if next_line > clause_line {
885 let base_col = self.col_of(next.span);
887 self.parse_indented_block(base_col)
888 } else {
889 let expr = self.parse_expr(0)?;
891 self.maybe_wrap_suffix_predicate(expr, clause_line)
892 }
893 }
894
895 fn maybe_wrap_suffix_predicate(&mut self, expr: Expr, clause_line: u32) -> Option<Expr> {
898 if self.at_eof()
899 || self.at(TokenKind::RBrace)
900 || self.line_of(self.peek().span) != clause_line
901 {
902 return Some(expr);
903 }
904
905 let mut tail = Vec::new();
906 while !self.at_eof()
907 && !self.at(TokenKind::RBrace)
908 && self.line_of(self.peek().span) == clause_line
909 {
910 if let Some(e) = self.try_parse_expr(0) {
911 tail.push(e);
912 } else {
913 let tok = self.advance();
915 tail.push(Expr::Ident(Ident {
916 span: tok.span,
917 name: self.text(tok.span).to_string(),
918 }));
919 }
920 }
921
922 if tail.is_empty() {
923 Some(expr)
924 } else {
925 let end = tail.last().unwrap().span();
926 Some(Expr::Predicate {
927 span: expr.span().merge(end),
928 subject: Box::new(expr),
929 tail,
930 })
931 }
932 }
933
934 fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
937 let start = self.peek().span;
938 let mut items = Vec::new();
939
940 while !self.at_eof()
941 && !self.at(TokenKind::RBrace)
942 && self.col_of(self.peek().span) >= base_col
943 {
944 if self.at(TokenKind::Let) {
946 let let_start = self.advance().span;
947 if let Some(name) = self.parse_ident_in("binding name") {
948 if self.expect(TokenKind::Eq).is_some() {
949 if let Some(value) = self.parse_expr(0) {
950 items.push(Expr::LetExpr {
951 span: let_start.merge(value.span()),
952 name,
953 value: Box::new(value),
954 });
955 continue;
956 }
957 }
958 }
959 break;
960 }
961
962 if let Some(expr) = self.parse_expr(0) {
963 items.push(expr);
964 } else {
965 self.advance();
966 break;
967 }
968 }
969
970 if items.len() == 1 {
971 Some(items.pop().unwrap())
972 } else {
973 let end = items.last().map(|e| e.span()).unwrap_or(start);
974 Some(Expr::Block {
975 span: start.merge(end),
976 items,
977 })
978 }
979 }
980}
981
982const BP_LAMBDA: u8 = 4;
988const BP_WHEN_GUARD: u8 = 5;
989const BP_OR: u8 = 10;
990const BP_AND: u8 = 20;
991const BP_COMPARE: u8 = 30;
992const BP_TRANSITION: u8 = 32;
993const BP_WITH_WHERE: u8 = 35;
994const BP_PROJECTION: u8 = 37;
995const BP_NULL_COALESCE: u8 = 40;
996const BP_ADD: u8 = 50;
997const BP_MUL: u8 = 60;
998const BP_PIPE: u8 = 65;
999const BP_PREFIX: u8 = 70;
1000const BP_POSTFIX: u8 = 80;
1001
1002impl<'s> Parser<'s> {
1003 pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1004 let mut lhs = self.parse_prefix()?;
1005
1006 loop {
1007 if let Some((l_bp, r_bp)) = self.infix_bp() {
1008 if l_bp < min_bp {
1009 break;
1010 }
1011 lhs = self.parse_infix(lhs, r_bp)?;
1012 } else if let Some(l_bp) = self.postfix_bp() {
1013 if l_bp < min_bp {
1014 break;
1015 }
1016 lhs = self.parse_postfix(lhs)?;
1017 } else {
1018 break;
1019 }
1020 }
1021
1022 Some(lhs)
1023 }
1024
1025 fn try_parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1028 let saved_pos = self.pos;
1029 let saved_diag_count = self.diagnostics.len();
1030 match self.parse_expr(min_bp) {
1031 Some(expr) => Some(expr),
1032 None => {
1033 self.pos = saved_pos;
1034 self.diagnostics.truncate(saved_diag_count);
1035 None
1036 }
1037 }
1038 }
1039
1040 fn parse_prefix(&mut self) -> Option<Expr> {
1043 match self.peek_kind() {
1044 TokenKind::Not => {
1045 let start = self.advance().span;
1046 if self.at(TokenKind::Exists) {
1047 self.advance();
1048 let operand = self.parse_expr(BP_PREFIX)?;
1049 Some(Expr::NotExists {
1050 span: start.merge(operand.span()),
1051 operand: Box::new(operand),
1052 })
1053 } else {
1054 let operand = self.parse_expr(BP_PREFIX)?;
1055 Some(Expr::Not {
1056 span: start.merge(operand.span()),
1057 operand: Box::new(operand),
1058 })
1059 }
1060 }
1061 TokenKind::Exists => {
1062 let next = self.peek_at(1).kind;
1065 if matches!(
1066 next,
1067 TokenKind::RParen
1068 | TokenKind::RBrace
1069 | TokenKind::RBracket
1070 | TokenKind::Comma
1071 | TokenKind::Eof
1072 ) {
1073 let id = self.parse_ident()?;
1074 return Some(Expr::Ident(id));
1075 }
1076 let start = self.advance().span;
1077 let operand = self.parse_expr(BP_PREFIX)?;
1078 Some(Expr::Exists {
1079 span: start.merge(operand.span()),
1080 operand: Box::new(operand),
1081 })
1082 }
1083 TokenKind::If => self.parse_if_expr(),
1084 TokenKind::For => self.parse_for_expr(),
1085 TokenKind::LBrace => self.parse_brace_expr(),
1086 TokenKind::LBracket => self.parse_list_literal(),
1087 TokenKind::LParen => self.parse_paren_expr(),
1088 TokenKind::Number => {
1089 let t = self.advance();
1090 Some(Expr::NumberLiteral {
1091 span: t.span,
1092 value: self.text(t.span).to_string(),
1093 })
1094 }
1095 TokenKind::Duration => {
1096 let t = self.advance();
1097 Some(Expr::DurationLiteral {
1098 span: t.span,
1099 value: self.text(t.span).to_string(),
1100 })
1101 }
1102 TokenKind::String => {
1103 let sl = self.parse_string()?;
1104 Some(Expr::StringLiteral(sl))
1105 }
1106 TokenKind::True => {
1107 let t = self.advance();
1108 Some(Expr::BoolLiteral {
1109 span: t.span,
1110 value: true,
1111 })
1112 }
1113 TokenKind::False => {
1114 let t = self.advance();
1115 Some(Expr::BoolLiteral {
1116 span: t.span,
1117 value: false,
1118 })
1119 }
1120 TokenKind::Null => {
1121 let t = self.advance();
1122 Some(Expr::Null { span: t.span })
1123 }
1124 TokenKind::Now => {
1125 let t = self.advance();
1126 Some(Expr::Now { span: t.span })
1127 }
1128 TokenKind::This => {
1129 let t = self.advance();
1130 Some(Expr::This { span: t.span })
1131 }
1132 TokenKind::Within => {
1133 let t = self.advance();
1134 Some(Expr::Within { span: t.span })
1135 }
1136 k if k.is_word() => {
1137 let id = self.parse_ident()?;
1138 Some(Expr::Ident(id))
1139 }
1140 TokenKind::Minus => {
1141 let start = self.advance().span;
1143 let operand = self.parse_expr(BP_PREFIX)?;
1144 Some(Expr::BinaryOp {
1145 span: start.merge(operand.span()),
1146 left: Box::new(Expr::NumberLiteral {
1147 span: start,
1148 value: "0".into(),
1149 }),
1150 op: BinaryOp::Sub,
1151 right: Box::new(operand),
1152 })
1153 }
1154 _ => {
1155 self.error(
1156 self.peek().span,
1157 format!(
1158 "expected expression (identifier, number, string, true/false, null, \
1159 if/for/not/exists, '(', '{{', '['), found {}",
1160 self.peek_kind(),
1161 ),
1162 );
1163 None
1164 }
1165 }
1166 }
1167
1168 fn infix_bp(&self) -> Option<(u8, u8)> {
1171 match self.peek_kind() {
1172 TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1175 TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1176 TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1177 TokenKind::And => Some((BP_AND, BP_AND + 1)),
1178 TokenKind::Eq | TokenKind::EqEq | TokenKind::BangEq => {
1179 Some((BP_COMPARE, BP_COMPARE + 1))
1180 }
1181 TokenKind::Lt => {
1182 if self.pos > 0 {
1185 let prev = self.tokens[self.pos - 1];
1186 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1187 return None;
1188 }
1189 }
1190 Some((BP_COMPARE, BP_COMPARE + 1))
1191 }
1192 TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1193 Some((BP_COMPARE, BP_COMPARE + 1))
1194 }
1195 TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1196 TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1198 Some((BP_COMPARE, BP_COMPARE + 1))
1199 }
1200 TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1201 TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1202 TokenKind::Includes => Some((BP_COMPARE, BP_COMPARE + 1)),
1203 TokenKind::Excludes => Some((BP_COMPARE, BP_COMPARE + 1)),
1204 TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1205 TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1206 TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1207 TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1208 TokenKind::DotDot => Some((BP_ADD + 1, BP_ADD + 2)), TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1210 TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1211 _ => None,
1212 }
1213 }
1214
1215 fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1216 let op_tok = self.advance();
1217 match op_tok.kind {
1218 TokenKind::FatArrow => {
1219 let body = self.parse_expr(r_bp)?;
1220 Some(Expr::Lambda {
1221 span: lhs.span().merge(body.span()),
1222 param: Box::new(lhs),
1223 body: Box::new(body),
1224 })
1225 }
1226 TokenKind::Pipe => {
1227 let rhs = self.parse_expr(r_bp)?;
1228 Some(Expr::Pipe {
1229 span: lhs.span().merge(rhs.span()),
1230 left: Box::new(lhs),
1231 right: Box::new(rhs),
1232 })
1233 }
1234 TokenKind::Or => {
1235 let rhs = self.parse_expr(r_bp)?;
1236 Some(Expr::LogicalOp {
1237 span: lhs.span().merge(rhs.span()),
1238 left: Box::new(lhs),
1239 op: LogicalOp::Or,
1240 right: Box::new(rhs),
1241 })
1242 }
1243 TokenKind::And => {
1244 let rhs = self.parse_expr(r_bp)?;
1245 Some(Expr::LogicalOp {
1246 span: lhs.span().merge(rhs.span()),
1247 left: Box::new(lhs),
1248 op: LogicalOp::And,
1249 right: Box::new(rhs),
1250 })
1251 }
1252 TokenKind::Eq | TokenKind::EqEq => {
1253 let rhs = self.parse_expr(r_bp)?;
1254 Some(Expr::Comparison {
1255 span: lhs.span().merge(rhs.span()),
1256 left: Box::new(lhs),
1257 op: ComparisonOp::Eq,
1258 right: Box::new(rhs),
1259 })
1260 }
1261 TokenKind::BangEq => {
1262 let rhs = self.parse_expr(r_bp)?;
1263 Some(Expr::Comparison {
1264 span: lhs.span().merge(rhs.span()),
1265 left: Box::new(lhs),
1266 op: ComparisonOp::NotEq,
1267 right: Box::new(rhs),
1268 })
1269 }
1270 TokenKind::Lt => {
1271 let rhs = self.parse_expr(r_bp)?;
1272 Some(Expr::Comparison {
1273 span: lhs.span().merge(rhs.span()),
1274 left: Box::new(lhs),
1275 op: ComparisonOp::Lt,
1276 right: Box::new(rhs),
1277 })
1278 }
1279 TokenKind::LtEq => {
1280 let rhs = self.parse_expr(r_bp)?;
1281 Some(Expr::Comparison {
1282 span: lhs.span().merge(rhs.span()),
1283 left: Box::new(lhs),
1284 op: ComparisonOp::LtEq,
1285 right: Box::new(rhs),
1286 })
1287 }
1288 TokenKind::Gt => {
1289 let rhs = self.parse_expr(r_bp)?;
1290 Some(Expr::Comparison {
1291 span: lhs.span().merge(rhs.span()),
1292 left: Box::new(lhs),
1293 op: ComparisonOp::Gt,
1294 right: Box::new(rhs),
1295 })
1296 }
1297 TokenKind::GtEq => {
1298 let rhs = self.parse_expr(r_bp)?;
1299 Some(Expr::Comparison {
1300 span: lhs.span().merge(rhs.span()),
1301 left: Box::new(lhs),
1302 op: ComparisonOp::GtEq,
1303 right: Box::new(rhs),
1304 })
1305 }
1306 TokenKind::In => {
1307 let rhs = self.parse_expr(r_bp)?;
1308 Some(Expr::In {
1309 span: lhs.span().merge(rhs.span()),
1310 element: Box::new(lhs),
1311 collection: Box::new(rhs),
1312 })
1313 }
1314 TokenKind::Not => {
1315 self.expect(TokenKind::In)?;
1317 let rhs = self.parse_expr(r_bp)?;
1318 Some(Expr::NotIn {
1319 span: lhs.span().merge(rhs.span()),
1320 element: Box::new(lhs),
1321 collection: Box::new(rhs),
1322 })
1323 }
1324 TokenKind::Where => {
1325 let rhs = self.parse_expr(r_bp)?;
1326 Some(Expr::Where {
1327 span: lhs.span().merge(rhs.span()),
1328 source: Box::new(lhs),
1329 condition: Box::new(rhs),
1330 })
1331 }
1332 TokenKind::With => {
1333 let rhs = self.parse_expr(r_bp)?;
1334 Some(Expr::With {
1335 span: lhs.span().merge(rhs.span()),
1336 source: Box::new(lhs),
1337 predicate: Box::new(rhs),
1338 })
1339 }
1340 TokenKind::QuestionQuestion => {
1341 let rhs = self.parse_expr(r_bp)?;
1342 Some(Expr::NullCoalesce {
1343 span: lhs.span().merge(rhs.span()),
1344 left: Box::new(lhs),
1345 right: Box::new(rhs),
1346 })
1347 }
1348 TokenKind::Plus => {
1349 let rhs = self.parse_expr(r_bp)?;
1350 Some(Expr::BinaryOp {
1351 span: lhs.span().merge(rhs.span()),
1352 left: Box::new(lhs),
1353 op: BinaryOp::Add,
1354 right: Box::new(rhs),
1355 })
1356 }
1357 TokenKind::Minus => {
1358 let rhs = self.parse_expr(r_bp)?;
1359 Some(Expr::BinaryOp {
1360 span: lhs.span().merge(rhs.span()),
1361 left: Box::new(lhs),
1362 op: BinaryOp::Sub,
1363 right: Box::new(rhs),
1364 })
1365 }
1366 TokenKind::Star => {
1367 let rhs = self.parse_expr(r_bp)?;
1368 Some(Expr::BinaryOp {
1369 span: lhs.span().merge(rhs.span()),
1370 left: Box::new(lhs),
1371 op: BinaryOp::Mul,
1372 right: Box::new(rhs),
1373 })
1374 }
1375 TokenKind::Slash => {
1376 if let Expr::Ident(ref id) = lhs {
1381 if self.peek_kind().is_word() {
1382 let next_text = self.text(self.peek().span);
1383 let is_qualified = next_text
1384 .chars()
1385 .next()
1386 .is_some_and(|c| c.is_uppercase())
1387 || matches!(
1388 self.peek_kind(),
1389 TokenKind::Config | TokenKind::Entity | TokenKind::Value
1390 );
1391 if is_qualified {
1392 let name_tok = self.advance();
1393 return Some(Expr::QualifiedName(QualifiedName {
1394 span: lhs.span().merge(name_tok.span),
1395 qualifier: Some(id.name.clone()),
1396 name: self.text(name_tok.span).to_string(),
1397 }));
1398 }
1399 }
1400 }
1401 let rhs = self.parse_expr(r_bp)?;
1402 Some(Expr::BinaryOp {
1403 span: lhs.span().merge(rhs.span()),
1404 left: Box::new(lhs),
1405 op: BinaryOp::Div,
1406 right: Box::new(rhs),
1407 })
1408 }
1409 TokenKind::ThinArrow => {
1410 let field = self.parse_ident_in("projection field")?;
1411 Some(Expr::ProjectionMap {
1412 span: lhs.span().merge(field.span),
1413 source: Box::new(lhs),
1414 field,
1415 })
1416 }
1417 TokenKind::TransitionsTo => {
1418 let rhs = self.parse_expr(r_bp)?;
1419 Some(Expr::TransitionsTo {
1420 span: lhs.span().merge(rhs.span()),
1421 subject: Box::new(lhs),
1422 new_state: Box::new(rhs),
1423 })
1424 }
1425 TokenKind::Becomes => {
1426 let rhs = self.parse_expr(r_bp)?;
1427 Some(Expr::Becomes {
1428 span: lhs.span().merge(rhs.span()),
1429 subject: Box::new(lhs),
1430 new_state: Box::new(rhs),
1431 })
1432 }
1433 TokenKind::Includes => {
1434 let rhs = self.parse_expr(r_bp)?;
1435 Some(Expr::Includes {
1436 span: lhs.span().merge(rhs.span()),
1437 collection: Box::new(lhs),
1438 element: Box::new(rhs),
1439 })
1440 }
1441 TokenKind::Excludes => {
1442 let rhs = self.parse_expr(r_bp)?;
1443 Some(Expr::Excludes {
1444 span: lhs.span().merge(rhs.span()),
1445 collection: Box::new(lhs),
1446 element: Box::new(rhs),
1447 })
1448 }
1449 TokenKind::When => {
1450 let rhs = self.parse_expr(r_bp)?;
1452 Some(Expr::WhenGuard {
1453 span: lhs.span().merge(rhs.span()),
1454 action: Box::new(lhs),
1455 condition: Box::new(rhs),
1456 })
1457 }
1458 TokenKind::DotDot => {
1459 let rhs = self.parse_expr(r_bp)?;
1460 Some(Expr::Range {
1461 span: lhs.span().merge(rhs.span()),
1462 start: Box::new(lhs),
1463 end: Box::new(rhs),
1464 })
1465 }
1466 _ => {
1467 self.error(
1468 op_tok.span,
1469 format!("unexpected infix operator {}", op_tok.kind),
1470 );
1471 None
1472 }
1473 }
1474 }
1475
1476 fn postfix_bp(&self) -> Option<u8> {
1479 match self.peek_kind() {
1480 TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
1481 TokenKind::QuestionMark => Some(BP_POSTFIX),
1482 TokenKind::Lt => {
1485 if self.pos > 0 {
1486 let prev = self.tokens[self.pos - 1];
1487 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1490 return Some(BP_POSTFIX);
1491 }
1492 }
1493 None
1494 }
1495 TokenKind::LParen => Some(BP_POSTFIX),
1496 TokenKind::LBrace => {
1497 let next = self.peek();
1503 let prev_end = if self.pos > 0 {
1504 self.tokens[self.pos - 1].span.end
1505 } else {
1506 0
1507 };
1508 if self.line_of(Span::new(prev_end, prev_end))
1510 == self.line_of(next.span)
1511 {
1512 Some(BP_POSTFIX)
1513 } else {
1514 None
1515 }
1516 }
1517 _ => None,
1518 }
1519 }
1520
1521 fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
1522 match self.peek_kind() {
1523 TokenKind::QuestionMark => {
1524 let end = self.advance().span;
1525 Some(Expr::TypeOptional {
1526 span: lhs.span().merge(end),
1527 inner: Box::new(lhs),
1528 })
1529 }
1530 TokenKind::Lt => {
1531 self.advance(); let mut args = Vec::new();
1534 while !self.at(TokenKind::Gt) && !self.at_eof() {
1536 args.push(self.parse_expr(BP_COMPARE + 1)?);
1537 self.eat(TokenKind::Comma);
1538 }
1539 let end = self.expect(TokenKind::Gt)?.span;
1540 Some(Expr::GenericType {
1541 span: lhs.span().merge(end),
1542 name: Box::new(lhs),
1543 args,
1544 })
1545 }
1546 TokenKind::Dot => {
1547 self.advance();
1548 let field = self.parse_ident_in("field name")?;
1549 Some(Expr::MemberAccess {
1550 span: lhs.span().merge(field.span),
1551 object: Box::new(lhs),
1552 field,
1553 })
1554 }
1555 TokenKind::QuestionDot => {
1556 self.advance();
1557 let field = self.parse_ident_in("field name")?;
1558 Some(Expr::OptionalAccess {
1559 span: lhs.span().merge(field.span),
1560 object: Box::new(lhs),
1561 field,
1562 })
1563 }
1564 TokenKind::LParen => {
1565 self.advance();
1566 let args = self.parse_call_args()?;
1567 let end = self.expect(TokenKind::RParen)?.span;
1568 Some(Expr::Call {
1569 span: lhs.span().merge(end),
1570 function: Box::new(lhs),
1571 args,
1572 })
1573 }
1574 TokenKind::LBrace => {
1575 self.advance();
1576 let fields = self.parse_join_fields()?;
1577 let end = self.expect(TokenKind::RBrace)?.span;
1578 Some(Expr::JoinLookup {
1579 span: lhs.span().merge(end),
1580 entity: Box::new(lhs),
1581 fields,
1582 })
1583 }
1584 _ => None,
1585 }
1586 }
1587
1588 fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
1591 let mut args = Vec::new();
1592 while !self.at(TokenKind::RParen) && !self.at_eof() {
1593 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1595 let name = self.parse_ident_in("argument name")?;
1596 self.advance(); let value = self.parse_expr(0)?;
1598 args.push(CallArg::Named(NamedArg {
1599 span: name.span.merge(value.span()),
1600 name,
1601 value,
1602 }));
1603 } else {
1604 let expr = self.parse_expr(0)?;
1605 args.push(CallArg::Positional(expr));
1606 }
1607 self.eat(TokenKind::Comma);
1608 }
1609 Some(args)
1610 }
1611
1612 fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
1615 let mut fields = Vec::new();
1616 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1617 let field = self.parse_ident_in("join field name")?;
1618 let value = if self.eat(TokenKind::Colon).is_some() {
1619 Some(self.parse_expr(0)?)
1620 } else {
1621 None
1622 };
1623 fields.push(JoinField {
1624 span: field.span.merge(
1625 value
1626 .as_ref()
1627 .map(|v| v.span())
1628 .unwrap_or(field.span),
1629 ),
1630 field,
1631 value,
1632 });
1633 self.eat(TokenKind::Comma);
1634 }
1635 Some(fields)
1636 }
1637
1638 fn parse_if_expr(&mut self) -> Option<Expr> {
1641 let start = self.advance().span; let mut branches = Vec::new();
1643
1644 let condition = self.parse_expr(0)?;
1646 self.expect(TokenKind::Colon)?;
1647 let body = self.parse_branch_body(start)?;
1648 branches.push(CondBranch {
1649 span: start.merge(body.span()),
1650 condition,
1651 body,
1652 });
1653
1654 let mut else_body = None;
1656 while self.at(TokenKind::Else) {
1657 let else_tok = self.advance();
1658 if self.at(TokenKind::If) {
1659 let if_start = self.advance().span;
1660 let cond = self.parse_expr(0)?;
1661 self.expect(TokenKind::Colon)?;
1662 let body = self.parse_branch_body(else_tok.span)?;
1663 branches.push(CondBranch {
1664 span: if_start.merge(body.span()),
1665 condition: cond,
1666 body,
1667 });
1668 } else {
1669 self.expect(TokenKind::Colon)?;
1670 let body = self.parse_branch_body(else_tok.span)?;
1671 else_body = Some(Box::new(body));
1672 break;
1673 }
1674 }
1675
1676 let end = else_body
1677 .as_ref()
1678 .map(|b| b.span())
1679 .or_else(|| branches.last().map(|b| b.body.span()))
1680 .unwrap_or(start);
1681
1682 Some(Expr::Conditional {
1683 span: start.merge(end),
1684 branches,
1685 else_body,
1686 })
1687 }
1688
1689 fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
1690 let keyword_line = self.line_of(keyword_span);
1691 let next_line = self.line_of(self.peek().span);
1692
1693 if next_line > keyword_line {
1694 let base_col = self.col_of(self.peek().span);
1695 self.parse_indented_block(base_col)
1696 } else {
1697 self.parse_expr(0)
1698 }
1699 }
1700
1701 fn parse_for_expr(&mut self) -> Option<Expr> {
1704 let start = self.advance().span; let binding = self.parse_ident_in("loop variable")?;
1706 self.expect(TokenKind::In)?;
1707
1708 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1710
1711 let filter = if self.eat(TokenKind::Where).is_some() {
1712 Some(Box::new(self.parse_expr(0)?))
1714 } else {
1715 None
1716 };
1717
1718 self.expect(TokenKind::Colon)?;
1719 let body = self.parse_branch_body(start)?;
1720
1721 Some(Expr::For {
1722 span: start.merge(body.span()),
1723 binding,
1724 collection: Box::new(collection),
1725 filter,
1726 body: Box::new(body),
1727 })
1728 }
1729
1730 fn parse_brace_expr(&mut self) -> Option<Expr> {
1733 let start = self.advance().span; if self.at(TokenKind::RBrace) {
1736 let end = self.advance().span;
1737 return Some(Expr::SetLiteral {
1738 span: start.merge(end),
1739 elements: Vec::new(),
1740 });
1741 }
1742
1743 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1745 return self.parse_object_literal(start);
1746 }
1747
1748 self.parse_set_literal(start)
1750 }
1751
1752 fn parse_list_literal(&mut self) -> Option<Expr> {
1753 let start = self.advance().span; let mut elements = Vec::new();
1755 while !self.at(TokenKind::RBracket) && !self.at_eof() {
1756 elements.push(self.parse_expr(0)?);
1757 self.eat(TokenKind::Comma);
1758 }
1759 let end = self.expect(TokenKind::RBracket)?.span;
1760 Some(Expr::ListLiteral {
1761 span: start.merge(end),
1762 elements,
1763 })
1764 }
1765
1766 fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
1767 let mut fields = Vec::new();
1768 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1769 let name = self.parse_ident_in("field name")?;
1770 self.expect(TokenKind::Colon)?;
1771 let value = self.parse_expr(0)?;
1772 fields.push(NamedArg {
1773 span: name.span.merge(value.span()),
1774 name,
1775 value,
1776 });
1777 self.eat(TokenKind::Comma);
1778 }
1779 let end = self.expect(TokenKind::RBrace)?.span;
1780 Some(Expr::ObjectLiteral {
1781 span: start.merge(end),
1782 fields,
1783 })
1784 }
1785
1786 fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
1787 let mut elements = Vec::new();
1788 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1789 elements.push(self.parse_expr(0)?);
1790 self.eat(TokenKind::Comma);
1791 }
1792 let end = self.expect(TokenKind::RBrace)?.span;
1793 Some(Expr::SetLiteral {
1794 span: start.merge(end),
1795 elements,
1796 })
1797 }
1798
1799 fn parse_paren_expr(&mut self) -> Option<Expr> {
1802 self.advance(); let expr = self.parse_expr(0)?;
1804 self.expect(TokenKind::RParen)?;
1805 Some(expr)
1806 }
1807}
1808
1809#[cfg(test)]
1814mod tests {
1815 use super::*;
1816 use crate::diagnostic::Severity;
1817
1818 fn parse_ok(src: &str) -> ParseResult {
1819 let owned;
1822 let input = if src.starts_with("-- allium:") {
1823 src
1824 } else {
1825 owned = format!("-- allium: 1\n{src}");
1826 &owned
1827 };
1828 let result = parse(input);
1829 if !result.diagnostics.is_empty() {
1830 for d in &result.diagnostics {
1831 eprintln!(
1832 " [{:?}] {} ({}..{})",
1833 d.severity, d.message, d.span.start, d.span.end
1834 );
1835 }
1836 }
1837 result
1838 }
1839
1840 #[test]
1841 fn version_marker() {
1842 let r = parse_ok("-- allium: 1\n");
1843 assert_eq!(r.module.version, Some(1));
1844 assert_eq!(r.diagnostics.len(), 0);
1845 }
1846
1847 #[test]
1848 fn version_missing_warns() {
1849 let r = parse("entity User {}");
1850 assert_eq!(r.module.version, None);
1851 assert_eq!(r.diagnostics.len(), 1);
1852 assert_eq!(r.diagnostics[0].severity, Severity::Warning);
1853 assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
1854 }
1855
1856 #[test]
1857 fn version_unsupported_errors() {
1858 let r = parse("-- allium: 99\nentity User {}");
1859 assert_eq!(r.module.version, Some(99));
1860 assert!(r.diagnostics.iter().any(|d|
1861 d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
1862 ), "expected unsupported version error, got: {:?}", r.diagnostics);
1863 }
1864
1865 #[test]
1866 fn empty_entity() {
1867 let r = parse_ok("entity User {}");
1868 assert_eq!(r.diagnostics.len(), 0);
1869 assert_eq!(r.module.declarations.len(), 1);
1870 match &r.module.declarations[0] {
1871 Decl::Block(b) => {
1872 assert_eq!(b.kind, BlockKind::Entity);
1873 assert_eq!(b.name.as_ref().unwrap().name, "User");
1874 }
1875 other => panic!("expected Block, got {other:?}"),
1876 }
1877 }
1878
1879 #[test]
1880 fn entity_with_fields() {
1881 let src = r#"entity Order {
1882 customer: Customer
1883 status: pending | active | completed
1884 total: Decimal
1885}"#;
1886 let r = parse_ok(src);
1887 assert_eq!(r.diagnostics.len(), 0);
1888 match &r.module.declarations[0] {
1889 Decl::Block(b) => {
1890 assert_eq!(b.items.len(), 3);
1891 }
1892 other => panic!("expected Block, got {other:?}"),
1893 }
1894 }
1895
1896 #[test]
1897 fn use_declaration() {
1898 let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
1899 assert_eq!(r.diagnostics.len(), 0);
1900 match &r.module.declarations[0] {
1901 Decl::Use(u) => {
1902 assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
1903 }
1904 other => panic!("expected Use, got {other:?}"),
1905 }
1906 }
1907
1908 #[test]
1909 fn enum_declaration() {
1910 let src = "enum OrderStatus { pending | shipped | delivered }";
1911 let r = parse_ok(src);
1912 assert_eq!(r.diagnostics.len(), 0);
1913 }
1914
1915 #[test]
1916 fn config_block() {
1917 let src = r#"config {
1918 max_retries: Integer = 3
1919 timeout: Duration = 24.hours
1920}"#;
1921 let r = parse_ok(src);
1926 assert_eq!(r.diagnostics.len(), 0);
1927 }
1928
1929 #[test]
1930 fn rule_declaration() {
1931 let src = r#"rule PlaceOrder {
1932 when: CustomerPlacesOrder(customer, items, total)
1933 requires: total > 0
1934 ensures: Order.created(customer: customer, status: pending, total: total)
1935}"#;
1936 let r = parse_ok(src);
1937 assert_eq!(r.diagnostics.len(), 0);
1938 match &r.module.declarations[0] {
1939 Decl::Block(b) => {
1940 assert_eq!(b.kind, BlockKind::Rule);
1941 assert_eq!(b.items.len(), 3);
1942 }
1943 other => panic!("expected Block, got {other:?}"),
1944 }
1945 }
1946
1947 #[test]
1948 fn expression_precedence() {
1949 let r = parse_ok("rule T { v: a + b * c }");
1950 match &r.module.declarations[0] {
1952 Decl::Block(b) => match &b.items[0].kind {
1953 BlockItemKind::Assignment { value, .. } => match value {
1954 Expr::BinaryOp { op, right, .. } => {
1955 assert_eq!(*op, BinaryOp::Add);
1956 assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
1957 }
1958 other => panic!("expected BinaryOp, got {other:?}"),
1959 },
1960 other => panic!("expected Assignment, got {other:?}"),
1961 },
1962 other => panic!("expected Block, got {other:?}"),
1963 }
1964 }
1965
1966 #[test]
1967 fn default_declaration() {
1968 let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
1969 let r = parse_ok(src);
1970 assert_eq!(r.diagnostics.len(), 0);
1971 }
1972
1973 #[test]
1974 fn open_question() {
1975 let src = r#"open question "Should admins be role-specific?""#;
1976 let r = parse_ok(src);
1977 assert_eq!(r.diagnostics.len(), 0);
1978 }
1979
1980 #[test]
1981 fn external_entity() {
1982 let src = "external entity Customer { email: String }";
1983 let r = parse_ok(src);
1984 assert_eq!(r.diagnostics.len(), 0);
1985 match &r.module.declarations[0] {
1986 Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
1987 other => panic!("expected Block, got {other:?}"),
1988 }
1989 }
1990
1991 #[test]
1992 fn where_expression() {
1993 let src = "entity E { active: items where status = active }";
1994 let r = parse_ok(src);
1995 assert_eq!(r.diagnostics.len(), 0);
1996 }
1997
1998 #[test]
1999 fn with_expression() {
2000 let src = "entity E { slots: InterviewSlot with candidacy = this }";
2001 let r = parse_ok(src);
2002 assert_eq!(r.diagnostics.len(), 0);
2003 }
2004
2005 #[test]
2006 fn lambda_expression() {
2007 let src = "entity E { v: items.any(i => i.active) }";
2008 let r = parse_ok(src);
2009 assert_eq!(r.diagnostics.len(), 0);
2010 }
2011
2012 #[test]
2013 fn deferred() {
2014 let src = "deferred InterviewerMatching.suggest";
2015 let r = parse_ok(src);
2016 assert_eq!(r.diagnostics.len(), 0);
2017 }
2018
2019 #[test]
2020 fn variant_declaration() {
2021 let src = "variant Email : Notification { subject: String }";
2022 let r = parse_ok(src);
2023 assert_eq!(r.diagnostics.len(), 0);
2024 }
2025
2026 #[test]
2029 fn projection_arrow() {
2030 let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2031 let r = parse_ok(src);
2032 assert_eq!(r.diagnostics.len(), 0);
2033 }
2034
2035 #[test]
2038 fn transitions_to_trigger() {
2039 let src = "rule R { when: Interview.status transitions_to scheduled\n ensures: Notification.created() }";
2040 let r = parse_ok(src);
2041 assert_eq!(r.diagnostics.len(), 0);
2042 }
2043
2044 #[test]
2045 fn becomes_trigger() {
2046 let src = "rule R { when: Interview.status becomes scheduled\n ensures: Notification.created() }";
2047 let r = parse_ok(src);
2048 assert_eq!(r.diagnostics.len(), 0);
2049 }
2050
2051 #[test]
2054 fn when_binding() {
2055 let src = "rule R {\n when: interview: Interview.status transitions_to scheduled\n ensures: Notification.created()\n}";
2056 let r = parse_ok(src);
2057 assert_eq!(r.diagnostics.len(), 0);
2058 let decl = &r.module.declarations[0];
2060 if let Decl::Block(b) = decl {
2061 if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2062 assert_eq!(keyword, "when");
2063 assert!(matches!(value, Expr::Binding { .. }));
2064 } else {
2065 panic!("expected clause");
2066 }
2067 } else {
2068 panic!("expected block decl");
2069 }
2070 }
2071
2072 #[test]
2073 fn when_binding_temporal() {
2074 let src = "rule R {\n when: invitation: Invitation.expires_at <= now\n ensures: Invitation.expired()\n}";
2075 let r = parse_ok(src);
2076 assert_eq!(r.diagnostics.len(), 0);
2077 }
2078
2079 #[test]
2080 fn when_binding_created() {
2081 let src = "rule R {\n when: batch: DigestBatch.created\n ensures: Email.created()\n}";
2082 let r = parse_ok(src);
2083 assert_eq!(r.diagnostics.len(), 0);
2084 }
2085
2086 #[test]
2087 fn facing_binding() {
2088 let src = "surface S {\n facing viewer: Interviewer\n exposes: InterviewList\n}";
2089 let r = parse_ok(src);
2090 assert_eq!(r.diagnostics.len(), 0);
2091 }
2092
2093 #[test]
2094 fn context_binding() {
2095 let src = "surface S {\n facing viewer: Interviewer\n context assignment: SlotConfirmation where interviewer = viewer\n}";
2096 let r = parse_ok(src);
2097 assert_eq!(r.diagnostics.len(), 0);
2098 }
2099
2100 #[test]
2103 fn rule_level_for() {
2104 let src = r#"rule ProcessDigests {
2105 when: schedule: DigestSchedule.next_run_at <= now
2106 for user in Users where notification_setting.digest_enabled:
2107 ensures: DigestBatch.created(user: user)
2108}"#;
2109 let r = parse_ok(src);
2110 assert_eq!(r.diagnostics.len(), 0);
2111 if let Decl::Block(b) = &r.module.declarations[0] {
2112 assert!(b.items.len() >= 2);
2114 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2115 } else {
2116 panic!("expected block decl");
2117 }
2118 }
2119
2120 #[test]
2123 fn let_in_ensures_block() {
2124 let src = r#"rule R {
2125 when: ScheduleInterview(candidacy, time, interviewers)
2126 ensures:
2127 let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2128 for interviewer in interviewers:
2129 SlotConfirmation.created(slot: slot, interviewer: interviewer)
2130}"#;
2131 let r = parse_ok(src);
2132 assert_eq!(r.diagnostics.len(), 0);
2133 }
2134
2135 #[test]
2138 fn provides_when_guard() {
2139 let src = "surface S {\n facing viewer: Interviewer\n provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2140 let r = parse_ok(src);
2141 assert_eq!(r.diagnostics.len(), 0);
2142 }
2143
2144 #[test]
2147 fn optional_type_suffix() {
2148 let src = "entity E { locked_until: Timestamp? }";
2149 let r = parse_ok(src);
2150 assert_eq!(r.diagnostics.len(), 0);
2151 }
2152
2153 #[test]
2154 fn optional_trigger_param() {
2155 let src = "rule R { when: Report(interviewer, interview, reason, details?)\n ensures: Done() }";
2156 let r = parse_ok(src);
2157 assert_eq!(r.diagnostics.len(), 0);
2158 }
2159
2160 #[test]
2163 fn qualified_config_access() {
2164 let src = "entity E { duration: oauth/config.session_duration }";
2165 let r = parse_ok(src);
2166 assert_eq!(r.diagnostics.len(), 0);
2167 }
2168
2169 #[test]
2172 fn realistic_spec() {
2173 let src = r#"-- allium: 1
2174
2175enum OrderStatus { pending | shipped | delivered }
2176
2177external entity Customer {
2178 email: String
2179 name: String
2180}
2181
2182entity Order {
2183 customer: Customer
2184 status: OrderStatus
2185 total: Decimal
2186 items: OrderItem with order = this
2187 shipped_items: items where status = shipped
2188 confirmed_items: items where status = confirmed -> item
2189 is_complete: status = delivered
2190 locked_until: Timestamp?
2191}
2192
2193config {
2194 max_retries: Integer = 3
2195 timeout: Duration = 24.hours
2196}
2197
2198rule PlaceOrder {
2199 when: CustomerPlacesOrder(customer, items, total)
2200 requires: total > 0
2201 ensures: Order.created(customer: customer, status: pending, total: total)
2202}
2203
2204rule ShipOrder {
2205 when: order: Order.status transitions_to shipped
2206 ensures: Email.created(to: order.customer.email, template: order_shipped)
2207}
2208
2209open question "How do we handle partial shipments?"
2210"#;
2211 let r = parse_ok(src);
2212 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2213 assert_eq!(r.module.version, Some(1));
2214 assert_eq!(r.module.declarations.len(), 7);
2215 }
2216
2217 #[test]
2218 fn extension_behaviour_excerpt() {
2219 let src = r#"value Document {
2222 uri: String
2223 text: String
2224}
2225
2226entity Finding {
2227 code: String
2228 severity: error | warning | info
2229 range: FindingRange
2230}
2231
2232entity DiagnosticsMode {
2233 value: strict | relaxed
2234}
2235
2236config {
2237 duplicateKey: String = "allium.config.duplicateKey"
2238}
2239
2240rule RefreshDiagnostics {
2241 when: DocumentOpened(document) or DocumentChanged(document)
2242 requires: document.language_id = "allium"
2243 ensures: FindingsComputed(document)
2244}
2245
2246surface DiagnosticsDashboard {
2247 facing viewer: Developer
2248 context doc: Document where viewer.active_document = doc
2249 provides: RunChecks(viewer) when doc.language_id = "allium"
2250 exposes: FindingList
2251}
2252
2253rule ProcessDigests {
2254 when: schedule: DigestSchedule.next_run_at <= now
2255 for user in Users where notification_setting.digest_enabled:
2256 let settings = user.notification_setting
2257 ensures: DigestBatch.created(user: user)
2258}
2259"#;
2260 let r = parse_ok(src);
2261 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2262 assert_eq!(r.module.declarations.len(), 7);
2264 }
2265
2266 #[test]
2267 fn suffix_predicate_simple() {
2268 let src = r#"rule R {
2269 when: RenameRequested(symbol)
2270 requires: symbol resolves_to_single_definition
2271 ensures: RenameApplied()
2272}"#;
2273 let r = parse_ok(src);
2274 assert_eq!(r.diagnostics.len(), 0);
2275 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2276 assert_eq!(b.items.len(), 3);
2278 let BlockItemKind::Clause { keyword, value } = &b.items[1].kind else { panic!() };
2280 assert_eq!(keyword, "requires");
2281 assert!(matches!(value, Expr::Predicate { .. }));
2282 }
2283
2284 #[test]
2285 fn suffix_predicate_with_arg() {
2286 let src = r#"rule R {
2287 when: X()
2288 requires: finding.code starts_with "allium."
2289}"#;
2290 let r = parse_ok(src);
2291 assert_eq!(r.diagnostics.len(), 0);
2292 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2293 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
2294 if let Expr::Predicate { subject, tail, .. } = value {
2295 assert!(matches!(subject.as_ref(), Expr::MemberAccess { .. }));
2296 assert_eq!(tail.len(), 2); } else {
2298 panic!("expected Predicate, got {:?}", value);
2299 }
2300 }
2301
2302 #[test]
2303 fn suffix_predicate_with_comparison() {
2304 let src = r#"rule R {
2305 when: X()
2306 requires: clause compares_literals = true
2307}"#;
2308 let r = parse_ok(src);
2309 assert_eq!(r.diagnostics.len(), 0);
2310 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2311 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
2312 if let Expr::Predicate { tail, .. } = value {
2314 assert_eq!(tail.len(), 1);
2315 assert!(matches!(tail[0], Expr::Comparison { .. }));
2316 } else {
2317 panic!("expected Predicate");
2318 }
2319 }
2320
2321 #[test]
2322 fn range_literal_in_list() {
2323 let src = r#"rule R {
2324 when: X()
2325 requires: width in [1..8]
2326}"#;
2327 let r = parse_ok(src);
2328 assert_eq!(r.diagnostics.len(), 0);
2329 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2330 let BlockItemKind::Clause { value, .. } = &b.items[1].kind else { panic!() };
2331 if let Expr::In { collection, .. } = value {
2333 if let Expr::ListLiteral { elements, .. } = collection.as_ref() {
2334 assert_eq!(elements.len(), 1);
2335 assert!(matches!(elements[0], Expr::Range { .. }));
2336 } else {
2337 panic!("expected ListLiteral");
2338 }
2339 } else {
2340 panic!("expected In");
2341 }
2342 }
2343
2344 #[test]
2345 fn exists_as_identifier() {
2346 let src = r#"rule R {
2347 when: X()
2348 ensures: CompletionItemAvailable(label: exists)
2349}"#;
2350 let r = parse_ok(src);
2351 assert_eq!(r.diagnostics.len(), 0);
2352 }
2353
2354 #[test]
2357 fn pipe_binds_tighter_than_or() {
2358 let src = "entity E { v: a or b | c }";
2360 let r = parse_ok(src);
2361 assert_eq!(r.diagnostics.len(), 0);
2362 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2363 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2364 let Expr::LogicalOp { op, right, .. } = value else {
2366 panic!("expected LogicalOp, got {value:?}");
2367 };
2368 assert_eq!(*op, LogicalOp::Or);
2369 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2371 }
2372
2373 #[test]
2376 fn variant_with_pipe_base() {
2377 let src = "variant Mixed : TypeA | TypeB";
2378 let r = parse_ok(src);
2379 assert_eq!(r.diagnostics.len(), 0);
2380 let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2381 assert!(matches!(v.base, Expr::Pipe { .. }));
2382 }
2383
2384 #[test]
2387 fn trigger_clause() {
2388 let src = "rule R { trigger: ExternalEvent(data)\n ensures: Done() }";
2389 let r = parse_ok(src);
2390 assert_eq!(r.diagnostics.len(), 0);
2391 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2392 let BlockItemKind::Clause { keyword, .. } = &b.items[0].kind else { panic!() };
2393 assert_eq!(keyword, "trigger");
2394 }
2395
2396 #[test]
2399 fn for_block_where_comparison() {
2400 let src = r#"rule R {
2401 when: X()
2402 for item in Items where item.status = active:
2403 ensures: Processed(item: item)
2404}"#;
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::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2409 assert!(filter.is_some());
2410 assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2411 }
2412
2413 #[test]
2416 fn for_expr_where_comparison() {
2417 let src = r#"rule R {
2418 when: X()
2419 ensures:
2420 for item in Items where item.active = true:
2421 Processed(item: item)
2422}"#;
2423 let r = parse_ok(src);
2424 assert_eq!(r.diagnostics.len(), 0);
2425 }
2426
2427 #[test]
2430 fn if_else_if_else() {
2431 let src = r#"rule R {
2432 when: X(v)
2433 ensures:
2434 if v < 10: Small()
2435 else if v < 100: Medium()
2436 else: Large()
2437}"#;
2438 let r = parse_ok(src);
2439 assert_eq!(r.diagnostics.len(), 0);
2440 }
2441
2442 #[test]
2445 fn null_coalesce_and_optional_chain() {
2446 let src = "entity E { v: a?.b ?? fallback }";
2447 let r = parse_ok(src);
2448 assert_eq!(r.diagnostics.len(), 0);
2449 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2450 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2451 assert!(matches!(value, Expr::NullCoalesce { .. }));
2453 }
2454
2455 #[test]
2458 fn generic_type_nested() {
2459 let src = "entity E { v: List<Set<String>> }";
2460 let r = parse_ok(src);
2461 assert_eq!(r.diagnostics.len(), 0);
2462 }
2463
2464 #[test]
2467 fn collection_literals() {
2468 let src = r#"rule R {
2469 when: X()
2470 ensures:
2471 let s = {a, b, c}
2472 let l = [1, 2, 3]
2473 let o = {name: "test", count: 42}
2474 Done()
2475}"#;
2476 let r = parse_ok(src);
2477 assert_eq!(r.diagnostics.len(), 0);
2478 }
2479
2480 #[test]
2483 fn given_block() {
2484 let src = "given { viewer: User\n time: Timestamp }";
2485 let r = parse_ok(src);
2486 assert_eq!(r.diagnostics.len(), 0);
2487 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2488 assert_eq!(b.kind, BlockKind::Given);
2489 assert!(b.name.is_none());
2490 }
2491
2492 #[test]
2495 fn actor_block() {
2496 let src = "actor Admin { identified_by: User where role = admin }";
2497 let r = parse_ok(src);
2498 assert_eq!(r.diagnostics.len(), 0);
2499 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2500 assert_eq!(b.kind, BlockKind::Actor);
2501 }
2502
2503 #[test]
2506 fn join_lookup() {
2507 let src = "entity E { match: Other{field_a, field_b: value} }";
2508 let r = parse_ok(src);
2509 assert_eq!(r.diagnostics.len(), 0);
2510 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2511 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2512 assert!(matches!(value, Expr::JoinLookup { .. }));
2513 }
2514
2515 #[test]
2518 fn includes_excludes() {
2519 let src = r#"rule R {
2520 when: X(a, b)
2521 requires: a.items includes b
2522 requires: a.items excludes b
2523 ensures: Done()
2524}"#;
2525 let r = parse_ok(src);
2526 assert_eq!(r.diagnostics.len(), 0);
2527 }
2528
2529 #[test]
2532 fn in_not_in_set() {
2533 let src = r#"rule R {
2534 when: X(s)
2535 requires: s in {a, b, c}
2536 requires: s not in {d, e}
2537 ensures: Done()
2538}"#;
2539 let r = parse_ok(src);
2540 assert_eq!(r.diagnostics.len(), 0);
2541 }
2542
2543 #[test]
2546 fn comprehensive_fixture() {
2547 let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
2548 let r = parse(src);
2549 assert_eq!(
2550 r.diagnostics.len(),
2551 0,
2552 "expected no errors in comprehensive fixture, got: {:?}",
2553 r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
2554 );
2555 assert!(r.module.declarations.len() > 30, "expected many declarations");
2556 }
2557
2558 #[test]
2561 fn error_expected_declaration() {
2562 let r = parse("-- allium: 1\n+ invalid");
2563 assert!(r.diagnostics.len() >= 1);
2564 let msg = &r.diagnostics[0].message;
2565 assert!(msg.contains("expected declaration"), "got: {msg}");
2566 assert!(msg.contains("entity"), "should list valid options, got: {msg}");
2567 assert!(msg.contains("rule"), "should list valid options, got: {msg}");
2568 }
2569
2570 #[test]
2571 fn error_expected_expression() {
2572 let r = parse("-- allium: 1\nentity E { v: }");
2573 assert!(r.diagnostics.len() >= 1);
2574 let msg = &r.diagnostics[0].message;
2575 assert!(msg.contains("expected expression"), "got: {msg}");
2576 assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
2577 }
2578
2579 #[test]
2580 fn error_expected_block_item() {
2581 let r = parse("-- allium: 1\nentity E { + }");
2582 assert!(r.diagnostics.len() >= 1);
2583 let msg = &r.diagnostics[0].message;
2584 assert!(msg.contains("expected block item"), "got: {msg}");
2585 }
2586
2587 #[test]
2588 fn error_expected_identifier() {
2589 let r = parse("-- allium: 1\nentity 123 {}");
2590 assert!(r.diagnostics.len() >= 1);
2591 let msg = &r.diagnostics[0].message;
2592 assert!(msg.contains("expected entity name"), "got: {msg}");
2594 assert!(msg.contains("number"), "should say what was found, got: {msg}");
2596 }
2597
2598 #[test]
2599 fn error_missing_brace() {
2600 let r = parse("entity E {");
2601 assert!(r.diagnostics.len() >= 1);
2602 let msg = &r.diagnostics[0].message;
2603 assert!(msg.contains("expected"), "got: {msg}");
2604 }
2605
2606 #[test]
2607 fn error_recovery_multiple() {
2608 let r = parse("entity E { + }\nentity F { - }");
2610 assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
2611 }
2612}