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() || self.eat(TokenKind::With).is_some()
700 {
701 Some(self.parse_expr(0)?)
704 } else {
705 None
706 };
707
708 self.expect(TokenKind::Colon)?;
709
710 let for_line = self.line_of(start);
712 let next_line = self.line_of(self.peek().span);
713
714 let items = if next_line > for_line {
715 let base_col = self.col_of(self.peek().span);
716 self.parse_indented_block_items(base_col)
717 } else {
718 let mut items = Vec::new();
720 if let Some(item) = self.parse_block_item() {
721 items.push(item);
722 }
723 items
724 };
725
726 let end = items
727 .last()
728 .map(|i| i.span)
729 .unwrap_or(start);
730
731 Some(BlockItem {
732 span: start.merge(end),
733 kind: BlockItemKind::ForBlock {
734 binding,
735 collection,
736 filter,
737 items,
738 },
739 })
740 }
741
742 fn parse_indented_block_items(&mut self, base_col: u32) -> Vec<BlockItem> {
744 let mut items = Vec::new();
745 while !self.at_eof()
746 && !self.at(TokenKind::RBrace)
747 && self.col_of(self.peek().span) >= base_col
748 {
749 if let Some(item) = self.parse_block_item() {
750 items.push(item);
751 } else {
752 self.advance();
753 break;
754 }
755 }
756 items
757 }
758
759 fn parse_if_block_item(&mut self, start: Span) -> Option<BlockItem> {
761 self.advance(); let mut branches = Vec::new();
763
764 let condition = self.parse_expr(0)?;
766 self.expect(TokenKind::Colon)?;
767 let if_line = self.line_of(start);
768 let items = self.parse_if_block_body(if_line);
769 branches.push(CondBlockBranch {
770 span: start.merge(items.last().map(|i| i.span).unwrap_or(start)),
771 condition,
772 items,
773 });
774
775 let mut else_items = None;
777 while self.at(TokenKind::Else) {
778 let else_tok = self.advance();
779 if self.at(TokenKind::If) {
780 let if_start = self.advance().span;
781 let cond = self.parse_expr(0)?;
782 self.expect(TokenKind::Colon)?;
783 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
784 branches.push(CondBlockBranch {
785 span: if_start.merge(body_items.last().map(|i| i.span).unwrap_or(if_start)),
786 condition: cond,
787 items: body_items,
788 });
789 } else {
790 self.expect(TokenKind::Colon)?;
791 let body_items = self.parse_if_block_body(self.line_of(else_tok.span));
792 else_items = Some(body_items);
793 break;
794 }
795 }
796
797 let end = else_items
798 .as_ref()
799 .and_then(|items| items.last().map(|i| i.span))
800 .or_else(|| branches.last().and_then(|b| b.items.last().map(|i| i.span)))
801 .unwrap_or(start);
802
803 Some(BlockItem {
804 span: start.merge(end),
805 kind: BlockItemKind::IfBlock {
806 branches,
807 else_items,
808 },
809 })
810 }
811
812 fn parse_if_block_body(&mut self, keyword_line: u32) -> Vec<BlockItem> {
814 let next_line = self.line_of(self.peek().span);
815 if next_line > keyword_line {
816 let base_col = self.col_of(self.peek().span);
817 self.parse_indented_block_items(base_col)
818 } else {
819 let mut items = Vec::new();
821 if let Some(item) = self.parse_block_item() {
822 items.push(item);
823 }
824 items
825 }
826 }
827
828 fn parse_assign_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
829 let name_tok = self.advance(); let name_text = self.text(name_tok.span).to_string();
831 self.advance(); let allows_binding = clause_allows_binding(&name_text);
834 let value = self.parse_clause_value_maybe_binding(start, allows_binding)?;
835 let value_span = value.span();
836
837 let kind = if is_clause_keyword(&name_text) {
838 BlockItemKind::Clause {
839 keyword: name_text,
840 value,
841 }
842 } else {
843 BlockItemKind::Assignment {
844 name: Ident {
845 span: name_tok.span,
846 name: name_text,
847 },
848 value,
849 }
850 };
851
852 Some(BlockItem {
853 span: start.merge(value_span),
854 kind,
855 })
856 }
857
858 fn parse_path_assignment_item(&mut self, start: Span) -> Option<BlockItem> {
860 let obj_tok = self.advance(); self.advance(); let field = self.parse_ident_in("field name")?;
863 self.advance(); let path = Expr::MemberAccess {
866 span: obj_tok.span.merge(field.span),
867 object: Box::new(Expr::Ident(Ident {
868 span: obj_tok.span,
869 name: self.text(obj_tok.span).to_string(),
870 })),
871 field,
872 };
873
874 let value = self.parse_clause_value(start)?;
875 let value_span = value.span();
876 Some(BlockItem {
877 span: start.merge(value_span),
878 kind: BlockItemKind::PathAssignment { path, value },
879 })
880 }
881
882 fn parse_param_or_clause_item(&mut self, start: Span) -> Option<BlockItem> {
883 let saved_pos = self.pos;
887 let _name_tok = self.advance();
888 self.advance(); let mut depth = 1u32;
892 while !self.at_eof() && depth > 0 {
893 match self.peek_kind() {
894 TokenKind::LParen => {
895 depth += 1;
896 self.advance();
897 }
898 TokenKind::RParen => {
899 depth -= 1;
900 self.advance();
901 }
902 _ => {
903 self.advance();
904 }
905 }
906 }
907
908 if self.at(TokenKind::Colon) {
909 self.pos = saved_pos;
911 let name = self.parse_ident_in("derived value name")?;
912 self.expect(TokenKind::LParen)?;
913 let params = self.parse_ident_list()?;
914 self.expect(TokenKind::RParen)?;
915 self.expect(TokenKind::Colon)?;
916 let value = self.parse_clause_value(start)?;
917 Some(BlockItem {
918 span: start.merge(value.span()),
919 kind: BlockItemKind::ParamAssignment {
920 name,
921 params,
922 value,
923 },
924 })
925 } else {
926 self.pos = saved_pos;
928 if self.peek_at(1).kind == TokenKind::Colon {
930 }
932 self.parse_assign_or_clause_item(start)
934 }
935 }
936
937 fn parse_ident_list(&mut self) -> Option<Vec<Ident>> {
938 let mut params = Vec::new();
939 if !self.at(TokenKind::RParen) {
940 params.push(self.parse_ident_in("parameter name")?);
941 while self.eat(TokenKind::Comma).is_some() {
942 params.push(self.parse_ident_in("parameter name")?);
943 }
944 }
945 Some(params)
946 }
947
948 fn parse_for_binding(&mut self) -> Option<ForBinding> {
950 if self.at(TokenKind::LParen) {
951 let start = self.advance().span; let mut idents = Vec::new();
953 idents.push(self.parse_ident_in("loop variable")?);
954 while self.eat(TokenKind::Comma).is_some() {
955 idents.push(self.parse_ident_in("loop variable")?);
956 }
957 let end = self.expect(TokenKind::RParen)?.span;
958 Some(ForBinding::Destructured(idents, start.merge(end)))
959 } else {
960 let ident = self.parse_ident_in("loop variable")?;
961 Some(ForBinding::Single(ident))
962 }
963 }
964
965 fn parse_clause_value_maybe_binding(
969 &mut self,
970 clause_start: Span,
971 allow_binding: bool,
972 ) -> Option<Expr> {
973 if allow_binding
974 && self.peek_kind().is_word()
975 && self.peek_at(1).kind == TokenKind::Colon
976 {
977 let clause_line = self.line_of(clause_start);
980 let next_line = self.line_of(self.peek().span);
981 let colon_is_block_item = next_line > clause_line
982 && self.peek_at(2).kind != TokenKind::Eof
983 && self.line_of(self.peek_at(2).span) == next_line;
984
985 if next_line == clause_line || colon_is_block_item {
986 let name = self.parse_ident_in("binding name")?;
987 self.advance(); let inner = self.parse_clause_value(clause_start)?;
989 return Some(Expr::Binding {
990 span: name.span.merge(inner.span()),
991 name,
992 value: Box::new(inner),
993 });
994 }
995 }
996 self.parse_clause_value(clause_start)
997 }
998
999 fn parse_clause_value(&mut self, clause_start: Span) -> Option<Expr> {
1002 let clause_line = self.line_of(clause_start);
1003 let next = self.peek();
1004 let next_line = self.line_of(next.span);
1005
1006 if next_line > clause_line {
1007 let base_col = self.col_of(next.span);
1012 let clause_col = self.col_of(clause_start);
1013 if base_col <= clause_col {
1014 return Some(Expr::Block {
1015 span: clause_start,
1016 items: Vec::new(),
1017 });
1018 }
1019 self.parse_indented_block(base_col)
1020 } else {
1021 self.parse_expr(0)
1023 }
1024 }
1025
1026 fn parse_indented_block(&mut self, base_col: u32) -> Option<Expr> {
1029 let start = self.peek().span;
1030 let mut items = Vec::new();
1031
1032 while !self.at_eof()
1033 && !self.at(TokenKind::RBrace)
1034 && self.col_of(self.peek().span) >= base_col
1035 {
1036 if self.at(TokenKind::Let) {
1038 let let_start = self.advance().span;
1039 if let Some(name) = self.parse_ident_in("binding name") {
1040 if self.expect(TokenKind::Eq).is_some() {
1041 if let Some(value) = self.parse_expr(0) {
1042 items.push(Expr::LetExpr {
1043 span: let_start.merge(value.span()),
1044 name,
1045 value: Box::new(value),
1046 });
1047 continue;
1048 }
1049 }
1050 }
1051 break;
1052 }
1053
1054 if let Some(expr) = self.parse_expr(0) {
1055 items.push(expr);
1056 } else {
1057 self.advance();
1058 break;
1059 }
1060 }
1061
1062 if items.len() == 1 {
1063 Some(items.pop().unwrap())
1064 } else {
1065 let end = items.last().map(|e| e.span()).unwrap_or(start);
1066 Some(Expr::Block {
1067 span: start.merge(end),
1068 items,
1069 })
1070 }
1071 }
1072}
1073
1074const BP_LAMBDA: u8 = 4;
1080const BP_WHEN_GUARD: u8 = 5;
1081const BP_OR: u8 = 10;
1082const BP_AND: u8 = 20;
1083const BP_COMPARE: u8 = 30;
1084const BP_TRANSITION: u8 = 32;
1085const BP_WITH_WHERE: u8 = 35;
1086const BP_PROJECTION: u8 = 37;
1087const BP_NULL_COALESCE: u8 = 40;
1088const BP_ADD: u8 = 50;
1089const BP_MUL: u8 = 60;
1090const BP_PIPE: u8 = 65;
1091const BP_PREFIX: u8 = 70;
1092const BP_POSTFIX: u8 = 80;
1093
1094impl<'s> Parser<'s> {
1095 pub fn parse_expr(&mut self, min_bp: u8) -> Option<Expr> {
1096 let mut lhs = self.parse_prefix()?;
1097
1098 loop {
1099 if let Some((l_bp, r_bp)) = self.infix_bp() {
1100 if l_bp < min_bp {
1101 break;
1102 }
1103 lhs = self.parse_infix(lhs, r_bp)?;
1104 } else if let Some(l_bp) = self.postfix_bp() {
1105 if l_bp < min_bp {
1106 break;
1107 }
1108 lhs = self.parse_postfix(lhs)?;
1109 } else {
1110 break;
1111 }
1112 }
1113
1114 Some(lhs)
1115 }
1116
1117 fn parse_prefix(&mut self) -> Option<Expr> {
1120 match self.peek_kind() {
1121 TokenKind::Not => {
1122 let start = self.advance().span;
1123 if self.at(TokenKind::Exists) {
1124 self.advance();
1125 let operand = self.parse_expr(BP_PREFIX)?;
1126 Some(Expr::NotExists {
1127 span: start.merge(operand.span()),
1128 operand: Box::new(operand),
1129 })
1130 } else {
1131 let operand = self.parse_expr(BP_PREFIX)?;
1132 Some(Expr::Not {
1133 span: start.merge(operand.span()),
1134 operand: Box::new(operand),
1135 })
1136 }
1137 }
1138 TokenKind::Exists => {
1139 let next = self.peek_at(1).kind;
1142 if matches!(
1143 next,
1144 TokenKind::RParen
1145 | TokenKind::RBrace
1146 | TokenKind::RBracket
1147 | TokenKind::Comma
1148 | TokenKind::Eof
1149 ) {
1150 let id = self.parse_ident()?;
1151 return Some(Expr::Ident(id));
1152 }
1153 let start = self.advance().span;
1154 let operand = self.parse_expr(BP_PREFIX)?;
1155 Some(Expr::Exists {
1156 span: start.merge(operand.span()),
1157 operand: Box::new(operand),
1158 })
1159 }
1160 TokenKind::If => self.parse_if_expr(),
1161 TokenKind::For => self.parse_for_expr(),
1162 TokenKind::LBrace => self.parse_brace_expr(),
1163 TokenKind::LBracket => self.parse_list_literal(),
1164 TokenKind::LParen => self.parse_paren_expr(),
1165 TokenKind::Number => {
1166 let t = self.advance();
1167 Some(Expr::NumberLiteral {
1168 span: t.span,
1169 value: self.text(t.span).to_string(),
1170 })
1171 }
1172 TokenKind::Duration => {
1173 let t = self.advance();
1174 Some(Expr::DurationLiteral {
1175 span: t.span,
1176 value: self.text(t.span).to_string(),
1177 })
1178 }
1179 TokenKind::String => {
1180 let sl = self.parse_string()?;
1181 Some(Expr::StringLiteral(sl))
1182 }
1183 TokenKind::True => {
1184 let t = self.advance();
1185 Some(Expr::BoolLiteral {
1186 span: t.span,
1187 value: true,
1188 })
1189 }
1190 TokenKind::False => {
1191 let t = self.advance();
1192 Some(Expr::BoolLiteral {
1193 span: t.span,
1194 value: false,
1195 })
1196 }
1197 TokenKind::Null => {
1198 let t = self.advance();
1199 Some(Expr::Null { span: t.span })
1200 }
1201 TokenKind::Now => {
1202 let t = self.advance();
1203 Some(Expr::Now { span: t.span })
1204 }
1205 TokenKind::This => {
1206 let t = self.advance();
1207 Some(Expr::This { span: t.span })
1208 }
1209 TokenKind::Within => {
1210 let t = self.advance();
1211 Some(Expr::Within { span: t.span })
1212 }
1213 k if k.is_word() => {
1214 let id = self.parse_ident()?;
1215 Some(Expr::Ident(id))
1216 }
1217 TokenKind::Star => {
1218 let t = self.advance();
1220 Some(Expr::Ident(Ident {
1221 span: t.span,
1222 name: "*".into(),
1223 }))
1224 }
1225 TokenKind::Minus => {
1226 let start = self.advance().span;
1228 let operand = self.parse_expr(BP_PREFIX)?;
1229 Some(Expr::BinaryOp {
1230 span: start.merge(operand.span()),
1231 left: Box::new(Expr::NumberLiteral {
1232 span: start,
1233 value: "0".into(),
1234 }),
1235 op: BinaryOp::Sub,
1236 right: Box::new(operand),
1237 })
1238 }
1239 _ => {
1240 self.error(
1241 self.peek().span,
1242 format!(
1243 "expected expression (identifier, number, string, true/false, null, \
1244 if/for/not/exists, '(', '{{', '['), found {}",
1245 self.peek_kind(),
1246 ),
1247 );
1248 None
1249 }
1250 }
1251 }
1252
1253 fn infix_bp(&self) -> Option<(u8, u8)> {
1256 match self.peek_kind() {
1257 TokenKind::FatArrow => Some((BP_LAMBDA, BP_LAMBDA - 1)), TokenKind::When => Some((BP_WHEN_GUARD, BP_WHEN_GUARD + 1)),
1260 TokenKind::Pipe => Some((BP_PIPE, BP_PIPE + 1)),
1261 TokenKind::Or => Some((BP_OR, BP_OR + 1)),
1262 TokenKind::And => Some((BP_AND, BP_AND + 1)),
1263 TokenKind::Eq | TokenKind::BangEq => {
1264 Some((BP_COMPARE, BP_COMPARE + 1))
1265 }
1266 TokenKind::Lt => {
1267 if self.pos > 0 {
1270 let prev = self.tokens[self.pos - 1];
1271 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1272 return None;
1273 }
1274 }
1275 Some((BP_COMPARE, BP_COMPARE + 1))
1276 }
1277 TokenKind::LtEq | TokenKind::Gt | TokenKind::GtEq => {
1278 Some((BP_COMPARE, BP_COMPARE + 1))
1279 }
1280 TokenKind::In => Some((BP_COMPARE, BP_COMPARE + 1)),
1281 TokenKind::Not if self.peek_at(1).kind == TokenKind::In => {
1283 Some((BP_COMPARE, BP_COMPARE + 1))
1284 }
1285 TokenKind::TransitionsTo => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1286 TokenKind::Becomes => Some((BP_TRANSITION, BP_TRANSITION + 1)),
1287 TokenKind::Where => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1288 TokenKind::With => Some((BP_WITH_WHERE, BP_WITH_WHERE + 1)),
1289 TokenKind::ThinArrow => Some((BP_PROJECTION, BP_PROJECTION + 1)),
1290 TokenKind::QuestionQuestion => Some((BP_NULL_COALESCE, BP_NULL_COALESCE + 1)),
1291 TokenKind::Plus | TokenKind::Minus => Some((BP_ADD, BP_ADD + 1)),
1292 TokenKind::Star | TokenKind::Slash => Some((BP_MUL, BP_MUL + 1)),
1293 _ => None,
1294 }
1295 }
1296
1297 fn parse_infix(&mut self, lhs: Expr, r_bp: u8) -> Option<Expr> {
1298 let op_tok = self.advance();
1299 match op_tok.kind {
1300 TokenKind::FatArrow => {
1301 let body = self.parse_expr(r_bp)?;
1302 Some(Expr::Lambda {
1303 span: lhs.span().merge(body.span()),
1304 param: Box::new(lhs),
1305 body: Box::new(body),
1306 })
1307 }
1308 TokenKind::Pipe => {
1309 let rhs = self.parse_expr(r_bp)?;
1310 Some(Expr::Pipe {
1311 span: lhs.span().merge(rhs.span()),
1312 left: Box::new(lhs),
1313 right: Box::new(rhs),
1314 })
1315 }
1316 TokenKind::Or => {
1317 let rhs = self.parse_expr(r_bp)?;
1318 Some(Expr::LogicalOp {
1319 span: lhs.span().merge(rhs.span()),
1320 left: Box::new(lhs),
1321 op: LogicalOp::Or,
1322 right: Box::new(rhs),
1323 })
1324 }
1325 TokenKind::And => {
1326 let rhs = self.parse_expr(r_bp)?;
1327 Some(Expr::LogicalOp {
1328 span: lhs.span().merge(rhs.span()),
1329 left: Box::new(lhs),
1330 op: LogicalOp::And,
1331 right: Box::new(rhs),
1332 })
1333 }
1334 TokenKind::Eq => {
1335 let rhs = self.parse_expr(r_bp)?;
1336 Some(Expr::Comparison {
1337 span: lhs.span().merge(rhs.span()),
1338 left: Box::new(lhs),
1339 op: ComparisonOp::Eq,
1340 right: Box::new(rhs),
1341 })
1342 }
1343 TokenKind::BangEq => {
1344 let rhs = self.parse_expr(r_bp)?;
1345 Some(Expr::Comparison {
1346 span: lhs.span().merge(rhs.span()),
1347 left: Box::new(lhs),
1348 op: ComparisonOp::NotEq,
1349 right: Box::new(rhs),
1350 })
1351 }
1352 TokenKind::Lt => {
1353 let rhs = self.parse_expr(r_bp)?;
1354 Some(Expr::Comparison {
1355 span: lhs.span().merge(rhs.span()),
1356 left: Box::new(lhs),
1357 op: ComparisonOp::Lt,
1358 right: Box::new(rhs),
1359 })
1360 }
1361 TokenKind::LtEq => {
1362 let rhs = self.parse_expr(r_bp)?;
1363 Some(Expr::Comparison {
1364 span: lhs.span().merge(rhs.span()),
1365 left: Box::new(lhs),
1366 op: ComparisonOp::LtEq,
1367 right: Box::new(rhs),
1368 })
1369 }
1370 TokenKind::Gt => {
1371 let rhs = self.parse_expr(r_bp)?;
1372 Some(Expr::Comparison {
1373 span: lhs.span().merge(rhs.span()),
1374 left: Box::new(lhs),
1375 op: ComparisonOp::Gt,
1376 right: Box::new(rhs),
1377 })
1378 }
1379 TokenKind::GtEq => {
1380 let rhs = self.parse_expr(r_bp)?;
1381 Some(Expr::Comparison {
1382 span: lhs.span().merge(rhs.span()),
1383 left: Box::new(lhs),
1384 op: ComparisonOp::GtEq,
1385 right: Box::new(rhs),
1386 })
1387 }
1388 TokenKind::In => {
1389 let rhs = self.parse_expr(r_bp)?;
1390 Some(Expr::In {
1391 span: lhs.span().merge(rhs.span()),
1392 element: Box::new(lhs),
1393 collection: Box::new(rhs),
1394 })
1395 }
1396 TokenKind::Not => {
1397 self.expect(TokenKind::In)?;
1399 let rhs = self.parse_expr(r_bp)?;
1400 Some(Expr::NotIn {
1401 span: lhs.span().merge(rhs.span()),
1402 element: Box::new(lhs),
1403 collection: Box::new(rhs),
1404 })
1405 }
1406 TokenKind::Where => {
1407 let rhs = self.parse_expr(r_bp)?;
1408 Some(Expr::Where {
1409 span: lhs.span().merge(rhs.span()),
1410 source: Box::new(lhs),
1411 condition: Box::new(rhs),
1412 })
1413 }
1414 TokenKind::With => {
1415 let rhs = self.parse_expr(r_bp)?;
1416 Some(Expr::With {
1417 span: lhs.span().merge(rhs.span()),
1418 source: Box::new(lhs),
1419 predicate: Box::new(rhs),
1420 })
1421 }
1422 TokenKind::QuestionQuestion => {
1423 let rhs = self.parse_expr(r_bp)?;
1424 Some(Expr::NullCoalesce {
1425 span: lhs.span().merge(rhs.span()),
1426 left: Box::new(lhs),
1427 right: Box::new(rhs),
1428 })
1429 }
1430 TokenKind::Plus => {
1431 let rhs = self.parse_expr(r_bp)?;
1432 Some(Expr::BinaryOp {
1433 span: lhs.span().merge(rhs.span()),
1434 left: Box::new(lhs),
1435 op: BinaryOp::Add,
1436 right: Box::new(rhs),
1437 })
1438 }
1439 TokenKind::Minus => {
1440 let rhs = self.parse_expr(r_bp)?;
1441 Some(Expr::BinaryOp {
1442 span: lhs.span().merge(rhs.span()),
1443 left: Box::new(lhs),
1444 op: BinaryOp::Sub,
1445 right: Box::new(rhs),
1446 })
1447 }
1448 TokenKind::Star => {
1449 let rhs = self.parse_expr(r_bp)?;
1450 Some(Expr::BinaryOp {
1451 span: lhs.span().merge(rhs.span()),
1452 left: Box::new(lhs),
1453 op: BinaryOp::Mul,
1454 right: Box::new(rhs),
1455 })
1456 }
1457 TokenKind::Slash => {
1458 if let Expr::Ident(ref id) = lhs {
1463 if self.peek_kind().is_word() {
1464 let next_text = self.text(self.peek().span);
1465 let is_qualified = next_text
1466 .chars()
1467 .next()
1468 .is_some_and(|c| c.is_uppercase())
1469 || matches!(
1470 self.peek_kind(),
1471 TokenKind::Config | TokenKind::Entity | TokenKind::Value
1472 );
1473 if is_qualified {
1474 let name_tok = self.advance();
1475 return Some(Expr::QualifiedName(QualifiedName {
1476 span: lhs.span().merge(name_tok.span),
1477 qualifier: Some(id.name.clone()),
1478 name: self.text(name_tok.span).to_string(),
1479 }));
1480 }
1481 }
1482 }
1483 let rhs = self.parse_expr(r_bp)?;
1484 Some(Expr::BinaryOp {
1485 span: lhs.span().merge(rhs.span()),
1486 left: Box::new(lhs),
1487 op: BinaryOp::Div,
1488 right: Box::new(rhs),
1489 })
1490 }
1491 TokenKind::ThinArrow => {
1492 let field = self.parse_ident_in("projection field")?;
1493 Some(Expr::ProjectionMap {
1494 span: lhs.span().merge(field.span),
1495 source: Box::new(lhs),
1496 field,
1497 })
1498 }
1499 TokenKind::TransitionsTo => {
1500 let rhs = self.parse_expr(r_bp)?;
1501 Some(Expr::TransitionsTo {
1502 span: lhs.span().merge(rhs.span()),
1503 subject: Box::new(lhs),
1504 new_state: Box::new(rhs),
1505 })
1506 }
1507 TokenKind::Becomes => {
1508 let rhs = self.parse_expr(r_bp)?;
1509 Some(Expr::Becomes {
1510 span: lhs.span().merge(rhs.span()),
1511 subject: Box::new(lhs),
1512 new_state: Box::new(rhs),
1513 })
1514 }
1515 TokenKind::When => {
1516 let rhs = self.parse_expr(r_bp)?;
1518 Some(Expr::WhenGuard {
1519 span: lhs.span().merge(rhs.span()),
1520 action: Box::new(lhs),
1521 condition: Box::new(rhs),
1522 })
1523 }
1524 _ => {
1525 self.error(
1526 op_tok.span,
1527 format!("unexpected infix operator {}", op_tok.kind),
1528 );
1529 None
1530 }
1531 }
1532 }
1533
1534 fn postfix_bp(&self) -> Option<u8> {
1537 match self.peek_kind() {
1538 TokenKind::Dot | TokenKind::QuestionDot => Some(BP_POSTFIX),
1539 TokenKind::QuestionMark => Some(BP_POSTFIX),
1540 TokenKind::Lt => {
1543 if self.pos > 0 {
1544 let prev = self.tokens[self.pos - 1];
1545 if prev.span.end == self.peek().span.start && prev.kind.is_word() {
1548 return Some(BP_POSTFIX);
1549 }
1550 }
1551 None
1552 }
1553 TokenKind::LParen => Some(BP_POSTFIX),
1554 TokenKind::LBrace => {
1555 let next = self.peek();
1561 let prev_end = if self.pos > 0 {
1562 self.tokens[self.pos - 1].span.end
1563 } else {
1564 0
1565 };
1566 if self.line_of(Span::new(prev_end, prev_end))
1568 == self.line_of(next.span)
1569 {
1570 Some(BP_POSTFIX)
1571 } else {
1572 None
1573 }
1574 }
1575 _ => None,
1576 }
1577 }
1578
1579 fn parse_postfix(&mut self, lhs: Expr) -> Option<Expr> {
1580 match self.peek_kind() {
1581 TokenKind::QuestionMark => {
1582 let end = self.advance().span;
1583 Some(Expr::TypeOptional {
1584 span: lhs.span().merge(end),
1585 inner: Box::new(lhs),
1586 })
1587 }
1588 TokenKind::Lt => {
1589 self.advance(); let mut args = Vec::new();
1592 while !self.at(TokenKind::Gt) && !self.at_eof() {
1594 args.push(self.parse_expr(BP_COMPARE + 1)?);
1595 self.eat(TokenKind::Comma);
1596 }
1597 let end = self.expect(TokenKind::Gt)?.span;
1598 Some(Expr::GenericType {
1599 span: lhs.span().merge(end),
1600 name: Box::new(lhs),
1601 args,
1602 })
1603 }
1604 TokenKind::Dot => {
1605 self.advance();
1606 let field = self.parse_ident_in("field name")?;
1607 Some(Expr::MemberAccess {
1608 span: lhs.span().merge(field.span),
1609 object: Box::new(lhs),
1610 field,
1611 })
1612 }
1613 TokenKind::QuestionDot => {
1614 self.advance();
1615 let field = self.parse_ident_in("field name")?;
1616 Some(Expr::OptionalAccess {
1617 span: lhs.span().merge(field.span),
1618 object: Box::new(lhs),
1619 field,
1620 })
1621 }
1622 TokenKind::LParen => {
1623 self.advance();
1624 let args = self.parse_call_args()?;
1625 let end = self.expect(TokenKind::RParen)?.span;
1626 Some(Expr::Call {
1627 span: lhs.span().merge(end),
1628 function: Box::new(lhs),
1629 args,
1630 })
1631 }
1632 TokenKind::LBrace => {
1633 self.advance();
1634 let fields = self.parse_join_fields()?;
1635 let end = self.expect(TokenKind::RBrace)?.span;
1636 Some(Expr::JoinLookup {
1637 span: lhs.span().merge(end),
1638 entity: Box::new(lhs),
1639 fields,
1640 })
1641 }
1642 _ => None,
1643 }
1644 }
1645
1646 fn parse_call_args(&mut self) -> Option<Vec<CallArg>> {
1649 let mut args = Vec::new();
1650 while !self.at(TokenKind::RParen) && !self.at_eof() {
1651 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1653 let name = self.parse_ident_in("argument name")?;
1654 self.advance(); let value = self.parse_expr(0)?;
1656 args.push(CallArg::Named(NamedArg {
1657 span: name.span.merge(value.span()),
1658 name,
1659 value,
1660 }));
1661 } else {
1662 let expr = self.parse_expr(0)?;
1663 args.push(CallArg::Positional(expr));
1664 }
1665 self.eat(TokenKind::Comma);
1666 }
1667 Some(args)
1668 }
1669
1670 fn parse_join_fields(&mut self) -> Option<Vec<JoinField>> {
1673 let mut fields = Vec::new();
1674 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1675 let field = self.parse_ident_in("join field name")?;
1676 let value = if self.eat(TokenKind::Colon).is_some() {
1677 Some(self.parse_expr(0)?)
1678 } else {
1679 None
1680 };
1681 fields.push(JoinField {
1682 span: field.span.merge(
1683 value
1684 .as_ref()
1685 .map(|v| v.span())
1686 .unwrap_or(field.span),
1687 ),
1688 field,
1689 value,
1690 });
1691 self.eat(TokenKind::Comma);
1692 }
1693 Some(fields)
1694 }
1695
1696 fn parse_if_expr(&mut self) -> Option<Expr> {
1699 let start = self.advance().span; let mut branches = Vec::new();
1701
1702 let condition = self.parse_expr(0)?;
1704 self.expect(TokenKind::Colon)?;
1705 let body = self.parse_branch_body(start)?;
1706 branches.push(CondBranch {
1707 span: start.merge(body.span()),
1708 condition,
1709 body,
1710 });
1711
1712 let mut else_body = None;
1714 while self.at(TokenKind::Else) {
1715 let else_tok = self.advance();
1716 if self.at(TokenKind::If) {
1717 let if_start = self.advance().span;
1718 let cond = self.parse_expr(0)?;
1719 self.expect(TokenKind::Colon)?;
1720 let body = self.parse_branch_body(else_tok.span)?;
1721 branches.push(CondBranch {
1722 span: if_start.merge(body.span()),
1723 condition: cond,
1724 body,
1725 });
1726 } else {
1727 self.expect(TokenKind::Colon)?;
1728 let body = self.parse_branch_body(else_tok.span)?;
1729 else_body = Some(Box::new(body));
1730 break;
1731 }
1732 }
1733
1734 let end = else_body
1735 .as_ref()
1736 .map(|b| b.span())
1737 .or_else(|| branches.last().map(|b| b.body.span()))
1738 .unwrap_or(start);
1739
1740 Some(Expr::Conditional {
1741 span: start.merge(end),
1742 branches,
1743 else_body,
1744 })
1745 }
1746
1747 fn parse_branch_body(&mut self, keyword_span: Span) -> Option<Expr> {
1748 let keyword_line = self.line_of(keyword_span);
1749 let next_line = self.line_of(self.peek().span);
1750
1751 if next_line > keyword_line {
1752 let base_col = self.col_of(self.peek().span);
1753 self.parse_indented_block(base_col)
1754 } else {
1755 self.parse_expr(0)
1756 }
1757 }
1758
1759 fn parse_for_expr(&mut self) -> Option<Expr> {
1762 let start = self.advance().span; let binding = self.parse_for_binding()?;
1764 self.expect(TokenKind::In)?;
1765
1766 let collection = self.parse_expr(BP_WITH_WHERE + 1)?;
1768
1769 let filter =
1770 if self.eat(TokenKind::Where).is_some() || self.eat(TokenKind::With).is_some() {
1771 Some(Box::new(self.parse_expr(0)?))
1773 } else {
1774 None
1775 };
1776
1777 self.expect(TokenKind::Colon)?;
1778 let body = self.parse_branch_body(start)?;
1779
1780 Some(Expr::For {
1781 span: start.merge(body.span()),
1782 binding,
1783 collection: Box::new(collection),
1784 filter,
1785 body: Box::new(body),
1786 })
1787 }
1788
1789 fn parse_brace_expr(&mut self) -> Option<Expr> {
1792 let start = self.advance().span; if self.at(TokenKind::RBrace) {
1795 let end = self.advance().span;
1796 return Some(Expr::SetLiteral {
1797 span: start.merge(end),
1798 elements: Vec::new(),
1799 });
1800 }
1801
1802 if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon {
1804 return self.parse_object_literal(start);
1805 }
1806
1807 self.parse_set_literal(start)
1809 }
1810
1811 fn parse_list_literal(&mut self) -> Option<Expr> {
1812 let start = self.advance().span; let mut elements = Vec::new();
1814 while !self.at(TokenKind::RBracket) && !self.at_eof() {
1815 elements.push(self.parse_expr(0)?);
1816 self.eat(TokenKind::Comma);
1817 }
1818 let end = self.expect(TokenKind::RBracket)?.span;
1819 Some(Expr::ListLiteral {
1820 span: start.merge(end),
1821 elements,
1822 })
1823 }
1824
1825 fn parse_object_literal(&mut self, start: Span) -> Option<Expr> {
1826 let mut fields = Vec::new();
1827 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1828 let name = self.parse_ident_in("field name")?;
1829 self.expect(TokenKind::Colon)?;
1830 let value = self.parse_expr(0)?;
1831 fields.push(NamedArg {
1832 span: name.span.merge(value.span()),
1833 name,
1834 value,
1835 });
1836 self.eat(TokenKind::Comma);
1837 }
1838 let end = self.expect(TokenKind::RBrace)?.span;
1839 Some(Expr::ObjectLiteral {
1840 span: start.merge(end),
1841 fields,
1842 })
1843 }
1844
1845 fn parse_set_literal(&mut self, start: Span) -> Option<Expr> {
1846 let mut elements = Vec::new();
1847 while !self.at(TokenKind::RBrace) && !self.at_eof() {
1848 elements.push(self.parse_expr(0)?);
1849 self.eat(TokenKind::Comma);
1850 }
1851 let end = self.expect(TokenKind::RBrace)?.span;
1852 Some(Expr::SetLiteral {
1853 span: start.merge(end),
1854 elements,
1855 })
1856 }
1857
1858 fn parse_paren_expr(&mut self) -> Option<Expr> {
1861 self.advance(); let expr = self.parse_expr(0)?;
1863 self.expect(TokenKind::RParen)?;
1864 Some(expr)
1865 }
1866}
1867
1868#[cfg(test)]
1873mod tests {
1874 use super::*;
1875 use crate::diagnostic::Severity;
1876
1877 fn parse_ok(src: &str) -> ParseResult {
1878 let owned;
1881 let input = if src.starts_with("-- allium:") {
1882 src
1883 } else {
1884 owned = format!("-- allium: 1\n{src}");
1885 &owned
1886 };
1887 let result = parse(input);
1888 if !result.diagnostics.is_empty() {
1889 for d in &result.diagnostics {
1890 eprintln!(
1891 " [{:?}] {} ({}..{})",
1892 d.severity, d.message, d.span.start, d.span.end
1893 );
1894 }
1895 }
1896 result
1897 }
1898
1899 #[test]
1900 fn version_marker() {
1901 let r = parse_ok("-- allium: 1\n");
1902 assert_eq!(r.module.version, Some(1));
1903 assert_eq!(r.diagnostics.len(), 0);
1904 }
1905
1906 #[test]
1907 fn version_missing_warns() {
1908 let r = parse("entity User {}");
1909 assert_eq!(r.module.version, None);
1910 assert_eq!(r.diagnostics.len(), 1);
1911 assert_eq!(r.diagnostics[0].severity, Severity::Warning);
1912 assert!(r.diagnostics[0].message.contains("missing version marker"), "got: {}", r.diagnostics[0].message);
1913 }
1914
1915 #[test]
1916 fn version_unsupported_errors() {
1917 let r = parse("-- allium: 99\nentity User {}");
1918 assert_eq!(r.module.version, Some(99));
1919 assert!(r.diagnostics.iter().any(|d|
1920 d.severity == Severity::Error && d.message.contains("unsupported allium version 99")
1921 ), "expected unsupported version error, got: {:?}", r.diagnostics);
1922 }
1923
1924 #[test]
1925 fn empty_entity() {
1926 let r = parse_ok("entity User {}");
1927 assert_eq!(r.diagnostics.len(), 0);
1928 assert_eq!(r.module.declarations.len(), 1);
1929 match &r.module.declarations[0] {
1930 Decl::Block(b) => {
1931 assert_eq!(b.kind, BlockKind::Entity);
1932 assert_eq!(b.name.as_ref().unwrap().name, "User");
1933 }
1934 other => panic!("expected Block, got {other:?}"),
1935 }
1936 }
1937
1938 #[test]
1939 fn entity_with_fields() {
1940 let src = r#"entity Order {
1941 customer: Customer
1942 status: pending | active | completed
1943 total: Decimal
1944}"#;
1945 let r = parse_ok(src);
1946 assert_eq!(r.diagnostics.len(), 0);
1947 match &r.module.declarations[0] {
1948 Decl::Block(b) => {
1949 assert_eq!(b.items.len(), 3);
1950 }
1951 other => panic!("expected Block, got {other:?}"),
1952 }
1953 }
1954
1955 #[test]
1956 fn use_declaration() {
1957 let r = parse_ok(r#"use "github.com/specs/oauth/abc123" as oauth"#);
1958 assert_eq!(r.diagnostics.len(), 0);
1959 match &r.module.declarations[0] {
1960 Decl::Use(u) => {
1961 assert_eq!(u.alias.as_ref().unwrap().name, "oauth");
1962 }
1963 other => panic!("expected Use, got {other:?}"),
1964 }
1965 }
1966
1967 #[test]
1968 fn enum_declaration() {
1969 let src = "enum OrderStatus { pending | shipped | delivered }";
1970 let r = parse_ok(src);
1971 assert_eq!(r.diagnostics.len(), 0);
1972 }
1973
1974 #[test]
1975 fn config_block() {
1976 let src = r#"config {
1977 max_retries: Integer = 3
1978 timeout: Duration = 24.hours
1979}"#;
1980 let r = parse_ok(src);
1985 assert_eq!(r.diagnostics.len(), 0);
1986 }
1987
1988 #[test]
1989 fn rule_declaration() {
1990 let src = r#"rule PlaceOrder {
1991 when: CustomerPlacesOrder(customer, items, total)
1992 requires: total > 0
1993 ensures: Order.created(customer: customer, status: pending, total: total)
1994}"#;
1995 let r = parse_ok(src);
1996 assert_eq!(r.diagnostics.len(), 0);
1997 match &r.module.declarations[0] {
1998 Decl::Block(b) => {
1999 assert_eq!(b.kind, BlockKind::Rule);
2000 assert_eq!(b.items.len(), 3);
2001 }
2002 other => panic!("expected Block, got {other:?}"),
2003 }
2004 }
2005
2006 #[test]
2007 fn expression_precedence() {
2008 let r = parse_ok("rule T { v: a + b * c }");
2009 match &r.module.declarations[0] {
2011 Decl::Block(b) => match &b.items[0].kind {
2012 BlockItemKind::Assignment { value, .. } => match value {
2013 Expr::BinaryOp { op, right, .. } => {
2014 assert_eq!(*op, BinaryOp::Add);
2015 assert!(matches!(**right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
2016 }
2017 other => panic!("expected BinaryOp, got {other:?}"),
2018 },
2019 other => panic!("expected Assignment, got {other:?}"),
2020 },
2021 other => panic!("expected Block, got {other:?}"),
2022 }
2023 }
2024
2025 #[test]
2026 fn default_declaration() {
2027 let src = r#"default Role admin = { name: "admin", permissions: { "read" } }"#;
2028 let r = parse_ok(src);
2029 assert_eq!(r.diagnostics.len(), 0);
2030 }
2031
2032 #[test]
2033 fn open_question() {
2034 let src = r#"open question "Should admins be role-specific?""#;
2035 let r = parse_ok(src);
2036 assert_eq!(r.diagnostics.len(), 0);
2037 }
2038
2039 #[test]
2040 fn external_entity() {
2041 let src = "external entity Customer { email: String }";
2042 let r = parse_ok(src);
2043 assert_eq!(r.diagnostics.len(), 0);
2044 match &r.module.declarations[0] {
2045 Decl::Block(b) => assert_eq!(b.kind, BlockKind::ExternalEntity),
2046 other => panic!("expected Block, got {other:?}"),
2047 }
2048 }
2049
2050 #[test]
2051 fn where_expression() {
2052 let src = "entity E { active: items where status = active }";
2053 let r = parse_ok(src);
2054 assert_eq!(r.diagnostics.len(), 0);
2055 }
2056
2057 #[test]
2058 fn with_expression() {
2059 let src = "entity E { slots: InterviewSlot with candidacy = this }";
2060 let r = parse_ok(src);
2061 assert_eq!(r.diagnostics.len(), 0);
2062 }
2063
2064 #[test]
2065 fn lambda_expression() {
2066 let src = "entity E { v: items.any(i => i.active) }";
2067 let r = parse_ok(src);
2068 assert_eq!(r.diagnostics.len(), 0);
2069 }
2070
2071 #[test]
2072 fn deferred() {
2073 let src = "deferred InterviewerMatching.suggest";
2074 let r = parse_ok(src);
2075 assert_eq!(r.diagnostics.len(), 0);
2076 }
2077
2078 #[test]
2079 fn variant_declaration() {
2080 let src = "variant Email : Notification { subject: String }";
2081 let r = parse_ok(src);
2082 assert_eq!(r.diagnostics.len(), 0);
2083 }
2084
2085 #[test]
2088 fn projection_arrow() {
2089 let src = "entity E { confirmed: confirmations where status = confirmed -> interviewer }";
2090 let r = parse_ok(src);
2091 assert_eq!(r.diagnostics.len(), 0);
2092 }
2093
2094 #[test]
2097 fn transitions_to_trigger() {
2098 let src = "rule R { when: Interview.status transitions_to scheduled\n ensures: Notification.created() }";
2099 let r = parse_ok(src);
2100 assert_eq!(r.diagnostics.len(), 0);
2101 }
2102
2103 #[test]
2104 fn becomes_trigger() {
2105 let src = "rule R { when: Interview.status becomes scheduled\n ensures: Notification.created() }";
2106 let r = parse_ok(src);
2107 assert_eq!(r.diagnostics.len(), 0);
2108 }
2109
2110 #[test]
2113 fn when_binding() {
2114 let src = "rule R {\n when: interview: Interview.status transitions_to scheduled\n ensures: Notification.created()\n}";
2115 let r = parse_ok(src);
2116 assert_eq!(r.diagnostics.len(), 0);
2117 let decl = &r.module.declarations[0];
2119 if let Decl::Block(b) = decl {
2120 if let BlockItemKind::Clause { keyword, value } = &b.items[0].kind {
2121 assert_eq!(keyword, "when");
2122 assert!(matches!(value, Expr::Binding { .. }));
2123 } else {
2124 panic!("expected clause");
2125 }
2126 } else {
2127 panic!("expected block decl");
2128 }
2129 }
2130
2131 #[test]
2132 fn when_binding_temporal() {
2133 let src = "rule R {\n when: invitation: Invitation.expires_at <= now\n ensures: Invitation.expired()\n}";
2134 let r = parse_ok(src);
2135 assert_eq!(r.diagnostics.len(), 0);
2136 }
2137
2138 #[test]
2139 fn when_binding_created() {
2140 let src = "rule R {\n when: batch: DigestBatch.created\n ensures: Email.created()\n}";
2141 let r = parse_ok(src);
2142 assert_eq!(r.diagnostics.len(), 0);
2143 }
2144
2145 #[test]
2146 fn facing_binding() {
2147 let src = "surface S {\n facing viewer: Interviewer\n exposes: InterviewList\n}";
2148 let r = parse_ok(src);
2149 assert_eq!(r.diagnostics.len(), 0);
2150 }
2151
2152 #[test]
2153 fn context_binding() {
2154 let src = "surface S {\n facing viewer: Interviewer\n context assignment: SlotConfirmation where interviewer = viewer\n}";
2155 let r = parse_ok(src);
2156 assert_eq!(r.diagnostics.len(), 0);
2157 }
2158
2159 #[test]
2162 fn rule_level_for() {
2163 let src = r#"rule ProcessDigests {
2164 when: schedule: DigestSchedule.next_run_at <= now
2165 for user in Users where notification_setting.digest_enabled:
2166 ensures: DigestBatch.created(user: user)
2167}"#;
2168 let r = parse_ok(src);
2169 assert_eq!(r.diagnostics.len(), 0);
2170 if let Decl::Block(b) = &r.module.declarations[0] {
2171 assert!(b.items.len() >= 2);
2173 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2174 } else {
2175 panic!("expected block decl");
2176 }
2177 }
2178
2179 #[test]
2182 fn let_in_ensures_block() {
2183 let src = r#"rule R {
2184 when: ScheduleInterview(candidacy, time, interviewers)
2185 ensures:
2186 let slot = InterviewSlot.created(time: time, candidacy: candidacy)
2187 for interviewer in interviewers:
2188 SlotConfirmation.created(slot: slot, interviewer: interviewer)
2189}"#;
2190 let r = parse_ok(src);
2191 assert_eq!(r.diagnostics.len(), 0);
2192 }
2193
2194 #[test]
2197 fn provides_when_guard() {
2198 let src = "surface S {\n facing viewer: Interviewer\n provides: ConfirmSlot(viewer, slot) when slot.status = pending\n}";
2199 let r = parse_ok(src);
2200 assert_eq!(r.diagnostics.len(), 0);
2201 }
2202
2203 #[test]
2206 fn optional_type_suffix() {
2207 let src = "entity E { locked_until: Timestamp? }";
2208 let r = parse_ok(src);
2209 assert_eq!(r.diagnostics.len(), 0);
2210 }
2211
2212 #[test]
2213 fn optional_trigger_param() {
2214 let src = "rule R { when: Report(interviewer, interview, reason, details?)\n ensures: Done() }";
2215 let r = parse_ok(src);
2216 assert_eq!(r.diagnostics.len(), 0);
2217 }
2218
2219 #[test]
2222 fn qualified_config_access() {
2223 let src = "entity E { duration: oauth/config.session_duration }";
2224 let r = parse_ok(src);
2225 assert_eq!(r.diagnostics.len(), 0);
2226 }
2227
2228 #[test]
2231 fn realistic_spec() {
2232 let src = r#"-- allium: 1
2233
2234enum OrderStatus { pending | shipped | delivered }
2235
2236external entity Customer {
2237 email: String
2238 name: String
2239}
2240
2241entity Order {
2242 customer: Customer
2243 status: OrderStatus
2244 total: Decimal
2245 items: OrderItem with order = this
2246 shipped_items: items where status = shipped
2247 confirmed_items: items where status = confirmed -> item
2248 is_complete: status = delivered
2249 locked_until: Timestamp?
2250}
2251
2252config {
2253 max_retries: Integer = 3
2254 timeout: Duration = 24.hours
2255}
2256
2257rule PlaceOrder {
2258 when: CustomerPlacesOrder(customer, items, total)
2259 requires: total > 0
2260 ensures: Order.created(customer: customer, status: pending, total: total)
2261}
2262
2263rule ShipOrder {
2264 when: order: Order.status transitions_to shipped
2265 ensures: Email.created(to: order.customer.email, template: order_shipped)
2266}
2267
2268open question "How do we handle partial shipments?"
2269"#;
2270 let r = parse_ok(src);
2271 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2272 assert_eq!(r.module.version, Some(1));
2273 assert_eq!(r.module.declarations.len(), 7);
2274 }
2275
2276 #[test]
2277 fn extension_behaviour_excerpt() {
2278 let src = r#"value Document {
2281 uri: String
2282 text: String
2283}
2284
2285entity Finding {
2286 code: String
2287 severity: error | warning | info
2288 range: FindingRange
2289}
2290
2291entity DiagnosticsMode {
2292 value: strict | relaxed
2293}
2294
2295config {
2296 duplicateKey: String = "allium.config.duplicateKey"
2297}
2298
2299rule RefreshDiagnostics {
2300 when: DocumentOpened(document) or DocumentChanged(document)
2301 requires: document.language_id = "allium"
2302 ensures: FindingsComputed(document)
2303}
2304
2305surface DiagnosticsDashboard {
2306 facing viewer: Developer
2307 context doc: Document where viewer.active_document = doc
2308 provides: RunChecks(viewer) when doc.language_id = "allium"
2309 exposes: FindingList
2310}
2311
2312rule ProcessDigests {
2313 when: schedule: DigestSchedule.next_run_at <= now
2314 for user in Users where notification_setting.digest_enabled:
2315 let settings = user.notification_setting
2316 ensures: DigestBatch.created(user: user)
2317}
2318"#;
2319 let r = parse_ok(src);
2320 assert_eq!(r.diagnostics.len(), 0, "expected no errors");
2321 assert_eq!(r.module.declarations.len(), 7);
2323 }
2324
2325 #[test]
2326 fn exists_as_identifier() {
2327 let src = r#"rule R {
2328 when: X()
2329 ensures: CompletionItemAvailable(label: exists)
2330}"#;
2331 let r = parse_ok(src);
2332 assert_eq!(r.diagnostics.len(), 0);
2333 }
2334
2335 #[test]
2338 fn pipe_binds_tighter_than_or() {
2339 let src = "entity E { v: a or b | c }";
2341 let r = parse_ok(src);
2342 assert_eq!(r.diagnostics.len(), 0);
2343 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2344 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2345 let Expr::LogicalOp { op, right, .. } = value else {
2347 panic!("expected LogicalOp, got {value:?}");
2348 };
2349 assert_eq!(*op, LogicalOp::Or);
2350 assert!(matches!(right.as_ref(), Expr::Pipe { .. }));
2352 }
2353
2354 #[test]
2357 fn variant_with_pipe_base() {
2358 let src = "variant Mixed : TypeA | TypeB";
2359 let r = parse_ok(src);
2360 assert_eq!(r.diagnostics.len(), 0);
2361 let Decl::Variant(v) = &r.module.declarations[0] else { panic!() };
2362 assert!(matches!(v.base, Expr::Pipe { .. }));
2363 }
2364
2365 #[test]
2368 fn for_block_where_comparison() {
2369 let src = r#"rule R {
2370 when: X()
2371 for item in Items where item.status = active:
2372 ensures: Processed(item: item)
2373}"#;
2374 let r = parse_ok(src);
2375 assert_eq!(r.diagnostics.len(), 0);
2376 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2377 let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2378 assert!(filter.is_some());
2379 assert!(matches!(filter.as_ref().unwrap(), Expr::Comparison { .. }));
2380 }
2381
2382 #[test]
2385 fn for_expr_where_comparison() {
2386 let src = r#"rule R {
2387 when: X()
2388 ensures:
2389 for item in Items where item.active = true:
2390 Processed(item: item)
2391}"#;
2392 let r = parse_ok(src);
2393 assert_eq!(r.diagnostics.len(), 0);
2394 }
2395
2396 #[test]
2399 fn if_else_if_else() {
2400 let src = r#"rule R {
2401 when: X(v)
2402 ensures:
2403 if v < 10: Small()
2404 else if v < 100: Medium()
2405 else: Large()
2406}"#;
2407 let r = parse_ok(src);
2408 assert_eq!(r.diagnostics.len(), 0);
2409 }
2410
2411 #[test]
2414 fn null_coalesce_and_optional_chain() {
2415 let src = "entity E { v: a?.b ?? fallback }";
2416 let r = parse_ok(src);
2417 assert_eq!(r.diagnostics.len(), 0);
2418 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2419 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2420 assert!(matches!(value, Expr::NullCoalesce { .. }));
2422 }
2423
2424 #[test]
2427 fn generic_type_nested() {
2428 let src = "entity E { v: List<Set<String>> }";
2429 let r = parse_ok(src);
2430 assert_eq!(r.diagnostics.len(), 0);
2431 }
2432
2433 #[test]
2436 fn collection_literals() {
2437 let src = r#"rule R {
2438 when: X()
2439 ensures:
2440 let s = {a, b, c}
2441 let l = [1, 2, 3]
2442 let o = {name: "test", count: 42}
2443 Done()
2444}"#;
2445 let r = parse_ok(src);
2446 assert_eq!(r.diagnostics.len(), 0);
2447 }
2448
2449 #[test]
2452 fn given_block() {
2453 let src = "given { viewer: User\n time: Timestamp }";
2454 let r = parse_ok(src);
2455 assert_eq!(r.diagnostics.len(), 0);
2456 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2457 assert_eq!(b.kind, BlockKind::Given);
2458 assert!(b.name.is_none());
2459 }
2460
2461 #[test]
2464 fn actor_block() {
2465 let src = "actor Admin { identified_by: User where role = admin }";
2466 let r = parse_ok(src);
2467 assert_eq!(r.diagnostics.len(), 0);
2468 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2469 assert_eq!(b.kind, BlockKind::Actor);
2470 }
2471
2472 #[test]
2475 fn join_lookup() {
2476 let src = "entity E { match: Other{field_a, field_b: value} }";
2477 let r = parse_ok(src);
2478 assert_eq!(r.diagnostics.len(), 0);
2479 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2480 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2481 assert!(matches!(value, Expr::JoinLookup { .. }));
2482 }
2483
2484 #[test]
2487 fn in_not_in_set() {
2488 let src = r#"rule R {
2489 when: X(s)
2490 requires: s in {a, b, c}
2491 requires: s not in {d, e}
2492 ensures: Done()
2493}"#;
2494 let r = parse_ok(src);
2495 assert_eq!(r.diagnostics.len(), 0);
2496 }
2497
2498 #[test]
2501 fn comprehensive_fixture() {
2502 let src = include_str!("../tests/fixtures/comprehensive-edge-cases.allium");
2503 let r = parse(src);
2504 assert_eq!(
2505 r.diagnostics.len(),
2506 0,
2507 "expected no errors in comprehensive fixture, got: {:?}",
2508 r.diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>(),
2509 );
2510 assert!(r.module.declarations.len() > 30, "expected many declarations");
2511 }
2512
2513 #[test]
2516 fn error_expected_declaration() {
2517 let r = parse("-- allium: 1\n+ invalid");
2518 assert!(r.diagnostics.len() >= 1);
2519 let msg = &r.diagnostics[0].message;
2520 assert!(msg.contains("expected declaration"), "got: {msg}");
2521 assert!(msg.contains("entity"), "should list valid options, got: {msg}");
2522 assert!(msg.contains("rule"), "should list valid options, got: {msg}");
2523 }
2524
2525 #[test]
2526 fn error_expected_expression() {
2527 let r = parse("-- allium: 1\nentity E { v: }");
2528 assert!(r.diagnostics.len() >= 1);
2529 let msg = &r.diagnostics[0].message;
2530 assert!(msg.contains("expected expression"), "got: {msg}");
2531 assert!(msg.contains("identifier"), "should list valid starters, got: {msg}");
2532 }
2533
2534 #[test]
2535 fn error_expected_block_item() {
2536 let r = parse("-- allium: 1\nentity E { + }");
2537 assert!(r.diagnostics.len() >= 1);
2538 let msg = &r.diagnostics[0].message;
2539 assert!(msg.contains("expected block item"), "got: {msg}");
2540 }
2541
2542 #[test]
2543 fn error_expected_identifier() {
2544 let r = parse("-- allium: 1\nentity 123 {}");
2545 assert!(r.diagnostics.len() >= 1);
2546 let msg = &r.diagnostics[0].message;
2547 assert!(msg.contains("expected entity name"), "got: {msg}");
2549 assert!(msg.contains("number"), "should say what was found, got: {msg}");
2551 }
2552
2553 #[test]
2554 fn error_missing_brace() {
2555 let r = parse("entity E {");
2556 assert!(r.diagnostics.len() >= 1);
2557 let msg = &r.diagnostics[0].message;
2558 assert!(msg.contains("expected"), "got: {msg}");
2559 }
2560
2561 #[test]
2562 fn error_recovery_multiple() {
2563 let r = parse("entity E { + }\nentity F { - }");
2565 assert!(r.diagnostics.len() >= 2, "expected at least 2 errors, got {}", r.diagnostics.len());
2566 }
2567
2568 #[test]
2569 fn error_dedup_same_line() {
2570 let r = parse("-- allium: 1\n+ - * /");
2572 let errors: Vec<_> = r.diagnostics.iter()
2573 .filter(|d| d.severity == crate::diagnostic::Severity::Error)
2574 .collect();
2575 assert_eq!(errors.len(), 1, "expected 1 error for same-line bad tokens, got {}", errors.len());
2576 }
2577
2578 #[test]
2579 fn for_block() {
2580 let src = r#"rule R {
2581 when: X()
2582 for user in Users where user.active:
2583 ensures: Notified(user: user)
2584}"#;
2585 let r = parse_ok(src);
2586 assert_eq!(r.diagnostics.len(), 0);
2587 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2588 assert!(matches!(b.items[1].kind, BlockItemKind::ForBlock { .. }));
2589 }
2590
2591 #[test]
2592 fn for_expr() {
2593 let src = r#"rule R {
2594 when: X(project)
2595 ensures:
2596 let total = for task in project.tasks: task.effort
2597 Done(total: total)
2598}"#;
2599 let r = parse_ok(src);
2600 assert_eq!(r.diagnostics.len(), 0);
2601 }
2602
2603 #[test]
2604 fn for_where() {
2605 let src = r#"rule R {
2606 when: X()
2607 for item in Items where item.active:
2608 ensures: Processed(item: item)
2609}"#;
2610 let r = parse_ok(src);
2611 assert_eq!(r.diagnostics.len(), 0);
2612 }
2613
2614 #[test]
2615 fn for_with() {
2616 let src = r#"rule R {
2617 when: X()
2618 for slot in Slot with slot.role = reviewer:
2619 ensures: Reviewed(slot: slot)
2620}"#;
2621 let r = parse_ok(src);
2622 assert_eq!(r.diagnostics.len(), 0);
2623 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2624 let BlockItemKind::ForBlock { filter, .. } = &b.items[1].kind else { panic!() };
2625 assert!(filter.is_some());
2626 }
2627
2628 #[test]
2629 fn block_level_if() {
2630 let src = r#"rule R {
2631 when: X(task)
2632 if task.priority = high:
2633 ensures: Escalated(task: task)
2634}"#;
2635 let r = parse_ok(src);
2636 assert_eq!(r.diagnostics.len(), 0);
2637 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2638 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2639 panic!("expected IfBlock, got {:?}", b.items[1].kind);
2640 };
2641 assert_eq!(branches.len(), 1);
2642 assert!(else_items.is_none());
2643 }
2644
2645 #[test]
2646 fn block_level_if_else() {
2647 let src = r#"rule R {
2648 when: X(score)
2649 if score > 80:
2650 ensures: High()
2651 else if score > 40:
2652 ensures: Medium()
2653 else:
2654 ensures: Low()
2655}"#;
2656 let r = parse_ok(src);
2657 assert_eq!(r.diagnostics.len(), 0);
2658 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2659 let BlockItemKind::IfBlock { branches, else_items } = &b.items[1].kind else {
2660 panic!("expected IfBlock, got {:?}", b.items[1].kind);
2661 };
2662 assert_eq!(branches.len(), 2);
2663 assert!(else_items.is_some());
2664 }
2665
2666 #[test]
2667 fn wildcard_type_parameter() {
2668 let src = "entity E { codec: Codec<*> }";
2669 let r = parse_ok(src);
2670 assert_eq!(r.diagnostics.len(), 0);
2671 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2672 let BlockItemKind::Assignment { value, .. } = &b.items[0].kind else { panic!() };
2673 if let Expr::GenericType { args, .. } = value {
2674 assert_eq!(args.len(), 1);
2675 if let Expr::Ident(id) = &args[0] {
2676 assert_eq!(id.name, "*");
2677 } else {
2678 panic!("expected wildcard ident, got {:?}", args[0]);
2679 }
2680 } else {
2681 panic!("expected GenericType, got {:?}", value);
2682 }
2683 }
2684
2685 #[test]
2686 fn guidance_clause_comment_only_value() {
2687 let src = r#"rule R {
2688 guidance: -- just a comment
2689 ensures: Done()
2690}"#;
2691 let r = parse_ok(src);
2692 assert_eq!(r.diagnostics.len(), 0);
2693 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2694 assert_eq!(b.items.len(), 2);
2695 let BlockItemKind::Clause { keyword, value } = &b.items[0].kind else { panic!() };
2697 assert_eq!(keyword, "guidance");
2698 assert!(matches!(value, Expr::Block { items, .. } if items.is_empty()));
2699 }
2700
2701 #[test]
2702 fn for_expr_with_filter() {
2703 let src = r#"rule R {
2704 when: X(project)
2705 ensures:
2706 let total = for task in project.tasks with task.active: task.effort
2707 Done(total: total)
2708}"#;
2709 let r = parse_ok(src);
2710 assert_eq!(r.diagnostics.len(), 0);
2711 }
2712
2713 #[test]
2714 fn for_destructured_binding() {
2715 let src = r#"rule R {
2716 when: X()
2717 for (key, value) in Pairs where key != null:
2718 ensures: Processed(key: key, value: value)
2719}"#;
2720 let r = parse_ok(src);
2721 assert_eq!(r.diagnostics.len(), 0);
2722 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2723 let BlockItemKind::ForBlock { binding, .. } = &b.items[1].kind else { panic!() };
2724 assert!(matches!(binding, ForBinding::Destructured(ids, _) if ids.len() == 2));
2725 }
2726
2727 #[test]
2728 fn dot_path_assignment() {
2729 let src = r#"entity Shard {
2730 ShardGroup.shard_cache: Shard with group = this
2731}"#;
2732 let r = parse_ok(src);
2733 assert_eq!(r.diagnostics.len(), 0);
2734 let Decl::Block(b) = &r.module.declarations[0] else { panic!() };
2735 let BlockItemKind::PathAssignment { path, .. } = &b.items[0].kind else {
2736 panic!("expected PathAssignment, got {:?}", b.items[0].kind);
2737 };
2738 assert!(matches!(path, Expr::MemberAccess { .. }));
2739 }
2740
2741 #[test]
2742 fn language_reference_fixture() {
2743 let src = include_str!("../tests/fixtures/language-reference-constructs.allium");
2744 let r = parse(src);
2745 let errors: Vec<_> = r.diagnostics.iter()
2746 .filter(|d| d.severity == Severity::Error)
2747 .collect();
2748 assert_eq!(
2749 errors.len(),
2750 0,
2751 "expected no errors in language-reference fixture, got: {:?}",
2752 errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
2753 );
2754 }
2755
2756 #[test]
2773 fn spec_for_bare_form() {
2774 let src = r#"rule ProcessDigests {
2776 when: schedule: DigestSchedule.next_run_at <= now
2777 for user in Users where notification_setting.digest_enabled:
2778 let settings = user.notification_setting
2779 ensures: DigestBatch.created(user: user)
2780}"#;
2781 let r = parse_ok(src);
2782 assert_eq!(r.diagnostics.len(), 0);
2783 }
2784
2785 #[test]
2786 fn spec_reject_for_each() {
2787 let src = r#"rule R {
2789 when: X()
2790 for each user in Users where user.active:
2791 ensures: Notified(user: user)
2792}"#;
2793 let r = parse_ok(src);
2794 assert!(
2795 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2796 "expected error for `for each` (not in spec), but parsed without errors"
2797 );
2798 }
2799
2800 #[test]
2803 fn spec_reject_double_equals() {
2804 let src = "rule R { when: X(a)\n requires: a.status == active\n ensures: Done() }";
2806 let r = parse_ok(src);
2807 assert!(
2808 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2809 "expected error for `==` (not in spec), but parsed without errors"
2810 );
2811 }
2812
2813 #[test]
2816 fn spec_reject_system_block() {
2817 let src = "system PaymentGateway {\n timeout: 30.seconds\n}";
2819 let r = parse_ok(src);
2820 assert!(
2821 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2822 "expected error for `system` block (not in spec), but parsed without errors"
2823 );
2824 }
2825
2826 #[test]
2829 fn spec_reject_tags_clause() {
2830 let src = r#"rule R {
2832 when: MigrationTriggered()
2833 tags: infrastructure, migration
2834 ensures: MigrationComplete()
2835}"#;
2836 let r = parse_ok(src);
2837 assert!(
2838 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2839 "expected error for `tags:` clause (not in spec), but parsed without errors"
2840 );
2841 }
2842
2843 #[test]
2846 fn spec_reject_includes_operator() {
2847 let src = r#"rule R {
2849 when: X(a, b)
2850 requires: a.items includes b
2851 ensures: Done()
2852}"#;
2853 let r = parse_ok(src);
2854 assert!(
2855 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2856 "expected error for `includes` operator (not in spec), but parsed without errors"
2857 );
2858 }
2859
2860 #[test]
2861 fn spec_reject_excludes_operator() {
2862 let src = r#"rule R {
2864 when: X(a, b)
2865 requires: a.items excludes b
2866 ensures: Done()
2867}"#;
2868 let r = parse_ok(src);
2869 assert!(
2870 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2871 "expected error for `excludes` operator (not in spec), but parsed without errors"
2872 );
2873 }
2874
2875 #[test]
2878 fn spec_reject_range_literal() {
2879 let src = r#"rule R {
2881 when: X(v)
2882 requires: v in [1..100]
2883 ensures: Done()
2884}"#;
2885 let r = parse_ok(src);
2886 assert!(
2887 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2888 "expected error for `..` range (not in spec), but parsed without errors"
2889 );
2890 }
2891
2892 #[test]
2895 fn spec_within_in_actor() {
2896 let src = r#"actor WorkspaceAdmin {
2898 within: Workspace
2899 identified_by: User where role = admin
2900}"#;
2901 let r = parse_ok(src);
2902 assert_eq!(r.diagnostics.len(), 0, "within: in actor should parse cleanly");
2903 }
2904
2905 #[test]
2908 fn spec_reject_module_declaration() {
2909 let src = "module my_spec";
2911 let r = parse_ok(src);
2912 assert!(
2913 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2914 "expected error for `module` declaration (not in spec), but parsed without errors"
2915 );
2916 }
2917
2918 #[test]
2921 fn spec_reject_module_level_guidance() {
2922 let src = r#"guidance: "All rules must be idempotent""#;
2924 let r = parse_ok(src);
2925 assert!(
2926 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2927 "expected error for module-level `guidance:` (not in spec), but parsed without errors"
2928 );
2929 }
2930
2931 #[test]
2934 fn spec_guarantee_in_surface() {
2935 let src = r#"surface S {
2937 facing viewer: User
2938 guarantee: DataIntegrity
2939}"#;
2940 let r = parse_ok(src);
2941 assert_eq!(r.diagnostics.len(), 0, "guarantee: in surface should parse cleanly");
2942 }
2943
2944 #[test]
2945 fn spec_timeout_in_surface() {
2946 let src = r#"surface InvitationView {
2948 facing recipient: Candidate
2949 context invitation: ResourceInvitation where email = recipient.email
2950 timeout: InvitationExpires
2951}"#;
2952 let r = parse_ok(src);
2953 assert_eq!(r.diagnostics.len(), 0, "timeout: in surface should parse cleanly");
2954 }
2955
2956 #[test]
2957 fn spec_timeout_in_surface_with_when() {
2958 let src = r#"surface InvitationView {
2960 facing recipient: Candidate
2961 context invitation: ResourceInvitation where email = recipient.email
2962 timeout: InvitationExpires when invitation.expires_at <= now
2963}"#;
2964 let r = parse_ok(src);
2965 assert_eq!(r.diagnostics.len(), 0, "timeout: with when guard should parse cleanly");
2966 }
2967
2968 #[test]
2971 fn spec_reject_suffix_predicate() {
2972 let src = r#"rule R {
2974 when: X()
2975 requires: finding.code starts_with "allium."
2976 ensures: Done()
2977}"#;
2978 let r = parse_ok(src);
2979 assert!(
2980 r.diagnostics.iter().any(|d| d.severity == Severity::Error),
2981 "expected error for suffix predicate (not in spec), but parsed without errors"
2982 );
2983 }
2984
2985 #[test]
2988 fn spec_add_remove_in_ensures() {
2989 let src = r#"rule R {
2992 when: AssignInterviewer(interview, new_interviewer)
2993 ensures:
2994 interview.interviewers.add(new_interviewer)
2995}"#;
2996 let r = parse_ok(src);
2997 assert_eq!(r.diagnostics.len(), 0, ".add() should parse cleanly");
2998 }
2999
3000 #[test]
3001 fn spec_remove_in_ensures() {
3002 let src = r#"rule R {
3003 when: RemoveInterviewer(interview, leaving)
3004 ensures:
3005 interview.interviewers.remove(leaving)
3006}"#;
3007 let r = parse_ok(src);
3008 assert_eq!(r.diagnostics.len(), 0, ".remove() should parse cleanly");
3009 }
3010
3011 #[test]
3014 fn spec_first_last_access() {
3015 let src = "entity E { latest: attempts.last\n earliest: attempts.first }";
3017 let r = parse_ok(src);
3018 assert_eq!(r.diagnostics.len(), 0, ".first/.last should parse cleanly");
3019 }
3020
3021 #[test]
3024 fn spec_set_arithmetic() {
3025 let src = r#"entity Role {
3027 permissions: Set<String>
3028 inherited: Set<String>
3029 all_permissions: permissions + inherited
3030 removed: old_mentions - new_mentions
3031}"#;
3032 let r = parse_ok(src);
3033 assert_eq!(r.diagnostics.len(), 0, "set arithmetic should parse cleanly");
3034 }
3035
3036 #[test]
3039 fn spec_discard_binding_in_trigger() {
3040 let src = r#"rule R {
3042 when: _: LogProcessor.last_flush_check <= now
3043 ensures: Flushed()
3044}"#;
3045 let r = parse_ok(src);
3046 assert_eq!(r.diagnostics.len(), 0, "discard binding _ in trigger should parse cleanly");
3047 }
3048
3049 #[test]
3050 fn spec_discard_in_trigger_params() {
3051 let src = r#"rule R {
3053 when: SomeEvent(_, slot)
3054 ensures: Processed(slot: slot)
3055}"#;
3056 let r = parse_ok(src);
3057 assert_eq!(r.diagnostics.len(), 0, "discard _ in trigger params should parse cleanly");
3058 }
3059
3060 #[test]
3061 fn spec_discard_in_for() {
3062 let src = r#"rule R {
3064 when: X(items)
3065 ensures:
3066 for _ in items: Counted()
3067}"#;
3068 let r = parse_ok(src);
3069 assert_eq!(r.diagnostics.len(), 0, "discard _ in for should parse cleanly");
3070 }
3071
3072 #[test]
3075 fn spec_default_with_object_literal() {
3076 let src = r#"default InterviewType all_in_one = { name: "All in one", duration: 75.minutes }"#;
3078 let r = parse_ok(src);
3079 assert_eq!(r.diagnostics.len(), 0, "default with object literal should parse cleanly");
3080 }
3081
3082 #[test]
3083 fn spec_default_multiline_object() {
3084 let src = r#"default Role viewer = {
3086 name: "viewer",
3087 permissions: { "documents.read" }
3088}"#;
3089 let r = parse_ok(src);
3090 assert_eq!(r.diagnostics.len(), 0, "multi-line default with object literal should parse cleanly");
3091 }
3092
3093 #[test]
3096 fn spec_surface_related_clause() {
3097 let src = r#"surface InterviewerDashboard {
3099 facing viewer: Interviewer
3100 context assignment: SlotConfirmation where interviewer = viewer
3101 related: InterviewDetail(assignment.slot.interview) when assignment.slot.interview != null
3102}"#;
3103 let r = parse_ok(src);
3104 assert_eq!(r.diagnostics.len(), 0, "related: in surface should parse cleanly");
3105 }
3106
3107 #[test]
3108 fn spec_surface_let_binding() {
3109 let src = r#"surface S {
3111 facing viewer: User
3112 let comments = Comments where parent = viewer
3113 exposes: CommentList
3114}"#;
3115 let r = parse_ok(src);
3116 assert_eq!(r.diagnostics.len(), 0, "let in surface should parse cleanly");
3117 }
3118
3119 #[test]
3120 fn spec_surface_multiline_context_where() {
3121 let src = r#"surface InterviewerPendingAssignments {
3123 facing viewer: Interviewer
3124 context assignment: InterviewAssignment
3125 where interviewer = viewer and status = pending
3126 exposes: AssignmentList
3127}"#;
3128 let r = parse_ok(src);
3129 assert_eq!(r.diagnostics.len(), 0, "multi-line context where should parse cleanly");
3130 }
3131
3132 #[test]
3135 fn spec_for_in_surface_provides() {
3136 let src = r#"surface TaskBoard {
3138 facing viewer: User
3139 for task in Task where task.assignee = viewer:
3140 provides: CompleteTask(viewer, task) when task.status = in_progress
3141 exposes: KanbanBoard
3142}"#;
3143 let r = parse_ok(src);
3144 assert_eq!(r.diagnostics.len(), 0, "for in surface provides should parse cleanly");
3145 }
3146
3147 #[test]
3150 fn spec_use_without_alias() {
3151 let src = r#"use "github.com/specs/notifications/def456""#;
3153 let r = parse_ok(src);
3154 assert_eq!(r.diagnostics.len(), 0, "use without alias should parse cleanly");
3155 }
3156
3157 #[test]
3160 fn spec_empty_external_entity() {
3161 let src = "external entity Commentable {}";
3163 let r = parse_ok(src);
3164 assert_eq!(r.diagnostics.len(), 0, "empty external entity should parse cleanly");
3165 }
3166
3167 #[test]
3170 fn spec_surface_multiline_provides() {
3171 let src = r#"surface ProjectDashboard {
3173 facing viewer: ProjectManager
3174 context project: Project where owner = viewer
3175 provides:
3176 CreateTask(viewer, project) when project.status = active
3177 ArchiveProject(viewer, project) when project.tasks.all(t => t.status = completed)
3178 exposes: TaskList
3179}"#;
3180 let r = parse_ok(src);
3181 assert_eq!(r.diagnostics.len(), 0, "multi-line provides should parse cleanly");
3182 }
3183
3184 #[test]
3187 fn spec_surface_multiline_exposes() {
3188 let src = r#"surface InterviewerDashboard {
3190 facing viewer: Interviewer
3191 context assignment: SlotConfirmation where interviewer = viewer
3192 exposes:
3193 assignment.slot.time
3194 assignment.status
3195}"#;
3196 let r = parse_ok(src);
3197 assert_eq!(r.diagnostics.len(), 0, "multi-line exposes should parse cleanly");
3198 }
3199}