1use cel_parser::{Expr, ExprSpan, Literal, ParseError, Ty, ty::check_expr_with_type_resolver};
18
19use crate::TypeRegistry;
20use crate::ast::{BindingDecl, CellFilter, OutDecl, RequireBlock, Sheet, SheetItem, TypeExpr};
21use crate::type_registry::TypeShape;
22
23pub fn check_sheet(
72 sheet: &Sheet,
73 registry: &TypeRegistry,
74 lookup: &cel_parser::OpLookup,
75) -> Vec<ParseError> {
76 let mut diagnostics = Vec::new();
77 let declared = declared_cell_types(sheet, registry, lookup);
78 let shapes = &declared.shapes;
79 let resolve = |name: &str| -> Ty { declared.values.get(name).cloned().unwrap_or(Ty::Any) };
80 let type_resolver = registry.cel_type_resolver();
81 for item in &sheet.items {
82 match item {
83 SheetItem::Cell(cell) => {
84 check_cell_initializer(
85 cell.type_name.as_ref(),
86 cell.initializer.as_ref(),
87 registry,
88 &type_resolver,
89 lookup,
90 &mut diagnostics,
91 );
92 check_filter(
93 &cell.name,
94 cell.filter.as_ref(),
95 registry,
96 &resolve,
97 &type_resolver,
98 lookup,
99 &mut diagnostics,
100 );
101 check_requirements(
102 cell.require.as_ref(),
103 &resolve,
104 &type_resolver,
105 lookup,
106 &mut diagnostics,
107 );
108 }
109 SheetItem::Source(source) => {
110 check_cell_initializer(
111 source.type_name.as_ref(),
112 source.initializer.as_ref(),
113 registry,
114 &type_resolver,
115 lookup,
116 &mut diagnostics,
117 );
118 check_filter(
119 &source.name,
120 source.filter.as_ref(),
121 registry,
122 &resolve,
123 &type_resolver,
124 lookup,
125 &mut diagnostics,
126 );
127 check_requirements(
128 source.require.as_ref(),
129 &resolve,
130 &type_resolver,
131 lookup,
132 &mut diagnostics,
133 );
134 }
135 SheetItem::Relationship(rel) => {
136 for binding in &rel.bindings {
137 check_binding(
138 binding,
139 registry,
140 shapes,
141 &resolve,
142 &type_resolver,
143 lookup,
144 &mut diagnostics,
145 );
146 }
147 }
148 SheetItem::Conditional(cond) => {
149 diagnostics.extend(
150 check_expr_with_type_resolver(
151 &cond.match_expr,
152 &resolve,
153 &type_resolver,
154 lookup,
155 )
156 .1,
157 );
158 for branch in &cond.branches {
159 for rel in &branch.relationships {
160 for binding in &rel.bindings {
161 check_binding(
162 binding,
163 registry,
164 shapes,
165 &resolve,
166 &type_resolver,
167 lookup,
168 &mut diagnostics,
169 );
170 }
171 }
172 }
173 if let Some(default) = &cond.default {
174 for rel in &default.relationships {
175 for binding in &rel.bindings {
176 check_binding(
177 binding,
178 registry,
179 shapes,
180 &resolve,
181 &type_resolver,
182 lookup,
183 &mut diagnostics,
184 );
185 }
186 }
187 }
188 }
189 SheetItem::Out(out_decl) => check_out(
190 out_decl,
191 registry,
192 &declared,
193 &resolve,
194 &type_resolver,
195 lookup,
196 &mut diagnostics,
197 ),
198 SheetItem::Error { .. } => {} }
200 }
201 diagnostics
202}
203
204fn shape_to_ty(shape: &TypeShape, registry: &TypeRegistry) -> Ty {
208 match shape {
209 TypeShape::Named(type_id) => registry
210 .resolved_leaf_type(*type_id)
211 .map(|leaf| Ty::from_leaf(&leaf))
212 .unwrap_or(Ty::Any),
213 TypeShape::Tuple(elements) => Ty::Tuple(
214 elements
215 .iter()
216 .map(|element| shape_to_ty(element, registry))
217 .collect(),
218 ),
219 TypeShape::Array(element) => Ty::Array(Box::new(shape_to_ty(element, registry))),
220 TypeShape::Callable(parameters, result) => Ty::Callable(
221 parameters
222 .iter()
223 .map(|p| shape_to_ty(p, registry))
224 .collect(),
225 Box::new(shape_to_ty(result, registry)),
226 ),
227 }
228}
229
230fn literal_matches_declared_ty(lit: &Literal, declared: &Ty) -> bool {
233 Ty::from_literal(lit).unifies_with(declared)
234}
235
236struct DeclaredTypes {
240 values: std::collections::HashMap<String, Ty>,
242 shapes: std::collections::HashMap<String, TypeShape>,
244}
245
246fn declared_cell_types(
260 sheet: &Sheet,
261 registry: &TypeRegistry,
262 lookup: &cel_parser::OpLookup,
263) -> DeclaredTypes {
264 fn resolve_annotation_shape(
269 type_expr: Option<&crate::ast::TypeExpr>,
270 registry: &TypeRegistry,
271 ) -> Option<TypeShape> {
272 type_expr.and_then(|type_expr| registry.resolve(type_expr).ok())
273 }
274
275 let mut map = std::collections::HashMap::new();
276 let mut shapes = std::collections::HashMap::new();
277 for item in &sheet.items {
278 match item {
279 SheetItem::Cell(cell) => {
280 let shape = resolve_annotation_shape(cell.type_name.as_ref(), registry);
281 let ty = shape
282 .as_ref()
283 .map(|shape| shape_to_ty(shape, registry))
284 .unwrap_or(Ty::Any);
285 if let Some(shape) = shape {
286 shapes.insert(cell.name.clone(), shape);
287 }
288 map.insert(cell.name.clone(), ty);
289 }
290 SheetItem::Source(source) => {
291 let shape = resolve_annotation_shape(source.type_name.as_ref(), registry);
292 let ty = shape
293 .as_ref()
294 .map(|shape| shape_to_ty(shape, registry))
295 .unwrap_or(Ty::Any);
296 if let Some(shape) = shape {
297 shapes.insert(source.name.clone(), shape);
298 }
299 map.insert(source.name.clone(), ty);
300 }
301 _ => {}
302 }
303 }
304 let resolve_cells = |name: &str| -> Ty { map.get(name).cloned().unwrap_or(Ty::Any) };
305 let type_resolver = registry.cel_type_resolver();
306 let mut out_types = std::collections::HashMap::new();
307 for item in &sheet.items {
308 if let SheetItem::Out(out_decl) = item {
309 let shape = resolve_annotation_shape(out_decl.type_name.as_ref(), registry);
310 let ty = shape
311 .as_ref()
312 .map(|shape| shape_to_ty(shape, registry))
313 .unwrap_or_else(|| {
314 check_expr_with_type_resolver(
315 &out_decl.initializer,
316 &resolve_cells,
317 &type_resolver,
318 lookup,
319 )
320 .0
321 });
322 if let Some(shape) = shape {
323 shapes.insert(out_decl.name.clone(), shape);
324 }
325 out_types.insert(out_decl.name.clone(), ty);
326 }
327 }
328 map.extend(out_types);
329 DeclaredTypes {
330 values: map,
331 shapes,
332 }
333}
334
335fn expr_matches_shape(
349 expr: &Expr,
350 shape: &TypeShape,
351 registry: &TypeRegistry,
352 resolve: &impl Fn(&str) -> Ty,
353 resolve_type: &impl cel_parser::TypeResolver,
354 lookup: &cel_parser::OpLookup,
355 diagnostics: &mut Vec<ParseError>,
356) {
357 match (expr, shape) {
358 (Expr::Tuple { elements, .. }, TypeShape::Tuple(expected)) => {
359 if elements.len() != expected.len() {
360 diagnostics
361 .extend(check_expr_with_type_resolver(expr, resolve, resolve_type, lookup).1);
362 diagnostics.push(ParseError::new_range(
363 format!(
364 "expected a {}-element tuple `{}`, got {}",
365 expected.len(),
366 registry.display_name(shape),
367 elements.len()
368 ),
369 expr.span().start,
370 expr.span().end,
371 ));
372 return;
373 }
374 for (element, element_shape) in elements.iter().zip(expected) {
375 expr_matches_shape(
376 element,
377 element_shape,
378 registry,
379 resolve,
380 resolve_type,
381 lookup,
382 diagnostics,
383 );
384 }
385 }
386 (
387 Expr::If {
388 cond,
389 then_branch,
390 else_branch,
391 ..
392 },
393 TypeShape::Tuple(_),
394 ) => {
395 let (_, cond_diags) =
396 check_expr_with_type_resolver(cond, resolve, resolve_type, lookup);
397 diagnostics.extend(cond_diags);
398 expr_matches_shape(
399 then_branch,
400 shape,
401 registry,
402 resolve,
403 resolve_type,
404 lookup,
405 diagnostics,
406 );
407 if let Some(else_branch) = else_branch {
408 expr_matches_shape(
409 else_branch,
410 shape,
411 registry,
412 resolve,
413 resolve_type,
414 lookup,
415 diagnostics,
416 );
417 }
418 }
419 (_, TypeShape::Tuple(_)) => {
420 let (actual, nested) =
421 check_expr_with_type_resolver(expr, resolve, resolve_type, lookup);
422 if !nested.is_empty() {
423 diagnostics.extend(nested);
424 return;
425 }
426 if actual != Ty::Any && shape_to_ty(shape, registry).unifies_with(&actual) {
427 return;
428 }
429 diagnostics.push(ParseError::new_range(
430 format!("expected tuple `{}`", registry.display_name(shape)),
431 expr.span().start,
432 expr.span().end,
433 ));
434 }
435 (Expr::Tuple { .. }, TypeShape::Named(_)) => {
436 diagnostics
437 .extend(check_expr_with_type_resolver(expr, resolve, resolve_type, lookup).1);
438 diagnostics.push(ParseError::new_range(
439 format!("expected `{}`, got a tuple", registry.display_name(shape)),
440 expr.span().start,
441 expr.span().end,
442 ));
443 }
444 (_, TypeShape::Named(_) | TypeShape::Array(_) | TypeShape::Callable(_, _)) => {
445 let declared = shape_to_ty(shape, registry);
446 let (actual, body_diags) =
447 check_expr_with_type_resolver(expr, resolve, resolve_type, lookup);
448 let valid = body_diags.is_empty();
449 diagnostics.extend(body_diags);
450 if valid && !declared.unifies_with(&actual) {
451 diagnostics.push(ParseError::new_range(
452 format!(
453 "expression produces `{}`, but `{}` was expected",
454 actual.name(),
455 declared.name()
456 ),
457 expr.span().start,
458 expr.span().end,
459 ));
460 }
461 }
462 }
463}
464
465fn check_cell_initializer(
477 type_name: Option<&TypeExpr>,
478 initializer: Option<&Expr>,
479 registry: &TypeRegistry,
480 resolve_type: &impl cel_parser::TypeResolver,
481 lookup: &cel_parser::OpLookup,
482 diagnostics: &mut Vec<ParseError>,
483) {
484 let Some(expr) = initializer else {
485 return;
486 };
487 let resolve = |_: &str| Ty::Any;
488 let Some(shape) = type_name.and_then(|type_expr| registry.resolve(type_expr).ok()) else {
489 diagnostics.extend(check_expr_with_type_resolver(expr, &resolve, resolve_type, lookup).1);
490 return;
491 };
492 if let TypeShape::Tuple(_) = shape {
493 expr_matches_shape(
494 expr,
495 &shape,
496 registry,
497 &resolve,
498 resolve_type,
499 lookup,
500 diagnostics,
501 );
502 return;
503 }
504 if let Expr::Literal {
505 value: literal,
506 span: lit_span,
507 } = expr
508 {
509 let declared = shape_to_ty(&shape, registry);
510 if !literal_matches_declared_ty(literal, &declared) {
511 diagnostics.push(ParseError::new_range(
512 format!("literal cannot be used as type `{}`", declared.name()),
513 lit_span.start,
514 lit_span.end,
515 ));
516 }
517 return;
518 }
519 expr_matches_shape(
520 expr,
521 &shape,
522 registry,
523 &resolve,
524 resolve_type,
525 lookup,
526 diagnostics,
527 );
528}
529
530fn expr_references_ident(expr: &Expr, name: &str) -> bool {
537 match expr {
538 Expr::Literal { .. } => false,
539 Expr::Ident { name: ident, .. } => ident == name,
540 Expr::Struct { fields, .. } => fields
541 .iter()
542 .any(|field| expr_references_ident(&field.value, name)),
543 Expr::Op { operands, .. } => operands.iter().any(|e| expr_references_ident(e, name)),
544 Expr::Apply { callee, args, .. } => {
545 expr_references_ident(callee, name)
546 || args.iter().any(|e| expr_references_ident(e, name))
547 }
548 Expr::Member { receiver, args, .. } => {
549 expr_references_ident(receiver, name)
550 || args
551 .iter()
552 .flatten()
553 .any(|e| expr_references_ident(e, name))
554 }
555 Expr::Tuple { elements, .. } => elements.iter().any(|e| expr_references_ident(e, name)),
556 Expr::Array { elements, .. } => elements.iter().any(|e| expr_references_ident(e, name)),
557 Expr::TupleIndex { base, .. } => expr_references_ident(base, name),
558 Expr::If {
559 cond,
560 then_branch,
561 else_branch,
562 ..
563 } => {
564 expr_references_ident(cond, name)
565 || expr_references_ident(then_branch, name)
566 || else_branch
567 .as_deref()
568 .is_some_and(|e| expr_references_ident(e, name))
569 }
570 Expr::Logical { lhs, rhs, .. } => {
571 expr_references_ident(lhs, name) || expr_references_ident(rhs, name)
572 }
573 Expr::Cast { expr, .. } => expr_references_ident(expr, name),
574 Expr::Closure { params, body, .. } => {
575 !params.iter().any(|param| param.name == name) && expr_references_ident(body, name)
576 }
577 }
578}
579
580fn check_filter(
601 name: &str,
602 filter: Option<&CellFilter>,
603 registry: &TypeRegistry,
604 resolve: &impl Fn(&str) -> Ty,
605 resolve_type: &impl cel_parser::TypeResolver,
606 lookup: &cel_parser::OpLookup,
607 diagnostics: &mut Vec<ParseError>,
608) {
609 let Some(filter) = filter else {
610 return;
611 };
612
613 let own_ty = resolve(name);
614 let body_resolve = |ident: &str| -> Ty {
615 if ident == "_" {
616 own_ty.clone()
617 } else {
618 resolve(ident)
619 }
620 };
621
622 let (body_ty, body_diags) =
623 check_expr_with_type_resolver(&filter.body, &body_resolve, resolve_type, lookup);
624 diagnostics.extend(body_diags);
625 let range = body_ty.type_id().and_then(|id| registry.range_entry(id));
626 let mismatch = if let Some(range) = range {
627 own_ty != Ty::Any && own_ty.type_id() != Some(range.element_type_id)
628 } else {
629 !own_ty.unifies_with(&body_ty)
630 };
631 if mismatch {
632 let requirement = if range.is_some() {
633 "range bounds must be"
634 } else {
635 "must produce"
636 };
637 diagnostics.push(ParseError::new_range(
638 format!("cell `{name}`: filter {requirement} `{}`", own_ty.name()),
639 filter.body.span().start,
640 filter.body.span().end,
641 ));
642 }
643
644 if range.is_none() && !expr_references_ident(&filter.body, "_") {
645 diagnostics.push(ParseError::new_range(
646 "filter must reference `_` (the value being filtered)".to_string(),
647 filter.span.start,
648 filter.span.end,
649 ));
650 }
651}
652
653fn check_tuple_output_body(
666 body: &Expr,
667 outputs: &[(String, ExprSpan)],
668 resolve: &impl Fn(&str) -> Ty,
669 resolve_type: &impl cel_parser::TypeResolver,
670 lookup: &cel_parser::OpLookup,
671 diagnostics: &mut Vec<ParseError>,
672) {
673 match body {
674 Expr::Tuple { elements, .. } if elements.len() == outputs.len() => {
675 for (element, (name, _)) in elements.iter().zip(outputs) {
676 let (element_ty, element_diags) =
677 check_expr_with_type_resolver(element, resolve, resolve_type, lookup);
678 diagnostics.extend(element_diags);
679 let declared = resolve(name);
680 if !declared.unifies_with(&element_ty) {
681 diagnostics.push(ParseError::new_range(
682 format!(
683 "binding output `{name}` produces `{}`, but is declared `{}`",
684 element_ty.name(),
685 declared.name()
686 ),
687 element.span().start,
688 element.span().end,
689 ));
690 }
691 }
692 }
693 Expr::If {
694 cond,
695 then_branch,
696 else_branch,
697 ..
698 } => {
699 let (_, cond_diags) =
700 check_expr_with_type_resolver(cond, resolve, resolve_type, lookup);
701 diagnostics.extend(cond_diags);
702 check_tuple_output_body(
703 then_branch,
704 outputs,
705 resolve,
706 resolve_type,
707 lookup,
708 diagnostics,
709 );
710 if let Some(else_branch) = else_branch {
711 check_tuple_output_body(
712 else_branch,
713 outputs,
714 resolve,
715 resolve_type,
716 lookup,
717 diagnostics,
718 );
719 }
720 }
721 other => {
722 let (body_ty, body_diags) =
723 check_expr_with_type_resolver(other, resolve, resolve_type, lookup);
724 let valid = body_diags.is_empty();
725 diagnostics.extend(body_diags);
726 if !valid {
727 return;
728 }
729 if let Ty::Tuple(elements) = &body_ty
730 && elements.len() == outputs.len()
731 {
732 for (element_ty, (name, _)) in elements.iter().zip(outputs) {
733 let declared = resolve(name);
734 if !declared.unifies_with(element_ty) {
735 diagnostics.push(ParseError::new_range(
736 format!(
737 "binding output `{name}` produces `{}`, but is declared `{}`",
738 element_ty.name(),
739 declared.name()
740 ),
741 other.span().start,
742 other.span().end,
743 ));
744 }
745 }
746 return;
747 }
748 if body_ty == Ty::Any && !matches!(other, Expr::Tuple { .. }) {
749 return;
750 }
751 let n = outputs.len();
752 diagnostics.push(ParseError::new_range(
753 format!("binding declares {n} outputs but its body is not a {n}-tuple"),
754 other.span().start,
755 other.span().end,
756 ));
757 }
758 }
759}
760
761fn check_binding(
772 binding: &BindingDecl,
773 registry: &TypeRegistry,
774 shapes: &std::collections::HashMap<String, TypeShape>,
775 resolve: &impl Fn(&str) -> Ty,
776 resolve_type: &impl cel_parser::TypeResolver,
777 lookup: &cel_parser::OpLookup,
778 diagnostics: &mut Vec<ParseError>,
779) {
780 if binding.destructure {
781 check_tuple_output_body(
782 &binding.body,
783 &binding.outputs,
784 resolve,
785 resolve_type,
786 lookup,
787 diagnostics,
788 );
789 return;
790 }
791 let Some((name, _)) = binding.outputs.first() else {
792 let (_, body_diags) =
793 check_expr_with_type_resolver(&binding.body, resolve, resolve_type, lookup);
794 diagnostics.extend(body_diags);
795 return;
796 };
797 if let Some(shape @ TypeShape::Tuple(_)) = shapes.get(name) {
798 expr_matches_shape(
799 &binding.body,
800 shape,
801 registry,
802 resolve,
803 resolve_type,
804 lookup,
805 diagnostics,
806 );
807 return;
808 }
809 let (body_ty, body_diags) =
810 check_expr_with_type_resolver(&binding.body, resolve, resolve_type, lookup);
811 diagnostics.extend(body_diags);
812 if let Expr::Tuple { elements, .. } = &binding.body {
813 let n = elements.len();
814 diagnostics.push(ParseError::new_range(
815 format!("binding declares 1 output but its body is a {n}-tuple"),
816 binding.body.span().start,
817 binding.body.span().end,
818 ));
819 return;
820 }
821 let declared = resolve(name);
822 if !declared.unifies_with(&body_ty) {
823 diagnostics.push(ParseError::new_range(
824 format!(
825 "binding body produces `{}`, but `{name}` is declared `{}`",
826 body_ty.name(),
827 declared.name()
828 ),
829 binding.body.span().start,
830 binding.body.span().end,
831 ));
832 }
833}
834
835fn check_out(
846 out_decl: &OutDecl,
847 registry: &TypeRegistry,
848 declared: &DeclaredTypes,
849 resolve: &impl Fn(&str) -> Ty,
850 resolve_type: &impl cel_parser::TypeResolver,
851 lookup: &cel_parser::OpLookup,
852 diagnostics: &mut Vec<ParseError>,
853) {
854 if let Some(shape @ TypeShape::Tuple(_)) = declared.shapes.get(&out_decl.name) {
855 expr_matches_shape(
856 &out_decl.initializer,
857 shape,
858 registry,
859 resolve,
860 resolve_type,
861 lookup,
862 diagnostics,
863 );
864 } else {
865 let (body_ty, body_diags) =
866 check_expr_with_type_resolver(&out_decl.initializer, resolve, resolve_type, lookup);
867 diagnostics.extend(body_diags);
868 if out_decl.type_name.is_some() {
869 let declared = resolve(&out_decl.name);
870 if !declared.unifies_with(&body_ty) {
871 diagnostics.push(ParseError::new_range(
872 format!(
873 "out `{}` body produces `{}`, but is declared `{}`",
874 out_decl.name,
875 body_ty.name(),
876 declared.name()
877 ),
878 out_decl.initializer.span().start,
879 out_decl.initializer.span().end,
880 ));
881 }
882 }
883 }
884 check_filter(
885 &out_decl.name,
886 out_decl.filter.as_ref(),
887 registry,
888 resolve,
889 resolve_type,
890 lookup,
891 diagnostics,
892 );
893 check_requirements(
894 out_decl.require.as_ref(),
895 resolve,
896 resolve_type,
897 lookup,
898 diagnostics,
899 );
900}
901
902fn check_requirements(
910 require: Option<&RequireBlock>,
911 resolve: &impl Fn(&str) -> Ty,
912 resolve_type: &impl cel_parser::TypeResolver,
913 lookup: &cel_parser::OpLookup,
914 diagnostics: &mut Vec<ParseError>,
915) {
916 let Some(require) = require else {
917 return;
918 };
919 for requirement in &require.requirements {
920 let (req_ty, req_diags) =
921 check_expr_with_type_resolver(&requirement.body, resolve, resolve_type, lookup);
922 diagnostics.extend(req_diags);
923 if !req_ty.unifies_with(&Ty::Bool) {
924 diagnostics.push(ParseError::new_range(
925 match &requirement.name {
926 Some(name) => format!(
927 "requirement `{name}` produces `{}`, but requirements must be `bool`",
928 req_ty.name()
929 ),
930 None => format!(
931 "requirement produces `{}`, but requirements must be `bool`",
932 req_ty.name()
933 ),
934 },
935 requirement.body.span().start,
936 requirement.body.span().end,
937 ));
938 }
939 }
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945 use crate::AdamAstParser;
946
947 fn parse(source: &str) -> Sheet {
948 AdamAstParser::new().parse_str(source).unwrap()
949 }
950
951 #[derive(Clone, Debug, PartialEq)]
952 struct Custom(i32);
953
954 #[test]
955 fn cell_initializer_matching_its_annotation_has_no_diagnostic() {
956 let sheet = parse("sheet s { cell x: i32 = 1; }");
957 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
958 assert!(diags.is_empty());
959 }
960
961 #[test]
962 fn cell_initializer_mismatched_with_its_annotation_is_a_diagnostic() {
963 let sheet = parse("sheet s { cell x: i32 = 1.0; }");
965 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
966 assert_eq!(diags.len(), 1);
967 }
968
969 #[test]
970 fn source_initializer_mismatched_with_its_annotation_is_a_diagnostic() {
971 let sheet = parse("sheet s { source x: i32 = 1.0; }");
973 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
974 assert_eq!(diagnostics.len(), 1);
975 }
976
977 #[test]
978 fn custom_array_annotations_use_the_registry_type_resolver() {
979 let sheet = parse(
980 "sheet s { \
981 out values := []: [Custom]; \
982 out nested := [[]: [Custom]]: [[Custom]]; \
983 }",
984 );
985 let mut registry = TypeRegistry::new();
986 registry.register_no_default::<Custom>("Custom");
987
988 let diagnostics = check_sheet(&sheet, ®istry, &cel_parser::OpLookup::new());
989 assert!(diagnostics.is_empty(), "{diagnostics:?}");
990 }
991
992 #[test]
993 fn custom_array_annotations_reject_exact_scalar_type_mismatches() {
994 let sheet = parse("sheet s { out values := [1]: [Custom]; }");
995 let mut registry = TypeRegistry::new();
996 registry.register_no_default::<Custom>("Custom");
997
998 let diagnostics = check_sheet(&sheet, ®istry, &cel_parser::OpLookup::new());
999 assert_eq!(diagnostics.len(), 1, "{diagnostics:?}");
1000 assert!(
1001 diagnostics[0]
1002 .message()
1003 .contains("expected `Custom`, found `i32`"),
1004 "got: {}",
1005 diagnostics[0].message()
1006 );
1007 }
1008
1009 #[test]
1010 fn unknown_custom_array_annotation_type_is_reported() {
1011 let sheet = parse("sheet s { out values := []: [Missing]; }");
1012 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1013 assert_eq!(diagnostics.len(), 1);
1014 assert!(
1015 diagnostics[0].message().contains("unknown type `Missing`"),
1016 "got: {}",
1017 diagnostics[0].message()
1018 );
1019 }
1020
1021 #[test]
1022 fn tuple_typed_array_annotations_are_accepted_by_cel_checking() {
1023 let sheet = parse("sheet s { out values := []: [(i32, f64)]; }");
1024 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1025 assert!(diagnostics.is_empty(), "{diagnostics:?}");
1026 }
1027
1028 #[test]
1029 fn cell_requirement_non_bool_body_is_a_diagnostic() {
1030 let sheet = parse("sheet s { cell x: i32 = 5 require { @positive x; }; }");
1031 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1032 assert_eq!(diagnostics.len(), 1);
1033 }
1034
1035 #[test]
1036 fn cell_initializer_non_literal_expression_is_checked() {
1037 let sheet = parse("sheet s { cell x: f64 = 1 + 2; }");
1038 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1039 let messages: Vec<&str> = diags.iter().map(cel_parser::ParseError::message).collect();
1040 assert_eq!(
1041 messages,
1042 ["expression produces `i32`, but `f64` was expected"]
1043 );
1044 }
1045
1046 #[test]
1047 fn cell_initializer_array_annotation_diagnostics_still_surface() {
1048 let sheet = parse("sheet s { cell x: f64 = [0, 1]: [f64]; }");
1051 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1052 let messages: Vec<&str> = diags.iter().map(cel_parser::ParseError::message).collect();
1053 assert_eq!(
1054 messages,
1055 vec!["array elements must match the annotation exactly: expected `f64`, found `i32`"]
1056 );
1057 }
1058
1059 #[test]
1060 fn cell_with_only_an_annotation_has_nothing_to_cross_check() {
1061 let sheet = parse("sheet s { cell x: i32; }");
1062 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1063 assert!(diags.is_empty());
1064 }
1065
1066 #[test]
1067 fn cell_initializer_unsuffixed_int_literal_mismatched_with_a_declared_unsigned_type_is_a_diagnostic()
1068 {
1069 let sheet = parse("sheet s { cell x: u32 = 1; }");
1074 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1075 assert_eq!(diags.len(), 1);
1076 }
1077
1078 #[test]
1079 fn cell_initializer_unsuffixed_int_literal_mismatched_with_a_declared_float_type_is_a_diagnostic()
1080 {
1081 let sheet = parse("sheet s { cell x: f64 = 1; }");
1084 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1085 assert_eq!(diags.len(), 1);
1086 }
1087
1088 #[test]
1089 fn cell_initializer_char_literal_against_any_registered_type_is_a_diagnostic() {
1090 let sheet = parse("sheet s { cell x: i32 = 'a'; }");
1093 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1094 assert_eq!(diags.len(), 1);
1095 }
1096
1097 #[test]
1098 fn cell_annotated_with_an_unregistered_type_name_is_never_flagged() {
1099 let sheet = parse("sheet s { cell x: WidgetHandle = 1; }");
1100 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1101 assert!(diags.is_empty());
1102 }
1103
1104 #[test]
1105 fn binding_single_output_matching_declared_type_has_no_diagnostic() {
1106 let sheet = parse(
1107 "sheet s { cell width: f64; cell height: f64; cell area: f64; \
1108 relationship { area := width * height; } }",
1109 );
1110 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1111 assert!(diags.is_empty());
1112 }
1113
1114 #[test]
1115 fn binding_single_output_mismatched_with_declared_type_is_a_diagnostic() {
1116 let sheet = parse(
1117 "sheet s { cell width: f64; cell height: f64; cell area: i32; \
1118 relationship { area := width * height; } }",
1119 );
1120 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1121 assert_eq!(diags.len(), 1);
1122 }
1123
1124 #[test]
1125 fn binding_multi_output_matching_tuple_has_no_diagnostic() {
1126 let sheet = parse(
1127 "sheet s { cell a: i32; cell b: i32; cell sum: i32; cell diff: i32; \
1128 relationship { (sum, diff) := (a + b, a - b); } }",
1129 );
1130 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1131 assert!(diags.is_empty());
1132 }
1133
1134 #[test]
1136 fn binding_multi_output_iife_defers_unknown_shape_but_checks_children() {
1137 for (body, expected) in [
1138 ("(|x: i32| (x, x))(1)", 0),
1139 ("(|x: i32| (x, true + x))(1)", 1),
1140 ("(|x: i32| (x, true))(1)", 1),
1141 ("(|x: i32| (x,))(1)", 1),
1142 ("1", 1),
1143 ("(1, 2, 3)", 1),
1144 ] {
1145 let sheet = parse(&format!(
1146 "sheet s {{ cell a: i32; cell b: i32; relationship {{ (a,b) := {body}; }} }}"
1147 ));
1148 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1149 assert_eq!(diags.len(), expected, "{body}: {diags:?}");
1150 }
1151 }
1152
1153 #[test]
1155 fn tuple_call_results_match_declared_shapes() {
1156 for (annotation, expected) in [("(i32, i32)", 0), ("(i32, bool)", 1)] {
1157 let sheet = parse(&format!(
1158 "sheet s {{ cell pair: {annotation} = (|x: i32| (x,x))(1); }}"
1159 ));
1160 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1161 assert_eq!(diags.len(), expected, "{annotation}: {diags:?}");
1162 }
1163 }
1164
1165 #[test]
1166 fn binding_multi_output_arity_mismatch_is_a_diagnostic() {
1167 let sheet = parse(
1168 "sheet s { cell a: i32; cell b: i32; cell sum: i32; cell diff: i32; \
1169 relationship { (sum, diff) := a + b; } }",
1170 );
1171 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1172 assert_eq!(diags.len(), 1);
1173 }
1174
1175 #[test]
1176 fn binding_single_output_with_a_tuple_shaped_body_is_a_diagnostic() {
1177 let sheet = parse(
1180 "sheet s { cell a: i32; cell b: i32; cell out: i32; \
1181 relationship { out := (a, b); } }",
1182 );
1183 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1184 assert_eq!(diags.len(), 1);
1185 }
1186
1187 #[test]
1188 fn binding_multi_output_per_element_type_mismatch_is_a_diagnostic() {
1189 let sheet = parse(
1190 "sheet s { cell a: i32; cell b: i32; cell sum: i32; cell diff: f64; \
1191 relationship { (sum, diff) := (a + b, a - b); } }",
1192 );
1193 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1194 assert_eq!(diags.len(), 1);
1195 }
1196
1197 #[test]
1198 fn an_operator_error_inside_a_binding_body_surfaces() {
1199 let sheet = parse(
1200 "sheet s { cell name: String; cell count: i32; cell out: i32; \
1201 relationship { out := name + count; } }",
1202 );
1203 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1204 assert_eq!(diags.len(), 1);
1205 }
1206
1207 #[test]
1208 fn conditional_branch_and_default_bindings_are_both_checked() {
1209 let sheet = parse(
1210 "sheet s { cell mode: i32; cell a: i32; cell b: i32; cell out: i32; \
1211 conditional mode { \
1212 0i32 => { relationship { out := a; } }, \
1213 _ => { relationship { out := b; } }, \
1214 } }",
1215 );
1216 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1217 assert!(diags.is_empty());
1218 }
1219
1220 #[test]
1221 fn a_cell_with_no_type_annotation_unifies_with_anything_used_in_a_binding() {
1222 let sheet = parse(
1225 "sheet s { cell a = 1; cell out: i32; \
1226 relationship { out := a; } }",
1227 );
1228 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1229 assert!(diags.is_empty());
1230 }
1231
1232 #[test]
1233 fn recovered_error_items_are_skipped_without_panicking() {
1234 let sheet =
1235 parse("sheet s { cell good: i32 = 1; cell bad unknown_syntax cell after: i32 = 2; }");
1236 assert!(
1237 !sheet.errors.is_empty(),
1238 "fixture must actually recover an error item"
1239 );
1240 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1241 assert!(diags.is_empty());
1242 }
1243
1244 #[test]
1245 fn out_body_matching_its_annotation_has_no_diagnostic() {
1246 let sheet = parse(
1247 "sheet s { cell width: f64; cell height: f64; \
1248 out area: f64 := width * height; }",
1249 );
1250 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1251 assert!(diags.is_empty());
1252 }
1253
1254 #[test]
1255 fn out_body_mismatched_with_its_annotation_is_a_diagnostic() {
1256 let sheet = parse(
1257 "sheet s { cell width: f64; cell height: f64; \
1258 out area: i32 := width * height; }",
1259 );
1260 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1261 assert_eq!(diags.len(), 1);
1262 }
1263
1264 #[test]
1265 fn out_with_no_annotation_infers_its_type_and_has_no_diagnostic() {
1266 let sheet = parse(
1269 "sheet s { cell width: f64; cell height: f64; \
1270 out area := width * height; }",
1271 );
1272 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1273 assert!(diags.is_empty());
1274 }
1275
1276 #[test]
1277 fn requirement_with_bool_body_has_no_diagnostic() {
1278 let sheet = parse(
1279 "sheet s { cell width: f64; cell max_width: f64; \
1280 out area: f64 := width require { \
1281 @max_width width <= max_width; \
1282 }; }",
1283 );
1284 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1285 assert!(diags.is_empty());
1286 }
1287
1288 #[test]
1289 fn requirement_with_non_bool_body_is_a_diagnostic() {
1290 let sheet = parse(
1291 "sheet s { cell width: f64; \
1292 out area: f64 := width require { \
1293 @bogus width; \
1294 }; }",
1295 );
1296 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1297 assert_eq!(diags.len(), 1);
1298 }
1299
1300 #[test]
1301 fn unlabeled_requirement_with_non_bool_body_is_a_diagnostic() {
1302 let sheet = parse(
1303 "sheet s { cell width: f64; \
1304 out area: f64 := width require { \
1305 width; \
1306 }; }",
1307 );
1308 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1309 assert_eq!(diags.len(), 1);
1310 }
1311
1312 #[test]
1313 fn cell_tuple_initializer_matching_its_annotation_has_no_diagnostic() {
1314 let sheet = parse("sheet s { cell a: (i32, f64) = (1, 2.5); }");
1315 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1316 assert!(diags.is_empty());
1317 }
1318
1319 #[test]
1320 fn cell_tuple_initializer_arity_mismatch_is_a_diagnostic() {
1321 let sheet = parse("sheet s { cell a: (i32, f64, i32) = (1, 2.5); }");
1322 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1323 assert_eq!(diags.len(), 1);
1324 }
1325
1326 #[test]
1327 fn cell_tuple_initializer_element_type_mismatch_is_a_diagnostic() {
1328 let sheet = parse("sheet s { cell a: (i32, i32) = (1, 2.5); }");
1329 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1330 assert_eq!(diags.len(), 1);
1331 }
1332
1333 #[test]
1334 fn cell_nested_tuple_initializer_matching_its_annotation_has_no_diagnostic() {
1335 let sheet = parse("sheet s { cell a: (i32, (f64, String)) = (1, (2.5, \"x\")); }");
1336 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1337 assert!(diags.is_empty());
1338 }
1339
1340 #[test]
1341 fn filter_with_matching_types_has_no_diagnostic() {
1342 let sheet = parse("sheet s { cell a: i32 = 1 filter _; }");
1343 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1344 assert!(diags.is_empty());
1345 }
1346
1347 #[test]
1348 fn filter_with_matching_types_on_a_source_has_no_diagnostic() {
1349 let sheet = parse("sheet s { source a: i32 = 1 filter _; }");
1350 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1351 assert!(diags.is_empty());
1352 }
1353
1354 #[test]
1355 fn filter_referencing_a_cell_has_no_diagnostic() {
1356 let sheet = parse(
1357 "sheet s { cell hi: i32 = 100; cell a: i32 = 1 filter if _ > hi { hi } else { _ }; }",
1358 );
1359 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1360 assert!(diags.is_empty());
1361 }
1362
1363 #[test]
1364 fn filter_body_type_mismatch_is_a_diagnostic() {
1365 let sheet = parse("sheet s { cell a: i32 = 1 filter _ > 0; }");
1367 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1368 assert_eq!(diags.len(), 1);
1369 }
1370
1371 #[test]
1372 fn filter_without_underscore_is_a_diagnostic() {
1373 let sheet = parse("sheet s { cell a: i32 = 1 filter 1; }");
1374 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1375 assert_eq!(diags.len(), 1);
1376 }
1377
1378 #[test]
1380 fn closure_parameter_shadows_only_value_references() {
1381 for (source, references) in [
1382 ("|_: i32| _", false),
1383 ("|x: i32| _", true),
1384 ("|_: i32| value._", false),
1385 ("|_: i32| _ { x: value }", false),
1386 ("|x: i32| Record { x: _ }.x", true),
1387 ] {
1388 let expr =
1389 cel_parser::Parser::<cel_parser::AstContext>::new(cel_parser::OpLookup::new())
1390 .parse_str_ast(source)
1391 .unwrap();
1392 assert_eq!(expr_references_ident(&expr, "_"), references, "{source}");
1393 }
1394 }
1395
1396 #[test]
1398 fn record_filter_dependencies_only_visit_field_values() {
1399 let sheet = parse("sheet s { cell a: i32 = 1 filter Record { value: _ }; }");
1400 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1401 assert_eq!(diagnostics.len(), 1);
1402 assert!(diagnostics[0].message().contains("unknown record type"));
1403 for source in [
1404 "sheet s { cell a: i32 = 1 filter Record { _: 1 }; }",
1405 "sheet s { cell a: i32 = 1 filter _ { value: 1 }; }",
1406 ] {
1407 let sheet = parse(source);
1408 let diagnostics =
1409 check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1410 assert_eq!(diagnostics.len(), 2);
1411 assert!(
1412 diagnostics
1413 .iter()
1414 .any(|d| d.message().contains("must reference `_`"))
1415 );
1416 }
1417 }
1418
1419 #[test]
1420 fn filter_body_type_mismatch_on_an_out_is_a_diagnostic() {
1421 let sheet = parse("sheet s { cell width: i32 = 1; out area: i32 := width filter _ > 0; }");
1424 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1425 assert_eq!(diags.len(), 1);
1426 }
1427
1428 #[test]
1429 fn filter_without_underscore_on_an_out_is_a_diagnostic() {
1430 let sheet = parse("sheet s { cell width: i32 = 1; out area: i32 := width filter 1; }");
1432 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1433 assert_eq!(diags.len(), 1);
1434 }
1435
1436 #[test]
1437 fn filter_body_type_mismatch_on_a_source_is_a_diagnostic() {
1438 let sheet = parse("sheet s { source a: i32 = 1 filter _ > 0; }");
1441 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1442 assert_eq!(diags.len(), 1);
1443 }
1444
1445 #[test]
1446 fn filter_without_underscore_on_a_source_is_a_diagnostic() {
1447 let sheet = parse("sheet s { source a: i32 = 1 filter 1; }");
1450 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1451 assert_eq!(diags.len(), 1);
1452 }
1453
1454 #[test]
1455 fn filter_on_a_tuple_typed_cell_is_supported() {
1456 let sheet = parse("sheet s { cell a: (i32, f64) = (1, 2.5) filter (_.0, _.1); }");
1457 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1458 assert!(diags.is_empty());
1459 }
1460
1461 #[test]
1462 fn filter_references_underscore_nested_inside_a_call_has_no_missing_underscore_diagnostic() {
1463 let sheet = parse("sheet s { cell a: i32 = 1 filter if true { _ } else { 1 }; }");
1466 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1467 assert!(diags.is_empty());
1468 }
1469
1470 #[test]
1472 fn member_filter_references_are_operands_not_labels() {
1473 for body in ["_.accessor", "value.accessor(_)", "_.accessor()"] {
1474 let sheet = parse(&format!("sheet s {{ cell value: i32 = 1 filter {body}; }}"));
1475 assert!(
1476 check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new()).is_empty(),
1477 "{body}"
1478 );
1479 }
1480 let sheet = parse("sheet s { cell value: i32 = 1 filter value._; }");
1481 assert_eq!(
1482 check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new()).len(),
1483 1
1484 );
1485 }
1486
1487 #[test]
1488 fn binding_single_tuple_typed_output_matching_body_has_no_diagnostic() {
1489 let sheet = parse(
1490 "sheet s { cell a: i32; cell b: i32; cell pair: (i32, i32); \
1491 relationship { pair := (a, b); } }",
1492 );
1493 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1494 assert!(diags.is_empty());
1495 }
1496
1497 #[test]
1498 fn binding_single_tuple_typed_output_element_type_mismatch_is_a_diagnostic() {
1499 let sheet = parse(
1500 "sheet s { cell a: i32; cell b: f64; cell pair: (i32, i32); \
1501 relationship { pair := (a, b); } }",
1502 );
1503 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1504 assert_eq!(diags.len(), 1);
1505 }
1506
1507 #[test]
1508 fn binding_single_tuple_typed_output_if_else_body_matching_both_branches_has_no_diagnostic() {
1509 let sheet = parse(
1510 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell pair: (i32, i32); \
1511 relationship { pair := \
1512 if cond { (a, b) } else { (b, a) }; \
1513 } }",
1514 );
1515 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1516 assert!(diags.is_empty());
1517 }
1518
1519 #[test]
1520 fn binding_single_tuple_typed_output_if_without_else_matching_then_branch_has_no_diagnostic() {
1521 let sheet = parse(
1522 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell pair: (i32, i32); \
1523 relationship { pair := \
1524 if cond { (a, b) }; \
1525 } }",
1526 );
1527 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1528 assert!(diags.is_empty());
1529 }
1530
1531 #[test]
1532 fn binding_single_tuple_typed_output_if_else_if_chain_matching_all_branches_has_no_diagnostic()
1533 {
1534 let sheet = parse(
1535 "sheet s { cell mode: i32; cell a: i32; cell b: i32; cell pair: (i32, i32); \
1536 relationship { pair := \
1537 if mode == 0 { (a, b) } else if mode == 1 { (b, a) } else { (0, 0) }; \
1538 } }",
1539 );
1540 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1541 assert!(diags.is_empty());
1542 }
1543
1544 #[test]
1545 fn binding_single_tuple_typed_output_if_else_body_element_mismatch_in_each_branch_is_two_diagnostics()
1546 {
1547 let sheet = parse(
1554 "sheet s { cell cond: bool; cell a: i32; cell c: f64; cell pair: (i32, i32); \
1555 relationship { pair := \
1556 if cond { (a, c) } else { (a, c) }; \
1557 } }",
1558 );
1559 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1560 assert_eq!(diags.len(), 2);
1561 }
1562
1563 #[test]
1564 fn binding_multi_output_if_else_body_matching_both_branches_has_no_diagnostic() {
1565 let sheet = parse(
1566 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell sum: i32; cell diff: i32; \
1567 relationship { (sum, diff) := \
1568 if cond { (a + b, a - b) } else { (a - b, a + b) }; \
1569 } }",
1570 );
1571 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1572 assert!(diags.is_empty());
1573 }
1574
1575 #[test]
1576 fn binding_multi_output_if_else_body_element_mismatch_in_a_branch_is_a_diagnostic() {
1577 let sheet = parse(
1578 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell c: f64; \
1579 cell sum: i32; cell diff: i32; \
1580 relationship { (sum, diff) := \
1581 if cond { (a, b) } else { (a, c) }; \
1582 } }",
1583 );
1584 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1585 assert_eq!(diags.len(), 1);
1586 }
1587
1588 #[test]
1589 fn binding_single_element_tuple_destructure_has_no_diagnostic() {
1590 let sheet =
1591 parse("sheet s { cell a: i32; cell out: i32; relationship { (out,) := (a,); } }");
1592 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1593 assert!(diags.is_empty());
1594 }
1595
1596 #[test]
1597 fn binding_single_element_tuple_destructure_type_mismatch_is_a_diagnostic() {
1598 let sheet =
1599 parse("sheet s { cell a: f64; cell out: i32; relationship { (out,) := (a,); } }");
1600 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1601 assert_eq!(diags.len(), 1);
1602 }
1603
1604 #[test]
1605 fn binding_single_element_tuple_destructure_arity_mismatch_is_a_diagnostic() {
1606 let sheet = parse("sheet s { cell a: i32; cell out: i32; relationship { (out,) := a; } }");
1607 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1608 assert_eq!(diags.len(), 1);
1609 }
1610
1611 #[test]
1612 fn filter_range_inclusive_body_does_not_require_underscore() {
1613 let sheet = parse("sheet s { cell a: i32 filter 0..=100; }");
1614 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1615 assert!(diags.is_empty());
1616 }
1617
1618 #[test]
1620 fn inferred_range_filters_agree_with_executable() {
1621 for source in [
1622 "sheet s { cell a: i32 filter (|x: i32| 0..=x)(100); }",
1623 "sheet s { source a: f64 = 1.0 filter 0.0..=100.0; }",
1624 "sheet s { out a: i32 := 1 filter (|x: i32| 0..=x)(100); }",
1625 ] {
1626 assert!(
1627 crate::AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
1628 .parse_str(source)
1629 .is_ok(),
1630 "{source}"
1631 );
1632 let diags = check_sheet(
1633 &parse(source),
1634 &TypeRegistry::new(),
1635 &cel_parser::OpLookup::new(),
1636 );
1637 assert!(diags.is_empty(), "{source}: {diags:?}");
1638 }
1639 for source in [
1640 "sheet s { cell a: i32 filter 0.0..=100.0; }",
1641 "sheet s { source a: i32 = 1 filter (|x: f64| 0.0..=x)(100.0); }",
1642 "sheet s { out a: i32 := 1 filter 0.0..=100.0; }",
1643 "sheet s { cell a: i32 filter 0..100; }",
1644 ] {
1645 assert!(
1646 crate::AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
1647 .parse_str(source)
1648 .is_err(),
1649 "{source}"
1650 );
1651 let diags = check_sheet(
1652 &parse(source),
1653 &TypeRegistry::new(),
1654 &cel_parser::OpLookup::new(),
1655 );
1656 assert!(!diags.is_empty(), "{source}");
1657 }
1658 }
1659}