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 {
33 Self::new()
34 }
35}
36
37impl AdamAstParser {
38 #[must_use]
47 pub fn new() -> Self {
48 AdamAstParser {
49 cel: CelParser::new(OpLookup::new()),
50 }
51 }
52
53 pub fn parse_str(&mut self, source: &str) -> Result<ast::Sheet> {
91 use std::str::FromStr;
92 let stream = proc_macro2::TokenStream::from_str(source)
93 .map_err(|e| cel_parser::ParseError::from_lex_error(source, e))?;
94 let mut cursor =
95 TokenCursor::new(cel_parser::lex_lexer::LexLexer::new(stream.into_iter()).peekable());
96 let doc_comment = cursor.consume_doc_comment_run(true);
97 let mut sheet = self.parse_sheet(&mut cursor)?;
98 if sheet.parameters.is_some() {
99 return Err(cursor.err_at("expected one root sheet without parameters"));
100 }
101 if let Some((text, span)) = doc_comment {
102 sheet.doc_comment = Some(text);
103 sheet.span.start = span;
104 }
105 if let Some(tok) = cursor.peek_token() {
106 use cel_parser::lex_lexer::HasSpan;
107 return Err(cel_parser::ParseError::new("unexpected token", tok.span()));
108 }
109 Ok(sheet)
110 }
111
112 pub fn parse_module_str(&mut self, source: &str) -> Result<ast::Module> {
131 use std::str::FromStr;
132 let stream = proc_macro2::TokenStream::from_str(source)
133 .map_err(|e| cel_parser::ParseError::from_lex_error(source, e))?;
134 let mut cursor =
135 TokenCursor::new(cel_parser::lex_lexer::LexLexer::new(stream.into_iter()).peekable());
136 let mut module = ast::Module {
137 items: Vec::new(),
138 errors: Vec::new(),
139 comments: Vec::new(),
140 };
141 while cursor.peek_token().is_some() {
142 let doc = cursor.consume_doc_comment_run(true);
143 let start = doc
144 .as_ref()
145 .map_or_else(|| cursor.peek_span(), |(_, span)| *span);
146 cursor.set_last_span(start);
147 let keyword_span = cursor.peek_span();
148 let result = if cursor.is_keyword("use") {
149 self.parse_use_decl(&mut cursor, keyword_span)
150 .map(|mut import| {
151 if let Some((text, span)) = doc {
152 import.doc_comment = Some(text);
153 import.span.start = span;
154 }
155 ast::ModuleItem::Use(import)
156 })
157 } else {
158 self.parse_sheet(&mut cursor).map(|mut sheet| {
159 if let Some((text, span)) = doc {
160 sheet.doc_comment = Some(text);
161 sheet.span.start = span;
162 }
163 ast::ModuleItem::Sheet(sheet)
164 })
165 };
166 match result {
167 Ok(item) => {
168 if let ast::ModuleItem::Sheet(sheet) = &item {
169 module.errors.extend(sheet.errors.iter().cloned());
170 }
171 module.items.push(item);
172 }
173 Err(error) => {
174 module.errors.push(error);
175 let end = cursor.skip_to_module_item()?;
176 module
177 .items
178 .push(ast::ModuleItem::Error(ast::ExprSpan { start, end }));
179 }
180 }
181 }
182 Ok(module)
183 }
184
185 fn parse_path(&mut self, cursor: &mut TokenCursor) -> Result<Vec<(String, ast::ExprSpan)>> {
189 let (name, span) = self.parse_module_identifier(cursor)?;
190 let mut path = vec![(name, point(span))];
191 while cursor.consume_punct(":") {
192 cursor.expect_punct(":")?;
193 let (name, span) = self.parse_module_identifier(cursor)?;
194 path.push((name, point(span)));
195 }
196 Ok(path)
197 }
198
199 fn parse_module_identifier(
203 &mut self,
204 cursor: &mut TokenCursor,
205 ) -> Result<(String, proc_macro2::Span)> {
206 use cel_parser::lex_lexer::Token;
207 if matches!(cursor.peek_token(), Some(Token::Identifier(id)) if !crate::module::valid_identifier(&id.to_string()))
208 {
209 return Err(cursor.err_at("expected module identifier"));
210 }
211 cursor.consume_ident()
212 }
213
214 fn parse_use_decl(
219 &mut self,
220 cursor: &mut TokenCursor,
221 start: proc_macro2::Span,
222 ) -> Result<ast::UseDecl> {
223 let path = self.parse_path(cursor)?;
224 let alias = if cursor.is_keyword("as") {
225 let (name, span) = self.parse_module_identifier(cursor)?;
226 Some((name, point(span)))
227 } else {
228 None
229 };
230 let end = cursor.expect_punct(";")?;
231 Ok(ast::UseDecl {
232 keyword_span: point(start),
233 doc_comment: None,
234 path,
235 alias,
236 span: ast::ExprSpan { start, end },
237 })
238 }
239
240 fn parse_parameters(
244 &mut self,
245 cursor: &mut TokenCursor,
246 ) -> Result<Option<Vec<ast::ParameterDecl>>> {
247 use cel_parser::lex_lexer::Token;
248 use proc_macro2::Delimiter;
249 if !matches!(
250 cursor.peek_token(),
251 Some(Token::OpenDelim {
252 delimiter: Delimiter::Parenthesis,
253 ..
254 })
255 ) {
256 return Ok(None);
257 }
258 cursor.expect_open_paren()?;
259 let mut parameters = Vec::new();
260 if !cursor.at_close_paren() {
261 loop {
262 let (name, span) = cursor.consume_ident()?;
263 cursor.expect_punct(":")?;
264 let type_name = self.parse_type_expr(cursor)?;
265 parameters.push(ast::ParameterDecl {
266 name,
267 name_span: point(span),
268 type_name,
269 });
270 if !cursor.consume_punct(",") {
271 break;
272 }
273 }
274 }
275 cursor.expect_close_paren()?;
276 Ok(Some(parameters))
277 }
278
279 fn parse_sheet(&mut self, cursor: &mut TokenCursor) -> Result<ast::Sheet> {
281 let sheet_start = cursor.peek_span();
282 if !cursor.is_keyword("sheet") {
283 return Err(cursor.err_at("expected `sheet`"));
284 }
285 let (name, name_span) = self.parse_module_identifier(cursor)?;
286 let parameters = self.parse_parameters(cursor)?;
287 let open_span = cursor.expect_open_brace()?;
288 let mut items = Vec::new();
289 let mut errors = Vec::new();
290 while !cursor.at_close_brace() {
291 let doc = cursor.consume_doc_comment_run(false);
292 let item_start = doc
293 .as_ref()
294 .map(|(_, span)| *span)
295 .unwrap_or_else(|| cursor.peek_span());
296 cursor.set_last_span(item_start);
297 let target_depth = cursor.depth();
298 match self.parse_sheet_item(cursor) {
299 Ok(mut item) => {
300 if let Some((text, doc_span)) = &doc {
301 item.set_doc_comment(text.clone(), *doc_span);
302 }
303 items.push(item);
304 }
305 Err(e) => {
306 errors.push(e);
307 let recovery_fallback = cursor.last_span();
308 let item_end =
309 cursor.skip_to_recovery_point(target_depth, recovery_fallback)?;
310 items.push(ast::SheetItem::Error {
311 span: ast::ExprSpan {
312 start: item_start,
313 end: item_end,
314 },
315 leading_comment: None,
316 doc_comment: doc.map(|(text, _)| text),
317 trailing_line_comment: None,
318 blank_line_before: false,
319 });
320 }
321 }
322 }
323 let close_span = cursor.expect_close_brace()?;
324 Ok(ast::Sheet {
325 keyword_span: point(sheet_start),
326 name,
327 name_span: point(name_span),
328 parameters,
329 items,
330 leading_comment: None,
331 doc_comment: None,
332 trailing_comment: None,
333 blank_line_before_close: false,
334 open_brace_span: point(open_span),
335 span: ast::ExprSpan {
336 start: sheet_start,
337 end: close_span,
338 },
339 errors,
340 })
341 }
342
343 fn parse_sheet_item(&mut self, cursor: &mut TokenCursor) -> Result<ast::SheetItem> {
345 use cel_parser::lex_lexer::{HasSpan, Token};
346 match cursor.peek_token() {
347 Some(Token::Identifier(id)) if id == "cell" => {
348 self.parse_cell_decl(cursor).map(ast::SheetItem::Cell)
349 }
350 Some(Token::Identifier(id)) if id == "relationship" => {
351 match self.parse_relationship_item(cursor)? {
352 ast::BranchItem::Relationship(rel) => Ok(ast::SheetItem::Relationship(rel)),
353 ast::BranchItem::RelationshipUse(call) => {
354 Ok(ast::SheetItem::RelationshipUse(call))
355 }
356 }
357 }
358 Some(Token::Identifier(id)) if id == "conditional" => self
359 .parse_conditional_decl(cursor)
360 .map(ast::SheetItem::Conditional),
361 Some(Token::Identifier(id)) if id == "out" => {
362 self.parse_out_decl(cursor).map(ast::SheetItem::Out)
363 }
364 Some(Token::Identifier(id)) if id == "source" => {
365 self.parse_source_decl(cursor).map(ast::SheetItem::Source)
366 }
367 Some(tok) => Err(cel_parser::ParseError::new(
368 "expected `cell`, `relationship`, `conditional`, `out`, or `source`",
369 tok.span(),
370 )),
371 None => Err(cel_parser::ParseError::new(
372 "unexpected end of input",
373 proc_macro2::Span::call_site(),
374 )),
375 }
376 }
377
378 fn parse_cell_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::CellDecl> {
381 let decl_start = cursor.peek_span();
382 cursor.is_keyword("cell");
383 let (name, name_span) = cursor.consume_ident()?;
384 let (type_name, initializer) = if cursor.consume_punct(":") {
385 let type_name = self.parse_type_expr(cursor)?;
386 let initializer = if cursor.consume_punct("=") {
387 Some(self.parse_cel_expression(cursor)?)
388 } else {
389 None
390 };
391 (Some(type_name), initializer)
392 } else if cursor.consume_punct("=") {
393 (None, Some(self.parse_cel_expression(cursor)?))
394 } else {
395 return Err(cursor.err_at("expected `:` or `=` in cell declaration"));
396 };
397 let filter = if cursor.is_keyword("filter") {
398 let filter_start = cursor.last_span();
399 Some(self.parse_cell_filter(cursor, filter_start)?)
400 } else {
401 None
402 };
403 let require = if cursor.is_keyword("require") {
404 Some(self.parse_require_block(cursor)?)
405 } else {
406 None
407 };
408 let semi_span = cursor.expect_punct(";")?;
409 Ok(ast::CellDecl {
410 name,
411 name_span: point(name_span),
412 type_name,
413 initializer,
414 filter,
415 require,
416 leading_comment: None,
417 doc_comment: None,
418 trailing_line_comment: None,
419 blank_line_before: false,
420 span: ast::ExprSpan {
421 start: decl_start,
422 end: semi_span,
423 },
424 })
425 }
426
427 fn parse_source_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::SourceDecl> {
432 let decl_start = cursor.peek_span();
433 cursor.is_keyword("source");
434 let (name, name_span) = cursor.consume_ident()?;
435 let (type_name, initializer) = if cursor.consume_punct(":") {
436 let type_name = self.parse_type_expr(cursor)?;
437 let initializer = if cursor.consume_punct("=") {
438 Some(self.parse_cel_expression(cursor)?)
439 } else {
440 None
441 };
442 (Some(type_name), initializer)
443 } else if cursor.consume_punct("=") {
444 (None, Some(self.parse_cel_expression(cursor)?))
445 } else {
446 return Err(cursor.err_at("expected `:` or `=` in source declaration"));
447 };
448 let filter = if cursor.is_keyword("filter") {
449 let filter_start = cursor.last_span();
450 Some(self.parse_cell_filter(cursor, filter_start)?)
451 } else {
452 None
453 };
454 let require = if cursor.is_keyword("require") {
455 Some(self.parse_require_block(cursor)?)
456 } else {
457 None
458 };
459 let semi_span = cursor.expect_punct(";")?;
460 Ok(ast::SourceDecl {
461 name,
462 name_span: point(name_span),
463 type_name,
464 initializer,
465 filter,
466 require,
467 leading_comment: None,
468 doc_comment: None,
469 trailing_line_comment: None,
470 blank_line_before: false,
471 span: ast::ExprSpan {
472 start: decl_start,
473 end: semi_span,
474 },
475 })
476 }
477
478 fn parse_cell_filter(
483 &mut self,
484 cursor: &mut TokenCursor,
485 filter_start: proc_macro2::Span,
486 ) -> Result<ast::CellFilter> {
487 let body = self.parse_cel_expression(cursor)?;
488 let body_end = body.span().end;
489 Ok(ast::CellFilter {
490 body,
491 span: ast::ExprSpan {
492 start: filter_start,
493 end: body_end,
494 },
495 })
496 }
497
498 fn parse_type_expr(&mut self, cursor: &mut TokenCursor) -> Result<ast::TypeExpr> {
501 let tokens = cursor.take_tokens().expect("tokens present");
502 self.cel.set_lex_tokens(tokens);
503 let result = self.cel.parse_type_expression();
504 cursor.set_tokens(self.cel.take_lex_tokens().expect("tokens set"));
505 cursor.absorb_unbalanced_delimiters(self.cel.unbalanced_delimiter_count())?;
506 result
507 }
508
509 fn parse_relationship_item(&mut self, cursor: &mut TokenCursor) -> Result<ast::BranchItem> {
513 use cel_parser::lex_lexer::Token;
514 use proc_macro2::Delimiter;
515 let start = cursor.peek_span();
516 cursor.is_keyword("relationship");
517 if matches!(
518 cursor.peek_token(),
519 Some(Token::OpenDelim {
520 delimiter: Delimiter::Brace,
521 ..
522 })
523 ) {
524 return self
525 .parse_relationship_body(cursor, start)
526 .map(ast::BranchItem::Relationship);
527 }
528 let path = self.parse_path(cursor)?;
529 cursor.expect_open_paren()?;
530 let mut arguments = Vec::new();
531 if !cursor.at_close_paren() {
532 loop {
533 let (name, name_span) = cursor.consume_ident()?;
534 cursor.expect_punct(":")?;
535 let (cell, cell_span) = cursor.consume_ident()?;
536 arguments.push(ast::NamedArgument {
537 name,
538 name_span: point(name_span),
539 cell,
540 cell_span: point(cell_span),
541 });
542 if !cursor.consume_punct(",") {
543 break;
544 }
545 }
546 }
547 cursor.expect_close_paren()?;
548 let end = cursor.expect_punct(";")?;
549 Ok(ast::BranchItem::RelationshipUse(ast::RelationshipUse {
550 keyword_span: point(start),
551 path,
552 arguments,
553 opening_comments: Vec::new(),
554 leading_comment: None,
555 doc_comment: None,
556 trailing_line_comment: None,
557 blank_line_before: false,
558 span: ast::ExprSpan { start, end },
559 }))
560 }
561
562 fn parse_relationship_body(
567 &mut self,
568 cursor: &mut TokenCursor,
569 decl_start: proc_macro2::Span,
570 ) -> Result<ast::RelationshipDecl> {
571 let open_span = cursor.expect_open_brace()?;
572 let mut bindings = Vec::new();
573 while !cursor.at_close_brace() {
574 bindings.push(self.parse_binding(cursor)?);
575 }
576 let close_span = cursor.expect_close_brace()?;
577 Ok(ast::RelationshipDecl {
578 bindings,
579 leading_comment: None,
580 doc_comment: None,
581 trailing_line_comment: None,
582 blank_line_before: false,
583 trailing_comment: None,
584 blank_line_before_close: false,
585 open_brace_span: point(open_span),
586 span: ast::ExprSpan {
587 start: decl_start,
588 end: close_span,
589 },
590 })
591 }
592
593 fn parse_binding(&mut self, cursor: &mut TokenCursor) -> Result<ast::BindingDecl> {
595 let decl_start = cursor.peek_span();
596 let (outputs, destructure) = parse_binding_target(cursor)?;
597 cursor.expect_punct(":=")?;
598 let body = self.parse_cel_expression(cursor)?;
599 let semi_span = cursor.expect_punct(";")?;
600 Ok(ast::BindingDecl {
601 outputs,
602 destructure,
603 body,
604 leading_comment: None,
605 trailing_line_comment: None,
606 blank_line_before: false,
607 span: ast::ExprSpan {
608 start: decl_start,
609 end: semi_span,
610 },
611 })
612 }
613
614 fn parse_conditional_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::ConditionalDecl> {
616 use cel_parser::lex_lexer::Token;
617 let decl_start = cursor.peek_span();
618 cursor.is_keyword("conditional");
619 let match_expr = self.parse_cel_expression_restricted(
620 cursor,
621 cel_parser::ExpressionRestriction::NoStructLiteral,
622 )?;
623 let outer_open = cursor.expect_open_brace()?;
624 let mut branches = Vec::new();
625 let mut default = None;
626 while !cursor.at_close_brace() {
627 if matches!(cursor.peek_token(), Some(Token::Identifier(id)) if id == "_") {
628 let underscore_span = cursor.peek_span();
629 cursor.advance();
630 cursor.expect_punct("=>")?;
631 let branch_open = cursor.expect_open_brace()?;
632 let relationships = self.parse_branch_relationships(cursor)?;
633 let close = cursor.expect_close_brace()?;
634 default = Some(ast::DefaultBranch {
635 leading_comment: None,
636 blank_line_before: false,
637 relationships,
638 trailing_comment: None,
639 trailing_line_comment: None,
640 blank_line_before_close: false,
641 open_brace_span: point(branch_open),
642 span: ast::ExprSpan {
643 start: underscore_span,
644 end: close,
645 },
646 });
647 break; }
649 let (negated, lit, pattern_start, lit_span) = cursor.consume_literal_pattern()?;
650 cursor.expect_punct("=>")?;
651 let branch_open = cursor.expect_open_brace()?;
652 let relationships = self.parse_branch_relationships(cursor)?;
653 let close = cursor.expect_close_brace()?;
654 branches.push(ast::ConditionalBranch {
655 literal: lit,
656 negated,
657 literal_span: point(lit_span),
658 relationships,
659 leading_comment: None,
660 trailing_line_comment: None,
661 blank_line_before: false,
662 trailing_comment: None,
663 blank_line_before_close: false,
664 open_brace_span: point(branch_open),
665 span: ast::ExprSpan {
666 start: pattern_start,
667 end: close,
668 },
669 });
670 }
671 let close_span = cursor.expect_close_brace()?;
672 Ok(ast::ConditionalDecl {
673 match_expr,
674 branches,
675 default,
676 leading_comment: None,
677 doc_comment: None,
678 trailing_line_comment: None,
679 blank_line_before: false,
680 trailing_comment: None,
681 blank_line_before_close: false,
682 open_brace_span: point(outer_open),
683 span: ast::ExprSpan {
684 start: decl_start,
685 end: close_span,
686 },
687 })
688 }
689
690 fn parse_branch_relationships(
693 &mut self,
694 cursor: &mut TokenCursor,
695 ) -> Result<Vec<ast::BranchItem>> {
696 use cel_parser::lex_lexer::Token;
697 let mut relationships = Vec::new();
698 while !cursor.at_close_brace() {
699 if !matches!(cursor.peek_token(), Some(Token::Identifier(id)) if id == "relationship") {
700 return Err(cursor.err_at("expected `relationship`"));
701 }
702 relationships.push(self.parse_relationship_item(cursor)?);
703 }
704 Ok(relationships)
705 }
706
707 fn parse_out_decl(&mut self, cursor: &mut TokenCursor) -> Result<ast::OutDecl> {
710 let decl_start = cursor.peek_span();
711 cursor.is_keyword("out");
712 let (name, name_span) = cursor.consume_ident()?;
713 let type_name = if cursor.consume_punct(":") {
714 Some(self.parse_type_expr(cursor)?)
715 } else {
716 None
717 };
718 cursor.expect_punct(":=")?;
719 let initializer = self.parse_cel_expression(cursor)?;
720 let filter = if cursor.is_keyword("filter") {
721 let filter_start = cursor.last_span();
722 Some(self.parse_cell_filter(cursor, filter_start)?)
723 } else {
724 None
725 };
726 let require = if cursor.is_keyword("require") {
727 Some(self.parse_require_block(cursor)?)
728 } else {
729 None
730 };
731 let semi_span = cursor.expect_punct(";")?;
732 Ok(ast::OutDecl {
733 name,
734 name_span: point(name_span),
735 type_name,
736 initializer,
737 filter,
738 require,
739 leading_comment: None,
740 doc_comment: None,
741 trailing_line_comment: None,
742 blank_line_before: false,
743 span: ast::ExprSpan {
744 start: decl_start,
745 end: semi_span,
746 },
747 })
748 }
749
750 fn parse_require_block(&mut self, cursor: &mut TokenCursor) -> Result<ast::RequireBlock> {
754 let open_span = cursor.expect_open_brace()?;
755 let mut requirements = Vec::new();
756 while !cursor.at_close_brace() {
757 requirements.push(self.parse_requirement(cursor)?);
758 }
759 let close_span = cursor.expect_close_brace()?;
760 Ok(ast::RequireBlock {
761 requirements,
762 trailing_comment: None,
763 blank_line_before_close: false,
764 open_brace_span: point(open_span),
765 span: ast::ExprSpan {
766 start: open_span,
767 end: close_span,
768 },
769 })
770 }
771
772 fn parse_requirement(&mut self, cursor: &mut TokenCursor) -> Result<ast::RequirementDecl> {
774 let decl_start = cursor.peek_span();
775 let (name, name_span) = if cursor.consume_punct("@") {
776 let (name, span) = cursor.consume_ident()?;
777 (Some(name), Some(point(span)))
778 } else {
779 (None, None)
780 };
781 let body = self.parse_cel_expression(cursor)?;
782 let semi_span = cursor.expect_punct(";")?;
783 Ok(ast::RequirementDecl {
784 name,
785 name_span,
786 body,
787 leading_comment: None,
788 trailing_line_comment: None,
789 blank_line_before: false,
790 span: ast::ExprSpan {
791 start: decl_start,
792 end: semi_span,
793 },
794 })
795 }
796
797 fn parse_cel_expression(&mut self, cursor: &mut TokenCursor) -> Result<cel_parser::Expr> {
801 self.parse_cel_expression_restricted(cursor, cel_parser::ExpressionRestriction::None)
802 }
803
804 fn parse_cel_expression_restricted(
812 &mut self,
813 cursor: &mut TokenCursor,
814 restriction: cel_parser::ExpressionRestriction,
815 ) -> Result<cel_parser::Expr> {
816 let tokens = cursor.take_tokens().expect("tokens present");
817 self.cel.set_lex_tokens(tokens);
818 let result = self
819 .cel
820 .parse_expression_ctx_restricted(restriction)
821 .map(cel_parser::AstContext::into_expr);
822 cursor.set_tokens(self.cel.take_lex_tokens().expect("tokens set"));
823 cursor.absorb_unbalanced_delimiters(self.cel.unbalanced_delimiter_count())?;
824 result
825 }
826}
827
828fn point(span: proc_macro2::Span) -> ast::ExprSpan {
830 ast::ExprSpan {
831 start: span,
832 end: span,
833 }
834}
835
836fn parse_binding_target(cursor: &mut TokenCursor) -> Result<(Vec<(String, ast::ExprSpan)>, bool)> {
844 if !cursor.at_open_paren() {
845 let (name, span) = cursor.consume_ident()?;
846 return Ok((vec![(name, point(span))], false));
847 }
848
849 cursor.expect_open_paren()?;
850 let (first_name, first_span) = cursor.consume_ident()?;
851 if !cursor.consume_punct(",") {
852 return Err(cursor.err_at("expected ',' in tuple binding target"));
853 }
854 if cursor.at_close_paren() {
855 cursor.expect_close_paren()?;
857 return Ok((vec![(first_name, point(first_span))], true));
858 }
859 let mut outputs = vec![(first_name, point(first_span))];
860 loop {
861 let (name, span) = cursor.consume_ident()?;
862 outputs.push((name, point(span)));
863 if cursor.at_close_paren() {
864 break;
865 }
866 if !cursor.consume_punct(",") {
867 return Err(cursor.err_at("expected ',' or closing parenthesis"));
868 }
869 }
870 cursor.expect_close_paren()?;
871 Ok((outputs, true))
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use cel_parser::Expr;
878
879 #[test]
881 fn parse_empty_sheet_has_no_items() {
882 let sheet = AdamAstParser::new().parse_str("sheet empty {}").unwrap();
883 assert_eq!(sheet.name, "empty");
884 assert!(sheet.items.is_empty());
885 assert!(sheet.errors.is_empty());
886 }
887
888 #[test]
890 fn parse_cell_with_annotation_and_initializer() {
891 let sheet = AdamAstParser::new()
892 .parse_str("sheet s { cell width: f64 = 1920.0; }")
893 .unwrap();
894 assert_eq!(sheet.items.len(), 1);
895 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
896 panic!("expected Cell");
897 };
898 assert_eq!(cell.name, "width");
899 assert!(matches!(
900 cell.type_name.as_ref().unwrap(),
901 ast::TypeExpr::Named { name: n, .. } if n == "f64"
902 ));
903 assert!(cell.initializer.is_some());
904 }
905
906 #[test]
908 fn parse_cell_annotation_only_has_no_initializer() {
909 let sheet = AdamAstParser::new()
910 .parse_str("sheet s { cell area: f64; }")
911 .unwrap();
912 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
913 panic!("expected Cell");
914 };
915 assert!(cell.type_name.is_some());
916 assert!(cell.initializer.is_none());
917 }
918
919 #[test]
921 fn parse_cell_initializer_only_has_no_type_name() {
922 let sheet = AdamAstParser::new()
923 .parse_str("sheet s { cell mode = 0i32; }")
924 .unwrap();
925 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
926 panic!("expected Cell");
927 };
928 assert!(cell.type_name.is_none());
929 assert!(cell.initializer.is_some());
930 }
931
932 #[test]
934 fn parse_source_decl_produces_a_source_decl_sheet_item() {
935 let sheet = AdamAstParser::new()
936 .parse_str("sheet s { source width: i32 = 4; }")
937 .unwrap();
938 assert!(matches!(sheet.items[0], ast::SheetItem::Source(_)));
939 }
940
941 #[test]
943 fn parse_source_with_a_filter() {
944 let sheet = AdamAstParser::new()
945 .parse_str("sheet s { source a: i32 = 1 filter _; }")
946 .unwrap();
947 let ast::SheetItem::Source(source) = &sheet.items[0] else {
948 panic!("expected Source");
949 };
950 let filter = source.filter.as_ref().expect("filter present");
951 assert!(matches!(&filter.body, Expr::Ident { name, .. } if name == "_"));
952 }
953
954 #[test]
956 fn parse_source_without_a_filter_leaves_it_none() {
957 let sheet = AdamAstParser::new()
958 .parse_str("sheet s { source a: i32 = 1; }")
959 .unwrap();
960 let ast::SheetItem::Source(source) = &sheet.items[0] else {
961 panic!("expected Source");
962 };
963 assert!(source.filter.is_none());
964 }
965
966 #[test]
968 fn parse_relationship_records_bindings_in_order() {
969 let sheet = AdamAstParser::new()
970 .parse_str(
971 r#"
972 sheet s {
973 relationship {
974 area := width * height;
975 width := area / height;
976 }
977 }
978 "#,
979 )
980 .unwrap();
981 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
982 panic!("expected Relationship");
983 };
984 assert_eq!(rel.bindings.len(), 2);
985 assert_eq!(rel.bindings[0].outputs[0].0, "area");
986 assert!(matches!(rel.bindings[0].body, Expr::Op { ref name, .. } if name == "*"));
987 }
988
989 #[test]
991 fn parse_binding_bare_identifier_target_is_not_a_destructure() {
992 let sheet = AdamAstParser::new()
993 .parse_str("sheet s { relationship { x := a; } }")
994 .unwrap();
995 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
996 panic!("expected Relationship");
997 };
998 assert!(!rel.bindings[0].destructure);
999 assert_eq!(rel.bindings[0].outputs.len(), 1);
1000 }
1001
1002 #[test]
1004 fn parse_binding_single_parenthesized_identifier_requires_a_comma() {
1005 for source in [
1006 "sheet s { relationship { (x) := a; } }",
1007 "sheet s { relationship { (x /* missing comma */) := a; } }",
1008 ] {
1009 let module = AdamAstParser::new().parse_module_str(source).unwrap();
1010 assert!(!module.errors.is_empty(), "{source}");
1011 }
1012 }
1013
1014 #[test]
1016 fn parse_binding_single_element_tuple_target_is_a_destructure() {
1017 let sheet = AdamAstParser::new()
1018 .parse_str("sheet s { relationship { (x,) := (a,); } }")
1019 .unwrap();
1020 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1021 panic!("expected Relationship");
1022 };
1023 assert!(rel.bindings[0].destructure);
1024 assert_eq!(rel.bindings[0].outputs.len(), 1);
1025 assert_eq!(rel.bindings[0].outputs[0].0, "x");
1026 }
1027
1028 #[test]
1030 fn parse_binding_multi_identifier_target_is_a_destructure() {
1031 let sheet = AdamAstParser::new()
1032 .parse_str("sheet s { relationship { (x, y) := (a, b); } }")
1033 .unwrap();
1034 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1035 panic!("expected Relationship");
1036 };
1037 assert!(rel.bindings[0].destructure);
1038 assert_eq!(
1039 rel.bindings[0]
1040 .outputs
1041 .iter()
1042 .map(|(name, _)| name.as_str())
1043 .collect::<Vec<_>>(),
1044 vec!["x", "y"]
1045 );
1046 }
1047
1048 #[test]
1050 fn multi_output_binding_without_parens_recovers_at_sheet_item_level() {
1051 let sheet = AdamAstParser::new()
1052 .parse_str(
1053 r#"
1054 sheet s {
1055 cell good_before: i32 = 1;
1056 relationship {
1057 x, y := (a, b);
1058 }
1059 cell good_after: i32 = 2;
1060 }
1061 "#,
1062 )
1063 .unwrap();
1064 assert_eq!(sheet.errors.len(), 1);
1065 assert_eq!(sheet.items.len(), 3);
1066 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1067 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1068 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1069 }
1070
1071 #[test]
1073 fn parse_conditional_records_branches_and_default() {
1074 let sheet = AdamAstParser::new()
1075 .parse_str(
1076 r#"
1077 sheet s {
1078 conditional mode {
1079 0i32 => { relationship { height := width; } }
1080 _ => { relationship { height := width; } }
1081 }
1082 }
1083 "#,
1084 )
1085 .unwrap();
1086 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1087 panic!("expected Conditional");
1088 };
1089 assert!(matches!(&cond.match_expr, Expr::Ident { name, .. } if name == "mode"));
1090 assert_eq!(cond.branches.len(), 1);
1091 assert!(!cond.branches[0].negated);
1092 assert!(cond.default.is_some());
1093 }
1094
1095 #[test]
1097 fn parse_conditional_records_a_negated_literal_branch_key() {
1098 let sheet = AdamAstParser::new()
1099 .parse_str(
1100 r#"
1101 sheet s {
1102 conditional mode {
1103 -1i32 => { relationship { height := width; } }
1104 _ => { relationship { height := width; } }
1105 }
1106 }
1107 "#,
1108 )
1109 .unwrap();
1110 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1111 panic!("expected Conditional");
1112 };
1113 assert_eq!(cond.branches.len(), 1);
1114 assert!(cond.branches[0].negated);
1115 }
1116
1117 #[test]
1119 fn parse_conditional_branch_dash_not_followed_by_a_literal_is_error() {
1120 let sheet = AdamAstParser::new()
1121 .parse_str(
1122 r#"
1123 sheet s {
1124 conditional mode {
1125 -width => { relationship { height := width; } }
1126 }
1127 }
1128 "#,
1129 )
1130 .unwrap();
1131 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1132 }
1133
1134 #[test]
1136 fn parse_conditional_branch_literal_with_invalid_suffix_is_error() {
1137 let sheet = AdamAstParser::new()
1141 .parse_str(
1142 r#"
1143 sheet s {
1144 conditional mode {
1145 10xyz => { relationship { height := width; } }
1146 }
1147 }
1148 "#,
1149 )
1150 .unwrap();
1151 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1152 }
1153
1154 #[test]
1156 fn parse_conditional_branch_negating_an_unsigned_literal_is_error() {
1157 let sheet = AdamAstParser::new()
1160 .parse_str(
1161 r#"
1162 sheet s {
1163 conditional mode {
1164 -1u32 => { relationship { height := width; } }
1165 }
1166 }
1167 "#,
1168 )
1169 .unwrap();
1170 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1171 }
1172
1173 #[test]
1175 fn parse_conditional_records_an_expression_match_subject() {
1176 let sheet = AdamAstParser::new()
1177 .parse_str(
1178 r#"
1179 sheet s {
1180 conditional a && b {
1181 _ => { relationship { height := width; } }
1182 }
1183 }
1184 "#,
1185 )
1186 .unwrap();
1187 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1188 panic!("expected Conditional");
1189 };
1190 assert!(matches!(
1191 &cond.match_expr,
1192 Expr::Logical {
1193 op: cel_parser::LogicalOp::And,
1194 ..
1195 }
1196 ));
1197 }
1198
1199 #[test]
1201 fn adam_condition_and_match_headers_reserve_braces() {
1202 for source in [
1203 "sheet s { conditional flag { _ => { relationship { x := if flag { 1 } else { 2 }; } } } }",
1204 "sheet s { conditional (TestPoint { x: 0.0, y: 1.0 }).x { _ => { relationship { x := TestPoint { x: 2.0, y: 3.0 }; } } } }",
1205 ] {
1206 let sheet = AdamAstParser::new().parse_str(source).unwrap();
1207 assert!(sheet.errors.is_empty(), "{:?}", sheet.errors);
1208 assert!(matches!(sheet.items[0], ast::SheetItem::Conditional(_)));
1209 let formatted = crate::fmt::format_sheet(&sheet, source);
1210 let reparsed = AdamAstParser::new().parse_str(&formatted).unwrap();
1211 assert!(
1212 reparsed.errors.is_empty(),
1213 "{formatted}\n{:?}",
1214 reparsed.errors
1215 );
1216 assert_eq!(crate::fmt::format_sheet(&reparsed, &formatted), formatted);
1217 }
1218 }
1219
1220 #[test]
1222 fn parse_conditional_branch_records_multiple_relationships() {
1223 let sheet = AdamAstParser::new()
1224 .parse_str(
1225 r#"
1226 sheet s {
1227 conditional mode {
1228 0i32 => {
1229 relationship { b := a; }
1230 relationship { d := c; }
1231 }
1232 }
1233 }
1234 "#,
1235 )
1236 .unwrap();
1237 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1238 panic!("expected Conditional");
1239 };
1240 assert_eq!(cond.branches[0].relationships.len(), 2);
1241 }
1242
1243 #[test]
1245 fn parse_conditional_default_branch_records_multiple_relationships() {
1246 let sheet = AdamAstParser::new()
1247 .parse_str(
1248 r#"
1249 sheet s {
1250 conditional mode {
1251 _ => {
1252 relationship { b := a; }
1253 relationship { d := c; }
1254 }
1255 }
1256 }
1257 "#,
1258 )
1259 .unwrap();
1260 let ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1261 panic!("expected Conditional");
1262 };
1263 assert_eq!(
1264 cond.default.as_ref().map(|d| d.relationships.len()),
1265 Some(2)
1266 );
1267 }
1268
1269 #[test]
1271 fn conditional_branch_bare_binding_without_relationship_wrapper_recovers() {
1272 let sheet = AdamAstParser::new()
1276 .parse_str(
1277 r#"
1278 sheet s {
1279 cell good_before: i32 = 1;
1280 conditional mode { 0i32 => { b := a; } }
1281 cell good_after: i32 = 2;
1282 }
1283 "#,
1284 )
1285 .unwrap();
1286 assert_eq!(sheet.errors.len(), 1);
1287 assert_eq!(sheet.items.len(), 3);
1288 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1289 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1290 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1291 }
1292
1293 #[test]
1295 fn parse_binding_body_is_a_cel_expr_tree() {
1296 let sheet = AdamAstParser::new()
1297 .parse_str("sheet s { relationship { c := (a + b, a - b); } }")
1298 .unwrap();
1299 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1300 panic!("expected Relationship");
1301 };
1302 assert!(matches!(rel.bindings[0].body, Expr::Tuple { .. }));
1303 }
1304
1305 #[test]
1307 fn parse_unknown_sheet_item_is_recorded_as_an_error_item() {
1308 let sheet = AdamAstParser::new()
1309 .parse_str("sheet s { bogus x; }")
1310 .unwrap();
1311 assert_eq!(sheet.errors.len(), 1);
1312 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1313 }
1314
1315 #[test]
1317 fn parse_malformed_cell_is_recorded_as_an_error_item() {
1318 let sheet = AdamAstParser::new()
1319 .parse_str("sheet s { cell x unknown_syntax }")
1320 .unwrap();
1321 assert_eq!(sheet.errors.len(), 1);
1322 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1323 }
1324
1325 #[test]
1327 fn recovery_records_an_error_item_and_continues_parsing() {
1328 let sheet = AdamAstParser::new()
1329 .parse_str(
1330 r#"
1331 sheet s {
1332 cell good_before: i32 = 1;
1333 cell bad unknown_syntax
1334 cell good_after: i32 = 2;
1335 }
1336 "#,
1337 )
1338 .unwrap();
1339 assert_eq!(sheet.items.len(), 3);
1340 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1341 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1342 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1343 assert_eq!(sheet.errors.len(), 1);
1344 }
1345
1346 #[test]
1348 fn recovery_error_span_covers_tokens_actually_consumed_by_the_failed_item() {
1349 let source = "sheet s { cell bad relationship { y := x; } }";
1355 let sheet = AdamAstParser::new().parse_str(source).unwrap();
1356 let ast::SheetItem::Error { span, .. } = &sheet.items[0] else {
1357 panic!("expected Error");
1358 };
1359 let range = cel_parser::SourceSpan {
1360 start: span.start.start(),
1361 end: span.end.end(),
1362 }
1363 .to_byte_range(source);
1364 assert_eq!(&source[range], "cell bad");
1365 }
1366
1367 #[test]
1369 fn recovery_collects_multiple_errors_from_multiple_malformed_items() {
1370 let sheet = AdamAstParser::new()
1371 .parse_str(
1372 r#"
1373 sheet s {
1374 cell bad1 unknown_syntax;
1375 cell bad2 unknown_syntax;
1376 cell good: i32 = 1;
1377 }
1378 "#,
1379 )
1380 .unwrap();
1381 assert_eq!(sheet.errors.len(), 2);
1382 assert!(matches!(sheet.items.last(), Some(ast::SheetItem::Cell(_))));
1383 }
1384
1385 #[test]
1387 fn well_formed_input_has_empty_errors() {
1388 let sheet = AdamAstParser::new()
1389 .parse_str("sheet s { cell x: i32 = 1; }")
1390 .unwrap();
1391 assert!(sheet.errors.is_empty());
1392 }
1393
1394 #[test]
1402 fn recovery_malformed_relationship_item_recovers() {
1403 let sheet = AdamAstParser::new()
1404 .parse_str(
1405 r#"
1406 sheet s {
1407 cell good_before: i32 = 1;
1408 relationship { bad }
1409 cell good_after: i32 = 2;
1410 }
1411 "#,
1412 )
1413 .unwrap();
1414 assert_eq!(sheet.errors.len(), 1);
1415 assert_eq!(sheet.items.len(), 3);
1416 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1417 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1418 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1419 }
1420
1421 #[test]
1425 fn recovery_malformed_conditional_item_recovers() {
1426 let sheet = AdamAstParser::new()
1427 .parse_str(
1428 r#"
1429 sheet s {
1430 cell good_before: i32 = 1;
1431 conditional m { bad }
1432 cell good_after: i32 = 2;
1433 }
1434 "#,
1435 )
1436 .unwrap();
1437 assert_eq!(sheet.errors.len(), 1);
1438 assert_eq!(sheet.items.len(), 3);
1439 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1440 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1441 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1442 }
1443
1444 #[test]
1450 fn recovery_malformed_binding_body_recovers_at_sheet_item_level() {
1451 let sheet = AdamAstParser::new()
1452 .parse_str(
1453 r#"
1454 sheet s {
1455 cell good_before: i32 = 1;
1456 relationship {
1457 b := a + ;
1458 }
1459 cell good_after: i32 = 2;
1460 }
1461 "#,
1462 )
1463 .unwrap();
1464 assert_eq!(sheet.errors.len(), 1);
1465 assert_eq!(sheet.items.len(), 3);
1466 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1467 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1468 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1469 }
1470
1471 #[test]
1482 fn recovery_cel_dangling_paren_recovers_at_sheet_item_level() {
1483 let sheet = AdamAstParser::new()
1484 .parse_str(
1485 r#"
1486 sheet s {
1487 cell good_before: i32 = 1;
1488 relationship { b := (+); }
1489 cell good_after: i32 = 2;
1490 }
1491 "#,
1492 )
1493 .unwrap();
1494 assert_eq!(sheet.errors.len(), 1);
1495 assert_eq!(sheet.items.len(), 3);
1496 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1497 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1498 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1499 }
1500
1501 #[test]
1511 fn recovery_if_expr_dangling_brace_recovers_at_sheet_item_level() {
1512 let sheet = AdamAstParser::new()
1513 .parse_str(
1514 r#"
1515 sheet s {
1516 cell good_before: i32 = 1;
1517 relationship { b := if a { }; }
1518 cell good_after: i32 = 2;
1519 }
1520 "#,
1521 )
1522 .unwrap();
1523 assert_eq!(sheet.errors.len(), 1);
1524 assert_eq!(sheet.items.len(), 3);
1525 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1526 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1527 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1528 }
1529
1530 #[test]
1541 fn recovery_keyword_shaped_garbage_inside_dangling_brace_recovers_cleanly() {
1542 let sheet = AdamAstParser::new()
1543 .parse_str(
1544 "sheet s { relationship { b := if a { + cell good: i32 = 1; }; } cell trailing: i32 = 2; }",
1545 )
1546 .unwrap();
1547 assert_eq!(sheet.errors.len(), 1);
1548 assert_eq!(sheet.items.len(), 2);
1549 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1550 assert!(matches!(sheet.items[1], ast::SheetItem::Cell(_)));
1551 }
1552
1553 #[test]
1555 fn parse_out_with_explicit_type_and_no_requirements() {
1556 let sheet = AdamAstParser::new()
1557 .parse_str(
1558 r#"
1559 sheet s {
1560 cell width: f64 = 4.0;
1561 cell height: f64 = 3.0;
1562 out area: f64 := width * height;
1563 }
1564 "#,
1565 )
1566 .unwrap();
1567 assert!(sheet.errors.is_empty());
1568 let ast::SheetItem::Out(out) = &sheet.items[2] else {
1569 panic!("expected Out");
1570 };
1571 assert_eq!(out.name, "area");
1572 assert!(matches!(
1573 out.type_name.as_ref().unwrap(),
1574 ast::TypeExpr::Named { name: n, .. } if n == "f64"
1575 ));
1576 assert!(matches!(out.initializer, Expr::Op { ref name, .. } if name == "*"));
1577 assert!(out.require.is_none());
1578 }
1579
1580 #[test]
1582 fn parse_out_with_no_type_annotation() {
1583 let sheet = AdamAstParser::new()
1584 .parse_str("sheet s { out area := width; }")
1585 .unwrap();
1586 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1587 panic!("expected Out");
1588 };
1589 assert!(out.type_name.is_none());
1590 }
1591
1592 #[test]
1594 fn parse_out_with_requirements_in_declaration_order() {
1595 let sheet = AdamAstParser::new()
1596 .parse_str(
1597 r#"
1598 sheet s {
1599 out area: f64 := width * height require {
1600 @max_area width * height <= max_area;
1601 @max_width width <= max_width;
1602 };
1603 }
1604 "#,
1605 )
1606 .unwrap();
1607 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1608 panic!("expected Out");
1609 };
1610 let require = out.require.as_ref().expect("require block present");
1611 assert_eq!(require.requirements.len(), 2);
1612 assert_eq!(require.requirements[0].name.as_deref(), Some("max_area"));
1613 assert_eq!(require.requirements[1].name.as_deref(), Some("max_width"));
1614 }
1615
1616 #[test]
1618 fn parse_requirement_without_a_label_leaves_name_none() {
1619 let sheet = AdamAstParser::new()
1620 .parse_str(
1621 r#"
1622 sheet s {
1623 out area: f64 := width * height require {
1624 width * height <= max_area;
1625 };
1626 }
1627 "#,
1628 )
1629 .unwrap();
1630 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1631 panic!("expected Out");
1632 };
1633 let require = out.require.as_ref().expect("require block present");
1634 assert_eq!(require.requirements[0].name, None);
1635 }
1636
1637 #[test]
1639 fn parse_malformed_out_is_recorded_as_an_error_item() {
1640 let sheet = AdamAstParser::new()
1641 .parse_str(
1642 r#"
1643 sheet s {
1644 cell good_before: i32 = 1;
1645 out area { bad }
1646 cell good_after: i32 = 2;
1647 }
1648 "#,
1649 )
1650 .unwrap();
1651 assert_eq!(sheet.errors.len(), 1);
1652 assert_eq!(sheet.items.len(), 3);
1653 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1654 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1655 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1656 }
1657
1658 #[test]
1660 fn parse_cell_with_explicit_tuple_type() {
1661 let sheet = AdamAstParser::new()
1662 .parse_str("sheet s { cell a: (i32, f64); }")
1663 .unwrap();
1664 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1665 panic!("expected Cell");
1666 };
1667 match cell.type_name.as_ref().unwrap() {
1668 ast::TypeExpr::Tuple { elements, .. } => {
1669 assert_eq!(elements.len(), 2);
1670 assert!(matches!(&elements[0], ast::TypeExpr::Named { name: n, .. } if n == "i32"));
1671 assert!(matches!(&elements[1], ast::TypeExpr::Named { name: n, .. } if n == "f64"));
1672 }
1673 other => panic!("expected Tuple, got {other:?}"),
1674 }
1675 }
1676
1677 #[test]
1679 fn parse_cell_with_a_filter() {
1680 let sheet = AdamAstParser::new()
1681 .parse_str("sheet s { cell a: i32 = 1 filter _; }")
1682 .unwrap();
1683 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1684 panic!("expected Cell");
1685 };
1686 let filter = cell.filter.as_ref().expect("filter present");
1687 assert!(matches!(&filter.body, Expr::Ident { name, .. } if name == "_"));
1688 }
1689
1690 #[test]
1692 fn parse_cell_with_a_filter_referencing_a_cell() {
1693 let sheet = AdamAstParser::new()
1694 .parse_str("sheet s { cell hi: i32 = 100; cell a: i32 = 1 filter _ + hi; }")
1695 .unwrap();
1696 let ast::SheetItem::Cell(cell) = &sheet.items[1] else {
1697 panic!("expected Cell");
1698 };
1699 let filter = cell.filter.as_ref().expect("filter present");
1700 match &filter.body {
1701 Expr::Op { name, operands, .. } => {
1702 assert_eq!(name, "+");
1703 assert!(matches!(&operands[0], Expr::Ident { name, .. } if name == "_"));
1704 assert!(matches!(&operands[1], Expr::Ident { name, .. } if name == "hi"));
1705 }
1706 other => panic!("expected Op, got {other:?}"),
1707 }
1708 }
1709
1710 #[test]
1712 fn parse_cell_without_a_filter_leaves_it_none() {
1713 let sheet = AdamAstParser::new()
1714 .parse_str("sheet s { cell a: i32 = 1; }")
1715 .unwrap();
1716 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1717 panic!("expected Cell");
1718 };
1719 assert!(cell.filter.is_none());
1720 }
1721
1722 #[test]
1724 fn recovery_malformed_filter_recovers_at_the_next_sheet_item() {
1725 let sheet = AdamAstParser::new()
1726 .parse_str(
1727 r#"
1728 sheet s {
1729 cell good_before: i32 = 1;
1730 cell bad: i32 = 1 filter |x: i32|;
1731 cell good_after: i32 = 2;
1732 }
1733 "#,
1734 )
1735 .unwrap();
1736 assert_eq!(sheet.errors.len(), 1);
1737 assert_eq!(sheet.items.len(), 3);
1738 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1739 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1740 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1741 }
1742
1743 #[test]
1745 fn parse_cell_with_nested_tuple_type() {
1746 let sheet = AdamAstParser::new()
1747 .parse_str("sheet s { cell a: (i32, (f64, String)); }")
1748 .unwrap();
1749 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1750 panic!("expected Cell");
1751 };
1752 let ast::TypeExpr::Tuple { elements, .. } = cell.type_name.as_ref().unwrap() else {
1753 panic!("expected top-level Tuple");
1754 };
1755 assert_eq!(elements.len(), 2);
1756 assert!(matches!(&elements[0], ast::TypeExpr::Named { name: n, .. } if n == "i32"));
1757 match &elements[1] {
1758 ast::TypeExpr::Tuple {
1759 elements: inner, ..
1760 } => assert_eq!(inner.len(), 2),
1761 other => panic!("expected nested Tuple, got {other:?}"),
1762 }
1763 }
1764
1765 #[test]
1767 fn parse_cell_with_empty_tuple_type() {
1768 let sheet = AdamAstParser::new()
1769 .parse_str("sheet s { cell a: (); }")
1770 .unwrap();
1771 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1772 panic!("expected Cell");
1773 };
1774 match cell.type_name.as_ref().unwrap() {
1775 ast::TypeExpr::Tuple { elements, .. } => assert!(elements.is_empty()),
1776 other => panic!("expected empty Tuple, got {other:?}"),
1777 }
1778 }
1779
1780 #[test]
1782 fn parse_cell_with_parenthesized_type_is_grouping_not_a_1_tuple() {
1783 let sheet = AdamAstParser::new()
1784 .parse_str("sheet s { cell a: (i32); }")
1785 .unwrap();
1786 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1787 panic!("expected Cell");
1788 };
1789 assert!(matches!(
1790 cell.type_name.as_ref().unwrap(),
1791 ast::TypeExpr::Named { name: n, .. } if n == "i32"
1792 ));
1793 }
1794
1795 #[test]
1797 fn parse_cell_with_1_tuple_type_requires_trailing_comma() {
1798 let sheet = AdamAstParser::new()
1799 .parse_str("sheet s { cell a: (i32,); }")
1800 .unwrap();
1801 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1802 panic!("expected Cell");
1803 };
1804 match cell.type_name.as_ref().unwrap() {
1805 ast::TypeExpr::Tuple { elements, .. } => assert_eq!(elements.len(), 1),
1806 other => panic!("expected 1-Tuple, got {other:?}"),
1807 }
1808 }
1809
1810 #[test]
1812 fn parse_cell_initializer_is_a_tuple_expr() {
1813 let sheet = AdamAstParser::new()
1814 .parse_str("sheet s { cell a = (1, 2.5); }")
1815 .unwrap();
1816 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1817 panic!("expected Cell");
1818 };
1819 assert!(matches!(cell.initializer, Some(Expr::Tuple { .. })));
1820 }
1821
1822 #[test]
1824 fn parse_out_with_explicit_tuple_type() {
1825 let sheet = AdamAstParser::new()
1826 .parse_str("sheet s { out a: (i32, f64) := (x, x); }")
1827 .unwrap();
1828 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1829 panic!("expected Out");
1830 };
1831 assert!(matches!(
1832 out.type_name.as_ref().unwrap(),
1833 ast::TypeExpr::Tuple { elements, .. } if elements.len() == 2
1834 ));
1835 }
1836
1837 #[test]
1839 fn malformed_tuple_type_recovers_at_the_next_sheet_item() {
1840 let sheet = AdamAstParser::new()
1846 .parse_str("sheet s { cell good_before: i32 = 1; cell bad: (i32 i32); cell good_after: i32 = 2; }")
1847 .unwrap();
1848 assert_eq!(sheet.errors.len(), 1);
1849 assert_eq!(sheet.items.len(), 3);
1850 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1851 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1852 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1853 }
1854
1855 #[test]
1857 fn malformed_generic_type_argument_list_recovers_at_the_next_sheet_item() {
1858 let sheet = AdamAstParser::new()
1861 .parse_str(
1862 "sheet s { cell good_before: i32 = 1; cell bad: Pair(i32 i32); cell good_after: i32 = 2; }",
1863 )
1864 .unwrap();
1865 assert_eq!(sheet.errors.len(), 1);
1866 assert_eq!(sheet.items.len(), 3);
1867 assert!(matches!(sheet.items[0], ast::SheetItem::Cell(_)));
1868 assert!(matches!(sheet.items[1], ast::SheetItem::Error { .. }));
1869 assert!(matches!(sheet.items[2], ast::SheetItem::Cell(_)));
1870 }
1871
1872 #[test]
1874 fn attaches_an_outer_doc_comment_to_a_cell() {
1875 let sheet = AdamAstParser::new()
1876 .parse_str("sheet s {\n /// the total\n cell x: i32 = 1;\n}")
1877 .unwrap();
1878 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1879 panic!("expected Cell");
1880 };
1881 assert_eq!(cell.doc_comment.as_deref(), Some(" the total"));
1882 }
1883
1884 #[test]
1886 fn attaches_an_outer_doc_comment_to_a_relationship() {
1887 let sheet = AdamAstParser::new()
1888 .parse_str("sheet s {\n /// docs\n relationship { b := a; }\n}")
1889 .unwrap();
1890 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1891 panic!("expected Relationship");
1892 };
1893 assert_eq!(rel.doc_comment.as_deref(), Some(" docs"));
1894 }
1895
1896 #[test]
1898 fn attaches_an_outer_doc_comment_to_a_conditional() {
1899 let sheet = AdamAstParser::new()
1900 .parse_str(
1901 "sheet s {\n cell p: i32 = 0;\n /// docs\n conditional p {\n _ => { relationship { b := a; } }\n }\n}",
1902 )
1903 .unwrap();
1904 let ast::SheetItem::Conditional(cond) = &sheet.items[1] else {
1905 panic!("expected Conditional");
1906 };
1907 assert_eq!(cond.doc_comment.as_deref(), Some(" docs"));
1908 }
1909
1910 #[test]
1912 fn attaches_an_outer_doc_comment_to_an_out_decl() {
1913 let sheet = AdamAstParser::new()
1914 .parse_str("sheet s {\n /// docs\n out area: f64 := w;\n}")
1915 .unwrap();
1916 let ast::SheetItem::Out(out) = &sheet.items[0] else {
1917 panic!("expected Out");
1918 };
1919 assert_eq!(out.doc_comment.as_deref(), Some(" docs"));
1920 }
1921
1922 #[test]
1924 fn attaches_a_sheet_level_inner_doc_comment() {
1925 let sheet = AdamAstParser::new()
1926 .parse_str("//! module docs\nsheet s {\n cell x: i32 = 1;\n}")
1927 .unwrap();
1928 assert_eq!(sheet.doc_comment.as_deref(), Some(" module docs"));
1929 }
1930
1931 #[test]
1933 fn joins_consecutive_outer_doc_comment_lines() {
1934 let sheet = AdamAstParser::new()
1935 .parse_str("sheet s {\n /// line one\n /// line two\n cell x: i32 = 1;\n}")
1936 .unwrap();
1937 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1938 panic!("expected Cell");
1939 };
1940 assert_eq!(cell.doc_comment.as_deref(), Some(" line one\n line two"));
1941 }
1942
1943 #[test]
1945 fn a_doc_comment_binds_forward_across_a_blank_line() {
1946 let sheet = AdamAstParser::new()
1947 .parse_str("sheet s {\n /// docs\n\n cell x: i32 = 1;\n}")
1948 .unwrap();
1949 let ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1950 panic!("expected Cell");
1951 };
1952 assert_eq!(cell.doc_comment.as_deref(), Some(" docs"));
1953 }
1954
1955 #[test]
1957 fn a_plain_comment_and_a_doc_comment_coexist_in_source_order() {
1958 let source =
1969 "sheet s {\n cell w: i32 = 0;\n // TODO\n /// docs\n cell x: i32 = 1;\n}";
1970 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1971 crate::attach_trivia(source, &mut sheet);
1972 let ast::SheetItem::Cell(cell) = &sheet.items[1] else {
1973 panic!("expected Cell");
1974 };
1975 assert_eq!(cell.doc_comment.as_deref(), Some(" docs"));
1976 assert_eq!(
1977 cell.leading_comment,
1978 Some(ast::Comment::Line("TODO".to_string()))
1979 );
1980 }
1981
1982 #[test]
1984 fn a_doc_comment_before_a_binding_recovers_as_a_declaration_level_error() {
1985 let sheet = AdamAstParser::new()
1986 .parse_str(
1987 "sheet s {\n relationship {\n /// not allowed here\n b := a;\n }\n}",
1988 )
1989 .unwrap();
1990 assert!(!sheet.errors.is_empty());
1991 assert!(matches!(sheet.items[0], ast::SheetItem::Error { .. }));
1992 }
1993
1994 #[test]
1996 fn range_syntax_is_reachable_from_a_relationship_binding() {
1997 let sheet = AdamAstParser::new()
1998 .parse_str("sheet s { relationship { x := 1..5; } }")
1999 .unwrap();
2000 let ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
2001 panic!("expected a Relationship item, got {:?}", sheet.items[0]);
2002 };
2003 assert!(
2004 matches!(&rel.bindings[0].body, cel_parser::Expr::Op { name, .. } if name == "range"),
2005 "expected the binding body to be a range Expr::Op, got {:?}",
2006 rel.bindings[0].body
2007 );
2008 }
2009}