1use cel_parser::{AstContext, OpLookup, Parser as CelParser};
8
9use crate::ast;
10use crate::token_cursor::TokenCursor;
11
12type Result<T> = std::result::Result<T, cel_parser::ParseError>;
14
15pub struct AdamAstParser {
27 cel: CelParser<AstContext>,
28}
29
30impl Default for AdamAstParser {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl AdamAstParser {
37 #[must_use]
43 pub fn new() -> Self {
44 AdamAstParser {
45 cel: CelParser::new(OpLookup::new()),
46 }
47 }
48
49 pub fn parse_str(&mut self, source: &str) -> Result<ast::Sheet> {
74 use std::str::FromStr;
75 let stream = proc_macro2::TokenStream::from_str(source)
76 .map_err(|e| cel_parser::ParseError::from_lex_error(source, e))?;
77 let mut cursor =
78 TokenCursor::new(cel_parser::lex_lexer::LexLexer::new(stream.into_iter()).peekable());
79 let doc_comment = cursor.consume_doc_comment_run(true);
80 let mut sheet = self.parse_sheet(&mut cursor)?;
81 if let Some((text, span)) = doc_comment {
82 sheet.doc_comment = Some(text);
83 sheet.span.start = span;
84 }
85 if let Some(tok) = cursor.peek_token() {
86 use cel_parser::lex_lexer::HasSpan;
87 return Err(cel_parser::ParseError::new("unexpected token", tok.span()));
88 }
89 Ok(sheet)
90 }
91
92 fn parse_sheet(&mut self, cursor: &mut TokenCursor) -> Result<ast::Sheet> {
94 let sheet_start = cursor.peek_span();
95 if !cursor.is_keyword("sheet") {
96 return Err(cursor.err_at("expected `sheet`"));
97 }
98 let (name, name_span) = cursor.consume_ident()?;
99 let open_span = cursor.expect_open_brace()?;
100 let mut items = Vec::new();
101 let mut errors = Vec::new();
102 while !cursor.at_close_brace() {
103 let doc = cursor.consume_doc_comment_run(false);
104 let item_start = doc
105 .as_ref()
106 .map(|(_, span)| *span)
107 .unwrap_or_else(|| cursor.peek_span());
108 cursor.set_last_span(item_start);
109 let target_depth = cursor.depth();
110 match self.parse_sheet_item(cursor) {
111 Ok(mut item) => {
112 if let Some((text, doc_span)) = &doc {
113 item.set_doc_comment(text.clone(), *doc_span);
114 }
115 items.push(item);
116 }
117 Err(e) => {
118 errors.push(e);
119 let recovery_fallback = cursor.last_span();
120 let item_end = cursor.skip_to_recovery_point(target_depth, recovery_fallback);
121 items.push(ast::SheetItem::Error {
122 span: ast::ExprSpan {
123 start: item_start,
124 end: item_end,
125 },
126 leading_comment: None,
127 doc_comment: doc.map(|(text, _)| text),
128 trailing_line_comment: None,
129 blank_line_before: false,
130 });
131 }
132 }
133 }
134 let close_span = cursor.expect_close_brace()?;
135 Ok(ast::Sheet {
136 name,
137 name_span: point(name_span),
138 items,
139 leading_comment: None,
140 doc_comment: None,
141 trailing_comment: None,
142 blank_line_before_close: false,
143 open_brace_span: point(open_span),
144 span: ast::ExprSpan {
145 start: sheet_start,
146 end: close_span,
147 },
148 errors,
149 })
150 }
151
152 fn parse_sheet_item(&mut self, cursor: &mut TokenCursor) -> Result<ast::SheetItem> {
154 use cel_parser::lex_lexer::{HasSpan, Token};
155 match cursor.peek_token() {
156 Some(Token::Identifier(id)) if id == "cell" => {
157 self.parse_cell_decl(cursor).map(ast::SheetItem::Cell)
158 }
159 Some(Token::Identifier(id)) if id == "relationship" => self
160 .parse_relationship_decl(cursor)
161 .map(ast::SheetItem::Relationship),
162 Some(Token::Identifier(id)) if id == "conditional" => self
163 .parse_conditional_decl(cursor)
164 .map(ast::SheetItem::Conditional),
165 Some(Token::Identifier(id)) if id == "out" => {
166 self.parse_out_decl(cursor).map(ast::SheetItem::Out)
167 }
168 Some(Token::Identifier(id)) if id == "source" => {
169 self.parse_source_decl(cursor).map(ast::SheetItem::Source)
170 }
171 Some(tok) => Err(cel_parser::ParseError::new(
172 "expected `cell`, `relationship`, `conditional`, `out`, or `source`",
173 tok.span(),
174 )),
175 None => Err(cel_parser::ParseError::new(
176 "unexpected end of input",
177 proc_macro2::Span::call_site(),
178 )),
179 }
180 }
181
182 fn parse_cell_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::CellDecl> {
185 let decl_start = cursor.peek_span();
186 cursor.is_keyword("cell");
187 let (name, name_span) = cursor.consume_ident()?;
188 let (type_name, initializer) = if cursor.consume_punct(":") {
189 let type_name = self.parse_type_expr(cursor)?;
190 let initializer = if cursor.consume_punct("=") {
191 Some(self.parse_cel_expression(cursor)?)
192 } else {
193 None
194 };
195 (Some(type_name), initializer)
196 } else if cursor.consume_punct("=") {
197 (None, Some(self.parse_cel_expression(cursor)?))
198 } else {
199 return Err(cursor.err_at("expected `:` or `=` in cell declaration"));
200 };
201 let filter = if cursor.is_keyword("filter") {
202 let filter_start = cursor.last_span();
203 Some(self.parse_cell_filter(cursor, filter_start)?)
204 } else {
205 None
206 };
207 let require = if cursor.is_keyword("require") {
208 Some(self.parse_require_block(cursor)?)
209 } else {
210 None
211 };
212 let semi_span = cursor.expect_punct(";")?;
213 Ok(ast::CellDecl {
214 name,
215 name_span: point(name_span),
216 type_name,
217 initializer,
218 filter,
219 require,
220 leading_comment: None,
221 doc_comment: None,
222 trailing_line_comment: None,
223 blank_line_before: false,
224 span: ast::ExprSpan {
225 start: decl_start,
226 end: semi_span,
227 },
228 })
229 }
230
231 fn parse_source_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::SourceDecl> {
236 let decl_start = cursor.peek_span();
237 cursor.is_keyword("source");
238 let (name, name_span) = cursor.consume_ident()?;
239 let (type_name, initializer) = if cursor.consume_punct(":") {
240 let type_name = self.parse_type_expr(cursor)?;
241 let initializer = if cursor.consume_punct("=") {
242 Some(self.parse_cel_expression(cursor)?)
243 } else {
244 None
245 };
246 (Some(type_name), initializer)
247 } else if cursor.consume_punct("=") {
248 (None, Some(self.parse_cel_expression(cursor)?))
249 } else {
250 return Err(cursor.err_at("expected `:` or `=` in source declaration"));
251 };
252 let filter = if cursor.is_keyword("filter") {
253 let filter_start = cursor.last_span();
254 Some(self.parse_cell_filter(cursor, filter_start)?)
255 } else {
256 None
257 };
258 let require = if cursor.is_keyword("require") {
259 Some(self.parse_require_block(cursor)?)
260 } else {
261 None
262 };
263 let semi_span = cursor.expect_punct(";")?;
264 Ok(ast::SourceDecl {
265 name,
266 name_span: point(name_span),
267 type_name,
268 initializer,
269 filter,
270 require,
271 leading_comment: None,
272 doc_comment: None,
273 trailing_line_comment: None,
274 blank_line_before: false,
275 span: ast::ExprSpan {
276 start: decl_start,
277 end: semi_span,
278 },
279 })
280 }
281
282 fn parse_cell_filter(
287 &mut self,
288 cursor: &mut TokenCursor,
289 filter_start: proc_macro2::Span,
290 ) -> Result<ast::CellFilter> {
291 let body = self.parse_cel_expression(cursor)?;
292 let body_end = body.span().end;
293 Ok(ast::CellFilter {
294 body,
295 span: ast::ExprSpan {
296 start: filter_start,
297 end: body_end,
298 },
299 })
300 }
301
302 fn parse_type_expr(&mut self, cursor: &mut TokenCursor) -> Result<ast::TypeExpr> {
305 let tokens = cursor.take_tokens().expect("tokens present");
306 self.cel.set_lex_tokens(tokens);
307 let result = self.cel.parse_type_expression();
308 cursor.set_tokens(self.cel.take_lex_tokens().expect("tokens set"));
309 cursor.absorb_unbalanced_delimiters(self.cel.unbalanced_delimiter_count());
310 result
311 }
312
313 fn parse_relationship_decl(
315 &mut self,
316 cursor: &mut TokenCursor,
317 ) -> Result<ast::RelationshipDecl> {
318 let decl_start = cursor.peek_span();
319 cursor.is_keyword("relationship");
320 let open_span = cursor.expect_open_brace()?;
321 let mut bindings = Vec::new();
322 while !cursor.at_close_brace() {
323 bindings.push(self.parse_binding(cursor)?);
324 }
325 let close_span = cursor.expect_close_brace()?;
326 Ok(ast::RelationshipDecl {
327 bindings,
328 leading_comment: None,
329 doc_comment: None,
330 trailing_line_comment: None,
331 blank_line_before: false,
332 trailing_comment: None,
333 blank_line_before_close: false,
334 open_brace_span: point(open_span),
335 span: ast::ExprSpan {
336 start: decl_start,
337 end: close_span,
338 },
339 })
340 }
341
342 fn parse_binding(&mut self, cursor: &mut TokenCursor) -> Result<ast::BindingDecl> {
344 let decl_start = cursor.peek_span();
345 let (outputs, destructure) = parse_binding_target(cursor)?;
346 cursor.expect_punct(":=")?;
347 let body = self.parse_cel_expression(cursor)?;
348 let semi_span = cursor.expect_punct(";")?;
349 Ok(ast::BindingDecl {
350 outputs,
351 destructure,
352 body,
353 leading_comment: None,
354 trailing_line_comment: None,
355 blank_line_before: false,
356 span: ast::ExprSpan {
357 start: decl_start,
358 end: semi_span,
359 },
360 })
361 }
362
363 fn parse_conditional_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::ConditionalDecl> {
365 use cel_parser::lex_lexer::Token;
366 let decl_start = cursor.peek_span();
367 cursor.is_keyword("conditional");
368 let match_expr = self.parse_cel_expression_restricted(
369 cursor,
370 cel_parser::ExpressionRestriction::NoStructLiteral,
371 )?;
372 let outer_open = cursor.expect_open_brace()?;
373 let mut branches = Vec::new();
374 let mut default = None;
375 while !cursor.at_close_brace() {
376 if matches!(cursor.peek_token(), Some(Token::Identifier(id)) if id == "_") {
377 let underscore_span = cursor.peek_span();
378 cursor.advance();
379 cursor.expect_punct("=>")?;
380 let branch_open = cursor.expect_open_brace()?;
381 let relationships = self.parse_branch_relationships(cursor)?;
382 let close = cursor.expect_close_brace()?;
383 cursor.consume_punct(",");
384 default = Some(ast::DefaultBranch {
385 relationships,
386 trailing_comment: None,
387 trailing_line_comment: None,
388 blank_line_before_close: false,
389 open_brace_span: point(branch_open),
390 span: ast::ExprSpan {
391 start: underscore_span,
392 end: close,
393 },
394 });
395 break; }
397 let (negated, lit, pattern_start, lit_span) = cursor.consume_literal_pattern()?;
398 cursor.expect_punct("=>")?;
399 let branch_open = cursor.expect_open_brace()?;
400 let relationships = self.parse_branch_relationships(cursor)?;
401 let close = cursor.expect_close_brace()?;
402 cursor.consume_punct(",");
403 branches.push(ast::ConditionalBranch {
404 literal: lit,
405 negated,
406 literal_span: point(lit_span),
407 relationships,
408 leading_comment: None,
409 trailing_line_comment: None,
410 blank_line_before: false,
411 trailing_comment: None,
412 blank_line_before_close: false,
413 open_brace_span: point(branch_open),
414 span: ast::ExprSpan {
415 start: pattern_start,
416 end: close,
417 },
418 });
419 }
420 let close_span = cursor.expect_close_brace()?;
421 Ok(ast::ConditionalDecl {
422 match_expr,
423 branches,
424 default,
425 leading_comment: None,
426 doc_comment: None,
427 trailing_line_comment: None,
428 blank_line_before: false,
429 trailing_comment: None,
430 blank_line_before_close: false,
431 open_brace_span: point(outer_open),
432 span: ast::ExprSpan {
433 start: decl_start,
434 end: close_span,
435 },
436 })
437 }
438
439 fn parse_branch_relationships(
442 &mut self,
443 cursor: &mut TokenCursor,
444 ) -> Result<Vec<ast::RelationshipDecl>> {
445 use cel_parser::lex_lexer::Token;
446 let mut relationships = Vec::new();
447 while !cursor.at_close_brace() {
448 if !matches!(cursor.peek_token(), Some(Token::Identifier(id)) if id == "relationship") {
449 return Err(cursor.err_at("expected `relationship`"));
450 }
451 relationships.push(self.parse_relationship_decl(cursor)?);
452 }
453 Ok(relationships)
454 }
455
456 fn parse_out_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::OutDecl> {
459 let decl_start = cursor.peek_span();
460 cursor.is_keyword("out");
461 let (name, name_span) = cursor.consume_ident()?;
462 let type_name = if cursor.consume_punct(":") {
463 Some(self.parse_type_expr(cursor)?)
464 } else {
465 None
466 };
467 cursor.expect_punct(":=")?;
468 let initializer = self.parse_cel_expression(cursor)?;
469 let filter = if cursor.is_keyword("filter") {
470 let filter_start = cursor.last_span();
471 Some(self.parse_cell_filter(cursor, filter_start)?)
472 } else {
473 None
474 };
475 let require = if cursor.is_keyword("require") {
476 Some(self.parse_require_block(cursor)?)
477 } else {
478 None
479 };
480 let semi_span = cursor.expect_punct(";")?;
481 Ok(ast::OutDecl {
482 name,
483 name_span: point(name_span),
484 type_name,
485 initializer,
486 filter,
487 require,
488 leading_comment: None,
489 doc_comment: None,
490 trailing_line_comment: None,
491 blank_line_before: false,
492 span: ast::ExprSpan {
493 start: decl_start,
494 end: semi_span,
495 },
496 })
497 }
498
499 fn parse_require_block(&mut self, cursor: &mut TokenCursor) -> Result<ast::RequireBlock> {
503 let open_span = cursor.expect_open_brace()?;
504 let mut requirements = Vec::new();
505 while !cursor.at_close_brace() {
506 requirements.push(self.parse_requirement(cursor)?);
507 }
508 let close_span = cursor.expect_close_brace()?;
509 Ok(ast::RequireBlock {
510 requirements,
511 trailing_comment: None,
512 blank_line_before_close: false,
513 open_brace_span: point(open_span),
514 span: ast::ExprSpan {
515 start: open_span,
516 end: close_span,
517 },
518 })
519 }
520
521 fn parse_requirement(&mut self, cursor: &mut TokenCursor) -> Result<ast::RequirementDecl> {
523 let decl_start = cursor.peek_span();
524 let (name, name_span) = if cursor.consume_punct("@") {
525 let (name, span) = cursor.consume_ident()?;
526 (Some(name), Some(point(span)))
527 } else {
528 (None, None)
529 };
530 let body = self.parse_cel_expression(cursor)?;
531 let semi_span = cursor.expect_punct(";")?;
532 Ok(ast::RequirementDecl {
533 name,
534 name_span,
535 body,
536 leading_comment: None,
537 trailing_line_comment: None,
538 blank_line_before: false,
539 span: ast::ExprSpan {
540 start: decl_start,
541 end: semi_span,
542 },
543 })
544 }
545
546 fn parse_cel_expression(&mut self, cursor: &mut TokenCursor) -> Result<cel_parser::Expr> {
550 self.parse_cel_expression_restricted(cursor, cel_parser::ExpressionRestriction::None)
551 }
552
553 fn parse_cel_expression_restricted(
561 &mut self,
562 cursor: &mut TokenCursor,
563 restriction: cel_parser::ExpressionRestriction,
564 ) -> Result<cel_parser::Expr> {
565 let tokens = cursor.take_tokens().expect("tokens present");
566 self.cel.set_lex_tokens(tokens);
567 let result = self
568 .cel
569 .parse_expression_ctx_restricted(restriction)
570 .map(cel_parser::AstContext::into_expr);
571 cursor.set_tokens(self.cel.take_lex_tokens().expect("tokens set"));
572 cursor.absorb_unbalanced_delimiters(self.cel.unbalanced_delimiter_count());
573 result
574 }
575}
576
577fn point(span: proc_macro2::Span) -> ast::ExprSpan {
579 ast::ExprSpan {
580 start: span,
581 end: span,
582 }
583}
584
585fn parse_binding_target(cursor: &mut TokenCursor) -> Result<(Vec<(String, ast::ExprSpan)>, bool)> {
592 if !cursor.at_open_paren() {
593 let (name, span) = cursor.consume_ident()?;
594 return Ok((vec![(name, point(span))], false));
595 }
596
597 cursor.expect_open_paren()?;
598 let (first_name, first_span) = cursor.consume_ident()?;
599 if cursor.at_close_paren() {
600 cursor.expect_close_paren()?;
602 return Ok((vec![(first_name, point(first_span))], false));
603 }
604 if !cursor.consume_punct(",") {
605 return Err(cursor.err_at("expected ',' or closing parenthesis"));
606 }
607 if cursor.at_close_paren() {
608 cursor.expect_close_paren()?;
610 return Ok((vec![(first_name, point(first_span))], true));
611 }
612 let mut outputs = vec![(first_name, point(first_span))];
613 loop {
614 let (name, span) = cursor.consume_ident()?;
615 outputs.push((name, point(span)));
616 if cursor.at_close_paren() {
617 break;
618 }
619 if !cursor.consume_punct(",") {
620 return Err(cursor.err_at("expected ',' or closing parenthesis"));
621 }
622 }
623 cursor.expect_close_paren()?;
624 Ok((outputs, true))
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use cel_parser::Expr;
631
632 #[test]
633 fn parse_empty_sheet_has_no_items() {
634 let sheet = AdamAstParser::new().parse_str("sheet empty {}").unwrap();
635 assert_eq!(sheet.name, "empty");
636 assert!(sheet.items.is_empty());
637 assert!(sheet.errors.is_empty());
638 }
639
640 #[test]
641 fn parse_cell_with_annotation_and_initializer() {
642 let sheet = AdamAstParser::new()
643 .parse_str("sheet s { cell width: f64 = 1920.0; }")
644 .unwrap();
645 assert_eq!(sheet.items.len(), 1);
646 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
647 panic!("expected Cell");
648 };
649 assert_eq!(cell.name, "width");
650 assert!(matches!(
651 cell.type_name.as_ref().unwrap(),
652 ast::TypeExpr::Named { name: n, .. } if n == "f64"
653 ));
654 assert!(cell.initializer.is_some());
655 }
656
657 #[test]
658 fn parse_cell_annotation_only_has_no_initializer() {
659 let sheet = AdamAstParser::new()
660 .parse_str("sheet s { cell area: f64; }")
661 .unwrap();
662 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
663 panic!("expected Cell");
664 };
665 assert!(cell.type_name.is_some());
666 assert!(cell.initializer.is_none());
667 }
668
669 #[test]
670 fn parse_cell_initializer_only_has_no_type_name() {
671 let sheet = AdamAstParser::new()
672 .parse_str("sheet s { cell mode = 0i32; }")
673 .unwrap();
674 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
675 panic!("expected Cell");
676 };
677 assert!(cell.type_name.is_none());
678 assert!(cell.initializer.is_some());
679 }
680
681 #[test]
682 fn parse_source_decl_produces_a_source_decl_sheet_item() {
683 let sheet = AdamAstParser::new()
684 .parse_str("sheet s { source width: i32 = 4; }")
685 .unwrap();
686 assert!(matches!(sheet.items[0], ast::SheetItem::Source(_)));
687 }
688
689 #[test]
690 fn parse_source_with_a_filter() {
691 let sheet = AdamAstParser::new()
692 .parse_str("sheet s { source a: i32 = 1 filter _; }")
693 .unwrap();
694 let ast::SheetItem::Source(source) = &sheet.items[0] else {
695 panic!("expected Source");
696 };
697 let filter = source.filter.as_ref().expect("filter present");
698 assert!(matches!(&filter.body, Expr::Ident { name, .. } if name == "_"));
699 }
700
701 #[test]
702 fn parse_source_without_a_filter_leaves_it_none() {
703 let sheet = AdamAstParser::new()
704 .parse_str("sheet s { source a: i32 = 1; }")
705 .unwrap();
706 let ast::SheetItem::Source(source) = &sheet.items[0] else {
707 panic!("expected Source");
708 };
709 assert!(source.filter.is_none());
710 }
711
712 #[test]
713 fn parse_relationship_records_bindings_in_order() {
714 let sheet = AdamAstParser::new()
715 .parse_str(
716 r#"
717 sheet s {
718 relationship {
719 area := width * height;
720 width := area / height;
721 }
722 }
723 "#,
724 )
725 .unwrap();
726 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
727 panic!("expected Relationship");
728 };
729 assert_eq!(rel.bindings.len(), 2);
730 assert_eq!(rel.bindings[0].outputs[0].0, "area");
731 assert!(matches!(rel.bindings[0].body, Expr::Op { ref name, .. } if name == "*"));
732 }
733
734 #[test]
735 fn parse_binding_bare_identifier_target_is_not_a_destructure() {
736 let sheet = AdamAstParser::new()
737 .parse_str("sheet s { relationship { x := a; } }")
738 .unwrap();
739 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
740 panic!("expected Relationship");
741 };
742 assert!(!rel.bindings[0].destructure);
743 assert_eq!(rel.bindings[0].outputs.len(), 1);
744 }
745
746 #[test]
747 fn parse_binding_grouped_single_identifier_target_is_not_a_destructure() {
748 let sheet = AdamAstParser::new()
749 .parse_str("sheet s { relationship { (x) := a; } }")
750 .unwrap();
751 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
752 panic!("expected Relationship");
753 };
754 assert!(!rel.bindings[0].destructure);
755 assert_eq!(rel.bindings[0].outputs.len(), 1);
756 }
757
758 #[test]
759 fn parse_binding_single_element_tuple_target_is_a_destructure() {
760 let sheet = AdamAstParser::new()
761 .parse_str("sheet s { relationship { (x,) := (a,); } }")
762 .unwrap();
763 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
764 panic!("expected Relationship");
765 };
766 assert!(rel.bindings[0].destructure);
767 assert_eq!(rel.bindings[0].outputs.len(), 1);
768 assert_eq!(rel.bindings[0].outputs[0].0, "x");
769 }
770
771 #[test]
772 fn parse_binding_multi_identifier_target_is_a_destructure() {
773 let sheet = AdamAstParser::new()
774 .parse_str("sheet s { relationship { (x, y) := (a, b); } }")
775 .unwrap();
776 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
777 panic!("expected Relationship");
778 };
779 assert!(rel.bindings[0].destructure);
780 assert_eq!(
781 rel.bindings[0]
782 .outputs
783 .iter()
784 .map(|(name, _)| name.as_str())
785 .collect::<Vec<_>>(),
786 vec!["x", "y"]
787 );
788 }
789
790 #[test]
791 fn multi_output_binding_without_parens_recovers_at_sheet_item_level() {
792 let sheet = AdamAstParser::new()
793 .parse_str(
794 r#"
795 sheet s {
796 cell good_before: i32 = 1;
797 relationship {
798 x, y := (a, b);
799 }
800 cell good_after: i32 = 2;
801 }
802 "#,
803 )
804 .unwrap();
805 assert_eq!(sheet.errors.len(), 1);
806 assert_eq!(sheet.items.len(), 3);
807 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
808 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
809 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
810 }
811
812 #[test]
813 fn parse_conditional_records_branches_and_default() {
814 let sheet = AdamAstParser::new()
815 .parse_str(
816 r#"
817 sheet s {
818 conditional mode {
819 0i32 => { relationship { height := width; } },
820 _ => { relationship { height := width; } },
821 }
822 }
823 "#,
824 )
825 .unwrap();
826 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
827 panic!("expected Conditional");
828 };
829 assert!(matches!(&cond.match_expr, Expr::Ident { name, .. } if name == "mode"));
830 assert_eq!(cond.branches.len(), 1);
831 assert!(!cond.branches[0].negated);
832 assert!(cond.default.is_some());
833 }
834
835 #[test]
836 fn parse_conditional_records_a_negated_literal_branch_key() {
837 let sheet = AdamAstParser::new()
838 .parse_str(
839 r#"
840 sheet s {
841 conditional mode {
842 -1i32 => { relationship { height := width; } },
843 _ => { relationship { height := width; } },
844 }
845 }
846 "#,
847 )
848 .unwrap();
849 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
850 panic!("expected Conditional");
851 };
852 assert_eq!(cond.branches.len(), 1);
853 assert!(cond.branches[0].negated);
854 }
855
856 #[test]
857 fn parse_conditional_branch_dash_not_followed_by_a_literal_is_error() {
858 let sheet = AdamAstParser::new()
859 .parse_str(
860 r#"
861 sheet s {
862 conditional mode {
863 -width => { relationship { height := width; } },
864 }
865 }
866 "#,
867 )
868 .unwrap();
869 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
870 }
871
872 #[test]
873 fn parse_conditional_branch_literal_with_invalid_suffix_is_error() {
874 let sheet = AdamAstParser::new()
878 .parse_str(
879 r#"
880 sheet s {
881 conditional mode {
882 10xyz => { relationship { height := width; } },
883 }
884 }
885 "#,
886 )
887 .unwrap();
888 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
889 }
890
891 #[test]
892 fn parse_conditional_branch_negating_an_unsigned_literal_is_error() {
893 let sheet = AdamAstParser::new()
896 .parse_str(
897 r#"
898 sheet s {
899 conditional mode {
900 -1u32 => { relationship { height := width; } },
901 }
902 }
903 "#,
904 )
905 .unwrap();
906 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
907 }
908
909 #[test]
910 fn parse_conditional_records_an_expression_match_subject() {
911 let sheet = AdamAstParser::new()
912 .parse_str(
913 r#"
914 sheet s {
915 conditional a && b {
916 _ => { relationship { height := width; } },
917 }
918 }
919 "#,
920 )
921 .unwrap();
922 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
923 panic!("expected Conditional");
924 };
925 assert!(matches!(
926 &cond.match_expr,
927 Expr::Logical {
928 op: cel_parser::LogicalOp::And,
929 ..
930 }
931 ));
932 }
933
934 #[test]
936 fn adam_condition_and_match_headers_reserve_braces() {
937 for source in [
938 "sheet s { conditional flag { _ => { relationship { x := if flag { 1 } else { 2 }; } } } }",
939 "sheet s { conditional (TestPoint { x: 0.0, y: 1.0 }).x { _ => { relationship { x := TestPoint { x: 2.0, y: 3.0 }; } } } }",
940 ] {
941 let sheet = AdamAstParser::new().parse_str(source).unwrap();
942 assert!(sheet.errors.is_empty(), "{:?}", sheet.errors);
943 assert!(matches!(sheet.items[0], ast::SheetItem::Conditional(_)));
944 let formatted = crate::fmt::format_sheet(&sheet, source);
945 let reparsed = AdamAstParser::new().parse_str(&formatted).unwrap();
946 assert!(
947 reparsed.errors.is_empty(),
948 "{formatted}\n{:?}",
949 reparsed.errors
950 );
951 assert_eq!(crate::fmt::format_sheet(&reparsed, &formatted), formatted);
952 }
953 }
954
955 #[test]
956 fn parse_conditional_branch_records_multiple_relationships() {
957 let sheet = AdamAstParser::new()
958 .parse_str(
959 r#"
960 sheet s {
961 conditional mode {
962 0i32 => {
963 relationship { b := a; }
964 relationship { d := c; }
965 },
966 }
967 }
968 "#,
969 )
970 .unwrap();
971 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
972 panic!("expected Conditional");
973 };
974 assert_eq!(cond.branches[0].relationships.len(), 2);
975 }
976
977 #[test]
978 fn parse_conditional_default_branch_records_multiple_relationships() {
979 let sheet = AdamAstParser::new()
980 .parse_str(
981 r#"
982 sheet s {
983 conditional mode {
984 _ => {
985 relationship { b := a; }
986 relationship { d := c; }
987 },
988 }
989 }
990 "#,
991 )
992 .unwrap();
993 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
994 panic!("expected Conditional");
995 };
996 assert_eq!(
997 cond.default.as_ref().map(|d| d.relationships.len()),
998 Some(2)
999 );
1000 }
1001
1002 #[test]
1003 fn conditional_branch_bare_binding_without_relationship_wrapper_recovers() {
1004 let sheet = AdamAstParser::new()
1008 .parse_str(
1009 r#"
1010 sheet s {
1011 cell good_before: i32 = 1;
1012 conditional mode { 0i32 => { b := a; } }
1013 cell good_after: i32 = 2;
1014 }
1015 "#,
1016 )
1017 .unwrap();
1018 assert_eq!(sheet.errors.len(), 1);
1019 assert_eq!(sheet.items.len(), 3);
1020 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1021 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1022 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1023 }
1024
1025 #[test]
1026 fn parse_binding_body_is_a_cel_expr_tree() {
1027 let sheet = AdamAstParser::new()
1028 .parse_str("sheet s { relationship { c := (a + b, a - b); } }")
1029 .unwrap();
1030 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1031 panic!("expected Relationship");
1032 };
1033 assert!(matches!(rel.bindings[0].body, Expr::Tuple { .. }));
1034 }
1035
1036 #[test]
1037 fn parse_unknown_sheet_item_is_recorded_as_an_error_item() {
1038 let sheet = AdamAstParser::new()
1039 .parse_str("sheet s { bogus x; }")
1040 .unwrap();
1041 assert_eq!(sheet.errors.len(), 1);
1042 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1043 }
1044
1045 #[test]
1046 fn parse_malformed_cell_is_recorded_as_an_error_item() {
1047 let sheet = AdamAstParser::new()
1048 .parse_str("sheet s { cell x unknown_syntax }")
1049 .unwrap();
1050 assert_eq!(sheet.errors.len(), 1);
1051 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1052 }
1053
1054 #[test]
1055 fn recovery_records_an_error_item_and_continues_parsing() {
1056 let sheet = AdamAstParser::new()
1057 .parse_str(
1058 r#"
1059 sheet s {
1060 cell good_before: i32 = 1;
1061 cell bad unknown_syntax
1062 cell good_after: i32 = 2;
1063 }
1064 "#,
1065 )
1066 .unwrap();
1067 assert_eq!(sheet.items.len(), 3);
1068 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1069 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1070 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1071 assert_eq!(sheet.errors.len(), 1);
1072 }
1073
1074 #[test]
1075 fn recovery_error_span_covers_tokens_actually_consumed_by_the_failed_item() {
1076 let source = "sheet s { cell bad relationship { y := x; } }";
1082 let sheet = AdamAstParser::new().parse_str(source).unwrap();
1083 let ast::SheetItem::Error { span, .. } = &sheet.items[0] else {
1084 panic!("expected Error");
1085 };
1086 let range = cel_parser::SourceSpan {
1087 start: span.start.start(),
1088 end: span.end.end(),
1089 }
1090 .to_byte_range(source);
1091 assert_eq!(&source[range], "cell bad");
1092 }
1093
1094 #[test]
1095 fn recovery_collects_multiple_errors_from_multiple_malformed_items() {
1096 let sheet = AdamAstParser::new()
1097 .parse_str(
1098 r#"
1099 sheet s {
1100 cell bad1 unknown_syntax;
1101 cell bad2 unknown_syntax;
1102 cell good: i32 = 1;
1103 }
1104 "#,
1105 )
1106 .unwrap();
1107 assert_eq!(sheet.errors.len(), 2);
1108 assert!(matches!(sheet.items.last(), Some(ast::SheetItem::Cell(_))));
1109 }
1110
1111 #[test]
1112 fn well_formed_input_has_empty_errors() {
1113 let sheet = AdamAstParser::new()
1114 .parse_str("sheet s { cell x: i32 = 1; }")
1115 .unwrap();
1116 assert!(sheet.errors.is_empty());
1117 }
1118
1119 #[test]
1127 fn recovery_malformed_relationship_item_recovers() {
1128 let sheet = AdamAstParser::new()
1129 .parse_str(
1130 r#"
1131 sheet s {
1132 cell good_before: i32 = 1;
1133 relationship { bad }
1134 cell good_after: i32 = 2;
1135 }
1136 "#,
1137 )
1138 .unwrap();
1139 assert_eq!(sheet.errors.len(), 1);
1140 assert_eq!(sheet.items.len(), 3);
1141 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1142 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1143 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1144 }
1145
1146 #[test]
1150 fn recovery_malformed_conditional_item_recovers() {
1151 let sheet = AdamAstParser::new()
1152 .parse_str(
1153 r#"
1154 sheet s {
1155 cell good_before: i32 = 1;
1156 conditional m { bad }
1157 cell good_after: i32 = 2;
1158 }
1159 "#,
1160 )
1161 .unwrap();
1162 assert_eq!(sheet.errors.len(), 1);
1163 assert_eq!(sheet.items.len(), 3);
1164 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1165 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1166 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1167 }
1168
1169 #[test]
1175 fn recovery_malformed_binding_body_recovers_at_sheet_item_level() {
1176 let sheet = AdamAstParser::new()
1177 .parse_str(
1178 r#"
1179 sheet s {
1180 cell good_before: i32 = 1;
1181 relationship {
1182 b := a + ;
1183 }
1184 cell good_after: i32 = 2;
1185 }
1186 "#,
1187 )
1188 .unwrap();
1189 assert_eq!(sheet.errors.len(), 1);
1190 assert_eq!(sheet.items.len(), 3);
1191 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1192 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1193 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1194 }
1195
1196 #[test]
1207 fn recovery_cel_dangling_paren_recovers_at_sheet_item_level() {
1208 let sheet = AdamAstParser::new()
1209 .parse_str(
1210 r#"
1211 sheet s {
1212 cell good_before: i32 = 1;
1213 relationship { b := (+); }
1214 cell good_after: i32 = 2;
1215 }
1216 "#,
1217 )
1218 .unwrap();
1219 assert_eq!(sheet.errors.len(), 1);
1220 assert_eq!(sheet.items.len(), 3);
1221 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1222 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1223 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1224 }
1225
1226 #[test]
1236 fn recovery_if_expr_dangling_brace_recovers_at_sheet_item_level() {
1237 let sheet = AdamAstParser::new()
1238 .parse_str(
1239 r#"
1240 sheet s {
1241 cell good_before: i32 = 1;
1242 relationship { b := if a { }; }
1243 cell good_after: i32 = 2;
1244 }
1245 "#,
1246 )
1247 .unwrap();
1248 assert_eq!(sheet.errors.len(), 1);
1249 assert_eq!(sheet.items.len(), 3);
1250 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1251 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1252 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1253 }
1254
1255 #[test]
1266 fn recovery_keyword_shaped_garbage_inside_dangling_brace_recovers_cleanly() {
1267 let sheet = AdamAstParser::new()
1268 .parse_str(
1269 "sheet s { relationship { b := if a { + cell good: i32 = 1; }; } cell trailing: i32 = 2; }",
1270 )
1271 .unwrap();
1272 assert_eq!(sheet.errors.len(), 1);
1273 assert_eq!(sheet.items.len(), 2);
1274 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1275 assert!(matches!(sheet.items[1], ast::SheetItem::Cell(_)));
1276 }
1277
1278 #[test]
1279 fn parse_out_with_explicit_type_and_no_requirements() {
1280 let sheet = AdamAstParser::new()
1281 .parse_str(
1282 r#"
1283 sheet s {
1284 cell width: f64 = 4.0;
1285 cell height: f64 = 3.0;
1286 out area: f64 := width * height;
1287 }
1288 "#,
1289 )
1290 .unwrap();
1291 assert!(sheet.errors.is_empty());
1292 let ast::SheetItem::Out(out) = &sheet.items[2] else {
1293 panic!("expected Out");
1294 };
1295 assert_eq!(out.name, "area");
1296 assert!(matches!(
1297 out.type_name.as_ref().unwrap(),
1298 ast::TypeExpr::Named { name: n, .. } if n == "f64"
1299 ));
1300 assert!(matches!(out.initializer, Expr::Op { ref name, .. } if name == "*"));
1301 assert!(out.require.is_none());
1302 }
1303
1304 #[test]
1305 fn parse_out_with_no_type_annotation() {
1306 let sheet = AdamAstParser::new()
1307 .parse_str("sheet s { out area := width; }")
1308 .unwrap();
1309 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1310 panic!("expected Out");
1311 };
1312 assert!(out.type_name.is_none());
1313 }
1314
1315 #[test]
1316 fn parse_out_with_requirements_in_declaration_order() {
1317 let sheet = AdamAstParser::new()
1318 .parse_str(
1319 r#"
1320 sheet s {
1321 out area: f64 := width * height require {
1322 @max_area width * height <= max_area;
1323 @max_width width <= max_width;
1324 };
1325 }
1326 "#,
1327 )
1328 .unwrap();
1329 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1330 panic!("expected Out");
1331 };
1332 let require = out.require.as_ref().expect("require block present");
1333 assert_eq!(require.requirements.len(), 2);
1334 assert_eq!(require.requirements[0].name.as_deref(), Some("max_area"));
1335 assert_eq!(require.requirements[1].name.as_deref(), Some("max_width"));
1336 }
1337
1338 #[test]
1339 fn parse_requirement_without_a_label_leaves_name_none() {
1340 let sheet = AdamAstParser::new()
1341 .parse_str(
1342 r#"
1343 sheet s {
1344 out area: f64 := width * height require {
1345 width * height <= max_area;
1346 };
1347 }
1348 "#,
1349 )
1350 .unwrap();
1351 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1352 panic!("expected Out");
1353 };
1354 let require = out.require.as_ref().expect("require block present");
1355 assert_eq!(require.requirements[0].name, None);
1356 }
1357
1358 #[test]
1359 fn parse_malformed_out_is_recorded_as_an_error_item() {
1360 let sheet = AdamAstParser::new()
1361 .parse_str(
1362 r#"
1363 sheet s {
1364 cell good_before: i32 = 1;
1365 out area { bad }
1366 cell good_after: i32 = 2;
1367 }
1368 "#,
1369 )
1370 .unwrap();
1371 assert_eq!(sheet.errors.len(), 1);
1372 assert_eq!(sheet.items.len(), 3);
1373 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1374 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1375 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1376 }
1377
1378 #[test]
1379 fn parse_cell_with_explicit_tuple_type() {
1380 let sheet = AdamAstParser::new()
1381 .parse_str("sheet s { cell a: (i32, f64); }")
1382 .unwrap();
1383 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1384 panic!("expected Cell");
1385 };
1386 match cell.type_name.as_ref().unwrap() {
1387 ast::TypeExpr::Tuple { elements, .. } => {
1388 assert_eq!(elements.len(), 2);
1389 assert!(matches!(&elements[0], ast::TypeExpr::Named { name: n, .. } if n == "i32"));
1390 assert!(matches!(&elements[1], ast::TypeExpr::Named { name: n, .. } if n == "f64"));
1391 }
1392 other => panic!("expected Tuple, got {other:?}"),
1393 }
1394 }
1395
1396 #[test]
1397 fn parse_cell_with_a_filter() {
1398 let sheet = AdamAstParser::new()
1399 .parse_str("sheet s { cell a: i32 = 1 filter _; }")
1400 .unwrap();
1401 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1402 panic!("expected Cell");
1403 };
1404 let filter = cell.filter.as_ref().expect("filter present");
1405 assert!(matches!(&filter.body, Expr::Ident { name, .. } if name == "_"));
1406 }
1407
1408 #[test]
1409 fn parse_cell_with_a_filter_referencing_a_cell() {
1410 let sheet = AdamAstParser::new()
1411 .parse_str("sheet s { cell hi: i32 = 100; cell a: i32 = 1 filter _ + hi; }")
1412 .unwrap();
1413 let ast::SheetItem::Cell(cell) = &sheet.items[1] else {
1414 panic!("expected Cell");
1415 };
1416 let filter = cell.filter.as_ref().expect("filter present");
1417 match &filter.body {
1418 Expr::Op { name, operands, .. } => {
1419 assert_eq!(name, "+");
1420 assert!(matches!(&operands[0], Expr::Ident { name, .. } if name == "_"));
1421 assert!(matches!(&operands[1], Expr::Ident { name, .. } if name == "hi"));
1422 }
1423 other => panic!("expected Op, got {other:?}"),
1424 }
1425 }
1426
1427 #[test]
1428 fn parse_cell_without_a_filter_leaves_it_none() {
1429 let sheet = AdamAstParser::new()
1430 .parse_str("sheet s { cell a: i32 = 1; }")
1431 .unwrap();
1432 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1433 panic!("expected Cell");
1434 };
1435 assert!(cell.filter.is_none());
1436 }
1437
1438 #[test]
1439 fn recovery_malformed_filter_recovers_at_the_next_sheet_item() {
1440 let sheet = AdamAstParser::new()
1441 .parse_str(
1442 r#"
1443 sheet s {
1444 cell good_before: i32 = 1;
1445 cell bad: i32 = 1 filter |x: i32|;
1446 cell good_after: i32 = 2;
1447 }
1448 "#,
1449 )
1450 .unwrap();
1451 assert_eq!(sheet.errors.len(), 1);
1452 assert_eq!(sheet.items.len(), 3);
1453 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1454 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1455 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1456 }
1457
1458 #[test]
1459 fn parse_cell_with_nested_tuple_type() {
1460 let sheet = AdamAstParser::new()
1461 .parse_str("sheet s { cell a: (i32, (f64, String)); }")
1462 .unwrap();
1463 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1464 panic!("expected Cell");
1465 };
1466 let ast::TypeExpr::Tuple { elements, .. } = cell.type_name.as_ref().unwrap() else {
1467 panic!("expected top-level Tuple");
1468 };
1469 assert_eq!(elements.len(), 2);
1470 assert!(matches!(&elements[0], ast::TypeExpr::Named { name: n, .. } if n == "i32"));
1471 match &elements[1] {
1472 ast::TypeExpr::Tuple {
1473 elements: inner, ..
1474 } => assert_eq!(inner.len(), 2),
1475 other => panic!("expected nested Tuple, got {other:?}"),
1476 }
1477 }
1478
1479 #[test]
1480 fn parse_cell_with_empty_tuple_type() {
1481 let sheet = AdamAstParser::new()
1482 .parse_str("sheet s { cell a: (); }")
1483 .unwrap();
1484 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1485 panic!("expected Cell");
1486 };
1487 match cell.type_name.as_ref().unwrap() {
1488 ast::TypeExpr::Tuple { elements, .. } => assert!(elements.is_empty()),
1489 other => panic!("expected empty Tuple, got {other:?}"),
1490 }
1491 }
1492
1493 #[test]
1494 fn parse_cell_with_parenthesized_type_is_grouping_not_a_1_tuple() {
1495 let sheet = AdamAstParser::new()
1496 .parse_str("sheet s { cell a: (i32); }")
1497 .unwrap();
1498 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1499 panic!("expected Cell");
1500 };
1501 assert!(matches!(
1502 cell.type_name.as_ref().unwrap(),
1503 ast::TypeExpr::Named { name: n, .. } if n == "i32"
1504 ));
1505 }
1506
1507 #[test]
1508 fn parse_cell_with_1_tuple_type_requires_trailing_comma() {
1509 let sheet = AdamAstParser::new()
1510 .parse_str("sheet s { cell a: (i32,); }")
1511 .unwrap();
1512 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1513 panic!("expected Cell");
1514 };
1515 match cell.type_name.as_ref().unwrap() {
1516 ast::TypeExpr::Tuple { elements, .. } => assert_eq!(elements.len(), 1),
1517 other => panic!("expected 1-Tuple, got {other:?}"),
1518 }
1519 }
1520
1521 #[test]
1522 fn parse_cell_initializer_is_a_tuple_expr() {
1523 let sheet = AdamAstParser::new()
1524 .parse_str("sheet s { cell a = (1, 2.5); }")
1525 .unwrap();
1526 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1527 panic!("expected Cell");
1528 };
1529 assert!(matches!(cell.initializer, Some(Expr::Tuple { .. })));
1530 }
1531
1532 #[test]
1533 fn parse_out_with_explicit_tuple_type() {
1534 let sheet = AdamAstParser::new()
1535 .parse_str("sheet s { out a: (i32, f64) := (x, x); }")
1536 .unwrap();
1537 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1538 panic!("expected Out");
1539 };
1540 assert!(matches!(
1541 out.type_name.as_ref().unwrap(),
1542 ast::TypeExpr::Tuple { elements, .. } if elements.len() == 2
1543 ));
1544 }
1545
1546 #[test]
1547 fn malformed_tuple_type_recovers_at_the_next_sheet_item() {
1548 let sheet = AdamAstParser::new()
1554 .parse_str("sheet s { cell good_before: i32 = 1; cell bad: (i32 i32); cell good_after: i32 = 2; }")
1555 .unwrap();
1556 assert_eq!(sheet.errors.len(), 1);
1557 assert_eq!(sheet.items.len(), 3);
1558 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1559 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1560 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1561 }
1562
1563 #[test]
1564 fn malformed_generic_type_argument_list_recovers_at_the_next_sheet_item() {
1565 let sheet = AdamAstParser::new()
1568 .parse_str(
1569 "sheet s { cell good_before: i32 = 1; cell bad: Pair(i32 i32); cell good_after: i32 = 2; }",
1570 )
1571 .unwrap();
1572 assert_eq!(sheet.errors.len(), 1);
1573 assert_eq!(sheet.items.len(), 3);
1574 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1575 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1576 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1577 }
1578
1579 #[test]
1580 fn attaches_an_outer_doc_comment_to_a_cell() {
1581 let sheet = AdamAstParser::new()
1582 .parse_str("sheet s {\n /// the total\n cell x: i32 = 1;\n}")
1583 .unwrap();
1584 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1585 panic!("expected Cell");
1586 };
1587 assert_eq!(cell.doc_comment.as_deref(), Some(" the total"));
1588 }
1589
1590 #[test]
1591 fn attaches_an_outer_doc_comment_to_a_relationship() {
1592 let sheet = AdamAstParser::new()
1593 .parse_str("sheet s {\n /// docs\n relationship { b := a; }\n}")
1594 .unwrap();
1595 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1596 panic!("expected Relationship");
1597 };
1598 assert_eq!(rel.doc_comment.as_deref(), Some(" docs"));
1599 }
1600
1601 #[test]
1602 fn attaches_an_outer_doc_comment_to_a_conditional() {
1603 let sheet = AdamAstParser::new()
1604 .parse_str(
1605 "sheet s {\n cell p: i32 = 0;\n /// docs\n conditional p {\n _ => { relationship { b := a; } }\n }\n}",
1606 )
1607 .unwrap();
1608 let ast::SheetItem::Conditional(cond) = &sheet.items[1] else {
1609 panic!("expected Conditional");
1610 };
1611 assert_eq!(cond.doc_comment.as_deref(), Some(" docs"));
1612 }
1613
1614 #[test]
1615 fn attaches_an_outer_doc_comment_to_an_out_decl() {
1616 let sheet = AdamAstParser::new()
1617 .parse_str("sheet s {\n /// docs\n out area: f64 := w;\n}")
1618 .unwrap();
1619 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1620 panic!("expected Out");
1621 };
1622 assert_eq!(out.doc_comment.as_deref(), Some(" docs"));
1623 }
1624
1625 #[test]
1626 fn attaches_a_sheet_level_inner_doc_comment() {
1627 let sheet = AdamAstParser::new()
1628 .parse_str("//! module docs\nsheet s {\n cell x: i32 = 1;\n}")
1629 .unwrap();
1630 assert_eq!(sheet.doc_comment.as_deref(), Some(" module docs"));
1631 }
1632
1633 #[test]
1634 fn joins_consecutive_outer_doc_comment_lines() {
1635 let sheet = AdamAstParser::new()
1636 .parse_str("sheet s {\n /// line one\n /// line two\n cell x: i32 = 1;\n}")
1637 .unwrap();
1638 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1639 panic!("expected Cell");
1640 };
1641 assert_eq!(cell.doc_comment.as_deref(), Some(" line one\n line two"));
1642 }
1643
1644 #[test]
1645 fn a_doc_comment_binds_forward_across_a_blank_line() {
1646 let sheet = AdamAstParser::new()
1647 .parse_str("sheet s {\n /// docs\n\n cell x: i32 = 1;\n}")
1648 .unwrap();
1649 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1650 panic!("expected Cell");
1651 };
1652 assert_eq!(cell.doc_comment.as_deref(), Some(" docs"));
1653 }
1654
1655 #[test]
1656 fn a_plain_comment_and_a_doc_comment_coexist_in_source_order() {
1657 let source =
1668 "sheet s {\n cell w: i32 = 0;\n // TODO\n /// docs\n cell x: i32 = 1;\n}";
1669 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1670 crate::attach_trivia(source, &mut sheet);
1671 let ast::SheetItem::Cell(cell) = &sheet.items[1] else {
1672 panic!("expected Cell");
1673 };
1674 assert_eq!(cell.doc_comment.as_deref(), Some(" docs"));
1675 assert_eq!(
1676 cell.leading_comment,
1677 Some(ast::Comment::Line("TODO".to_string()))
1678 );
1679 }
1680
1681 #[test]
1682 fn a_doc_comment_before_a_binding_recovers_as_a_declaration_level_error() {
1683 let sheet = AdamAstParser::new()
1684 .parse_str(
1685 "sheet s {\n relationship {\n /// not allowed here\n b := a;\n }\n}",
1686 )
1687 .unwrap();
1688 assert!(!sheet.errors.is_empty());
1689 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1690 }
1691
1692 #[test]
1693 fn range_syntax_is_reachable_from_a_relationship_binding() {
1694 let sheet = AdamAstParser::new()
1695 .parse_str("sheet s { relationship { x := 1..5; } }")
1696 .unwrap();
1697 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1698 panic!("expected a Relationship item, got {:?}", sheet.items[0]);
1699 };
1700 assert!(
1701 matches!(&rel.bindings[0].body, cel_parser::Expr::Op { name, .. } if name == "range"),
1702 "expected the binding body to be a range Expr::Op, got {:?}",
1703 rel.bindings[0].body
1704 );
1705 }
1706}