1use std::collections::{HashMap, HashSet};
6use std::rc::Rc;
7use std::str::FromStr;
8
9use indexmap::IndexMap;
10
11use adam_rs::{CellId, ErrorSite, RelationshipId, RequirementId, Sheet};
12use cel_parser::lex_lexer::{HasSpan, LexLexer};
13use cel_parser::{OpLookup, ParseError, SourceSpan};
14use proc_macro2::{Span, TokenStream};
15
16use crate::TypeRegistry;
17use crate::cell_value::{CellValue, cell_type_id};
18use crate::type_registry::TypeShape;
19
20pub type Result<T> = std::result::Result<T, ParseError>;
22
23fn shape_matches(shape: &TypeShape, value: &cel_runtime::ValueType) -> bool {
27 use cel_runtime::ValueKind;
28 match (shape, value.kind()) {
29 (TypeShape::Named(a), ValueKind::Leaf(b)) => a == b,
30 (TypeShape::Array(a), ValueKind::Array(b)) => shape_matches(a, b),
31 (TypeShape::Tuple(a), ValueKind::Tuple(b)) => {
32 a.len() == b.len()
33 && a.iter()
34 .zip(b)
35 .all(|(a, b)| shape_matches(a, &b.value_type))
36 }
37 (
38 TypeShape::Callable(a, ar),
39 ValueKind::Callable {
40 parameters: b,
41 result: br,
42 },
43 ) => {
44 a.len() == b.len()
45 && a.iter().zip(b).all(|(a, b)| shape_matches(a, b))
46 && shape_matches(ar, br)
47 }
48 _ => false,
49 }
50}
51
52pub struct ParsedSheet {
75 pub sheet: Sheet,
77 pub cell_names: IndexMap<String, (CellId, TypeShape)>,
79 pub output_names: IndexMap<String, CellId>,
83 pub requirement_names: HashMap<RequirementId, String>,
87 pub requirement_labels: HashMap<RequirementId, crate::SourceLabel>,
89 pub method_spans: HashMap<(RelationshipId, usize), SourceSpan>,
98 pub relationship_spans: HashMap<RelationshipId, SourceSpan>,
102 pub cell_spans: HashMap<CellId, SourceSpan>,
105 pub sources: Vec<crate::SourceId>,
107 pub(crate) anonymous_source: Option<crate::SourceId>,
109 pub instance_origins: Vec<crate::composition::InstanceOrigin>,
111 pub method_origins: HashMap<(RelationshipId, usize), adam_rs::MethodOrigin>,
113 pub cell_origins: HashMap<CellId, Vec<crate::composition::CellOrigin>>,
115 pub(crate) cell_shapes: HashMap<CellId, TypeShape>,
117}
118
119impl std::fmt::Debug for ParsedSheet {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.debug_struct("ParsedSheet")
125 .field("cell_names", &self.cell_names)
126 .field("output_names", &self.output_names)
127 .field("requirement_names", &self.requirement_names)
128 .field("method_spans", &self.method_spans)
129 .field("relationship_spans", &self.relationship_spans)
130 .field("cell_spans", &self.cell_spans)
131 .finish()
132 }
133}
134
135impl Default for ParsedSheet {
136 fn default() -> Self {
138 Self::new()
139 }
140}
141
142impl ParsedSheet {
143 pub(crate) fn retain_root_spans(&mut self, instance: &adam_rs::Instance) {
150 let occurrence = &self
151 .instance_origins
152 .last()
153 .expect("root expansion retained")
154 .occurrences[0];
155 debug_assert_eq!(&occurrence.id, instance.occurrences()[0].id());
156 let mut methods = HashMap::new();
157 for item in occurrence.authored_items() {
158 let Some(location) = &item.label.location else {
159 continue;
160 };
161 if let Some(slot) = &item.cell
162 && matches!(
163 item.role,
164 crate::AuthoredRole::Cell
165 | crate::AuthoredRole::Source
166 | crate::AuthoredRole::Out
167 )
168 {
169 let id = instance
170 .cell(&occurrence.id, slot)
171 .expect("authored local slot");
172 self.cell_spans.insert(id, location.span);
173 }
174 if matches!(
175 item.role,
176 crate::AuthoredRole::Relationship | crate::AuthoredRole::Method
177 ) && let Some(slot) = &item.relationship
178 {
179 for &relationship in instance
180 .relationship_group(&occurrence.id, slot)
181 .expect("authored root relationship slot")
182 {
183 if item.role == crate::AuthoredRole::Relationship {
184 self.relationship_spans.insert(relationship, location.span);
185 }
186 if let Some(index) = item.method_index {
187 methods.insert((relationship, index), location.span);
188 }
189 }
190 }
191 }
192 for (relationship, live, origin) in instance.method_origins() {
193 if origin.occurrence != occurrence.id {
194 continue;
195 }
196 if let Some(span) = methods.get(&(relationship, origin.authored_index)) {
197 self.method_spans.insert((relationship, live), *span);
198 }
199 }
200 }
201
202 pub(crate) fn validation_diagnostic(
208 &self,
209 error: adam_rs::Error,
210 root: &crate::SourceLabel,
211 instance: &adam_rs::Instance,
212 ) -> crate::ModuleDiagnostic {
213 let mut labels = Vec::new();
214 let wanted: HashSet<_> = error
215 .sites()
216 .iter()
217 .filter_map(|site| {
218 if let ErrorSite::Relationship(id) = site {
219 Some(*id)
220 } else {
221 None
222 }
223 })
224 .collect();
225 let mut relationships = HashMap::new();
226 for occurrence in &self
227 .instance_origins
228 .last()
229 .expect("root expansion retained")
230 .occurrences
231 {
232 for item in occurrence.authored_items() {
233 if matches!(
234 item.role,
235 crate::AuthoredRole::Relationship | crate::AuthoredRole::Out
236 ) && let Some(slot) = &item.relationship
237 {
238 for &id in instance
239 .relationship_group(&occurrence.id, slot)
240 .expect("authored relationship slot")
241 {
242 if wanted.contains(&id) {
243 relationships.insert(id, &item.label);
244 }
245 }
246 }
247 }
248 }
249 let names: HashMap<_, _> = self
250 .cell_names
251 .iter()
252 .map(|(name, (id, _))| (*id, name))
253 .collect();
254 let name = |id| names.get(&id).map(|name| (*name).clone());
255 for (index, site) in error.sites().iter().enumerate() {
256 let label = match site {
257 ErrorSite::Cell(id) => self
258 .cell_origins
259 .get(id)
260 .and_then(|origins| origins.first())
261 .map(|origin| &origin.label),
262 ErrorSite::Relationship(id) => relationships.get(id).copied(),
263 _ => None,
264 };
265 if let Some(label) = label {
266 let mut label = label.clone();
267 label.label = crate::error_labels::site_label(&error, index, &name);
268 labels.push(label);
269 }
270 }
271 if labels.is_empty() {
272 labels.push(root.clone());
273 }
274 crate::ModuleDiagnostic {
275 message: error.to_string(),
276 labels,
277 cause: Some(crate::ModuleCause::new(error)),
278 }
279 }
280
281 pub fn new() -> Self {
289 Self {
290 sheet: Sheet::new(),
291 cell_names: IndexMap::new(),
292 output_names: IndexMap::new(),
293 requirement_names: HashMap::new(),
294 requirement_labels: HashMap::new(),
295 method_spans: HashMap::new(),
296 relationship_spans: HashMap::new(),
297 cell_spans: HashMap::new(),
298 sources: Vec::new(),
299 anonymous_source: None,
300 instance_origins: Vec::new(),
301 method_origins: HashMap::new(),
302 cell_origins: HashMap::new(),
303 cell_shapes: HashMap::new(),
304 }
305 }
306
307 pub fn register_cell(
319 &mut self,
320 name: &str,
321 id: CellId,
322 shape: TypeShape,
323 types: &TypeRegistry,
324 ) -> std::result::Result<(), crate::ModuleDiagnostic> {
325 types
326 .validate_shape(&shape)
327 .map_err(crate::composition::diagnostic)?;
328 if self.cell_names.contains_key(name) {
329 return Err(crate::composition::diagnostic("duplicate root cell name"));
330 }
331 self.validate_cell_shape(id, &shape)
332 .map_err(crate::composition::diagnostic)?;
333 if self.sheet.cell_kind(id) == Some(adam_rs::CellKind::Out) {
334 self.output_names.insert(name.into(), id);
335 }
336 self.cell_shapes.insert(id, shape.clone());
337 self.cell_names.insert(name.into(), (id, shape));
338 Ok(())
339 }
340
341 pub fn add_compound_cell<R: cel_runtime::ValueRepr>(
358 &mut self,
359 name: &str,
360 value: R::Value,
361 types: &TypeRegistry,
362 ) -> std::result::Result<CellId, crate::ModuleDiagnostic> {
363 let shape = types
364 .shape_of(&R::value_type())
365 .map_err(crate::composition::diagnostic)?;
366 if matches!(shape, TypeShape::Named(_)) || self.cell_names.contains_key(name) {
367 return Err(crate::composition::diagnostic(
368 "expected compound shape and unused root name",
369 ));
370 }
371 let descriptor = types
372 .checked_value_type(&shape)
373 .map_err(crate::composition::diagnostic)?;
374 CellValue::validate(&descriptor).map_err(crate::composition::diagnostic)?;
375 let owned = cel_runtime::OwnedValue::new::<R>(value)
376 .with_value_type(descriptor)
377 .map_err(crate::composition::diagnostic)?;
378 let id = self.sheet.add_cell(CellValue(owned));
379 self.cell_shapes.insert(id, shape.clone());
380 self.cell_names.insert(name.into(), (id, shape));
381 Ok(id)
382 }
383
384 pub fn cell_shape(&self, id: CellId) -> Option<&TypeShape> {
393 self.cell_shapes.get(&id)
394 }
395
396 pub(crate) fn validate_cell_shape(&self, id: CellId, shape: &TypeShape) -> anyhow::Result<()> {
402 anyhow::ensure!(
403 self.sheet.cell_type_id(id) == Some(cell_type_id(shape)),
404 "invalid actual storage"
405 );
406 if !matches!(shape, TypeShape::Named(_)) {
407 let actual = self.sheet.read::<CellValue>(id)?;
408 anyhow::ensure!(
410 shape_matches(shape, actual.0.value_type()),
411 "complete shape mismatch"
412 );
413 }
414 Ok(())
415 }
416
417 pub fn read_compound<R: cel_runtime::ValueRepr>(&self, id: CellId) -> anyhow::Result<R::Value> {
433 let cell = self.sheet.read::<CellValue>(id)?;
434 anyhow::ensure!(
435 cell.0.value_type().same_shape(&R::value_type()),
436 "compound cell representation mismatch"
437 );
438 Ok(unsafe { cell.0.try_clone()?.into_typed_bound::<R>() })
440 }
441
442 pub fn write_compound<R: cel_runtime::ValueRepr>(
459 &mut self,
460 id: CellId,
461 value: R::Value,
462 ) -> anyhow::Result<()> {
463 let descriptor = self.sheet.read::<CellValue>(id)?.0.value_type().clone();
464 let value = cel_runtime::OwnedValue::new::<R>(value).with_value_type(descriptor)?;
465 self.sheet.write(id, CellValue(value))?;
466 Ok(())
467 }
468
469 pub fn source_compound<R: cel_runtime::ValueRepr>(
485 &self,
486 id: CellId,
487 ) -> anyhow::Result<R::Value> {
488 let cell = self.sheet.source::<CellValue>(id)?;
489 anyhow::ensure!(
490 cell.0.value_type().same_shape(&R::value_type()),
491 "compound cell representation mismatch"
492 );
493 Ok(unsafe { cell.0.try_clone()?.into_typed_bound::<R>() })
495 }
496
497 pub fn locate_error(&self, e: &adam_rs::Error) -> Vec<crate::SourceLabel> {
530 crate::error_labels::locate_error(self, e)
531 }
532
533 pub fn diagnostics(&self) -> crate::SheetDiagnostics {
549 crate::SheetDiagnostics::from_parsed(self)
550 }
551
552 pub fn anonymous_source(&self) -> Option<&crate::SourceId> {
562 self.anonymous_source.as_ref()
563 }
564}
565
566impl std::ops::Deref for ParsedSheet {
567 type Target = Sheet;
569
570 fn deref(&self) -> &Sheet {
572 &self.sheet
573 }
574}
575
576impl std::ops::DerefMut for ParsedSheet {
577 fn deref_mut(&mut self) -> &mut Sheet {
579 &mut self.sheet
580 }
581}
582
583pub struct AdamParser {
597 environment: Rc<crate::module::compile::Environment>,
598}
599
600impl AdamParser {
601 pub fn new(types: TypeRegistry, lookup: OpLookup) -> Self {
606 Self {
607 environment: Rc::new(crate::module::compile::Environment {
608 types: Rc::new(types),
609 lookup,
610 }),
611 }
612 }
613
614 pub fn parse_str(&mut self, source: &str) -> Result<ParsedSheet> {
629 let path: crate::ModulePath = "input".parse().expect("fixed valid module path");
630 let mut modules = crate::ModuleSet::new();
631 modules
632 .insert_source(path.clone(), "<input>", source)
633 .map_err(|error| frontend_error(source, error))?;
634 let compiled = crate::module::compile::compile(self.environment.clone(), modules)
635 .map_err(|mut errors| frontend_error(source, errors.remove(0)))?;
636 let mut roots = compiled.roots(&path);
637 let name = roots.next().ok_or_else(|| {
638 frontend_error(
639 source,
640 crate::composition::diagnostic("expected one root sheet"),
641 )
642 })?;
643 if roots.next().is_some() {
644 let labels = compiled
645 .roots(&path)
646 .filter_map(|name| {
647 compiled
648 .prepared_root(&path, name)
649 .map(|root| root.definition_label().clone())
650 })
651 .collect();
652 return Err(frontend_error(
653 source,
654 crate::ModuleDiagnostic {
655 message: "ambiguous roots: expected one root sheet".into(),
656 labels,
657 cause: None,
658 },
659 ));
660 }
661 let mut parsed = compiled
662 .instantiate_root(&path, name)
663 .map_err(|error| frontend_error(source, error))?;
664 parsed.anonymous_source = Some(
665 compiled
666 .sources()
667 .first()
668 .expect("the convenience frontend retains its supplied source")
669 .clone(),
670 );
671 Ok(parsed)
672 }
673}
674
675fn frontend_error(source: &str, error: crate::ModuleDiagnostic) -> ParseError {
681 let location = error
682 .labels
683 .iter()
684 .find_map(|label| label.location.as_ref());
685 let mut first = Span::call_site();
686 let mut last = None;
687 match TokenStream::from_str(source) {
688 Ok(tokens) => {
689 if let Some(location) = location {
690 for token in LexLexer::new(tokens.into_iter()) {
691 let span = token.span();
692 if span.start() == location.span.start {
693 first = span;
694 }
695 if span.end() == location.span.end {
696 last = Some(span);
697 }
698 }
699 }
700 }
701 Err(lexical) => {
702 let lexical = ParseError::from_lex_error(source, lexical);
703 first = lexical.span();
704 last = lexical.end_span();
705 }
706 }
707 let mut adapted = if let Some(last) = last {
708 ParseError::new_range(error.message.clone(), first, last)
709 } else {
710 ParseError::new(error.message.clone(), first)
711 };
712 adapted = adapted.with_secondary(
713 error
714 .labels
715 .iter()
716 .skip(1)
717 .filter_map(|label| {
718 label
719 .location
720 .as_ref()
721 .map(|location| cel_parser::SpanLabel {
722 span: location.span,
723 label: label.label.clone(),
724 })
725 })
726 .collect(),
727 );
728 adapted.with_cause(error)
729}
730
731#[cfg(test)]
736mod tests {
737 use super::*;
738 use crate::TypeRegistry;
739 use cel_parser::OpLookup;
740 use cel_runtime::{ArrayOf, Leaf};
741 use std::any::Any;
742
743 fn parser() -> AdamParser {
745 AdamParser::new(TypeRegistry::new(), OpLookup::new())
746 }
747
748 #[derive(Clone, Debug, PartialEq)]
750 struct Custom(i32);
751
752 #[derive(Clone)]
754 struct CountFn;
755
756 fn custom_lookup() -> OpLookup {
758 let mut lookup = OpLookup::new();
759 lookup.push_scope(|name, segment, arity, _span| match name {
760 "left" if arity == 0 => {
761 segment.op0(|| Custom(1));
762 Ok(true)
763 }
764 "right" if arity == 0 => {
765 segment.op0(|| Custom(2));
766 Ok(true)
767 }
768 "count" if arity == 0 => {
769 segment.op0(|| CountFn);
770 Ok(true)
771 }
772 "()" if arity == 2 => {
773 if segment.peek_stack_infos(1)[0]
774 .value_type
775 .array_element()
776 .is_some_and(|element| element.array_element().is_some())
777 {
778 segment
779 .op2_repr::<Leaf<CountFn>, ArrayOf<ArrayOf<Leaf<Custom>>>, Leaf<i32>, _>(
780 |_callee, array| array.len() as i32,
781 )?;
782 } else {
783 segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<Custom>>, Leaf<i32>, _>(
784 |_callee, array| array.len() as i32,
785 )?;
786 }
787 Ok(true)
788 }
789 _ => Ok(false),
790 });
791 lookup
792 }
793
794 #[test]
796 fn parse_empty_sheet() {
797 let _sheet = parser().parse_str("sheet empty {}").unwrap();
798 }
799
800 #[test]
802 fn parse_cell_with_annotation_and_initializer() {
803 let _sheet = parser()
804 .parse_str("sheet s { cell width: f64 = 1920.0; }")
805 .unwrap();
806 }
807
808 #[test]
810 fn parse_cell_annotation_only_uses_default() {
811 let _sheet = parser().parse_str("sheet s { cell area: f64; }").unwrap();
812 }
813
814 #[test]
816 fn parse_cell_initializer_infers_type() {
817 let _sheet = parser().parse_str("sheet s { cell mode = 0i32; }").unwrap();
818 }
819
820 #[test]
822 fn parse_cell_unknown_type_is_error() {
823 let result = parser().parse_str("sheet s { cell x: unknown_type; }");
824 assert!(result.is_err());
825 let err = result.expect_err("expected Err");
826 let msg = err.message().to_lowercase();
827 assert!(
828 msg.contains("unknown type") || msg.contains("unknown_type"),
829 "{msg}"
830 );
831 }
832
833 #[test]
835 fn parse_cell_missing_default_is_error() {
836 #[derive(PartialEq, Clone, Debug)]
838 struct NoDef(i32);
839 let mut reg = TypeRegistry::new();
840 reg.register_no_default::<NoDef>("NoDef");
841 let mut p = AdamParser::new(reg, OpLookup::new());
842 let result = p.parse_str("sheet s { cell x: NoDef; }");
843 assert!(result.is_err());
844 }
845
846 #[test]
848 fn parse_cell_initializer_supports_registry_typed_arrays() {
849 let mut reg = TypeRegistry::new();
850 reg.register_no_default::<Custom>("Custom");
851 let mut parser = AdamParser::new(reg, custom_lookup());
852
853 let parsed = parser
854 .parse_str(
855 "sheet s { \
856 cell values: i32 = count([left, right]: [Custom]); \
857 cell empty: i32 = count([]: [Custom]); \
858 cell nested: i32 = count([[]: [Custom]]: [[Custom]]); \
859 }",
860 )
861 .unwrap();
862
863 let (values_id, _) = parsed.cell_names["values"];
864 assert_eq!(*parsed.read::<i32>(values_id).unwrap(), 2);
865
866 let (empty_id, _) = parsed.cell_names["empty"];
867 assert_eq!(*parsed.read::<i32>(empty_id).unwrap(), 0);
868
869 let (nested_id, _) = parsed.cell_names["nested"];
870 assert_eq!(*parsed.read::<i32>(nested_id).unwrap(), 1);
871 }
872
873 #[test]
875 fn parse_cell_initializer_rejects_unknown_registry_typed_arrays() {
876 let mut parser = AdamParser::new(TypeRegistry::new(), custom_lookup());
877 let err = parser
878 .parse_str("sheet s { cell values = []: [Custom]; }")
879 .expect_err("unknown custom array annotations must fail");
880
881 assert!(
882 err.message().contains("unknown type `Custom`"),
883 "got: {}",
884 err.message()
885 );
886 }
887
888 #[test]
890 fn parse_cell_initializer_preserves_persistent_array_cells() {
891 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
892 let parsed = parser
893 .parse_str("sheet s { cell values = []: [(i32, f64)]; }")
894 .unwrap();
895 type Pairs = cel_runtime::ArrayOf<
897 cel_runtime::Cons<
898 cel_runtime::Leaf<i32>,
899 cel_runtime::Cons<cel_runtime::Leaf<f64>, cel_runtime::Nil>,
900 >,
901 >;
902 assert!(
903 parsed
904 .read_compound::<Pairs>(parsed.cell_names["values"].0)
905 .unwrap()
906 .is_empty()
907 );
908 }
909
910 #[test]
912 fn parse_source_decl_registers_a_source_kind_cell() {
913 let sheet = parser()
914 .parse_str("sheet s { source width: i32 = 4; }")
915 .unwrap();
916 let (width, _) = sheet.cell_names["width"];
917 assert_eq!(sheet.cell_kind(width), Some(adam_rs::CellKind::Source));
918 }
919
920 #[test]
922 fn parse_source_decl_requires_colon_or_equals() {
923 let result = parser().parse_str("sheet s { source width; }");
924 assert!(result.is_err());
925 }
926
927 #[test]
929 fn parse_cell_decl_with_a_require_block_attaches_requirements() {
930 let sheet = parser()
931 .parse_str("sheet s { cell x: i32 = 5 require { @positive x > 0; }; }")
932 .unwrap();
933 let (x, _) = sheet.cell_names["x"];
934 assert_eq!(sheet.cell_requirements(x).unwrap().len(), 1);
935 }
936
937 #[test]
939 fn parse_source_decl_with_a_require_block_attaches_requirements() {
940 let sheet = parser()
941 .parse_str("sheet s { source x: i32 = 5 require { @positive x > 0; }; }")
942 .unwrap();
943 let (x, _) = sheet.cell_names["x"];
944 assert_eq!(sheet.cell_requirements(x).unwrap().len(), 1);
945 }
946
947 #[test]
949 fn parse_source_decl_with_a_filter_clause_attaches_a_filter() {
950 let sheet = parser()
951 .parse_str("sheet s { source x: i32 = 5 filter 0..=10; }")
952 .unwrap();
953 let (x, _) = sheet.cell_names["x"];
954 assert!(sheet.filter_kind(x).is_some());
955 }
956
957 #[test]
959 fn parse_out_decl_with_a_filter_clause_attaches_a_filter() {
960 let sheet = parser()
961 .parse_str("sheet s { cell width: i32 = 4; out area := width filter 0..=100; }")
962 .unwrap();
963 let area = sheet.output_names["area"];
964 assert!(sheet.filter_kind(area).is_some());
965 }
966
967 #[test]
969 fn cell_filter_with_no_named_dependency_clamps_on_write() {
970 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
971 let mut parsed = parser
972 .parse_str(
973 "sheet s { cell a: i32 filter if _ < 1 { 1 } else if _ > 100 { 100 } else { _ }; }",
974 )
975 .unwrap();
976 let (cell_id, _) = parsed.cell_names["a"];
977 parsed.sheet.write(cell_id, 500i32).unwrap();
978 parsed.sheet.propagate().unwrap();
979 assert_eq!(*parsed.sheet.read::<i32>(cell_id).unwrap(), 100);
980 }
981
982 #[test]
984 fn cell_filter_referencing_a_cell_tracks_its_current_value() {
985 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
986 let mut parsed = parser
987 .parse_str(
988 "sheet s { \
989 cell hi: i32 = 100; \
990 cell a: i32 filter if _ < 1 { 1 } else if _ > hi { hi } else { _ }; \
991 }",
992 )
993 .unwrap();
994 let (a_id, _) = parsed.cell_names["a"];
995 let (hi_id, _) = parsed.cell_names["hi"];
996
997 parsed.sheet.write(a_id, 500i32).unwrap();
998 parsed.sheet.propagate().unwrap();
999 assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 100);
1000
1001 parsed.sheet.write(hi_id, 10i32).unwrap();
1002 parsed.sheet.write(a_id, 500i32).unwrap();
1003 parsed.sheet.propagate().unwrap();
1004 assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 10);
1005 }
1006
1007 #[test]
1009 fn cell_filter_referencing_the_same_value_twice_is_idempotent() {
1010 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1013 let mut parsed = parser
1014 .parse_str("sheet s { cell step: i32 = 10; cell a: i32 filter _ - (_ % step); }")
1015 .unwrap();
1016 let (a_id, _) = parsed.cell_names["a"];
1017 parsed.sheet.write(a_id, 27i32).unwrap();
1018 parsed.sheet.propagate().unwrap();
1019 assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 20);
1020 }
1021
1022 #[test]
1024 fn filter_dependency_cycle_reported_with_secondary_labels() {
1025 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1026 let ok_source = "sheet s {\n cell b: i32 = 0;\n cell a: i32 = 5;\n relationship { b := a; }\n cell c: i32 = 0 filter _ + b - b;\n}";
1027 assert!(parser.parse_str(ok_source).is_ok());
1028
1029 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1030 let source = "sheet s {\n cell b: i32 = 0;\n cell a: i32 = 5 filter _ + b - b;\n relationship { b := a; }\n}";
1033 let err = parser.parse_str(source).unwrap_err();
1034 assert_eq!(
1035 err.message(),
1036 "a filter or conditional depends on a cell it governs (dependency cycle)"
1037 );
1038 assert!(!err.secondary().is_empty());
1039 }
1040
1041 #[test]
1043 fn cell_filter_without_underscore_is_a_parse_error() {
1044 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1045 let err = parser.parse_str("sheet s { cell a: i32 filter 1; }");
1046 assert!(err.is_err());
1047 }
1048
1049 #[test]
1051 fn cell_filter_body_type_mismatch_is_a_parse_error() {
1052 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1053 let err = parser.parse_str("sheet s { cell a: i32 filter _ > 0; }");
1054 assert!(err.is_err());
1055 }
1056
1057 #[test]
1059 fn cell_filter_undeclared_identifier_is_a_parse_error() {
1060 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1061 let err = parser.parse_str("sheet s { cell a: i32 filter _ + nope; }");
1062 assert!(err.is_err());
1063 }
1064
1065 #[test]
1067 fn cell_filter_with_a_range_inclusive_body_clamps_on_write() {
1068 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1069 let mut parsed = parser
1070 .parse_str("sheet s { cell a: i32 filter 0..=100; }")
1071 .unwrap();
1072 let (a_id, _) = parsed.cell_names["a"];
1073 assert!(matches!(
1074 parsed.sheet.filter_kind(a_id),
1075 Some(adam_rs::FilterKind::Range { .. })
1076 ));
1077 parsed.sheet.write(a_id, 500i32).unwrap();
1078 parsed.sheet.propagate().unwrap();
1079 assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 100);
1080 }
1081
1082 #[test]
1085 fn public_range_bounds_rejects_invalid_scalar_inputs() {
1086 let parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
1087 .parse_str("sheet s { cell hi: f64 = 10.0; cell value: f64 filter 0.0..=hi; }")
1088 .unwrap();
1089 let Some(adam_rs::FilterKind::Range { bounds }) =
1090 parsed.sheet.filter_kind(parsed.cell_names["value"].0)
1091 else {
1092 panic!("expected range filter");
1093 };
1094 let invalid: [&[&dyn Any]; 4] = [&[&42u64], &[&1u8], &[], &[&10.0f64, &20.0f64]];
1095 for _ in 0..2 {
1096 for args in invalid {
1097 let (lo, hi) = bounds(&[&10.0f64]).unwrap();
1098 assert_eq!(*lo.downcast::<f64>().unwrap(), 0.0);
1099 assert_eq!(*hi.downcast::<f64>().unwrap(), 10.0);
1100 assert!(bounds(args).is_none(), "invalid scalar inputs accepted");
1103 let (_, hi) = bounds(&[&25.0f64]).unwrap();
1104 assert_eq!(*hi.downcast::<f64>().unwrap(), 25.0);
1105 }
1106 }
1107 }
1108
1109 #[test]
1112 fn public_range_bounds_rejects_invalid_compound_inputs() {
1113 use cel_runtime::{Cons, IntoCStackList, Leaf, Nil, OwnedValue};
1114
1115 let parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
1116 .parse_str(
1117 "sheet s { cell limits: (f64, f64) = (0.0, 10.0); \
1118 cell value: f64 filter limits.0..=limits.1; }",
1119 )
1120 .unwrap();
1121 let Some(adam_rs::FilterKind::Range { bounds }) =
1122 parsed.sheet.filter_kind(parsed.cell_names["value"].0)
1123 else {
1124 panic!("expected range filter");
1125 };
1126 type Limits = Cons<Leaf<f64>, Cons<Leaf<f64>, Nil>>;
1128 type WrongLimits = Cons<Leaf<f64>, Cons<Leaf<u64>, Nil>>;
1130 let valid = CellValue(OwnedValue::new::<Limits>((0.0, 10.0).into_c_stack_list()));
1131 let wrong_shape = CellValue(OwnedValue::new::<WrongLimits>(
1133 (0.0, 10.0f64.to_bits()).into_c_stack_list(),
1134 ));
1135 let invalid: [&[&dyn Any]; 4] = [&[&wrong_shape], &[&false], &[], &[&valid, &valid]];
1136 for _ in 0..2 {
1137 for args in invalid {
1138 let (lo, hi) = bounds(&[&valid]).unwrap();
1139 assert_eq!(*lo.downcast::<f64>().unwrap(), 0.0);
1140 assert_eq!(*hi.downcast::<f64>().unwrap(), 10.0);
1141 assert!(bounds(args).is_none(), "invalid compound inputs accepted");
1142 assert!(bounds(&[&valid]).is_some());
1143 }
1144 }
1145 }
1146
1147 #[test]
1149 fn cell_filter_range_does_not_require_underscore() {
1150 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1151 let result = parser.parse_str("sheet s { cell a: i32 filter 0..=100; }");
1152 assert!(result.is_ok());
1153 }
1154
1155 #[test]
1157 fn cell_filter_range_bounds_track_cell_dependencies_live() {
1158 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1159 let mut parsed = parser
1160 .parse_str(
1161 "sheet s { cell lo: i32 = 0; cell hi: i32 = 100; cell a: i32 filter lo..=hi; }",
1162 )
1163 .unwrap();
1164 let (a_id, _) = parsed.cell_names["a"];
1165 let (hi_id, _) = parsed.cell_names["hi"];
1166 assert_eq!(parsed.sheet.filter_range::<i32>(a_id), Some((0, 100)));
1167 parsed.sheet.write(hi_id, 10i32).unwrap();
1168 assert_eq!(parsed.sheet.filter_range::<i32>(a_id), Some((0, 10)));
1169 parsed.sheet.write(a_id, 500i32).unwrap();
1170 parsed.sheet.propagate().unwrap();
1171 assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 10);
1172 }
1173
1174 #[test]
1176 fn cell_filter_range_with_float_cell_type_works() {
1177 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1178 let mut parsed = parser
1179 .parse_str("sheet s { cell a: f64 filter 0.0..=100.0; }")
1180 .unwrap();
1181 let (a_id, _) = parsed.cell_names["a"];
1182 parsed.sheet.write(a_id, 500.0f64).unwrap();
1183 parsed.sheet.propagate().unwrap();
1184 assert_eq!(*parsed.sheet.read::<f64>(a_id).unwrap(), 100.0);
1185 }
1186
1187 #[test]
1189 fn cell_filter_range_with_mismatched_element_type_is_a_parse_error() {
1190 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1191 let err = parser
1192 .parse_str("sheet s { cell a: f64 filter 0..=100; }")
1193 .expect_err("expected Err");
1194 assert!(
1195 err.message().contains("filter range bounds must be"),
1196 "{}",
1197 err.message()
1198 );
1199 }
1200
1201 #[test]
1203 fn cell_filter_range_with_mismatched_element_type_and_underscore_is_still_a_range_error() {
1204 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1209 let err = parser
1210 .parse_str("sheet s { cell a: f64 filter (_ as i32)..=100; }")
1211 .expect_err("expected Err");
1212 assert!(
1213 err.message().contains("filter range bounds must be"),
1214 "{}",
1215 err.message()
1216 );
1217 }
1218
1219 #[test]
1221 fn cell_filter_range_returns_none_instead_of_panicking_when_a_bound_expression_fails_to_evaluate()
1222 {
1223 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1234 let mut parsed = parser
1235 .parse_str("sheet s { cell hi: i32 = 1; cell a: i32 filter 0..=(100 / hi); }")
1236 .unwrap();
1237 let (a_id, _) = parsed.cell_names["a"];
1238 let (hi_id, _) = parsed.cell_names["hi"];
1239
1240 assert_eq!(parsed.sheet.filter_range::<i32>(a_id), Some((0, 100)));
1241
1242 parsed.sheet.write(hi_id, 0i32).unwrap();
1247 assert_eq!(parsed.sheet.filter_range::<i32>(a_id), None);
1248 }
1249
1250 #[test]
1252 fn cell_filter_general_expression_still_compiles_to_opaque_kind() {
1253 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1254 let parsed = parser
1255 .parse_str("sheet s { cell a: i32 filter if _ < 0 { 0 } else { _ }; }")
1256 .unwrap();
1257 let (a_id, _) = parsed.cell_names["a"];
1258 assert!(matches!(
1259 parsed.sheet.filter_kind(a_id),
1260 Some(adam_rs::FilterKind::Opaque)
1261 ));
1262 }
1263
1264 #[test]
1266 fn filter_tracks_a_tuple_typed_range_cell_dynamically() {
1267 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1268 let mut parsed = parser
1269 .parse_str(
1270 "sheet s { \
1271 cell a_range: (i32, i32) = (1, 100); \
1272 cell max: i32 = 100; \
1273 relationship { a_range := (1, max); } \
1274 cell a: i32 filter if _ < a_range.0 { a_range.0 } \
1275 else if _ > a_range.1 { a_range.1 } else { _ }; \
1276 }",
1277 )
1278 .unwrap();
1279 let (a_id, _) = parsed.cell_names["a"];
1280 let (max_id, _) = parsed.cell_names["max"];
1281
1282 parsed.sheet.write(a_id, 500i32).unwrap();
1283 parsed.sheet.propagate().unwrap();
1284 assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 100);
1285
1286 parsed.sheet.write(max_id, 10i32).unwrap();
1287 parsed.sheet.propagate().unwrap();
1288 parsed.sheet.write(a_id, 500i32).unwrap();
1289 parsed.sheet.propagate().unwrap();
1290 assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 10);
1291 }
1292
1293 #[test]
1295 fn cell_filter_on_a_tuple_typed_cell_is_supported() {
1296 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1297 let sheet = parser
1298 .parse_str("sheet s { cell a: (i32, i32) filter (_.0, _.1); }")
1299 .unwrap();
1300 assert!(sheet.filter_kind(sheet.cell_names["a"].0).is_some());
1301 }
1302
1303 #[test]
1305 fn parse_filter_clause_attaches_a_filter() {
1306 let sheet = parser()
1307 .parse_str("sheet s { cell x: i32 = 0 filter 0..=10; }")
1308 .unwrap();
1309 let (x, _) = sheet.cell_names["x"];
1310 assert!(sheet.filter_kind(x).is_some());
1311 }
1312
1313 #[test]
1315 fn parse_filter_clause_with_a_label_is_now_a_parse_error() {
1316 let result = parser().parse_str("sheet s { cell x: i32 = 0 filter clamp: 0..=10; }");
1317 assert!(result.is_err());
1318 }
1319
1320 #[test]
1322 fn parse_multiple_cells() {
1323 let _sheet = parser()
1324 .parse_str(
1325 r#"
1326 sheet image_resize {
1327 cell width: f64 = 1920.0;
1328 cell height: f64 = 1080.0;
1329 cell area: f64;
1330 cell mode: i32 = 0;
1331 }
1332 "#,
1333 )
1334 .unwrap();
1335 }
1336
1337 #[test]
1339 fn parse_relationship_with_a_single_binding() {
1340 let mut sheet = parser()
1341 .parse_str(
1342 r#"
1343 sheet s {
1344 cell a: i32 = 2;
1345 cell b: i32 = 0;
1346 relationship {
1347 b := a;
1348 }
1349 }
1350 "#,
1351 )
1352 .unwrap();
1353 sheet.propagate().unwrap();
1354 let (b_id, _) = sheet.cell_names["b"].clone();
1355 assert_eq!(*sheet.read::<i32>(b_id).unwrap(), 2);
1356 }
1357
1358 #[test]
1360 fn parse_relationship_deduces_inputs_from_referenced_identifiers() {
1361 let mut sheet = parser()
1362 .parse_str(
1363 r#"
1364 sheet s {
1365 cell a: i32 = 2;
1366 cell b: i32 = 3;
1367 cell c: i32 = 0;
1368 relationship {
1369 c := a * b;
1370 }
1371 }
1372 "#,
1373 )
1374 .unwrap();
1375 sheet.propagate().unwrap();
1376 let (c_id, _) = sheet.cell_names["c"].clone();
1377 assert_eq!(*sheet.read::<i32>(c_id).unwrap(), 6);
1378 }
1379
1380 #[test]
1383 fn member_labels_are_not_cell_dependencies() {
1384 let mut lookup = OpLookup::new();
1385 lookup.push_scope(|name, segment, arity, _| {
1386 if matches!(name, "x" | "width") && arity == 1 {
1387 segment.op1(|value: f64| value * 2.0)?;
1388 return Ok(true);
1389 }
1390 Ok(false)
1391 });
1392 let mut sheet = AdamParser::new(TypeRegistry::new(), lookup)
1393 .parse_str(
1394 "sheet s { cell input: f64 = 3.0; cell x: bool = true; \
1395 cell width: bool = false; cell result: f64 = 0.0; \
1396 relationship { result := input.x().width; } }",
1397 )
1398 .unwrap();
1399 let input = sheet.cell_names["input"].0;
1400 let result = sheet.cell_names["result"].0;
1401 let (&(relationship, method), _) = sheet.method_spans.iter().next().unwrap();
1402 assert_eq!(
1403 sheet.method_inputs(relationship, method),
1404 Some([input].as_slice())
1405 );
1406 sheet.propagate().unwrap();
1407 assert_eq!(*sheet.read::<f64>(result).unwrap(), 12.0);
1408 sheet.write(input, 5.0f64).unwrap();
1409 sheet.propagate().unwrap();
1410 assert_eq!(*sheet.read::<f64>(result).unwrap(), 20.0);
1411 }
1412
1413 #[test]
1416 fn field_labels_and_member_names_are_not_cell_dependencies() {
1417 use cel_parser::{OperationSignature, RecordField, RecordSchema, ResolvedLeafType};
1418 use cel_runtime::ValueType;
1419 #[derive(Clone, Debug, PartialEq)]
1421 struct TestPoint {
1422 x: f64,
1423 y: f64,
1424 }
1425 let point = ResolvedLeafType::new("TestPoint", ValueType::leaf::<TestPoint>());
1426 let scalar = ResolvedLeafType::new("f64", ValueType::leaf::<f64>());
1427 let mut lookup = OpLookup::new();
1428 lookup
1429 .register_record(RecordSchema {
1430 ty: point.clone(),
1431 fields: vec![
1432 RecordField {
1433 name: "x",
1434 ty: scalar.clone().into(),
1435 },
1436 RecordField {
1437 name: "y",
1438 ty: scalar.clone().into(),
1439 },
1440 ],
1441 emit: Box::new(|s, order, _, _| {
1442 assert_eq!(order, [0, 1]);
1443 s.op2(|x: f64, y: f64| TestPoint { x, y })
1444 }),
1445 })
1446 .unwrap();
1447 lookup
1448 .register_operation(OperationSignature {
1449 name: "x",
1450 operands: vec![point.into()],
1451 result: scalar.into(),
1452 emit: Box::new(|s, _| s.op1(|p: TestPoint| p.x)),
1453 })
1454 .unwrap();
1455 let mut sheet = AdamParser::new(TypeRegistry::new(), lookup)
1456 .parse_str(
1457 "sheet s { cell a: f64 = 2.0; cell b: f64 = 3.0; \
1458 cell x: bool = false; cell y: bool = false; cell TestPoint: bool = true; \
1459 cell result: f64 = 0.0; \
1460 relationship { result := TestPoint { x: a, y: b }.x; } }",
1461 )
1462 .unwrap();
1463 let a = sheet.cell_names["a"].0;
1464 let b = sheet.cell_names["b"].0;
1465 let result = sheet.cell_names["result"].0;
1466 let (&(relationship, method), _) = sheet.method_spans.iter().next().unwrap();
1467 assert_eq!(
1468 sheet.method_inputs(relationship, method),
1469 Some([a, b].as_slice())
1470 );
1471 sheet.propagate().unwrap();
1472 assert_eq!(*sheet.read::<f64>(result).unwrap(), 2.0);
1473 sheet.write(a, 7.0f64).unwrap();
1474 sheet.propagate().unwrap();
1475 assert_eq!(*sheet.read::<f64>(result).unwrap(), 7.0);
1476 }
1477
1478 #[test]
1480 fn parse_relationship_with_multiple_bindings_lets_the_planner_pick_a_direction() {
1481 let mut sheet = parser()
1488 .parse_str(
1489 r#"
1490 sheet s {
1491 cell c: i32 = 0;
1492 cell a: i32 = 2;
1493 cell b: i32 = 3;
1494 relationship {
1495 c := a * b;
1496 a := c / b;
1497 b := c / a;
1498 }
1499 }
1500 "#,
1501 )
1502 .unwrap();
1503 sheet.propagate().unwrap();
1504 let (c_id, _) = sheet.cell_names["c"].clone();
1505 assert_eq!(*sheet.read::<i32>(c_id).unwrap(), 6);
1506 }
1507
1508 #[test]
1510 fn mismatched_method_cells_error_spans_the_relationship_block_not_the_sheet() {
1511 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1512 let source = "sheet s {\n cell a: i32;\n cell b: i32;\n\n relationship {\n a := b;\n b := 42;\n }\n}";
1513 let err = parser.parse_str(source).err().unwrap();
1514 assert_eq!(err.span().start().line, 7);
1519 }
1520
1521 #[test]
1523 fn successful_relationship_parse_populates_method_spans() {
1524 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1525 let source = "sheet s {\n cell a: i32;\n cell b: i32;\n\n relationship {\n a := b;\n }\n}";
1526 let parsed = parser.parse_str(source).unwrap();
1527 assert_eq!(parsed.method_spans.len(), 1);
1528 let ((_, idx), span) = parsed.method_spans.iter().next().unwrap();
1529 assert_eq!(*idx, 0);
1530 assert_eq!(span.start.line, 6);
1531 }
1532
1533 #[test]
1535 fn parse_populates_relationship_and_cell_spans() {
1536 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1537 let parsed = parser
1538 .parse_str("sheet s { cell a: i32 = 0; cell b: i32; relationship { b := a; } }")
1539 .unwrap();
1540 assert_eq!(parsed.cell_spans.len(), 2);
1541 assert_eq!(parsed.relationship_spans.len(), 1);
1542 for (_, (id, _)) in &parsed.cell_names {
1544 assert!(parsed.cell_spans.contains_key(id));
1545 }
1546 }
1547
1548 #[test]
1550 fn locate_error_resolves_a_cycle_to_multiple_ordered_spans() {
1551 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1552 let mut parsed = parser
1553 .parse_str(
1554 "sheet s { cell x: i32 = 0; cell y: i32 = 0; relationship { x := y + 1i32; } \
1555 relationship { y := x + 1i32; } }",
1556 )
1557 .unwrap();
1558 let err = parsed.propagate().unwrap_err();
1559 assert!(matches!(err, adam_rs::Error::Cycle { .. }));
1560 let located = parsed.locate_error(&err);
1561 let rel_spans = located.len();
1563 assert!(
1564 rel_spans >= 2,
1565 "expected >=2 spans, got {rel_spans}: {located:?}"
1566 );
1567 }
1568
1569 #[test]
1571 fn diagnostics_survive_dropping_the_model() {
1572 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1573 let mut parsed = parser
1574 .parse_str("sheet s { cell x: i32 = 0; cell y: i32; relationship { y := 10i32 / x; } }")
1575 .unwrap();
1576 let diagnostics = parsed.diagnostics();
1577 let error = parsed.propagate().unwrap_err();
1578 let before_drop = diagnostics.locate_error(&error);
1579 assert!(!before_drop.is_empty());
1580
1581 drop(parsed);
1582
1583 assert_eq!(diagnostics.locate_error(&error), before_drop);
1584 assert!(before_drop.iter().any(|label| {
1585 label
1586 .location
1587 .as_ref()
1588 .is_some_and(|location| location.source.text().contains("10i32 / x"))
1589 }));
1590 }
1591
1592 #[test]
1594 fn duplicate_output_set_error_underlines_both_bindings() {
1595 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
1596 let source =
1597 "sheet s { cell a: i32 = 0; cell b: i32; relationship { b := a; b := a + 1i32; } }";
1598 let err = parser.parse_str(source).unwrap_err();
1599 let out =
1600 err.format_rustc_style(source, "t.adm2", 1, &annotate_snippets::Renderer::plain());
1601 assert!(out.contains("outputs"), "{out}"); assert!(out.contains("collides with this earlier method"), "{out}");
1605 assert!(
1606 out.contains("output cell `b` is claimed more than once"),
1607 "{out}"
1608 );
1609 }
1610
1611 #[test]
1613 fn parse_binding_undeclared_output_is_an_error() {
1614 let result = parser().parse_str(
1615 r#"
1616 sheet s {
1617 cell a: i32 = 1;
1618 relationship {
1619 missing := a;
1620 }
1621 }
1622 "#,
1623 );
1624 assert!(result.is_err());
1625 }
1626
1627 #[test]
1629 fn parse_binding_multi_output_tuple_matches_existing_tuple_shape_rules() {
1630 let mut sheet = parser()
1631 .parse_str(
1632 r#"
1633 sheet s {
1634 cell w: i32 = 4;
1635 cell x: i32 = 0;
1636 cell y: i32 = 0;
1637 relationship {
1638 (x, y) := (w, w * 2);
1639 }
1640 }
1641 "#,
1642 )
1643 .unwrap();
1644 sheet.propagate().unwrap();
1645 let (x_id, _) = sheet.cell_names["x"].clone();
1646 let (y_id, _) = sheet.cell_names["y"].clone();
1647 assert_eq!(*sheet.read::<i32>(x_id).unwrap(), 4);
1648 assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 8);
1649 }
1650
1651 #[test]
1653 fn parse_binding_multi_output_arity_mismatch_is_an_error() {
1654 let result = parser().parse_str(
1655 r#"
1656 sheet s {
1657 cell w: i32 = 4;
1658 cell x: i32 = 0;
1659 cell y: i32 = 0;
1660 relationship {
1661 (x, y) := w;
1662 }
1663 }
1664 "#,
1665 );
1666 assert!(result.is_err());
1667 }
1668
1669 #[test]
1671 fn parse_binding_multi_output_without_parens_is_a_parse_error() {
1672 let result = parser().parse_str(
1673 r#"
1674 sheet s {
1675 cell w: i32 = 4;
1676 cell x: i32 = 0;
1677 cell y: i32 = 0;
1678 relationship {
1679 x, y := (w, w * 2);
1680 }
1681 }
1682 "#,
1683 );
1684 assert!(result.is_err());
1685 }
1686
1687 #[test]
1689 fn parse_binding_single_element_tuple_destructure_extracts_the_element() {
1690 let mut sheet = parser()
1691 .parse_str(
1692 r#"
1693 sheet s {
1694 cell w: i32 = 4;
1695 cell x: i32 = 0;
1696 relationship {
1697 (x,) := (w,);
1698 }
1699 }
1700 "#,
1701 )
1702 .unwrap();
1703 sheet.propagate().unwrap();
1704 let (x_id, _) = sheet.cell_names["x"].clone();
1705 assert_eq!(*sheet.read::<i32>(x_id).unwrap(), 4);
1706 }
1707
1708 #[test]
1711 fn parse_binding_single_parenthesized_identifier_without_comma_is_an_error() {
1712 let result = parser().parse_str(
1713 r#"
1714 sheet s {
1715 cell w: i32 = 4;
1716 cell x: i32 = 0;
1717 relationship {
1718 (x) := w;
1719 }
1720 }
1721 "#,
1722 );
1723 assert!(result.is_err());
1724 }
1725
1726 #[test]
1728 fn parse_out_with_direct_initializer_and_no_require_block() {
1729 let mut sheet = parser()
1730 .parse_str(
1731 r#"
1732 sheet s {
1733 cell a: i32 = 3;
1734 cell b: i32 = 4;
1735 out area: i32 := a * b;
1736 }
1737 "#,
1738 )
1739 .unwrap();
1740 sheet.propagate().unwrap();
1741 let cell_id = sheet.output_names["area"];
1742 assert_eq!(*sheet.sheet.read::<i32>(cell_id).unwrap(), 12);
1743 }
1744
1745 #[test]
1747 fn parse_out_with_no_type_annotation_infers_from_initializer() {
1748 let sheet = parser()
1749 .parse_str(
1750 r#"
1751 sheet s {
1752 cell a: i32 = 3;
1753 out doubled := a * 2;
1754 }
1755 "#,
1756 )
1757 .unwrap();
1758 let (_, shape) = sheet.cell_names["doubled"].clone();
1759 assert_eq!(
1760 shape,
1761 crate::type_registry::TypeShape::Named(std::any::TypeId::of::<i32>())
1762 );
1763 }
1764
1765 #[test]
1767 fn parse_out_with_a_require_block_registers_named_requirements() {
1768 let mut sheet = parser()
1769 .parse_str(
1770 r#"
1771 sheet s {
1772 cell a: i32 = 3;
1773 cell b: i32 = 4;
1774 out area: i32 := a * b require {
1775 @positive area > 0;
1776 @small area < 1000;
1777 };
1778 }
1779 "#,
1780 )
1781 .unwrap();
1782 sheet.propagate().unwrap();
1783 let output_id = sheet.output_names["area"];
1784 assert!(sheet.sheet.cell_requirements(output_id).unwrap().len() == 2);
1785 assert!(
1786 sheet
1787 .sheet
1788 .violated_requirements(output_id)
1789 .next()
1790 .is_none()
1791 );
1792 }
1793
1794 #[test]
1796 fn parse_out_require_block_requirement_can_violate() {
1797 let mut sheet = parser()
1798 .parse_str(
1799 r#"
1800 sheet s {
1801 cell a: i32 = 3;
1802 cell b: i32 = 4;
1803 out area: i32 := a * b require {
1804 @too_small area > 1000;
1805 };
1806 }
1807 "#,
1808 )
1809 .unwrap();
1810 sheet.propagate().unwrap();
1811 let output_id = sheet.output_names["area"];
1812 assert_eq!(sheet.sheet.violated_requirements(output_id).count(), 1);
1813 }
1814
1815 #[test]
1817 fn parse_requirement_non_bool_body_is_an_error() {
1818 let result = parser().parse_str(
1819 r#"
1820 sheet s {
1821 cell a: i32 = 3;
1822 out x: i32 := a require {
1823 @bad a;
1824 };
1825 }
1826 "#,
1827 );
1828 assert!(result.is_err());
1829 }
1830
1831 #[test]
1833 fn parses_a_sheet_with_an_outer_doc_comment_on_a_cell() {
1834 let parsed = parser()
1835 .parse_str("sheet s {\n /// the total\n cell x: i32 = 1;\n}")
1836 .unwrap();
1837 assert_eq!(parsed.cell_names.len(), 1);
1838 }
1839
1840 #[test]
1842 fn parses_a_sheet_with_an_inner_doc_comment() {
1843 let parsed = parser()
1844 .parse_str("//! module docs\nsheet s {\n cell x: i32 = 1;\n}")
1845 .unwrap();
1846 assert_eq!(parsed.cell_names.len(), 1);
1847 }
1848
1849 #[test]
1851 fn parses_a_sheet_with_doc_comments_on_every_declaration_kind() {
1852 let source = "//! module docs\nsheet s {\n /// a cell\n cell x: i32 = 1;\n\n /// another cell\n cell y: i32 = 2;\n\n /// a relationship\n relationship { y := x; }\n}";
1853 let parsed = parser().parse_str(source).unwrap();
1854 assert_eq!(parsed.cell_names.len(), 2);
1855 }
1856
1857 #[test]
1859 fn parse_binding_undeclared_input_is_error() {
1860 let result = parser().parse_str(
1861 r#"
1862 sheet s {
1863 cell x: f64 = 1.0;
1864 relationship { x := bogus; }
1865 }
1866 "#,
1867 );
1868 assert!(result.is_err());
1869 let err = result.expect_err("expected Err");
1870 let msg = err.message().to_lowercase();
1871 assert!(msg.contains("bogus") || msg.contains("undefined"), "{msg}");
1872 }
1873
1874 #[test]
1876 fn parse_method_output_type_mismatch_is_error() {
1877 let result = parser().parse_str(
1878 r#"
1879 sheet s {
1880 cell x: f64 = 0.0;
1881 cell n: i32 = 0;
1882 relationship { n := x; }
1883 }
1884 "#,
1885 );
1886 assert!(result.is_err(), "f64 body for i32 output must be an error");
1887 }
1888
1889 #[test]
1891 fn parse_method_output_tuple_arity_mismatch_is_error() {
1892 let result = parser().parse_str(
1893 r#"
1894 sheet s {
1895 cell a: i32 = 1;
1896 cell b: i32 = 2;
1897 cell x: i32;
1898 cell y: i32;
1899 cell z: i32;
1900 relationship { (x, y, z) := (a + b, a - b); }
1901 }
1902 "#,
1903 );
1904 assert!(
1905 result.is_err(),
1906 "2-tuple body for 3 declared outputs must be an error"
1907 );
1908 let err = result.expect_err("expected Err");
1909 let msg = err.message().to_lowercase();
1910 assert!(msg.contains("arity"), "{msg}");
1911 }
1912
1913 #[test]
1915 fn parse_method_output_type_mismatch_names_the_registered_type() {
1916 let result = parser().parse_str(
1917 r#"
1918 sheet s {
1919 cell a: String = "x";
1920 cell x: i32;
1921 relationship { x := a; }
1922 }
1923 "#,
1924 );
1925 let err = result.expect_err("a String body for an i32 output must be an error");
1926 let msg = err.message();
1927 assert!(msg.contains("got `String`"), "{msg}");
1928 assert!(!msg.contains("::"), "no Rust type path may leak: {msg}");
1929 }
1930
1931 #[test]
1933 fn parse_method_destructured_output_type_mismatch_names_the_registered_type() {
1934 let result = parser().parse_str(
1935 r#"
1936 sheet s {
1937 cell a: String = "x";
1938 cell b: i32 = 1;
1939 cell x: i32;
1940 cell y: i32;
1941 relationship { (x, y) := (a, b); }
1942 }
1943 "#,
1944 );
1945 let err = result.expect_err("a String tuple element for an i32 output must be an error");
1946 let msg = err.message();
1947 assert!(msg.contains("got `String`"), "{msg}");
1948 assert!(!msg.contains("::"), "no Rust type path may leak: {msg}");
1949 }
1950
1951 #[test]
1953 fn parse_method_output_tuple_element_type_mismatch_is_error() {
1954 let result = parser().parse_str(
1955 r#"
1956 sheet s {
1957 cell a: i32 = 1;
1958 cell b: f64 = 2.0;
1959 cell x: i32;
1960 cell y: i32;
1961 relationship { (x, y) := (a, b); }
1962 }
1963 "#,
1964 );
1965 assert!(
1966 result.is_err(),
1967 "f64 tuple element for an i32 output must be an error"
1968 );
1969 let err = result.expect_err("expected Err");
1970 let msg = err.message().to_lowercase();
1971 assert!(msg.contains("type mismatch"), "{msg}");
1972 }
1973
1974 #[test]
1976 fn parse_method_single_output_rejects_tuple_body() {
1977 let result = parser().parse_str(
1978 r#"
1979 sheet s {
1980 cell x: i32 = 1;
1981 cell y: i32;
1982 relationship { y := (x,); }
1983 }
1984 "#,
1985 );
1986 assert!(
1987 result.is_err(),
1988 "1-tuple body for a single declared output must be an error"
1989 );
1990 }
1991
1992 #[test]
1994 fn parse_method_single_tuple_typed_output() {
1995 let mut sheet = parser()
1996 .parse_str(
1997 r#"
1998 sheet s {
1999 cell a: i32 = 3;
2000 cell b: i32 = 4;
2001 cell pair: (i32, i32);
2002 relationship { pair := (a, b); }
2003 }
2004 "#,
2005 )
2006 .unwrap();
2007 sheet.propagate().unwrap();
2008 let (cell_id, _) = sheet.cell_names["pair"].clone();
2009 let value = sheet
2010 .read_compound::<cel_runtime::Cons<
2011 cel_runtime::Leaf<i32>,
2012 cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Nil>,
2013 >>(cell_id)
2014 .unwrap();
2015 let (a, b) = (value.1, value.0.1);
2016 assert_eq!((a, b), (3, 4));
2017 }
2018
2019 #[test]
2021 fn parse_method_tuple_typed_output_among_several() {
2022 let mut sheet = parser()
2023 .parse_str(
2024 r#"
2025 sheet s {
2026 cell a: i32 = 3;
2027 cell b: i32 = 4;
2028 cell pair: (i32, i32);
2029 cell extra: i32;
2030 relationship { (pair, extra) := ((a, b), a); }
2031 }
2032 "#,
2033 )
2034 .unwrap();
2035 sheet.propagate().unwrap();
2036 let (pair_id, _) = sheet.cell_names["pair"].clone();
2037 let (extra_id, _) = sheet.cell_names["extra"].clone();
2038 let pair = sheet
2039 .read_compound::<cel_runtime::Cons<
2040 cel_runtime::Leaf<i32>,
2041 cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Nil>,
2042 >>(pair_id)
2043 .unwrap();
2044 let (a, b) = (pair.1, pair.0.1);
2045 assert_eq!((a, b), (3, 4));
2046 assert_eq!(*sheet.read::<i32>(extra_id).unwrap(), 3);
2047 }
2048
2049 #[test]
2051 fn parse_method_with_tuple_typed_input_supports_field_indexing() {
2052 let mut sheet = parser()
2053 .parse_str(
2054 r#"
2055 sheet s {
2056 cell pair: (i32, i32) = (10, 20);
2057 cell sum: i32;
2058 relationship { sum := pair.0 + pair.1; }
2059 }
2060 "#,
2061 )
2062 .unwrap();
2063 sheet.propagate().unwrap();
2064 let (sum_id, _) = sheet.cell_names["sum"].clone();
2065 assert_eq!(*sheet.read::<i32>(sum_id).unwrap(), 30);
2066 }
2067
2068 #[test]
2070 fn parse_method_tuple_output_shape_mismatch_is_an_error() {
2071 let result = parser().parse_str(
2072 r#"
2073 sheet s {
2074 cell a: i32 = 1;
2075 cell b: f64 = 2.0;
2076 cell pair: (i32, i32);
2077 relationship { pair := (a, b); }
2078 }
2079 "#,
2080 );
2081 assert!(result.is_err());
2082 }
2083
2084 #[test]
2086 fn existing_multi_output_scalar_methods_still_work_unchanged() {
2087 let mut sheet = parser()
2090 .parse_str(
2091 r#"
2092 sheet s {
2093 cell a: i32 = 3;
2094 cell b: i32 = 4;
2095 cell sum: i32;
2096 cell diff: i32;
2097 relationship { (sum, diff) := (a + b, a - b); }
2098 }
2099 "#,
2100 )
2101 .unwrap();
2102 sheet.propagate().unwrap();
2103 let (sum_id, _) = sheet.cell_names["sum"].clone();
2104 let (diff_id, _) = sheet.cell_names["diff"].clone();
2105 assert_eq!(*sheet.read::<i32>(sum_id).unwrap(), 7);
2106 assert_eq!(*sheet.read::<i32>(diff_id).unwrap(), -1);
2107 }
2108
2109 #[test]
2111 fn parse_method_single_empty_tuple_typed_output() {
2112 let mut sheet = parser()
2113 .parse_str(
2114 r#"
2115 sheet s {
2116 cell x: i32 = 1;
2117 cell nothing: ();
2118 // `()` alone references no cell, and adam_rs rejects a method with no
2119 // inputs -- reference `x` via an if/else whose branches both still
2120 // evaluate to `()`, so the binding has a deduced input.
2121 relationship { nothing := if x > 0 { () } else { () }; }
2122 }
2123 "#,
2124 )
2125 .unwrap();
2126 sheet.propagate().unwrap();
2127 let (cell_id, _) = sheet.cell_names["nothing"].clone();
2128 sheet.read_compound::<cel_runtime::Nil>(cell_id).unwrap();
2129 }
2130
2131 #[test]
2133 fn parse_conditional_decl() {
2134 let _sheet = parser()
2135 .parse_str(
2136 r#"
2137 sheet image_resize {
2138 cell width: f64 = 1920.0;
2139 cell height: f64 = 1080.0;
2140 cell ratio: f64 = 1.0;
2141 cell mode: i32 = 0;
2142 conditional mode {
2143 0i32 => {
2144 relationship { height := width; }
2145 }
2146 1i32 => {
2147 relationship { height := width * ratio; }
2148 }
2149 _ => {
2150 relationship { height := width; }
2151 }
2152 }
2153 }
2154 "#,
2155 )
2156 .unwrap();
2157 }
2158
2159 #[test]
2161 fn parse_conditional_branch_with_multiple_relationships() {
2162 let mut sheet = parser()
2163 .parse_str(
2164 r#"
2165 sheet s {
2166 cell mode: i32 = 0;
2167 cell a: f64 = 2.0;
2168 cell b: f64 = 3.0;
2169 cell c: f64;
2170 cell d: f64 = 4.0;
2171 cell e: f64 = 5.0;
2172 cell f: f64;
2173 conditional mode {
2174 0i32 => {
2175 relationship { c := a * b; }
2176 relationship { f := d * e; }
2177 }
2178 }
2179 }
2180 "#,
2181 )
2182 .unwrap();
2183 sheet.propagate().unwrap();
2184 let c = sheet.cell_names["c"].0;
2185 let f = sheet.cell_names["f"].0;
2186 assert_eq!(*sheet.read::<f64>(c).unwrap(), 6.0);
2187 assert_eq!(*sheet.read::<f64>(f).unwrap(), 20.0);
2188 let a = sheet.cell_names["a"].0;
2189 sheet.write(a, 7.0f64).unwrap();
2190 sheet.propagate().unwrap();
2191 assert_eq!(*sheet.read::<f64>(c).unwrap(), 21.0);
2192 assert_eq!(*sheet.read::<f64>(f).unwrap(), 20.0);
2193 let d = sheet.cell_names["d"].0;
2194 sheet.write(d, 6.0f64).unwrap();
2195 sheet.propagate().unwrap();
2196 assert_eq!(*sheet.read::<f64>(c).unwrap(), 21.0);
2197 assert_eq!(*sheet.read::<f64>(f).unwrap(), 30.0);
2198 }
2199
2200 #[test]
2202 fn conditional_branch_bare_binding_without_relationship_wrapper_is_error() {
2203 let result = parser().parse_str(
2204 r#"
2205 sheet s {
2206 cell x: i32 = 0;
2207 conditional x { 0i32 => { x := x; } }
2208 }
2209 "#,
2210 );
2211 assert!(
2212 result.is_err(),
2213 "a conditional_branch body now requires relationship_decl, not a bare binding"
2214 );
2215 }
2216
2217 #[test]
2219 fn conditional_undeclared_match_cell_is_error() {
2220 let result = parser().parse_str(
2221 r#"
2222 sheet s {
2223 cell x: i32 = 0;
2224 conditional bogus { 0i32 => { relationship { x := x; } } }
2225 }
2226 "#,
2227 );
2228 assert!(result.is_err());
2229 let err = result.expect_err("expected Err");
2230 let msg = err.message().to_lowercase();
2231 assert!(msg.contains("bogus") || msg.contains("undeclared"), "{msg}");
2232 }
2233
2234 #[test]
2236 fn conditional_branch_literal_type_mismatch_is_error() {
2237 let result = parser().parse_str(
2238 r#"
2239 sheet s {
2240 cell mode: i32 = 0;
2241 cell x: f64 = 0.0;
2242 conditional mode { 1.0 => { relationship { x := x; } } }
2243 }
2244 "#,
2245 );
2246 assert!(
2247 result.is_err(),
2248 "float literal for i32 match cell must be an error"
2249 );
2250 }
2251
2252 #[test]
2254 fn conditional_branch_negative_literal_key_matches() {
2255 let mut sheet = parser()
2256 .parse_str(
2257 r#"
2258 sheet s {
2259 cell mode: i32 = -1;
2260 cell x: i32 = 1;
2261 cell y: i32 = 0;
2262 conditional mode {
2263 -1i32 => { relationship { y := x; } }
2264 }
2265 }
2266 "#,
2267 )
2268 .unwrap();
2269 sheet.propagate().unwrap();
2270 let (y_id, _) = sheet.cell_names["y"].clone();
2271 assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
2272 }
2273
2274 #[test]
2276 fn conditional_branch_negating_unsigned_literal_key_is_error() {
2277 let result = parser().parse_str(
2278 r#"
2279 sheet s {
2280 cell mode: u32 = 0;
2281 cell x: i32 = 1;
2282 cell y: i32 = 0;
2283 conditional mode {
2284 -1u32 => { relationship { y := x; } }
2285 }
2286 }
2287 "#,
2288 );
2289 assert!(
2290 result.is_err(),
2291 "negating an unsigned literal must be a parse error"
2292 );
2293 }
2294
2295 #[test]
2297 fn conditional_with_tuple_typed_match_cell_and_default_only_still_works() {
2298 let mut sheet = parser()
2301 .parse_str(
2302 r#"
2303 sheet s {
2304 cell mode: (i32, i32) = (0, 0);
2305 cell x: f64 = 1.0;
2306 cell y: f64;
2307 conditional mode {
2308 _ => { relationship { y := x * 2.0; } }
2309 }
2310 }
2311 "#,
2312 )
2313 .unwrap();
2314 sheet.propagate().unwrap();
2315 let (y_id, _) = sheet.cell_names["y"].clone();
2316 assert_eq!(*sheet.read::<f64>(y_id).unwrap(), 2.0);
2317 }
2318
2319 #[test]
2321 fn conditional_branch_tuple_literal_key_is_error() {
2322 let result = parser().parse_str(
2323 r#"
2324 sheet s {
2325 cell mode: (i32, i32) = (0, 0);
2326 cell x: f64 = 1.0;
2327 cell y: f64;
2328 conditional mode {
2329 (0, 0) => { relationship { y := x; } }
2330 _ => { relationship { y := x * 2.0; } }
2331 }
2332 }
2333 "#,
2334 );
2335 assert!(result.is_err(), "tuple branch key must be a parse error");
2336 }
2337
2338 #[test]
2340 fn conditional_on_a_two_cell_boolean_expression_activates_and_reacts_to_writes() {
2341 let mut sheet = parser()
2342 .parse_str(
2343 r#"
2344 sheet s {
2345 cell a: bool = false;
2346 cell b: bool = false;
2347 cell x: i32 = 1;
2348 cell y: i32 = 0;
2349 conditional a && b {
2350 true => { relationship { y := x; } }
2351 }
2352 }
2353 "#,
2354 )
2355 .unwrap();
2356 let (a_id, _) = sheet.cell_names["a"].clone();
2357 let (b_id, _) = sheet.cell_names["b"].clone();
2358 let (y_id, _) = sheet.cell_names["y"].clone();
2359
2360 sheet.write(a_id, true).unwrap();
2361 sheet.write(b_id, false).unwrap();
2362 sheet.propagate().unwrap();
2363 assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 0);
2364
2365 sheet.write(b_id, true).unwrap();
2366 sheet.propagate().unwrap();
2367 assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
2368 }
2369
2370 #[test]
2372 fn conditional_expression_referencing_the_same_cell_twice_compiles_and_evaluates() {
2373 let mut sheet = parser()
2374 .parse_str(
2375 r#"
2376 sheet s {
2377 cell a: bool = true;
2378 cell x: i32 = 1;
2379 cell y: i32 = 0;
2380 conditional a && a {
2381 true => { relationship { y := x; } }
2382 }
2383 }
2384 "#,
2385 )
2386 .unwrap();
2387 sheet.propagate().unwrap();
2388 let (y_id, _) = sheet.cell_names["y"].clone();
2389 assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
2390 }
2391
2392 #[test]
2394 fn conditional_bare_identifier_match_subject_still_works() {
2395 let mut sheet = parser()
2396 .parse_str(
2397 r#"
2398 sheet s {
2399 cell mode: i32 = 0;
2400 cell x: i32 = 1;
2401 cell y: i32 = 0;
2402 conditional mode {
2403 1i32 => { relationship { y := x; } }
2404 }
2405 }
2406 "#,
2407 )
2408 .unwrap();
2409 let (mode_id, _) = sheet.cell_names["mode"].clone();
2410 let (y_id, _) = sheet.cell_names["y"].clone();
2411
2412 sheet.propagate().unwrap();
2413 assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 0);
2414
2415 sheet.write(mode_id, 1_i32).unwrap();
2416 sheet.propagate().unwrap();
2417 assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
2418 }
2419
2420 #[test]
2422 fn adam_registered_match_header_reserves_braces() {
2423 let mut lookup = OpLookup::new();
2424 lookup
2425 .register_record(cel_parser::RecordSchema {
2426 ty: cel_parser::ResolvedLeafType::new(
2427 "Gate",
2428 cel_runtime::ValueType::leaf::<bool>(),
2429 ),
2430 fields: vec![cel_parser::RecordField {
2431 name: "value",
2432 ty: cel_parser::ResolvedLeafType::new(
2433 "bool",
2434 cel_runtime::ValueType::leaf::<bool>(),
2435 )
2436 .into(),
2437 }],
2438 emit: Box::new(|segment, _, _, _| segment.op1(|value: bool| value)),
2439 })
2440 .unwrap();
2441 let mut parser = AdamParser::new(TypeRegistry::new(), lookup);
2442 let mut sheet = parser
2443 .parse_str(
2444 "sheet s {
2445 cell flag: bool = true;
2446 cell input: i32 = 7;
2447 cell output: i32 = 0;
2448 conditional (Gate { value: flag }) {
2449 true => { relationship { output := if flag { input } else { 0 }; } }
2450 _ => { relationship { output := 0; } }
2451 }
2452 }",
2453 )
2454 .unwrap();
2455 let output = sheet.cell_names["output"].0;
2456 let flag = sheet.cell_names["flag"].0;
2457 sheet.propagate().unwrap();
2458 assert_eq!(*sheet.read::<i32>(output).unwrap(), 7);
2459 sheet.write(flag, false).unwrap();
2460 sheet.propagate().unwrap();
2461 assert_eq!(*sheet.read::<i32>(output).unwrap(), 0);
2462 }
2463
2464 #[test]
2466 fn conditional_branch_bare_identifier_key_is_error() {
2467 let result = parser().parse_str(
2470 r#"
2471 sheet s {
2472 cell mode: i32 = 0;
2473 cell x: i32 = 1;
2474 cell y: i32 = 0;
2475 conditional mode {
2476 mode => { relationship { y := x; } },
2477 }
2478 }
2479 "#,
2480 );
2481 assert!(
2482 result.is_err(),
2483 "bare identifier branch key must be a parse error"
2484 );
2485 }
2486
2487 #[test]
2489 fn conditional_expression_referencing_an_undeclared_identifier_is_a_parse_error() {
2490 let result = parser().parse_str(
2491 r#"
2492 sheet s {
2493 cell a: bool = true;
2494 conditional a && nope {
2495 true => { relationship { a := a; } }
2496 }
2497 }
2498 "#,
2499 );
2500 assert!(result.is_err());
2501 }
2502
2503 #[test]
2505 fn parse_cell_literal_type_mismatch_is_error() {
2506 let result = parser().parse_str("sheet s { cell x: i32 = 1.0; }");
2508 assert!(
2509 result.is_err(),
2510 "float literal for i32 annotation must be an error"
2511 );
2512 }
2513
2514 #[test]
2516 fn parse_and_propagate_sheet() {
2517 let mut p = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2518 let mut sheet = p
2519 .parse_str(
2520 r#"
2521 sheet image_resize {
2522 cell area: f64;
2523 cell width: f64 = 4.0;
2524 cell height: f64 = 3.0;
2525 relationship {
2526 area := width * height;
2527 width := area / height;
2528 height := area / width;
2529 }
2530 }
2531 "#,
2532 )
2533 .unwrap();
2534
2535 sheet.propagate().unwrap();
2536 let area = sheet.cell_names["area"].0;
2537 let width = sheet.cell_names["width"].0;
2538 assert_eq!(*sheet.read::<f64>(area).unwrap(), 12.0);
2539 sheet.write(area, 24.0f64).unwrap();
2540 sheet.propagate().unwrap();
2541 assert_eq!(*sheet.read::<f64>(width).unwrap(), 8.0);
2542 }
2543
2544 #[test]
2546 fn parse_duplicate_cell_is_error() {
2547 let result = parser().parse_str("sheet s { cell x: i32; cell x: f64; }");
2548 match result {
2549 Ok(_) => panic!("expected error for duplicate cell name"),
2550 Err(e) => {
2551 let msg = e.to_string();
2552 assert!(
2553 msg.contains("duplicate cell `x`"),
2554 "error mentions duplicate name: {msg}"
2555 );
2556 }
2557 }
2558 }
2559
2560 #[test]
2562 fn parse_str_returns_cell_names_in_declaration_order() {
2563 let parsed = parser()
2564 .parse_str("sheet s { cell z: i32 = 1; cell a: i32 = 2; cell m: i32 = 3; }")
2565 .unwrap();
2566 let names: Vec<&str> = parsed.cell_names.keys().map(String::as_str).collect();
2567 assert_eq!(names, vec!["z", "a", "m"]);
2568 }
2569
2570 #[test]
2572 fn parsed_sheet_derefs_to_sheet_for_propagate() {
2573 let mut parsed = parser().parse_str("sheet s { cell x: i32 = 1; }").unwrap();
2574 parsed.propagate().unwrap();
2576 }
2577
2578 #[test]
2580 fn parse_out_with_explicit_type_propagates_correctly() {
2581 let mut sheet = parser()
2582 .parse_str(
2583 r#"
2584 sheet s {
2585 cell width: f64 = 4.0;
2586 cell height: f64 = 3.0;
2587 out area: f64 := width * height;
2588 }
2589 "#,
2590 )
2591 .unwrap();
2592 sheet.propagate().unwrap();
2593 let cell_id = *sheet.output_names.get("area").expect("area registered");
2594 assert_eq!(*sheet.read::<f64>(cell_id).unwrap(), 12.0);
2595 }
2596
2597 #[test]
2599 fn parse_out_with_tuple_type_value_debug_formats_correctly() {
2600 let mut sheet = parser()
2601 .parse_str(
2602 r#"
2603 sheet s {
2604 cell x: i32 = 3;
2605 out pair: (i32, i32) := (x, x);
2606 }
2607 "#,
2608 )
2609 .unwrap();
2610 sheet.propagate().unwrap();
2611 let cell_id = *sheet.output_names.get("pair").unwrap();
2612 let value = sheet.read::<CellValue>(cell_id).unwrap();
2613 assert_eq!(format!("{value:?}"), "(3, 3)");
2614 }
2615
2616 #[test]
2618 fn parse_out_with_no_annotation_infers_type_from_writer_body() {
2619 let sheet = parser()
2620 .parse_str(
2621 r#"
2622 sheet s {
2623 cell width: f64 = 4.0;
2624 out doubled := width + width;
2625 }
2626 "#,
2627 )
2628 .unwrap();
2629 let (_, shape) = sheet.cell_names.get("doubled").unwrap().clone();
2630 assert_eq!(shape, TypeShape::Named(std::any::TypeId::of::<f64>()));
2631 }
2632
2633 #[test]
2635 fn parse_out_type_mismatch_is_error() {
2636 let result = parser().parse_str(
2637 r#"
2638 sheet s {
2639 cell width: f64 = 4.0;
2640 out area: i32 := width;
2641 }
2642 "#,
2643 );
2644 assert!(
2645 result.is_err(),
2646 "f64 body for an i32 annotation must be an error"
2647 );
2648 }
2649
2650 #[test]
2652 fn parse_out_with_requirements_reports_output_valid_and_violated() {
2653 let mut sheet = parser()
2654 .parse_str(
2655 r#"
2656 sheet s {
2657 cell width: f64 = 4.0;
2658 cell height: f64 = 3.0;
2659 cell max_area: f64 = 100.0;
2660 out area: f64 := width * height require {
2661 @max_area width * height <= max_area;
2662 };
2663 }
2664 "#,
2665 )
2666 .unwrap();
2667 sheet.propagate().unwrap();
2668 let output_id = *sheet.output_names.get("area").unwrap();
2669 assert!(sheet.cell_requirements_valid(output_id));
2670 assert_eq!(sheet.violated_requirements(output_id).count(), 0);
2671 }
2672
2673 #[test]
2675 fn parse_out_requirement_violation_is_reported_after_propagate() {
2676 let mut sheet = parser()
2677 .parse_str(
2678 r#"
2679 sheet s {
2680 cell width: f64 = 40.0;
2681 cell height: f64 = 30.0;
2682 cell max_area: f64 = 100.0;
2683 out area: f64 := width * height require {
2684 @max_area width * height <= max_area;
2685 };
2686 }
2687 "#,
2688 )
2689 .unwrap();
2690 sheet.propagate().unwrap();
2691 let output_id = *sheet.output_names.get("area").unwrap();
2692 assert!(!sheet.cell_requirements_valid(output_id));
2693 assert_eq!(sheet.violated_requirements(output_id).count(), 1);
2694 }
2695
2696 #[test]
2698 fn requirement_names_are_metadata_for_explicit_labels_only() {
2700 let mut parsed = parser()
2701 .parse_str(
2702 "sheet s {
2703 cell x: i32 = 1 require { @same x > 0i32; x > 0i32; x < 9i32; };
2704 source y: i32 = 2 require { @same y > 0i32; y > 0i32; };
2705 out z := x require { @last z < 0i32; z < 0i32; @first z > 9i32; };
2706 }",
2707 )
2708 .unwrap();
2709 let x = parsed.cell_names["x"].0;
2710 let y = parsed.cell_names["y"].0;
2711 let z = parsed.output_names["z"];
2712 assert_eq!(parsed.requirement_names.len(), 4);
2713 for cell in [x, y] {
2714 let ids = parsed.cell_requirements(cell).unwrap();
2715 assert_eq!(parsed.requirement_names[&ids[0]], "same");
2716 assert!(!parsed.requirement_names.contains_key(&ids[1]));
2717 }
2718 parsed.propagate().unwrap();
2719 let violated: Vec<_> = parsed.violated_requirements(z).collect();
2720 assert_eq!(violated, parsed.cell_requirements(z).unwrap());
2721 assert_eq!(parsed.requirement_names[&violated[0]], "last");
2722 assert!(!parsed.requirement_names.contains_key(&violated[1]));
2723 assert_eq!(parsed.requirement_names[&violated[2]], "first");
2724 }
2725
2726 #[test]
2728 fn duplicate_requirement_labels_are_rejected_per_declaration() {
2730 for declaration in [
2731 "cell x: i32 = 1",
2732 "source x: i32 = 1",
2733 "cell y: i32 = 1; out x := y",
2734 ] {
2735 let source =
2736 format!("sheet s {{ {declaration} require {{ @dup x > 0i32; @dup x < 9i32; }}; }}");
2737 assert!(parser().parse_str(&source).is_err(), "{source}");
2738 }
2739 }
2740
2741 #[test]
2743 fn parse_out_with_an_unlabeled_requirement_is_enforced() {
2744 let mut sheet = parser()
2745 .parse_str(
2746 r#"
2747 sheet s {
2748 cell width: f64 = 40.0;
2749 cell height: f64 = 30.0;
2750 cell max_area: f64 = 100.0;
2751 out area: f64 := width * height require {
2752 width * height <= max_area;
2753 };
2754 }
2755 "#,
2756 )
2757 .unwrap();
2758 sheet.propagate().unwrap();
2759 let output_id = *sheet.output_names.get("area").unwrap();
2760 assert_eq!(sheet.violated_requirements(output_id).count(), 1);
2761 }
2762
2763 #[test]
2765 fn parse_out_with_a_bare_identifier_unlabeled_requirement_is_enforced() {
2766 let mut sheet = parser()
2767 .parse_str(
2768 r#"
2769 sheet s {
2770 cell is_valid: bool = false;
2771 out area: i32 := 4 require {
2772 is_valid;
2773 };
2774 }
2775 "#,
2776 )
2777 .unwrap();
2778 sheet.propagate().unwrap();
2779 let output_id = *sheet.output_names.get("area").unwrap();
2780 assert_eq!(sheet.violated_requirements(output_id).count(), 1);
2781 }
2782
2783 #[test]
2785 fn parse_out_cell_referenced_elsewhere_succeeds() {
2786 let result = parser().parse_str(
2789 r#"
2790 sheet s {
2791 cell width: f64 = 4.0;
2792 cell height: f64 = 3.0;
2793 out area: f64 := width * height;
2794 relationship { width := area; }
2795 }
2796 "#,
2797 );
2798 assert!(
2799 result.is_ok(),
2800 "referencing an out cell as another relationship's input must not be an error"
2801 );
2802 }
2803
2804 #[test]
2806 fn parse_out_infers_structural_array_without_registration() {
2807 let result = parser().parse_str(
2808 r#"
2809 sheet s {
2810 cell dummy: i32 = 0;
2811 out x := []: [i32];
2812 }
2813 "#,
2814 );
2815 let mut sheet = result.unwrap();
2816 sheet.propagate().unwrap();
2817 assert!(
2818 sheet
2819 .read_compound::<cel_runtime::ArrayOf<cel_runtime::Leaf<i32>>>(
2820 sheet.cell_names["x"].0
2821 )
2822 .unwrap()
2823 .is_empty()
2824 );
2825 }
2826
2827 #[test]
2830 fn empty_tuple_output_executes_its_writer() {
2831 let mut lookup = OpLookup::new();
2832 lookup.push_scope(|name, segment, arity, _| {
2833 if name == "bad" && arity == 0 {
2834 segment.op0r(|| -> anyhow::Result<i32> { anyhow::bail!("unit writer failed") });
2835 return Ok(true);
2836 }
2837 Ok(false)
2838 });
2839 let mut sheet = AdamParser::new(TypeRegistry::new(), lookup)
2840 .parse_str("sheet s { cell input: i32 = 0; cell nothing: (); relationship { nothing := (input, bad, ()).2; } }")
2841 .unwrap();
2842 let error = sheet.propagate().unwrap_err();
2843 assert!(error.to_string().contains("unit writer failed"), "{error}");
2844 }
2845
2846 #[test]
2848 fn parse_out_cell_referenced_in_conditional_succeeds() {
2849 let result = parser().parse_str(
2852 r#"
2853 sheet s {
2854 cell width: f64 = 4.0;
2855 cell height: f64 = 3.0;
2856 cell mode: i32 = 0;
2857 out area: f64 := width * height;
2858 conditional mode {
2859 0i32 => { relationship { width := area; } }
2860 }
2861 }
2862 "#,
2863 );
2864 assert!(
2865 result.is_ok(),
2866 "referencing an out cell as a conditional branch relationship's input must not be an error"
2867 );
2868 }
2869
2870 #[test]
2872 fn parse_cell_with_explicit_tuple_type_and_initializer() {
2873 let mut p = parser();
2874 let parsed = p
2875 .parse_str("sheet s { cell a: (i32, f64) = (1, 2.5); }")
2876 .unwrap();
2877 let (cell_id, shape) = parsed.cell_names["a"].clone();
2878 assert_eq!(
2879 shape,
2880 TypeShape::Tuple(vec![
2881 TypeShape::Named(std::any::TypeId::of::<i32>()),
2882 TypeShape::Named(std::any::TypeId::of::<f64>()),
2883 ])
2884 );
2885 let value = parsed
2886 .read_compound::<cel_runtime::Cons<
2887 cel_runtime::Leaf<i32>,
2888 cel_runtime::Cons<cel_runtime::Leaf<f64>, cel_runtime::Nil>,
2889 >>(cell_id)
2890 .unwrap();
2891 let (a, b) = (value.1, value.0.1);
2892 assert_eq!((a, b), (1, 2.5));
2893 }
2894
2895 #[test]
2897 fn parse_cell_with_tuple_type_and_no_initializer_uses_recursive_default() {
2898 let parsed = parser()
2899 .parse_str("sheet s { cell a: (i32, f64); }")
2900 .unwrap();
2901 let (cell_id, _) = parsed.cell_names["a"].clone();
2902 let value = parsed
2903 .read_compound::<cel_runtime::Cons<
2904 cel_runtime::Leaf<i32>,
2905 cel_runtime::Cons<cel_runtime::Leaf<f64>, cel_runtime::Nil>,
2906 >>(cell_id)
2907 .unwrap();
2908 let (a, b) = (value.1, value.0.1);
2909 assert_eq!((a, b), (0, 0.0));
2910 }
2911
2912 #[test]
2914 fn parse_cell_with_tuple_initializer_arity_mismatch_is_an_error() {
2915 let result = parser().parse_str("sheet s { cell a: (i32, f64, i32) = (1, 2.5); }");
2916 assert!(result.is_err());
2917 }
2918
2919 #[test]
2921 fn parse_cell_with_nested_tuple_type_round_trips() {
2922 let parsed = parser()
2923 .parse_str("sheet s { cell a: (i32, (f64, String)) = (1, (2.5, \"x\")); }")
2924 .unwrap();
2925 let (cell_id, _) = parsed.cell_names["a"].clone();
2926 type Inner = cel_runtime::Cons<
2928 cel_runtime::Leaf<f64>,
2929 cel_runtime::Cons<cel_runtime::Leaf<String>, cel_runtime::Nil>,
2930 >;
2931 type Outer =
2933 cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Cons<Inner, cel_runtime::Nil>>;
2934 let value = parsed.read_compound::<Outer>(cell_id).unwrap();
2935 assert_eq!(value.1, 1);
2936 let (b, c) = (value.0.1.1, value.0.1.0.1);
2937 assert_eq!((b, c), (2.5, "x".to_string()));
2938 }
2939
2940 #[test]
2942 fn parse_out_with_explicit_tuple_type_infers_and_stores_correctly() {
2943 let mut sheet = parser()
2944 .parse_str(
2945 r#"
2946 sheet s {
2947 cell x: i32 = 3;
2948 out pair: (i32, i32) := (x, x);
2949 }
2950 "#,
2951 )
2952 .unwrap();
2953 sheet.propagate().unwrap();
2954 let cell_id = *sheet.output_names.get("pair").unwrap();
2955 let value = sheet
2956 .read_compound::<cel_runtime::Cons<
2957 cel_runtime::Leaf<i32>,
2958 cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Nil>,
2959 >>(cell_id)
2960 .unwrap();
2961 let (a, b) = (value.1, value.0.1);
2962 assert_eq!((a, b), (3, 3));
2963 }
2964
2965 #[test]
2967 fn parse_cell_range_initializer_fails_cleanly_at_type_inference_not_grammar() {
2968 let result = parser().parse_str("sheet s { cell x = 1i32..5i32; }");
2973 assert!(result.is_err());
2974 let err = result.expect_err("expected Err");
2975 assert!(err.message().contains("cannot infer a type"));
2976 }
2977
2978 #[test]
2981 fn shape_error_out_preserves_context_and_span() {
2982 for expression in ["1i32..5i32", "(1i32, 1i32..5i32)"] {
2983 let source = format!("sheet s {{\nout result := {expression};\n}}");
2984 let err = parser().parse_str(&source).unwrap_err();
2985 assert!(err.message().contains("cannot infer a type"));
2986 assert_eq!(
2987 err.span().start(),
2988 proc_macro2::LineColumn { line: 2, column: 4 }
2989 );
2990 assert_eq!(
2991 err.span().end(),
2992 proc_macro2::LineColumn {
2993 line: 2,
2994 column: 10
2995 }
2996 );
2997 assert!(err.message().starts_with("out `result`:"));
2998 }
2999 }
3000
3001 #[test]
3004 fn shape_error_match_preserves_context_and_span() {
3005 for (expression, end_column) in [("1i32..5i32", 16), ("(1i32, 1i32..5i32)", 30)] {
3006 let source = format!("sheet s {{\nconditional {expression} {{ _ => {{}} }}\n}}");
3007 let err = parser().parse_str(&source).unwrap_err();
3008 assert!(err.message().contains("cannot infer a type"));
3009 assert_eq!(
3010 err.span().start(),
3011 proc_macro2::LineColumn {
3012 line: 2,
3013 column: 12
3014 }
3015 );
3016 assert_eq!(
3017 err.span().end(),
3018 proc_macro2::LineColumn {
3019 line: 2,
3020 column: end_column
3021 }
3022 );
3023 assert!(err.message().starts_with("match expression:"));
3024 }
3025 }
3026
3027 #[test]
3030 fn shape_error_output_preserves_context_and_span() {
3031 for (shape, expression) in [("i32", "1i32..5i32"), ("(i32, i32)", "(1i32, 1i32..5i32)")] {
3032 let source = format!(
3033 "sheet s {{\ncell result: {shape};\nrelationship {{\nresult := {expression};\n}}\n}}"
3034 );
3035 let err = parser().parse_str(&source).unwrap_err();
3036 assert!(err.message().contains("cannot infer a type"));
3037 assert_eq!(
3038 err.span().start(),
3039 proc_macro2::LineColumn { line: 4, column: 0 }
3040 );
3041 assert_eq!(
3042 err.span().end(),
3043 proc_macro2::LineColumn { line: 4, column: 6 }
3044 );
3045 assert!(err.message().starts_with("output `result`:"));
3046 }
3047 }
3048
3049 #[test]
3052 fn shape_error_initializer_preserves_context_and_span() {
3053 for expression in ["1i32..5i32", "(1i32, 1i32..5i32)"] {
3054 let source = format!("sheet s {{\ncell result = {expression};\n}}");
3055 let err = parser().parse_str(&source).unwrap_err();
3056 assert!(err.message().contains("cannot infer a type"));
3057 assert_eq!(
3058 err.span().start(),
3059 proc_macro2::LineColumn { line: 2, column: 5 }
3060 );
3061 assert_eq!(
3062 err.span().end(),
3063 proc_macro2::LineColumn {
3064 line: 2,
3065 column: 11
3066 }
3067 );
3068 assert!(err.message().starts_with("cell `result`:"));
3069 }
3070 }
3071
3072 #[test]
3075 fn shape_error_source_preserves_context_and_span() {
3076 for (declaration, message) in [
3077 ("source result: i32 = true;", "type mismatch"),
3078 ("source result = 1i32..5i32;", "cannot infer a type"),
3079 ("source result = (1i32, 1i32..5i32);", "cannot infer a type"),
3080 ] {
3081 let source = format!("sheet s {{\n{declaration}\n}}");
3082 let err = parser().parse_str(&source).unwrap_err();
3083 assert!(err.message().contains(message));
3084 assert!(
3085 err.message().starts_with("source `result`:"),
3086 "{}",
3087 err.message()
3088 );
3089 assert_eq!(
3090 err.span().start(),
3091 proc_macro2::LineColumn { line: 2, column: 7 }
3092 );
3093 assert_eq!(
3094 err.span().end(),
3095 proc_macro2::LineColumn {
3096 line: 2,
3097 column: 13
3098 }
3099 );
3100 }
3101 }
3102
3103 #[test]
3106 fn shape_error_registered_mismatches_preserve_context() {
3107 for (source, prefix) in [
3108 ("sheet s { out result: i32 := true; }", "out `result`:"),
3109 (
3110 "sheet s { cell result: i32; relationship { result := true; } }",
3111 "output `result`:",
3112 ),
3113 ] {
3114 let err = parser().parse_str(source).unwrap_err();
3115 assert!(err.message().contains("type mismatch"));
3116 assert!(err.message().starts_with(prefix));
3117 }
3118 }
3119
3120 #[test]
3122 fn dependency_cycle_validation_error_spans_the_governed_cell() {
3123 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
3124 let source = "sheet s {\n cell mode: i32 = 0;\n cell other: i32 = 0;\n\n conditional mode {\n 0i32 => {\n relationship {\n mode := other;\n other := mode;\n }\n }\n }\n}";
3127 let err = parser.parse_str(source).unwrap_err();
3128 assert_eq!(err.span().start().line, 2);
3129 }
3130
3131 #[test]
3143 fn conditional_invalid_relationship_and_cell_sites_render_as_secondary_labels() {
3144 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
3145 let source = "sheet s {\n cell a: i32 = 0;\n cell b: i32 = 0;\n cell p: i32 = 0;\n relationship {\n p := a;\n }\n\n conditional p {\n 0i32 => {\n relationship {\n a := b;\n }\n }\n }\n}";
3149 let err = parser.parse_str(source).unwrap_err();
3150 let out =
3151 err.format_rustc_style(source, "t.adm2", 1, &annotate_snippets::Renderer::plain());
3152 assert!(out.contains("cell `p` is part of the cycle"), "{out}");
3153 }
3154
3155 #[test]
3157 fn out_decl_duplicate_requirement_error_spans_the_duplicate_label() {
3159 let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
3160 let source = "sheet s {\n cell w: i32 = 1;\n out a: i32 := w require {\n @pos a > 0;\n @pos a > 0;\n };\n}";
3163 let err = parser.parse_str(source).unwrap_err();
3164 assert_eq!(err.span().start().line, 5);
3165 assert!(err.to_string().contains("duplicate requirement `pos`"));
3166 }
3167}