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(
74 sheet: &Sheet,
75 registry: &TypeRegistry,
76 lookup: &cel_parser::OpLookup,
77) -> Vec<ParseError> {
78 let mut diagnostics = Vec::new();
79 if sheet.parameters.is_some() {
80 diagnostics.push(ParseError::new(
81 "parameterized definitions require module lowering",
82 sheet.name_span.start,
83 ));
84 }
85 let declared = declared_cell_types(sheet, registry, lookup);
86 let shapes = &declared.shapes;
87 let resolve = |name: &str| -> Ty { declared.values.get(name).cloned().unwrap_or(Ty::Any) };
88 let type_resolver = registry.cel_type_resolver();
89 for item in &sheet.items {
90 match item {
91 SheetItem::Cell(cell) => {
92 check_cell_initializer(
93 cell.type_name.as_ref(),
94 cell.initializer.as_ref(),
95 registry,
96 &type_resolver,
97 lookup,
98 &mut diagnostics,
99 );
100 check_filter(
101 &cell.name,
102 cell.filter.as_ref(),
103 registry,
104 &resolve,
105 &type_resolver,
106 lookup,
107 &mut diagnostics,
108 );
109 check_requirements(
110 cell.require.as_ref(),
111 &resolve,
112 &type_resolver,
113 lookup,
114 &mut diagnostics,
115 );
116 }
117 SheetItem::Source(source) => {
118 check_cell_initializer(
119 source.type_name.as_ref(),
120 source.initializer.as_ref(),
121 registry,
122 &type_resolver,
123 lookup,
124 &mut diagnostics,
125 );
126 check_filter(
127 &source.name,
128 source.filter.as_ref(),
129 registry,
130 &resolve,
131 &type_resolver,
132 lookup,
133 &mut diagnostics,
134 );
135 check_requirements(
136 source.require.as_ref(),
137 &resolve,
138 &type_resolver,
139 lookup,
140 &mut diagnostics,
141 );
142 }
143 SheetItem::Relationship(rel) => {
144 for binding in &rel.bindings {
145 check_binding(
146 binding,
147 registry,
148 shapes,
149 &resolve,
150 &type_resolver,
151 lookup,
152 &mut diagnostics,
153 );
154 }
155 }
156 SheetItem::RelationshipUse(call) => {
157 diagnostics.push(ParseError::new(
158 "relationship uses require module lowering",
159 call.span.start,
160 ));
161 }
162 SheetItem::Conditional(cond) => {
163 diagnostics.extend(
164 check_expr_with_type_resolver(
165 &cond.match_expr,
166 &resolve,
167 &type_resolver,
168 lookup,
169 )
170 .1,
171 );
172 for branch in &cond.branches {
173 for rel in &branch.relationships {
174 let rel = match rel {
175 crate::ast::BranchItem::Relationship(rel) => rel,
176 crate::ast::BranchItem::RelationshipUse(call) => {
177 diagnostics.push(ParseError::new(
178 "relationship uses require module lowering",
179 call.span.start,
180 ));
181 continue;
182 }
183 };
184 for binding in &rel.bindings {
185 check_binding(
186 binding,
187 registry,
188 shapes,
189 &resolve,
190 &type_resolver,
191 lookup,
192 &mut diagnostics,
193 );
194 }
195 }
196 }
197 if let Some(default) = &cond.default {
198 for rel in &default.relationships {
199 let rel = match rel {
200 crate::ast::BranchItem::Relationship(rel) => rel,
201 crate::ast::BranchItem::RelationshipUse(call) => {
202 diagnostics.push(ParseError::new(
203 "relationship uses require module lowering",
204 call.span.start,
205 ));
206 continue;
207 }
208 };
209 for binding in &rel.bindings {
210 check_binding(
211 binding,
212 registry,
213 shapes,
214 &resolve,
215 &type_resolver,
216 lookup,
217 &mut diagnostics,
218 );
219 }
220 }
221 }
222 }
223 SheetItem::Out(out_decl) => check_out(
224 out_decl,
225 registry,
226 &declared,
227 &resolve,
228 &type_resolver,
229 lookup,
230 &mut diagnostics,
231 ),
232 SheetItem::Error { .. } => {} }
234 }
235 diagnostics
236}
237
238fn shape_to_ty(shape: &TypeShape, registry: &TypeRegistry) -> Ty {
242 match shape {
243 TypeShape::Named(type_id) => registry
244 .resolved_leaf_type(*type_id)
245 .map(|leaf| Ty::from_leaf(&leaf))
246 .unwrap_or(Ty::Any),
247 TypeShape::Tuple(elements) => Ty::Tuple(
248 elements
249 .iter()
250 .map(|element| shape_to_ty(element, registry))
251 .collect(),
252 ),
253 TypeShape::Array(element) => Ty::Array(Box::new(shape_to_ty(element, registry))),
254 TypeShape::Callable(parameters, result) => Ty::Callable(
255 parameters
256 .iter()
257 .map(|p| shape_to_ty(p, registry))
258 .collect(),
259 Box::new(shape_to_ty(result, registry)),
260 ),
261 }
262}
263
264fn literal_matches_declared_ty(lit: &Literal, declared: &Ty) -> bool {
267 Ty::from_literal(lit).unifies_with(declared)
268}
269
270struct DeclaredTypes {
274 values: std::collections::HashMap<String, Ty>,
276 shapes: std::collections::HashMap<String, TypeShape>,
278}
279
280fn declared_cell_types(
296 sheet: &Sheet,
297 registry: &TypeRegistry,
298 lookup: &cel_parser::OpLookup,
299) -> DeclaredTypes {
300 fn resolve_annotation_shape(
305 type_expr: Option<&crate::ast::TypeExpr>,
306 registry: &TypeRegistry,
307 ) -> Option<TypeShape> {
308 type_expr.and_then(|type_expr| registry.resolve(type_expr).ok())
309 }
310
311 let mut map = std::collections::HashMap::new();
312 let mut shapes = std::collections::HashMap::new();
313 for item in &sheet.items {
314 match item {
315 SheetItem::Cell(cell) => {
316 let shape = resolve_annotation_shape(cell.type_name.as_ref(), registry);
317 let ty = shape
318 .as_ref()
319 .map(|shape| shape_to_ty(shape, registry))
320 .unwrap_or(Ty::Any);
321 if let Some(shape) = shape {
322 shapes.insert(cell.name.clone(), shape);
323 }
324 map.insert(cell.name.clone(), ty);
325 }
326 SheetItem::Source(source) => {
327 let shape = resolve_annotation_shape(source.type_name.as_ref(), registry);
328 let ty = shape
329 .as_ref()
330 .map(|shape| shape_to_ty(shape, registry))
331 .unwrap_or(Ty::Any);
332 if let Some(shape) = shape {
333 shapes.insert(source.name.clone(), shape);
334 }
335 map.insert(source.name.clone(), ty);
336 }
337 _ => {}
338 }
339 }
340 let resolve_cells = |name: &str| -> Ty { map.get(name).cloned().unwrap_or(Ty::Any) };
341 let type_resolver = registry.cel_type_resolver();
342 let mut out_types = std::collections::HashMap::new();
343 for item in &sheet.items {
344 if let SheetItem::Out(out_decl) = item {
345 let shape = resolve_annotation_shape(out_decl.type_name.as_ref(), registry);
346 let ty = shape
347 .as_ref()
348 .map(|shape| shape_to_ty(shape, registry))
349 .unwrap_or_else(|| {
350 check_expr_with_type_resolver(
351 &out_decl.initializer,
352 &resolve_cells,
353 &type_resolver,
354 lookup,
355 )
356 .0
357 });
358 if let Some(shape) = shape {
359 shapes.insert(out_decl.name.clone(), shape);
360 }
361 out_types.insert(out_decl.name.clone(), ty);
362 }
363 }
364 map.extend(out_types);
365 DeclaredTypes {
366 values: map,
367 shapes,
368 }
369}
370
371fn expr_matches_shape(
385 expr: &Expr,
386 shape: &TypeShape,
387 registry: &TypeRegistry,
388 resolve: &impl Fn(&str) -> Ty,
389 resolve_type: &impl cel_parser::TypeResolver,
390 lookup: &cel_parser::OpLookup,
391 diagnostics: &mut Vec<ParseError>,
392) {
393 match (expr, shape) {
394 (Expr::Tuple { elements, .. }, TypeShape::Tuple(expected)) => {
395 if elements.len() != expected.len() {
396 diagnostics
397 .extend(check_expr_with_type_resolver(expr, resolve, resolve_type, lookup).1);
398 diagnostics.push(ParseError::new_range(
399 format!(
400 "expected a {}-element tuple `{}`, got {}",
401 expected.len(),
402 registry.display_name(shape),
403 elements.len()
404 ),
405 expr.span().start,
406 expr.span().end,
407 ));
408 return;
409 }
410 for (element, element_shape) in elements.iter().zip(expected) {
411 expr_matches_shape(
412 element,
413 element_shape,
414 registry,
415 resolve,
416 resolve_type,
417 lookup,
418 diagnostics,
419 );
420 }
421 }
422 (
423 Expr::If {
424 cond,
425 then_branch,
426 else_branch,
427 ..
428 },
429 TypeShape::Tuple(_),
430 ) => {
431 check_condition(cond, resolve, resolve_type, lookup, diagnostics);
432 expr_matches_shape(
433 then_branch,
434 shape,
435 registry,
436 resolve,
437 resolve_type,
438 lookup,
439 diagnostics,
440 );
441 if let Some(else_branch) = else_branch {
442 expr_matches_shape(
443 else_branch,
444 shape,
445 registry,
446 resolve,
447 resolve_type,
448 lookup,
449 diagnostics,
450 );
451 }
452 }
453 (_, TypeShape::Tuple(_)) => {
454 let (actual, nested) =
455 check_expr_with_type_resolver(expr, resolve, resolve_type, lookup);
456 if !nested.is_empty() {
457 diagnostics.extend(nested);
458 return;
459 }
460 if actual != Ty::Any && shape_to_ty(shape, registry).unifies_with(&actual) {
461 return;
462 }
463 diagnostics.push(ParseError::new_range(
464 format!("expected tuple `{}`", registry.display_name(shape)),
465 expr.span().start,
466 expr.span().end,
467 ));
468 }
469 (Expr::Tuple { .. }, TypeShape::Named(_)) => {
470 diagnostics
471 .extend(check_expr_with_type_resolver(expr, resolve, resolve_type, lookup).1);
472 diagnostics.push(ParseError::new_range(
473 format!("expected `{}`, got a tuple", registry.display_name(shape)),
474 expr.span().start,
475 expr.span().end,
476 ));
477 }
478 (_, TypeShape::Named(_) | TypeShape::Array(_) | TypeShape::Callable(_, _)) => {
479 let declared = shape_to_ty(shape, registry);
480 let (actual, body_diags) =
481 check_expr_with_type_resolver(expr, resolve, resolve_type, lookup);
482 let valid = body_diags.is_empty();
483 diagnostics.extend(body_diags);
484 if valid && !declared.unifies_with(&actual) {
485 diagnostics.push(ParseError::new_range(
486 format!(
487 "expression produces `{}`, but `{}` was expected",
488 actual.name(),
489 declared.name()
490 ),
491 expr.span().start,
492 expr.span().end,
493 ));
494 }
495 }
496 }
497}
498
499fn check_cell_initializer(
511 type_name: Option<&TypeExpr>,
512 initializer: Option<&Expr>,
513 registry: &TypeRegistry,
514 resolve_type: &impl cel_parser::TypeResolver,
515 lookup: &cel_parser::OpLookup,
516 diagnostics: &mut Vec<ParseError>,
517) {
518 let Some(expr) = initializer else {
519 return;
520 };
521 let resolve = |_: &str| Ty::Any;
522 let Some(shape) = type_name.and_then(|type_expr| registry.resolve(type_expr).ok()) else {
523 diagnostics.extend(check_expr_with_type_resolver(expr, &resolve, resolve_type, lookup).1);
524 return;
525 };
526 if let TypeShape::Tuple(_) = shape {
527 expr_matches_shape(
528 expr,
529 &shape,
530 registry,
531 &resolve,
532 resolve_type,
533 lookup,
534 diagnostics,
535 );
536 return;
537 }
538 if let Expr::Literal {
539 value: literal,
540 span: lit_span,
541 } = expr
542 {
543 let declared = shape_to_ty(&shape, registry);
544 if !literal_matches_declared_ty(literal, &declared) {
545 diagnostics.push(ParseError::new_range(
546 format!("literal cannot be used as type `{}`", declared.name()),
547 lit_span.start,
548 lit_span.end,
549 ));
550 }
551 return;
552 }
553 expr_matches_shape(
554 expr,
555 &shape,
556 registry,
557 &resolve,
558 resolve_type,
559 lookup,
560 diagnostics,
561 );
562}
563
564fn expr_references_ident(expr: &Expr, name: &str) -> bool {
572 match expr {
573 Expr::Literal { .. } => false,
574 Expr::Ident { name: ident, .. } => ident == name,
575 Expr::Struct { fields, .. } => fields
576 .iter()
577 .any(|field| expr_references_ident(&field.value, name)),
578 Expr::Op { operands, .. } => operands.iter().any(|e| expr_references_ident(e, name)),
579 Expr::Apply { callee, args, .. } => {
580 expr_references_ident(callee, name)
581 || args.iter().any(|e| expr_references_ident(e, name))
582 }
583 Expr::Member { receiver, args, .. } => {
584 expr_references_ident(receiver, name)
585 || args
586 .iter()
587 .flatten()
588 .any(|e| expr_references_ident(e, name))
589 }
590 Expr::Tuple { elements, .. } => elements.iter().any(|e| expr_references_ident(e, name)),
591 Expr::Array { elements, .. } => elements.iter().any(|e| expr_references_ident(e, name)),
592 Expr::TupleIndex { base, .. } => expr_references_ident(base, name),
593 Expr::If {
594 cond,
595 then_branch,
596 else_branch,
597 ..
598 } => {
599 expr_references_ident(cond, name)
600 || expr_references_ident(then_branch, name)
601 || else_branch
602 .as_deref()
603 .is_some_and(|e| expr_references_ident(e, name))
604 }
605 Expr::Logical { lhs, rhs, .. } => {
606 expr_references_ident(lhs, name) || expr_references_ident(rhs, name)
607 }
608 Expr::Cast { expr, .. } => expr_references_ident(expr, name),
609 Expr::Closure { params, body, .. } => {
610 !params.iter().any(|param| param.name == name) && expr_references_ident(body, name)
611 }
612 }
613}
614
615fn check_filter(
636 name: &str,
637 filter: Option<&CellFilter>,
638 registry: &TypeRegistry,
639 resolve: &impl Fn(&str) -> Ty,
640 resolve_type: &impl cel_parser::TypeResolver,
641 lookup: &cel_parser::OpLookup,
642 diagnostics: &mut Vec<ParseError>,
643) {
644 let Some(filter) = filter else {
645 return;
646 };
647
648 let own_ty = resolve(name);
649 let body_resolve = |ident: &str| -> Ty {
650 if ident == "_" {
651 own_ty.clone()
652 } else {
653 resolve(ident)
654 }
655 };
656
657 let (body_ty, body_diags) =
658 check_expr_with_type_resolver(&filter.body, &body_resolve, resolve_type, lookup);
659 diagnostics.extend(body_diags);
660 let range = body_ty.type_id().and_then(|id| registry.range_entry(id));
661 let mismatch = if let Some(range) = range {
662 own_ty != Ty::Any && own_ty.type_id() != Some(range.element_type_id)
663 } else {
664 !own_ty.unifies_with(&body_ty)
665 };
666 if mismatch {
667 let requirement = if range.is_some() {
668 "range bounds must be"
669 } else {
670 "must produce"
671 };
672 diagnostics.push(ParseError::new_range(
673 format!("cell `{name}`: filter {requirement} `{}`", own_ty.name()),
674 filter.body.span().start,
675 filter.body.span().end,
676 ));
677 }
678
679 if range.is_none() && !expr_references_ident(&filter.body, "_") {
680 diagnostics.push(ParseError::new_range(
681 "filter must reference `_` (the value being filtered)".to_string(),
682 filter.span.start,
683 filter.span.end,
684 ));
685 }
686}
687
688fn check_tuple_output_body(
701 body: &Expr,
702 outputs: &[(String, ExprSpan)],
703 resolve: &impl Fn(&str) -> Ty,
704 resolve_type: &impl cel_parser::TypeResolver,
705 lookup: &cel_parser::OpLookup,
706 diagnostics: &mut Vec<ParseError>,
707) {
708 match body {
709 Expr::Tuple { elements, .. } if elements.len() == outputs.len() => {
710 for (element, (name, _)) in elements.iter().zip(outputs) {
711 let (element_ty, element_diags) =
712 check_expr_with_type_resolver(element, resolve, resolve_type, lookup);
713 diagnostics.extend(element_diags);
714 let declared = resolve(name);
715 if !declared.unifies_with(&element_ty) {
716 diagnostics.push(ParseError::new_range(
717 format!(
718 "binding output `{name}` produces `{}`, but is declared `{}`",
719 element_ty.name(),
720 declared.name()
721 ),
722 element.span().start,
723 element.span().end,
724 ));
725 }
726 }
727 }
728 Expr::If {
729 cond,
730 then_branch,
731 else_branch,
732 ..
733 } => {
734 check_condition(cond, resolve, resolve_type, lookup, diagnostics);
735 check_tuple_output_body(
736 then_branch,
737 outputs,
738 resolve,
739 resolve_type,
740 lookup,
741 diagnostics,
742 );
743 if let Some(else_branch) = else_branch {
744 check_tuple_output_body(
745 else_branch,
746 outputs,
747 resolve,
748 resolve_type,
749 lookup,
750 diagnostics,
751 );
752 }
753 }
754 other => {
755 let (body_ty, body_diags) =
756 check_expr_with_type_resolver(other, resolve, resolve_type, lookup);
757 let valid = body_diags.is_empty();
758 diagnostics.extend(body_diags);
759 if !valid {
760 return;
761 }
762 if let Ty::Tuple(elements) = &body_ty
763 && elements.len() == outputs.len()
764 {
765 for (element_ty, (name, _)) in elements.iter().zip(outputs) {
766 let declared = resolve(name);
767 if !declared.unifies_with(element_ty) {
768 diagnostics.push(ParseError::new_range(
769 format!(
770 "binding output `{name}` produces `{}`, but is declared `{}`",
771 element_ty.name(),
772 declared.name()
773 ),
774 other.span().start,
775 other.span().end,
776 ));
777 }
778 }
779 return;
780 }
781 if body_ty == Ty::Any && !matches!(other, Expr::Tuple { .. }) {
782 return;
783 }
784 let n = outputs.len();
785 diagnostics.push(ParseError::new_range(
786 format!("binding declares {n} outputs but its body is not a {n}-tuple"),
787 other.span().start,
788 other.span().end,
789 ));
790 }
791 }
792}
793
794fn check_condition(
798 cond: &Expr,
799 resolve: &impl Fn(&str) -> Ty,
800 resolve_type: &impl cel_parser::TypeResolver,
801 lookup: &cel_parser::OpLookup,
802 diagnostics: &mut Vec<ParseError>,
803) {
804 let (ty, nested) = check_expr_with_type_resolver(cond, resolve, resolve_type, lookup);
805 let valid = nested.is_empty();
806 diagnostics.extend(nested);
807 if valid && !ty.unifies_with(&Ty::Bool) {
808 diagnostics.push(ParseError::new_range(
809 "if condition must produce `bool`",
810 cond.span().start,
811 cond.span().end,
812 ));
813 }
814}
815
816fn check_binding(
827 binding: &BindingDecl,
828 registry: &TypeRegistry,
829 shapes: &std::collections::HashMap<String, TypeShape>,
830 resolve: &impl Fn(&str) -> Ty,
831 resolve_type: &impl cel_parser::TypeResolver,
832 lookup: &cel_parser::OpLookup,
833 diagnostics: &mut Vec<ParseError>,
834) {
835 if binding.destructure {
836 check_tuple_output_body(
837 &binding.body,
838 &binding.outputs,
839 resolve,
840 resolve_type,
841 lookup,
842 diagnostics,
843 );
844 return;
845 }
846
847 let Some((name, _)) = binding.outputs.first() else {
848 let (_, body_diags) =
849 check_expr_with_type_resolver(&binding.body, resolve, resolve_type, lookup);
850 diagnostics.extend(body_diags);
851 return;
852 };
853 if let Some(shape @ TypeShape::Tuple(_)) = shapes.get(name) {
854 expr_matches_shape(
855 &binding.body,
856 shape,
857 registry,
858 resolve,
859 resolve_type,
860 lookup,
861 diagnostics,
862 );
863 return;
864 }
865 let (body_ty, body_diags) =
866 check_expr_with_type_resolver(&binding.body, resolve, resolve_type, lookup);
867 diagnostics.extend(body_diags);
868 let declared = resolve(name);
869 if let Expr::Tuple { elements, .. } = &binding.body
870 && declared != Ty::Any
871 {
872 let n = elements.len();
873 diagnostics.push(ParseError::new_range(
874 format!("binding declares 1 output but its body is a {n}-tuple"),
875 binding.body.span().start,
876 binding.body.span().end,
877 ));
878 return;
879 }
880 if !declared.unifies_with(&body_ty) {
881 diagnostics.push(ParseError::new_range(
882 format!(
883 "binding body produces `{}`, but `{name}` is declared `{}`",
884 body_ty.name(),
885 declared.name()
886 ),
887 binding.body.span().start,
888 binding.body.span().end,
889 ));
890 }
891}
892
893fn check_out(
904 out_decl: &OutDecl,
905 registry: &TypeRegistry,
906 declared: &DeclaredTypes,
907 resolve: &impl Fn(&str) -> Ty,
908 resolve_type: &impl cel_parser::TypeResolver,
909 lookup: &cel_parser::OpLookup,
910 diagnostics: &mut Vec<ParseError>,
911) {
912 if let Some(shape @ TypeShape::Tuple(_)) = declared.shapes.get(&out_decl.name) {
913 expr_matches_shape(
914 &out_decl.initializer,
915 shape,
916 registry,
917 resolve,
918 resolve_type,
919 lookup,
920 diagnostics,
921 );
922 } else {
923 let (body_ty, body_diags) =
924 check_expr_with_type_resolver(&out_decl.initializer, resolve, resolve_type, lookup);
925 diagnostics.extend(body_diags);
926 if out_decl.type_name.is_some() {
927 let declared = resolve(&out_decl.name);
928 if !declared.unifies_with(&body_ty) {
929 diagnostics.push(ParseError::new_range(
930 format!(
931 "out `{}` body produces `{}`, but is declared `{}`",
932 out_decl.name,
933 body_ty.name(),
934 declared.name()
935 ),
936 out_decl.initializer.span().start,
937 out_decl.initializer.span().end,
938 ));
939 }
940 }
941 }
942 check_filter(
943 &out_decl.name,
944 out_decl.filter.as_ref(),
945 registry,
946 resolve,
947 resolve_type,
948 lookup,
949 diagnostics,
950 );
951 check_requirements(
952 out_decl.require.as_ref(),
953 resolve,
954 resolve_type,
955 lookup,
956 diagnostics,
957 );
958}
959
960fn check_requirements(
968 require: Option<&RequireBlock>,
969 resolve: &impl Fn(&str) -> Ty,
970 resolve_type: &impl cel_parser::TypeResolver,
971 lookup: &cel_parser::OpLookup,
972 diagnostics: &mut Vec<ParseError>,
973) {
974 let Some(require) = require else {
975 return;
976 };
977 for requirement in &require.requirements {
978 let (req_ty, req_diags) =
979 check_expr_with_type_resolver(&requirement.body, resolve, resolve_type, lookup);
980 diagnostics.extend(req_diags);
981 if !req_ty.unifies_with(&Ty::Bool) {
982 diagnostics.push(ParseError::new_range(
983 match &requirement.name {
984 Some(name) => format!(
985 "requirement `{name}` produces `{}`, but requirements must be `bool`",
986 req_ty.name()
987 ),
988 None => format!(
989 "requirement produces `{}`, but requirements must be `bool`",
990 req_ty.name()
991 ),
992 },
993 requirement.body.span().start,
994 requirement.body.span().end,
995 ));
996 }
997 }
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003 use crate::AdamAstParser;
1004
1005 fn parse(source: &str) -> Sheet {
1007 let sheet = AdamAstParser::new().parse_str(source).unwrap();
1008 assert!(sheet.errors.is_empty(), "{:?}", sheet.errors);
1009 sheet
1010 }
1011
1012 #[derive(Clone, Debug, PartialEq)]
1014 struct Custom(i32);
1015
1016 #[test]
1018 fn cell_initializer_matching_its_annotation_has_no_diagnostic() {
1019 let sheet = parse("sheet s { cell x: i32 = 1; }");
1020 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1021 assert!(diags.is_empty());
1022 }
1023
1024 #[test]
1026 fn cell_initializer_mismatched_with_its_annotation_is_a_diagnostic() {
1027 let sheet = parse("sheet s { cell x: i32 = 1.0; }");
1029 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1030 assert_eq!(diags.len(), 1);
1031 }
1032
1033 #[test]
1035 fn source_initializer_mismatched_with_its_annotation_is_a_diagnostic() {
1036 let sheet = parse("sheet s { source x: i32 = 1.0; }");
1038 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1039 assert_eq!(diagnostics.len(), 1);
1040 }
1041
1042 #[test]
1044 fn custom_array_annotations_use_the_registry_type_resolver() {
1045 let sheet = parse(
1046 "sheet s { \
1047 out values := []: [Custom]; \
1048 out nested := [[]: [Custom]]: [[Custom]]; \
1049 }",
1050 );
1051 let mut registry = TypeRegistry::new();
1052 registry.register_no_default::<Custom>("Custom");
1053
1054 let diagnostics = check_sheet(&sheet, ®istry, &cel_parser::OpLookup::new());
1055 assert!(diagnostics.is_empty(), "{diagnostics:?}");
1056 }
1057
1058 #[test]
1060 fn custom_array_annotations_reject_exact_scalar_type_mismatches() {
1061 let sheet = parse("sheet s { out values := [1]: [Custom]; }");
1062 let mut registry = TypeRegistry::new();
1063 registry.register_no_default::<Custom>("Custom");
1064
1065 let diagnostics = check_sheet(&sheet, ®istry, &cel_parser::OpLookup::new());
1066 assert_eq!(diagnostics.len(), 1, "{diagnostics:?}");
1067 assert!(
1068 diagnostics[0]
1069 .message()
1070 .contains("expected `Custom`, found `i32`"),
1071 "got: {}",
1072 diagnostics[0].message()
1073 );
1074 }
1075
1076 #[test]
1078 fn unknown_custom_array_annotation_type_is_reported() {
1079 let sheet = parse("sheet s { out values := []: [Missing]; }");
1080 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1081 assert_eq!(diagnostics.len(), 1);
1082 assert!(
1083 diagnostics[0].message().contains("unknown type `Missing`"),
1084 "got: {}",
1085 diagnostics[0].message()
1086 );
1087 }
1088
1089 #[test]
1091 fn tuple_typed_array_annotations_are_accepted_by_cel_checking() {
1092 let sheet = parse("sheet s { out values := []: [(i32, f64)]; }");
1093 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1094 assert!(diagnostics.is_empty(), "{diagnostics:?}");
1095 }
1096
1097 #[test]
1099 fn cell_requirement_non_bool_body_is_a_diagnostic() {
1100 let sheet = parse("sheet s { cell x: i32 = 5 require { @positive x; }; }");
1101 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1102 assert_eq!(diagnostics.len(), 1);
1103 }
1104
1105 #[test]
1107 fn cell_initializer_non_literal_expression_is_checked() {
1108 let sheet = parse("sheet s { cell x: f64 = 1 + 2; }");
1109 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1110 let messages: Vec<&str> = diags.iter().map(cel_parser::ParseError::message).collect();
1111 assert_eq!(
1112 messages,
1113 ["expression produces `i32`, but `f64` was expected"]
1114 );
1115 }
1116
1117 #[test]
1119 fn cell_initializer_array_annotation_diagnostics_still_surface() {
1120 let sheet = parse("sheet s { cell x: f64 = [0, 1]: [f64]; }");
1123 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1124 let messages: Vec<&str> = diags.iter().map(cel_parser::ParseError::message).collect();
1125 assert_eq!(
1126 messages,
1127 vec!["array elements must match the annotation exactly: expected `f64`, found `i32`"]
1128 );
1129 }
1130
1131 #[test]
1133 fn cell_with_only_an_annotation_has_nothing_to_cross_check() {
1134 let sheet = parse("sheet s { cell x: i32; }");
1135 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1136 assert!(diags.is_empty());
1137 }
1138
1139 #[test]
1141 fn cell_initializer_unsuffixed_int_literal_mismatched_with_a_declared_unsigned_type_is_a_diagnostic()
1142 {
1143 let sheet = parse("sheet s { cell x: u32 = 1; }");
1148 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1149 assert_eq!(diags.len(), 1);
1150 }
1151
1152 #[test]
1154 fn cell_initializer_unsuffixed_int_literal_mismatched_with_a_declared_float_type_is_a_diagnostic()
1155 {
1156 let sheet = parse("sheet s { cell x: f64 = 1; }");
1159 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1160 assert_eq!(diags.len(), 1);
1161 }
1162
1163 #[test]
1165 fn cell_initializer_char_literal_against_any_registered_type_is_a_diagnostic() {
1166 let sheet = parse("sheet s { cell x: i32 = 'a'; }");
1169 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1170 assert_eq!(diags.len(), 1);
1171 }
1172
1173 #[test]
1175 fn cell_annotated_with_an_unregistered_type_name_is_never_flagged() {
1176 let sheet = parse("sheet s { cell x: WidgetHandle = 1; }");
1177 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1178 assert!(diags.is_empty());
1179 }
1180
1181 #[test]
1183 fn binding_single_output_matching_declared_type_has_no_diagnostic() {
1184 let sheet = parse(
1185 "sheet s { cell width: f64; cell height: f64; cell area: f64; \
1186 relationship { area := width * height; } }",
1187 );
1188 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1189 assert!(diags.is_empty());
1190 }
1191
1192 #[test]
1194 fn binding_single_output_mismatched_with_declared_type_is_a_diagnostic() {
1195 let sheet = parse(
1196 "sheet s { cell width: f64; cell height: f64; cell area: i32; \
1197 relationship { area := width * height; } }",
1198 );
1199 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1200 assert_eq!(diags.len(), 1);
1201 }
1202
1203 #[test]
1205 fn binding_multi_output_matching_tuple_has_no_diagnostic() {
1206 let sheet = parse(
1207 "sheet s { cell a: i32; cell b: i32; cell sum: i32; cell diff: i32; \
1208 relationship { (sum, diff) := (a + b, a - b); } }",
1209 );
1210 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1211 assert!(diags.is_empty());
1212 }
1213
1214 #[test]
1216 fn binding_multi_output_iife_defers_unknown_shape_but_checks_children() {
1217 for (body, expected) in [
1218 ("(|x: i32| (x, x))(1)", 0),
1219 ("(|x: i32| (x, true + x))(1)", 1),
1220 ("(|x: i32| (x, true))(1)", 1),
1221 ("(|x: i32| (x,))(1)", 1),
1222 ("1", 1),
1223 ("(1, 2, 3)", 1),
1224 ] {
1225 let sheet = parse(&format!(
1226 "sheet s {{ cell a: i32; cell b: i32; relationship {{ (a,b) := {body}; }} }}"
1227 ));
1228 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1229 assert_eq!(diags.len(), expected, "{body}: {diags:?}");
1230 }
1231 }
1232
1233 #[test]
1235 fn tuple_call_results_match_declared_shapes() {
1236 for (annotation, expected) in [("(i32, i32)", 0), ("(i32, bool)", 1)] {
1237 let sheet = parse(&format!(
1238 "sheet s {{ cell pair: {annotation} = (|x: i32| (x,x))(1); }}"
1239 ));
1240 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1241 assert_eq!(diags.len(), expected, "{annotation}: {diags:?}");
1242 }
1243 }
1244
1245 #[test]
1247 fn binding_multi_output_arity_mismatch_is_a_diagnostic() {
1248 let sheet = parse(
1249 "sheet s { cell a: i32; cell b: i32; cell sum: i32; cell diff: i32; \
1250 relationship { (sum, diff) := a + b; } }",
1251 );
1252 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1253 assert_eq!(diags.len(), 1);
1254 }
1255
1256 #[test]
1258 fn binding_single_output_with_a_tuple_shaped_body_is_a_diagnostic() {
1259 let sheet = parse(
1262 "sheet s { cell a: i32; cell b: i32; cell out: i32; \
1263 relationship { out := (a, b); } }",
1264 );
1265 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1266 assert_eq!(diags.len(), 1);
1267 }
1268
1269 #[test]
1271 fn binding_multi_output_per_element_type_mismatch_is_a_diagnostic() {
1272 let sheet = parse(
1273 "sheet s { cell a: i32; cell b: i32; cell sum: i32; cell diff: f64; \
1274 relationship { (sum, diff) := (a + b, a - b); } }",
1275 );
1276 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1277 assert_eq!(diags.len(), 1);
1278 }
1279
1280 #[test]
1282 fn an_operator_error_inside_a_binding_body_surfaces() {
1283 let sheet = parse(
1284 "sheet s { cell name: String; cell count: i32; cell out: i32; \
1285 relationship { out := name + count; } }",
1286 );
1287 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1288 assert_eq!(diags.len(), 1);
1289 }
1290
1291 #[test]
1293 fn conditional_branch_and_default_bindings_are_both_checked() {
1294 let sheet = parse(
1295 "sheet s { cell mode: i32; cell a: i32; cell b: i32; cell out: i32; \
1296 conditional mode { \
1297 0i32 => { relationship { out := a; } } \
1298 _ => { relationship { out := b; } } \
1299 } }",
1300 );
1301 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1302 assert!(diags.is_empty());
1303 let invalid = parse(
1304 "sheet s { cell mode: i32; cell a: bool; cell b: f64; cell out: i32; \
1305 conditional mode { \
1306 0i32 => { relationship { out := a; } } \
1307 _ => { relationship { out := b; } } \
1308 } }",
1309 );
1310 let diags = check_sheet(&invalid, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1311 assert_eq!(diags.len(), 2);
1312 }
1313
1314 #[test]
1316 fn a_cell_with_no_type_annotation_unifies_with_anything_used_in_a_binding() {
1317 let sheet = parse(
1320 "sheet s { cell a = 1; cell out: i32; \
1321 relationship { out := a; } }",
1322 );
1323 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1324 assert!(diags.is_empty());
1325 }
1326
1327 #[test]
1329 fn recovered_error_items_are_skipped_without_panicking() {
1330 let sheet = AdamAstParser::new()
1331 .parse_str(
1332 "sheet s { cell good: i32 = 1; cell bad unknown_syntax cell after: i32 = 2; }",
1333 )
1334 .unwrap();
1335 assert!(
1336 !sheet.errors.is_empty(),
1337 "fixture must actually recover an error item"
1338 );
1339 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1340 assert!(diags.is_empty());
1341 }
1342
1343 #[test]
1345 fn out_body_matching_its_annotation_has_no_diagnostic() {
1346 let sheet = parse(
1347 "sheet s { cell width: f64; cell height: f64; \
1348 out area: f64 := width * height; }",
1349 );
1350 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1351 assert!(diags.is_empty());
1352 }
1353
1354 #[test]
1356 fn out_body_mismatched_with_its_annotation_is_a_diagnostic() {
1357 let sheet = parse(
1358 "sheet s { cell width: f64; cell height: f64; \
1359 out area: i32 := width * height; }",
1360 );
1361 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1362 assert_eq!(diags.len(), 1);
1363 }
1364
1365 #[test]
1367 fn out_with_no_annotation_infers_its_type_and_has_no_diagnostic() {
1368 let sheet = parse(
1371 "sheet s { cell width: f64; cell height: f64; \
1372 out area := width * height; }",
1373 );
1374 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1375 assert!(diags.is_empty());
1376 }
1377
1378 #[test]
1380 fn requirement_with_bool_body_has_no_diagnostic() {
1381 let sheet = parse(
1382 "sheet s { cell width: f64; cell max_width: f64; \
1383 out area: f64 := width require { \
1384 @max_width width <= max_width; \
1385 }; }",
1386 );
1387 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1388 assert!(diags.is_empty());
1389 }
1390
1391 #[test]
1393 fn requirement_with_non_bool_body_is_a_diagnostic() {
1394 let sheet = parse(
1395 "sheet s { cell width: f64; \
1396 out area: f64 := width require { \
1397 @bogus width; \
1398 }; }",
1399 );
1400 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1401 assert_eq!(diags.len(), 1);
1402 }
1403
1404 #[test]
1406 fn unlabeled_requirement_with_non_bool_body_is_a_diagnostic() {
1407 let sheet = parse(
1408 "sheet s { cell width: f64; \
1409 out area: f64 := width require { \
1410 width; \
1411 }; }",
1412 );
1413 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1414 assert_eq!(diags.len(), 1);
1415 }
1416
1417 #[test]
1419 fn cell_tuple_initializer_matching_its_annotation_has_no_diagnostic() {
1420 let sheet = parse("sheet s { cell a: (i32, f64) = (1, 2.5); }");
1421 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1422 assert!(diags.is_empty());
1423 }
1424
1425 #[test]
1427 fn cell_tuple_initializer_arity_mismatch_is_a_diagnostic() {
1428 let sheet = parse("sheet s { cell a: (i32, f64, i32) = (1, 2.5); }");
1429 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1430 assert_eq!(diags.len(), 1);
1431 }
1432
1433 #[test]
1435 fn cell_tuple_initializer_element_type_mismatch_is_a_diagnostic() {
1436 let sheet = parse("sheet s { cell a: (i32, i32) = (1, 2.5); }");
1437 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1438 assert_eq!(diags.len(), 1);
1439 }
1440
1441 #[test]
1443 fn cell_nested_tuple_initializer_matching_its_annotation_has_no_diagnostic() {
1444 let sheet = parse("sheet s { cell a: (i32, (f64, String)) = (1, (2.5, \"x\")); }");
1445 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1446 assert!(diags.is_empty());
1447 }
1448
1449 #[test]
1451 fn filter_with_matching_types_has_no_diagnostic() {
1452 let sheet = parse("sheet s { cell a: i32 = 1 filter _; }");
1453 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1454 assert!(diags.is_empty());
1455 }
1456
1457 #[test]
1459 fn filter_with_matching_types_on_a_source_has_no_diagnostic() {
1460 let sheet = parse("sheet s { source a: i32 = 1 filter _; }");
1461 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1462 assert!(diags.is_empty());
1463 }
1464
1465 #[test]
1467 fn filter_referencing_a_cell_has_no_diagnostic() {
1468 let sheet = parse(
1469 "sheet s { cell hi: i32 = 100; cell a: i32 = 1 filter if _ > hi { hi } else { _ }; }",
1470 );
1471 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1472 assert!(diags.is_empty());
1473 }
1474
1475 #[test]
1477 fn filter_body_type_mismatch_is_a_diagnostic() {
1478 let sheet = parse("sheet s { cell a: i32 = 1 filter _ > 0; }");
1480 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1481 assert_eq!(diags.len(), 1);
1482 }
1483
1484 #[test]
1486 fn filter_without_underscore_is_a_diagnostic() {
1487 let sheet = parse("sheet s { cell a: i32 = 1 filter 1; }");
1488 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1489 assert_eq!(diags.len(), 1);
1490 }
1491
1492 #[test]
1494 fn closure_parameter_shadows_only_value_references() {
1495 for (source, references) in [
1496 ("|_: i32| _", false),
1497 ("|x: i32| _", true),
1498 ("|_: i32| value._", false),
1499 ("|_: i32| _ { x: value }", false),
1500 ("|x: i32| Record { x: _ }.x", true),
1501 ] {
1502 let expr =
1503 cel_parser::Parser::<cel_parser::AstContext>::new(cel_parser::OpLookup::new())
1504 .parse_str_ast(source)
1505 .unwrap();
1506 assert_eq!(expr_references_ident(&expr, "_"), references, "{source}");
1507 }
1508 }
1509
1510 #[test]
1512 fn record_filter_dependencies_only_visit_field_values() {
1513 let sheet = parse("sheet s { cell a: i32 = 1 filter Record { value: _ }; }");
1514 let diagnostics = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1515 assert_eq!(diagnostics.len(), 1);
1516 assert!(diagnostics[0].message().contains("unknown record type"));
1517 for source in [
1518 "sheet s { cell a: i32 = 1 filter Record { _: 1 }; }",
1519 "sheet s { cell a: i32 = 1 filter _ { value: 1 }; }",
1520 ] {
1521 let sheet = parse(source);
1522 let diagnostics =
1523 check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1524 assert_eq!(diagnostics.len(), 2);
1525 assert!(
1526 diagnostics
1527 .iter()
1528 .any(|d| d.message().contains("must reference `_`"))
1529 );
1530 }
1531 }
1532
1533 #[test]
1535 fn filter_body_type_mismatch_on_an_out_is_a_diagnostic() {
1536 let sheet = parse("sheet s { cell width: i32 = 1; out area: i32 := width filter _ > 0; }");
1539 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1540 assert_eq!(diags.len(), 1);
1541 }
1542
1543 #[test]
1545 fn filter_without_underscore_on_an_out_is_a_diagnostic() {
1546 let sheet = parse("sheet s { cell width: i32 = 1; out area: i32 := width filter 1; }");
1548 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1549 assert_eq!(diags.len(), 1);
1550 }
1551
1552 #[test]
1554 fn filter_body_type_mismatch_on_a_source_is_a_diagnostic() {
1555 let sheet = parse("sheet s { source a: i32 = 1 filter _ > 0; }");
1558 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1559 assert_eq!(diags.len(), 1);
1560 }
1561
1562 #[test]
1564 fn filter_without_underscore_on_a_source_is_a_diagnostic() {
1565 let sheet = parse("sheet s { source a: i32 = 1 filter 1; }");
1568 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1569 assert_eq!(diags.len(), 1);
1570 }
1571
1572 #[test]
1574 fn filter_on_a_tuple_typed_cell_is_supported() {
1575 let sheet = parse("sheet s { cell a: (i32, f64) = (1, 2.5) filter (_.0, _.1); }");
1576 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1577 assert!(diags.is_empty());
1578 }
1579
1580 #[test]
1582 fn filter_references_underscore_nested_inside_a_call_has_no_missing_underscore_diagnostic() {
1583 let sheet = parse("sheet s { cell a: i32 = 1 filter if true { _ } else { 1 }; }");
1586 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1587 assert!(diags.is_empty());
1588 }
1589
1590 #[test]
1592 fn member_filter_references_are_operands_not_labels() {
1593 for body in ["_.accessor", "value.accessor(_)", "_.accessor()"] {
1594 let sheet = parse(&format!("sheet s {{ cell value: i32 = 1 filter {body}; }}"));
1595 assert!(
1596 check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new()).is_empty(),
1597 "{body}"
1598 );
1599 }
1600 let sheet = parse("sheet s { cell value: i32 = 1 filter value._; }");
1601 assert_eq!(
1602 check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new()).len(),
1603 1
1604 );
1605 }
1606
1607 #[test]
1609 fn binding_single_tuple_typed_output_matching_body_has_no_diagnostic() {
1610 let sheet = parse(
1611 "sheet s { cell a: i32; cell b: i32; cell pair: (i32, i32); \
1612 relationship { pair := (a, b); } }",
1613 );
1614 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1615 assert!(diags.is_empty());
1616 }
1617
1618 #[test]
1620 fn binding_single_tuple_typed_output_element_type_mismatch_is_a_diagnostic() {
1621 let sheet = parse(
1622 "sheet s { cell a: i32; cell b: f64; cell pair: (i32, i32); \
1623 relationship { pair := (a, b); } }",
1624 );
1625 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1626 assert_eq!(diags.len(), 1);
1627 }
1628
1629 #[test]
1631 fn binding_single_tuple_typed_output_if_else_body_matching_both_branches_has_no_diagnostic() {
1632 let sheet = parse(
1633 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell pair: (i32, i32); \
1634 relationship { pair := \
1635 if cond { (a, b) } else { (b, a) }; \
1636 } }",
1637 );
1638 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1639 assert!(diags.is_empty());
1640 }
1641
1642 #[test]
1644 fn binding_single_tuple_typed_output_if_without_else_matching_then_branch_has_no_diagnostic() {
1645 let sheet = parse(
1646 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell pair: (i32, i32); \
1647 relationship { pair := \
1648 if cond { (a, b) }; \
1649 } }",
1650 );
1651 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1652 assert!(diags.is_empty());
1653 }
1654
1655 #[test]
1657 fn binding_single_tuple_typed_output_if_else_if_chain_matching_all_branches_has_no_diagnostic()
1658 {
1659 let sheet = parse(
1660 "sheet s { cell mode: i32; cell a: i32; cell b: i32; cell pair: (i32, i32); \
1661 relationship { pair := \
1662 if mode == 0 { (a, b) } else if mode == 1 { (b, a) } else { (0, 0) }; \
1663 } }",
1664 );
1665 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1666 assert!(diags.is_empty());
1667 }
1668
1669 #[test]
1671 fn binding_single_tuple_typed_output_if_else_body_element_mismatch_in_each_branch_is_two_diagnostics()
1672 {
1673 let sheet = parse(
1680 "sheet s { cell cond: bool; cell a: i32; cell c: f64; cell pair: (i32, i32); \
1681 relationship { pair := \
1682 if cond { (a, c) } else { (a, c) }; \
1683 } }",
1684 );
1685 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1686 assert_eq!(diags.len(), 2);
1687 }
1688
1689 #[test]
1691 fn binding_multi_output_if_else_body_matching_both_branches_has_no_diagnostic() {
1692 let sheet = parse(
1693 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell sum: i32; cell diff: i32; \
1694 relationship { (sum, diff) := \
1695 if cond { (a + b, a - b) } else { (a - b, a + b) }; \
1696 } }",
1697 );
1698 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1699 assert!(diags.is_empty());
1700 }
1701
1702 #[test]
1704 fn binding_multi_output_if_else_body_element_mismatch_in_a_branch_is_a_diagnostic() {
1705 let sheet = parse(
1706 "sheet s { cell cond: bool; cell a: i32; cell b: i32; cell c: f64; \
1707 cell sum: i32; cell diff: i32; \
1708 relationship { (sum, diff) := \
1709 if cond { (a, b) } else { (a, c) }; \
1710 } }",
1711 );
1712 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1713 assert_eq!(diags.len(), 1);
1714 }
1715
1716 #[test]
1718 fn binding_single_element_tuple_destructure_has_no_diagnostic() {
1719 let sheet =
1720 parse("sheet s { cell a: i32; cell out: i32; relationship { (out,) := (a,); } }");
1721 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1722 assert!(diags.is_empty());
1723 }
1724
1725 #[test]
1727 fn binding_single_element_tuple_destructure_type_mismatch_is_a_diagnostic() {
1728 let sheet =
1729 parse("sheet s { cell a: f64; cell out: i32; relationship { (out,) := (a,); } }");
1730 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1731 assert_eq!(diags.len(), 1);
1732 }
1733
1734 #[test]
1736 fn binding_single_element_tuple_destructure_arity_mismatch_is_a_diagnostic() {
1737 let sheet = parse("sheet s { cell a: i32; cell out: i32; relationship { (out,) := a; } }");
1738 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1739 assert_eq!(diags.len(), 1);
1740 }
1741
1742 #[test]
1744 fn filter_range_inclusive_body_does_not_require_underscore() {
1745 let sheet = parse("sheet s { cell a: i32 filter 0..=100; }");
1746 let diags = check_sheet(&sheet, &TypeRegistry::new(), &cel_parser::OpLookup::new());
1747 assert!(diags.is_empty());
1748 }
1749
1750 #[test]
1752 fn inferred_range_filters_agree_with_executable() {
1753 for source in [
1754 "sheet s { cell a: i32 filter (|x: i32| 0..=x)(100); }",
1755 "sheet s { source a: f64 = 1.0 filter 0.0..=100.0; }",
1756 "sheet s { out a: i32 := 1 filter (|x: i32| 0..=x)(100); }",
1757 ] {
1758 assert!(
1759 crate::AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
1760 .parse_str(source)
1761 .is_ok(),
1762 "{source}"
1763 );
1764 let diags = check_sheet(
1765 &parse(source),
1766 &TypeRegistry::new(),
1767 &cel_parser::OpLookup::new(),
1768 );
1769 assert!(diags.is_empty(), "{source}: {diags:?}");
1770 }
1771 for source in [
1772 "sheet s { cell a: i32 filter 0.0..=100.0; }",
1773 "sheet s { source a: i32 = 1 filter (|x: f64| 0.0..=x)(100.0); }",
1774 "sheet s { out a: i32 := 1 filter 0.0..=100.0; }",
1775 "sheet s { cell a: i32 filter 0..100; }",
1776 ] {
1777 assert!(
1778 crate::AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
1779 .parse_str(source)
1780 .is_err(),
1781 "{source}"
1782 );
1783 let diags = check_sheet(
1784 &parse(source),
1785 &TypeRegistry::new(),
1786 &cel_parser::OpLookup::new(),
1787 );
1788 assert!(!diags.is_empty(), "{source}");
1789 }
1790 }
1791}