Skip to main content

adam_lang/
parser.rs

1//! Builds live roots through the checked module compiler and native instantiator.
2//!
3//! See the crate root's [`# Grammar`](crate#grammar) section for the full EBNF.
4
5use 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
20/// Parser result type.
21pub type Result<T> = std::result::Result<T, ParseError>;
22
23/// Checks a complete shape against an existing canonical descriptor without a registry.
24///
25/// - Complexity: O(n) descriptor nodes.
26fn 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
52// ---------------------------------------------------------------------------
53// ParsedSheet
54// ---------------------------------------------------------------------------
55
56/// Owns a live [`Sheet`], ordered public root names, complete shapes and provenance.
57///
58/// Derefs to [`Sheet`] so callers that only need sheet methods (e.g.
59/// `propagate`) can use the result exactly as if it were a `Sheet`.
60/// [`Self::new`] constructs a source-free destination; [`AdamParser::parse_str`]
61/// retains the ordinary source-parsing convenience.
62///
63/// # Examples
64///
65/// ```
66/// use adam_lang::{AdamParser, TypeRegistry};
67/// use cel_parser::OpLookup;
68/// let parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
69///     .parse_str("sheet s { cell x: i32 = 1 require { @positive x > 0i32; }; }")?;
70/// let id = parsed.cell_requirements(parsed.cell_names["x"].0).unwrap()[0];
71/// assert_eq!(parsed.requirement_names[&id], "positive");
72/// # Ok::<(), cel_parser::ParseError>(())
73/// ```
74pub struct ParsedSheet {
75    /// The constructed sheet.
76    pub sheet: Sheet,
77    /// Cell name → `(CellId, TypeShape)`, in declaration order.
78    pub cell_names: IndexMap<String, (CellId, TypeShape)>,
79    /// Output name → `CellId`, in declaration order — parity with `cell_names`, for callers
80    /// that need to look up `Sheet::cell_requirements_valid`/`Sheet::violated_requirements` by
81    /// name.
82    pub output_names: IndexMap<String, CellId>,
83    /// Explicit `@name` labels keyed by requirement ID; unnamed requirements have no entry.
84    /// Lookup takes expected O(1) time. Iterate the sheet's requirement IDs, not this map,
85    /// to preserve attachment or violation order.
86    pub requirement_names: HashMap<RequirementId, String>,
87    /// Requirement IDs mapped to their authored source labels.
88    pub requirement_labels: HashMap<RequirementId, crate::SourceLabel>,
89    /// `(RelationshipId, method index)` → the source span of a root-local binding.
90    /// Lets a caller translate an `adam_rs::Error`'s
91    /// `ErrorSite::Method` (raised well after parsing, e.g. from `Sheet::propagate`) back
92    /// to a source location.
93    ///
94    /// This legacy single-source view excludes included component methods and output
95    /// writers. Their complete owning metadata remains in `method_origins` and
96    /// `instance_origins`.
97    pub method_spans: HashMap<(RelationshipId, usize), SourceSpan>,
98    /// `RelationshipId` → the source span of a root-local `relationship { ... }` block.
99    /// Resolves an `adam_rs::Error`'s
100    /// `ErrorSite::Relationship` (e.g. from `Error::Conflict`) back to a source location.
101    pub relationship_spans: HashMap<RelationshipId, SourceSpan>,
102    /// `CellId` → the source span of a root-local `cell`, `source`, or `out` name.
103    /// Included private declarations retain their owning locations in `cell_origins`.
104    pub cell_spans: HashMap<CellId, SourceSpan>,
105    /// Retains independent source identities used by instantiated definitions.
106    pub sources: Vec<crate::SourceId>,
107    /// Identifies the anonymous convenience frontend's source for display-name overrides.
108    pub(crate) anonymous_source: Option<crate::SourceId>,
109    /// Retains parent-linked provenance for direct template invocations.
110    pub instance_origins: Vec<crate::composition::InstanceOrigin>,
111    /// Maps compact live method indices to occurrence and authored method identity.
112    pub method_origins: HashMap<(RelationshipId, usize), adam_rs::MethodOrigin>,
113    /// Retains every occurrence-local declaration/formal mapped to each live cell.
114    pub cell_origins: HashMap<CellId, Vec<crate::composition::CellOrigin>>,
115    /// Retains full private shapes without exposing private names in root maps.
116    pub(crate) cell_shapes: HashMap<CellId, TypeShape>,
117}
118
119impl std::fmt::Debug for ParsedSheet {
120    /// Formats public name and root-span metadata without evaluating the sheet.
121    ///
122    /// - Complexity: O(b) time and formatter output space for b rendered bytes.
123    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    /// Constructs an empty source-free destination.
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142impl ParsedSheet {
143    /// Retains the legacy root-only span views from owner-checked native identities.
144    ///
145    /// Private occurrences remain in the owning provenance tables, never in these
146    /// unqualified single-source maps.
147    /// - Precondition: `instance` is this destination's most recent root expansion.
148    /// - Complexity: O(a + m) authored root records and live method origins.
149    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    /// Retains native validation causes and source-owning site labels, primary cell first.
203    ///
204    /// Falls back to the real root declaration only when no live site resolves.
205    /// - Precondition: `instance` identifies this destination's root expansion.
206    /// - Complexity: O(c + a + s) public names, expanded authored records and native sites.
207    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    /// Constructs an empty source-free destination without invoking a parser.
282    ///
283    /// Register manually added root cells with [`Self::register_cell`] before binding.
284    /// # Examples
285    /// ```
286    /// assert_eq!(adam_lang::ParsedSheet::new().cells().count(), 0);
287    /// ```
288    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    /// Registers an existing root cell's checked shape and public name.
308    ///
309    /// Rejection leaves metadata and graph unchanged. Private cells are registered
310    /// automatically without root names by typed template instantiation.
311    /// Root output cells also enter `output_names`.
312    /// # Errors
313    /// Rejects duplicate names, unknown shapes/IDs, storage or complete-shape mismatches.
314    /// - Complexity: Expected O(n + l) shape nodes and name bytes.
315    ///
316    /// # Examples
317    /// See [`crate::composition`].
318    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    /// Adds a source-free ordinary compound cell with a checked canonical witness.
342    ///
343    /// # Errors
344    /// Rejects duplicate names, unknown leaves, or missing cell clone/equality capabilities
345    /// before graph mutation.
346    /// - Complexity: O(n) shape nodes plus value capability preparation.
347    ///
348    /// # Examples
349    /// ```
350    /// use cel_runtime::{Array, ArrayOf, Leaf};
351    /// let mut sheet = adam_lang::ParsedSheet::new();
352    /// let id = sheet.add_compound_cell::<ArrayOf<Leaf<i32>>>(
353    ///     "xs", Array::from_vec(vec![1]), &adam_lang::TypeRegistry::new())?;
354    /// assert_eq!(sheet.read_compound::<ArrayOf<Leaf<i32>>>(id)?.len(), 1);
355    /// # Ok::<(), Box<dyn std::error::Error>>(())
356    /// ```
357    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    /// Borrows a root or hygienically private cell's complete registered shape.
385    ///
386    /// Returns `None` for unregistered handles.
387    ///
388    /// # Examples
389    /// ```
390    /// assert!(adam_lang::ParsedSheet::new().cell_names.is_empty());
391    /// ```
392    pub fn cell_shape(&self, id: CellId) -> Option<&TypeShape> {
393        self.cell_shapes.get(&id)
394    }
395
396    /// Checks a live cell's concrete storage and full compound identity.
397    ///
398    /// # Errors
399    /// Rejects unknown handles, different storage or recursive shape.
400    /// - Complexity: O(n) compound shape nodes.
401    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            // Compare structural identity directly; no registry or carrier inference is needed.
409            anyhow::ensure!(
410                shape_matches(shape, actual.0.value_type()),
411                "complete shape mismatch"
412            );
413        }
414        Ok(())
415    }
416
417    /// Clones a compound cell into its exact canonical witness representation.
418    ///
419    /// # Errors
420    /// Rejects unknown cells, concrete leaf storage, and recursive shape mismatches.
421    /// - Complexity: O(n + v) validation and cloning for n shape nodes and v values.
422    ///
423    /// # Examples
424    /// ```
425    /// use adam_lang::{AdamParser, TypeRegistry};
426    /// use cel_runtime::{ArrayOf, Leaf};
427    /// let sheet = AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
428    ///     .parse_str("sheet s { cell xs: [i32]; }").unwrap();
429    /// assert!(sheet.read_compound::<ArrayOf<Leaf<i32>>>(sheet.cell_names["xs"].0)?.is_empty());
430    /// # Ok::<(), anyhow::Error>(())
431    /// ```
432    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        // SAFETY: The checked host boundary just validated the complete witness.
439        Ok(unsafe { cell.0.try_clone()?.into_typed_bound::<R>() })
440    }
441
442    /// Writes a compound aspiration after validating its complete witness identity.
443    ///
444    /// Existing cell capabilities, rather than witness defaults, govern propagation.
445    /// # Errors
446    /// Rejects unknown cells, leaf storage, or any recursive shape mismatch before writing.
447    /// - Complexity: O(n) validation and descriptor construction for n shape nodes.
448    ///
449    /// # Examples
450    /// ```
451    /// use adam_lang::{AdamParser, TypeRegistry};
452    /// use cel_runtime::{Array, ArrayOf, Leaf};
453    /// let mut sheet = AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
454    ///     .parse_str("sheet s { source xs: [i32]; }").unwrap();
455    /// sheet.write_compound::<ArrayOf<Leaf<i32>>>(sheet.cell_names["xs"].0, Array::from_vec(vec![1]))?;
456    /// # Ok::<(), anyhow::Error>(())
457    /// ```
458    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    /// Clones the original compound aspiration, independently of any filter result.
470    ///
471    /// # Errors
472    /// Rejects unknown cells, concrete leaf storage, or a mismatched recursive witness.
473    /// - Complexity: O(n + v) for n descriptor nodes and v cloned values.
474    ///
475    /// # Examples
476    /// ```
477    /// use adam_lang::{AdamParser, TypeRegistry};
478    /// use cel_runtime::{ArrayOf, Leaf};
479    /// let sheet = AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
480    ///     .parse_str("sheet s { source xs: [i32]; }").unwrap();
481    /// assert!(sheet.source_compound::<ArrayOf<Leaf<i32>>>(sheet.cell_names["xs"].0)?.is_empty());
482    /// # Ok::<(), anyhow::Error>(())
483    /// ```
484    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        // SAFETY: The complete witness was just checked at this host boundary.
494        Ok(unsafe { cell.0.try_clone()?.into_typed_bound::<R>() })
495    }
496
497    /// Resolves native error sites to owning labels and actual invocation context.
498    ///
499    /// Definition labels precede root-to-child invocation context. Inner CEL spans
500    /// resolve against the failed method, conditional, or requirement definition.
501    /// Native definitions retain labels without locations; unknown sites are skipped.
502    ///
503    /// - Complexity: O(o + m + c + k + p × a + q × a + l²) time and
504    ///   O(o + m + c + k + l) space for occurrences o, methods m, public cells c,
505    ///   conditionals k, candidate cell origins p, visited occurrence/ancestor records q,
506    ///   local authored items a and returned labels l.
507    ///
508    /// # Examples
509    ///
510    /// ```rust
511    /// use adam_lang::{AdamParser, TypeRegistry};
512    /// use cel_parser::OpLookup;
513    ///
514    /// let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
515    /// // Two relationships forming an algebraic loop with no external source for either
516    /// // cell: `x` needs `y` and `y` needs `x`, so `propagate` can't pick a valid method
517    /// // assignment and returns `Error::Cycle`.
518    /// let mut parsed = parser
519    ///     .parse_str(
520    ///         "sheet s { cell x: i32 = 0; cell y: i32 = 0; \
521    ///          relationship { x := y + 1i32; } relationship { y := x + 1i32; } }",
522    ///     )
523    ///     .unwrap();
524    /// let e = parsed.propagate().unwrap_err();
525    /// let located = parsed.locate_error(&e);
526    /// assert!(!located.is_empty());
527    /// assert!(!located[0].label.is_empty());
528    /// ```
529    pub fn locate_error(&self, e: &adam_rs::Error) -> Vec<crate::SourceLabel> {
530        crate::error_labels::locate_error(self, e)
531    }
532
533    /// Copies source and provenance metadata without retaining the live sheet.
534    ///
535    /// # Examples
536    ///
537    /// ```
538    /// use adam_lang::{AdamParser, TypeRegistry};
539    /// let parsed = AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
540    ///     .parse_str("sheet s { cell x: i32 = 1; }")?;
541    /// let diagnostics = parsed.diagnostics();
542    /// assert!(diagnostics.sources().iter().any(|source| source.text().contains("cell x")));
543    /// # Ok::<(), cel_parser::ParseError>(())
544    /// ```
545    ///
546    /// - Complexity: O(m) time and space for retained metadata size m; immutable
547    ///   source owners and authored item slices are shared.
548    pub fn diagnostics(&self) -> crate::SheetDiagnostics {
549        crate::SheetDiagnostics::from_parsed(self)
550    }
551
552    /// Borrows the anonymous frontend source whose display name the caller supplies.
553    ///
554    /// Explicit module roots and programmatic sheets return `None`; their owning
555    /// file names must not be replaced by a renderer's caller-source arguments.
556    ///
557    /// # Examples
558    /// ```
559    /// assert!(adam_lang::ParsedSheet::new().anonymous_source().is_none());
560    /// ```
561    pub fn anonymous_source(&self) -> Option<&crate::SourceId> {
562        self.anonymous_source.as_ref()
563    }
564}
565
566impl std::ops::Deref for ParsedSheet {
567    /// Exposes the underlying runtime constraint sheet.
568    type Target = Sheet;
569
570    /// Borrows the underlying runtime sheet.
571    fn deref(&self) -> &Sheet {
572        &self.sheet
573    }
574}
575
576impl std::ops::DerefMut for ParsedSheet {
577    /// Exclusively borrows the underlying runtime sheet.
578    fn deref_mut(&mut self) -> &mut Sheet {
579        &mut self.sheet
580    }
581}
582
583/// Owns one immutable type/operation environment for repeatable root construction.
584///
585/// Install libraries and register types before construction. Each parse creates a
586/// fresh module collection and live graph; no execution state is shared.
587///
588/// # Examples
589/// ```
590/// let mut parser = adam_lang::AdamParser::new(adam_lang::TypeRegistry::new(),
591///     cel_parser::OpLookup::new());
592/// let parsed = parser.parse_str("sheet app { cell x: i32 = 7; }")?;
593/// assert_eq!(*parsed.read::<i32>(parsed.cell_names["x"].0).unwrap(), 7);
594/// # Ok::<(), cel_parser::ParseError>(())
595/// ```
596pub struct AdamParser {
597    environment: Rc<crate::module::compile::Environment>,
598}
599
600impl AdamParser {
601    /// Takes ownership of types and operations, shared immutably with compiled factories.
602    ///
603    /// # Examples
604    /// See [`AdamParser`].
605    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    /// Constructs the sole root in a source module without propagation.
615    ///
616    /// Reusable definitions may accompany the root. All definitions and inactive
617    /// branches check before construction. Zero or multiple roots require explicit
618    /// selection through [`crate::CompiledModules::instantiate_root`].
619    ///
620    /// # Errors
621    /// Returns syntax, checking, selection, construction or combined validation
622    /// errors. The error source retains the original owning [`crate::ModuleDiagnostic`].
623    /// - Complexity: Module compilation, native construction and combined validation;
624    ///   adapting a diagnostic additionally scans O(n) source tokens.
625    ///
626    /// # Examples
627    /// See [`AdamParser`].
628    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
675/// Adapts a module error to the existing single-source parser boundary without losing it.
676///
677/// Real token positions supply proc-macro spans; source-owning labels and concrete
678/// native causes remain available through `Error::source`.
679/// - Complexity: O(n + l) source tokens and diagnostic labels.
680fn 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// ---------------------------------------------------------------------------
732// Tests
733// ---------------------------------------------------------------------------
734
735#[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    /// Creates an executable parser with built-in types and operations.
744    fn parser() -> AdamParser {
745        AdamParser::new(TypeRegistry::new(), OpLookup::new())
746    }
747
748    /// Identifies a registered test leaf distinct from built-in integers.
749    #[derive(Clone, Debug, PartialEq)]
750    struct Custom(i32);
751
752    /// Identifies the custom array-counting callable in test lookup.
753    #[derive(Clone)]
754    struct CountFn;
755
756    /// Resolves custom test leaves and array-counting calls.
757    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    /// Accepts an empty executable sheet.
795    #[test]
796    fn parse_empty_sheet() {
797        let _sheet = parser().parse_str("sheet empty {}").unwrap();
798    }
799
800    /// Accepts a cell with a matching annotation and initializer.
801    #[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    /// Accepts a defaultable cell without an explicit initializer.
809    #[test]
810    fn parse_cell_annotation_only_uses_default() {
811        let _sheet = parser().parse_str("sheet s { cell area: f64; }").unwrap();
812    }
813
814    /// Accepts an initialized cell without an annotation.
815    #[test]
816    fn parse_cell_initializer_infers_type() {
817        let _sheet = parser().parse_str("sheet s { cell mode = 0i32; }").unwrap();
818    }
819
820    /// Reports an unknown cell annotation by name.
821    #[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    /// Rejects an uninitialized leaf that has no registered default.
834    #[test]
835    fn parse_cell_missing_default_is_error() {
836        /// Supplies a test leaf without default construction.
837        #[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    /// Evaluates typed arrays containing registered custom leaves.
847    #[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    /// Rejects an array annotation naming an unknown custom leaf.
874    #[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    /// Retains persistent compound array values through cell initialization.
889    #[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        /// Describes persistent arrays of the fixture's nested tuple elements.
896        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    /// Records a source declaration as a source-kind runtime cell.
911    #[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    /// Rejects a source declaration lacking both annotation and initializer.
921    #[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    /// Registers requirements attached to an ordinary cell.
928    #[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    /// Registers requirements attached to a source cell.
938    #[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    /// Registers a source cell's filter.
948    #[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    /// Registers an output cell's filter.
958    #[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    /// Clamps writes through a filter without named dependencies.
968    #[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    /// Evaluates a filter against the current value of its named dependency.
983    #[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    /// Deduplicates repeated filter dependencies without changing the result.
1008    #[test]
1009    fn cell_filter_referencing_the_same_value_twice_is_idempotent() {
1010        // Snap-to-grid: `_ - (_ % step)` — `_` referenced twice must denote the same value both
1011        // times, not two independent parameters.
1012        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    /// Attaches related source labels to a filter-dependency cycle.
1023    #[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        // The parser only lets a filter reference already-declared cells, so the cycle becomes
1031        // visible when the later relationship adds the reverse dependency edge.
1032        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    /// Rejects a non-range filter that never references its candidate value.
1042    #[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    /// Rejects a filter whose result does not match the governed cell type.
1050    #[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    /// Rejects an unknown identifier in a filter.
1058    #[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    /// Clamps writes to an inclusive range.
1066    #[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    /// Rejects invalid public erased inputs on every call without poisoning later bounds.
1083    /// Rejects malformed scalar range arguments at the public boundary.
1084    #[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                // The first mismatch has f64's size and alignment, so the pre-fix
1101                // regression fails by accepting it, without an undersized raw read.
1102                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    /// Checks complete compound shapes, carrier identity, arity, and recovery.
1110    /// Rejects malformed compound range arguments at the public boundary.
1111    #[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        /// Describes the valid floating-point tuple used as range bounds.
1127        type Limits = Cons<Leaf<f64>, Cons<Leaf<f64>, Nil>>;
1128        /// Describes an incompatible tuple carrier supplied to the checked range boundary.
1129        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        // Identical field sizes and valid f64 bits keep this regression safe pre-fix.
1132        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    /// Accepts range filters without a candidate-value placeholder.
1148    #[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    /// Updates effective range bounds when their cell dependencies change.
1156    #[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    /// Applies inclusive range filtering to floating-point cells.
1175    #[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    /// Rejects a range whose element type differs from the governed cell.
1188    #[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    /// Reports range element mismatch even when a bound references the candidate.
1202    #[test]
1203    fn cell_filter_range_with_mismatched_element_type_and_underscore_is_still_a_range_error() {
1204        // Confirms the "range bounds must be" check fires unconditionally on a recognized
1205        // RangeInclusive-shaped output, even though `_` is referenced here (cast to `i32`, the
1206        // range's own element type, rather than left at the cell's declared `f64`) — it must not
1207        // be swallowed by the opaque `underscore_used` path.
1208        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    /// Leaves range bounds unavailable when their expression evaluation fails.
1220    #[test]
1221    fn cell_filter_range_returns_none_instead_of_panicking_when_a_bound_expression_fails_to_evaluate()
1222     {
1223        // `100 / hi` is a registered CEL builtin (`/` on `i32`, see
1224        // `cel_parser::op_table::OpLookup`) that returns `Err` at runtime on division by zero
1225        // rather than panicking or wrapping — `parse_cell_filter` only validates this range
1226        // filter's *type* at parse time, so evaluating the bound expression with `hi == 0` must
1227        // still be handled at runtime. Before this fix, `FilterKind::Range::bounds` was
1228        // infallible and `parse_cell_filter`'s wrapping closure used `.expect(..)`, which would
1229        // panic here instead of degrading to `None`.
1230        // `hi` starts at a non-zero value so `add_filter`'s own immediate clamp of `a`'s default
1231        // value (0) succeeds during parsing (`0..=(100 / 1)`); the failing evaluation is
1232        // triggered afterwards, purely through `filter_range`, by writing `hi` to 0.
1233        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        // Puts the range expression's own evaluation into a failing state (division by zero)
1243        // without going through `add_filter`'s clamp path at all — this is exactly the "some
1244        // other write puts an endpoint's dependency into a bad state" scenario from the review
1245        // finding, and must degrade to `None` rather than panicking.
1246        parsed.sheet.write(hi_id, 0i32).unwrap();
1247        assert_eq!(parsed.sheet.filter_range::<i32>(a_id), None);
1248    }
1249
1250    /// Keeps non-range filter expressions opaque.
1251    #[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    /// Tracks changing bounds supplied by a tuple-valued cell.
1265    #[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    /// Accepts filtering a tuple-valued cell.
1294    #[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    /// Attaches a parsed filter clause to its cell.
1304    #[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    /// Rejects labels on filter clauses.
1314    #[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    /// Accepts multiple declarations in one sheet.
1321    #[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    /// Evaluates a single-binding relationship.
1338    #[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    /// Infers relationship dependencies from referenced cell identifiers.
1359    #[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    /// Verifies member labels never become dependencies or resolve same-named cells.
1381    /// Excludes member labels from relationship dependencies.
1382    #[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    /// Tracks only value expressions in a record-member binding.
1414    /// Excludes record labels and member names from cell dependency resolution.
1415    #[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        /// A record independent of any geometry library.
1420        #[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    /// Executes the planner-selected direction of a reversible relationship.
1479    #[test]
1480    fn parse_relationship_with_multiple_bindings_lets_the_planner_pick_a_direction() {
1481        // `c` is declared first (and so has the weakest cell strength — `adam_rs`'s
1482        // planner processes cells in *descending* strength order, preferentially
1483        // leaving the strongest cells as sources, so the earliest-declared cell is the
1484        // one left to be computed): with `a`/`b` fixed as sources, the only
1485        // self-consistent choice among these three mutually-referencing bindings is to
1486        // compute `c := a * b`.
1487        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    /// Attributes inconsistent method cell sets to their relationship block.
1509    #[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        // The mismatched binding (`b := 42;`, method index 1) is on line 7; the sheet's
1515        // opening line (1) must not be reported instead. `ParseError::span()` returns a
1516        // `proc_macro2::Span`, so `.start()` is a method call here, not a field access (unlike
1517        // `cel_parser::SourceSpan`, whose `start`/`end` are public `LineColumn` fields).
1518        assert_eq!(err.span().start().line, 7);
1519    }
1520
1521    /// Retains method source spans after successful parsing.
1522    #[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    /// Retains source spans for runtime relationships and cells.
1534    #[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        // every declared cell id has a span
1543        for (_, (id, _)) in &parsed.cell_names {
1544            assert!(parsed.cell_spans.contains_key(id));
1545        }
1546    }
1547
1548    /// Resolves a cycle diagnostic to ordered participating source spans.
1549    #[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        // both relationship blocks resolve to spans
1562        let rel_spans = located.len();
1563        assert!(
1564            rel_spans >= 2,
1565            "expected >=2 spans, got {rel_spans}: {located:?}"
1566        );
1567    }
1568
1569    /// Resolves an error from retained metadata after the failed model is dropped.
1570    #[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    /// Labels both bindings that produce the same output set.
1593    #[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        // Primary caret on the later binding, secondary carets on the earlier binding and the
1602        // shared output cell's own declaration.
1603        assert!(out.contains("outputs"), "{out}"); // message text
1604        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    /// Rejects an undeclared relationship output.
1612    #[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    /// Executes destructuring against the declared output tuple shapes.
1628    #[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    /// Rejects destructuring with the wrong number of tuple elements.
1652    #[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    /// Rejects multi-output targets without tuple parentheses.
1670    #[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    /// Extracts the sole value of a singleton tuple binding.
1688    #[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    /// Rejects a parenthesized binding name instead of treating it as a direct bind.
1709    /// Rejects a parenthesized binding target lacking the singleton comma.
1710    #[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    /// Evaluates an explicitly typed output without requirements.
1727    #[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    /// Evaluates an output with an inferred type.
1746    #[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    /// Registers explicitly named output requirements.
1766    #[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    /// Reports a violated output requirement.
1795    #[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    /// Rejects a requirement whose expression is not Boolean.
1816    #[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    /// Accepts an outer documentation comment on a cell.
1832    #[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    /// Accepts sheet-level inner documentation.
1841    #[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    /// Accepts documentation comments across supported declaration kinds.
1850    #[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    /// Rejects an undeclared relationship input.
1858    #[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    /// Rejects a method result that conflicts with its output annotation.
1875    #[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    /// Rejects a scalar multi-output method with the wrong tuple arity.
1890    #[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    /// Names the registered leaf in a method result mismatch.
1914    #[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    /// Names the registered leaf in a destructured result mismatch.
1932    #[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    /// Rejects a tuple method result containing the wrong element type.
1952    #[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    /// Rejects a tuple body assigned to a scalar output.
1975    #[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    /// Executes a method with one tuple-valued output.
1993    #[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    /// Executes a method combining tuple-valued and scalar outputs.
2020    #[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    /// Reads tuple input elements through field indexing.
2050    #[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    /// Rejects a method result with a different tuple shape.
2069    #[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    /// Checks both results of a scalar multi-output method.
2085    #[test]
2086    fn existing_multi_output_scalar_methods_still_work_unchanged() {
2087        // Regression: today's N-scalar-outputs mechanism must still behave identically after the
2088        // CompiledOutputs refactor.
2089        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    /// Executes a method producing the empty tuple.
2110    #[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    /// Executes a conditional declaration.
2132    #[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    /// Updates both relationships in an active branch after independent input edits.
2160    #[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    /// Rejects a conditional-arm binding outside a relationship block.
2201    #[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    /// Rejects an unknown conditional subject cell.
2218    #[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    /// Rejects a branch key with the wrong subject type.
2235    #[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    /// Selects the branch matching a negative literal key.
2253    #[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    /// Rejects a negated unsigned branch key.
2275    #[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    /// Executes a default-only conditional on a tuple-valued subject.
2296    #[test]
2297    fn conditional_with_tuple_typed_match_cell_and_default_only_still_works() {
2298        // A tuple isn't a `literal_pattern`, so a tuple-typed match cell can no longer pair with
2299        // a named branch key — only the `_` default branch remains reachable.
2300        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    /// Rejects a tuple literal as a conditional branch key.
2320    #[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    /// Reacts to changes in either cell of a Boolean conditional subject.
2339    #[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    /// Evaluates a conditional subject with repeated references to one cell.
2371    #[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    /// Evaluates a bare cell identifier as a conditional subject.
2393    #[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    /// Verifies runtime conditional headers accept grouped records and deduce field dependencies.
2421    #[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    /// Rejects a bare identifier as a branch key.
2465    #[test]
2466    fn conditional_branch_bare_identifier_key_is_error() {
2467        // A bare identifier isn't a `literal_pattern` either, even when it resolves (as a
2468        // client-registered constant would) to a value of the match subject's own type.
2469        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    /// Rejects an unknown identifier inside a conditional subject expression.
2488    #[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    /// Rejects a cell literal that conflicts with its annotation.
2504    #[test]
2505    fn parse_cell_literal_type_mismatch_is_error() {
2506        // Float literal for an i32 cell should be a parse error.
2507        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    /// Computes floating-point results in both directions after a strength-changing edit.
2515    #[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    /// Rejects duplicate cell declarations.
2545    #[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    /// Preserves cell declaration order in returned name metadata.
2561    #[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    /// Exposes runtime propagation through a parsed sheet.
2571    #[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        // Deref/DerefMut must make Sheet's methods directly callable.
2575        parsed.propagate().unwrap();
2576    }
2577
2578    /// Propagates an explicitly typed output.
2579    #[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    /// Formats a stored tuple output using its concrete value.
2598    #[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    /// Infers an output's runtime type from its writer.
2617    #[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    /// Rejects an output writer that conflicts with its annotation.
2634    #[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    /// Distinguishes valid and violated output requirements.
2651    #[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    /// Reports output requirement violations after propagation.
2674    #[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    /// Records metadata only for explicitly labeled requirements.
2697    #[test]
2698    /// Resolves explicit labels by returned IDs while preserving unnamed checks and order.
2699    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    /// Rejects repeated requirement labels within each declaration kind.
2727    #[test]
2728    /// Rejects repeated explicit labels locally for every declaration kind.
2729    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    /// Enforces an unlabeled output requirement.
2742    #[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    /// Enforces an unlabeled requirement whose body is a bare identifier.
2764    #[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    /// Accepts an output cell referenced by another expression.
2784    #[test]
2785    fn parse_out_cell_referenced_elsewhere_succeeds() {
2786        // `out` cells are no longer terminal (see `adam_rs::CellKind`): referencing an `out`
2787        // cell as another relationship's input is legal.
2788        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    /// Infers structural array outputs without registering the array carrier.
2805    #[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    /// Empty tuple writers execute their initializer and propagate runtime errors.
2828    /// Executes an output writer even when its result is the empty tuple.
2829    #[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    /// Accepts an output cell as a conditional subject.
2847    #[test]
2848    fn parse_out_cell_referenced_in_conditional_succeeds() {
2849        // `out` cells are no longer terminal (see `adam_rs::CellKind`): referencing an `out`
2850        // cell as a relationship input inside a conditional branch is legal.
2851        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    /// Initializes a cell with an explicitly declared tuple shape.
2871    #[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    /// Constructs recursive defaults for tuple-typed cells.
2896    #[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    /// Rejects an initializer with the wrong tuple arity.
2913    #[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    /// Stores and reads a nested tuple cell.
2920    #[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        /// Describes the fixture's nested tuple value.
2927        type Inner = cel_runtime::Cons<
2928            cel_runtime::Leaf<f64>,
2929            cel_runtime::Cons<cel_runtime::Leaf<String>, cel_runtime::Nil>,
2930        >;
2931        /// Describes the integer paired with the inner tuple.
2932        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    /// Stores the complete result of an explicitly typed tuple output.
2941    #[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    /// Reports an unsupported range initializer after syntax parsing succeeds.
2966    #[test]
2967    fn parse_cell_range_initializer_fails_cleanly_at_type_inference_not_grammar() {
2968        // `Range<i32>` isn't a registered adam-lang type — this must still fail today, but only
2969        // at `eval_segment_boxed`'s existing "cannot infer a type" check, proving the CEL-level
2970        // range parsing itself succeeded (rather than failing as "unexpected token" or similar
2971        // at the grammar level, which would indicate the entry-point swap didn't take effect).
2972        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    /// Verifies unregistered out results identify the declaration and its name token.
2979    /// Retains output context and source span in a shape diagnostic.
2980    #[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    /// Verifies unregistered match results identify the expression and its opening token.
3002    /// Retains conditional-subject context and span in a shape diagnostic.
3003    #[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    /// Verifies unregistered binding results identify the output and its binding token.
3028    /// Retains binding-output context and span in a shape diagnostic.
3029    #[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    /// Verifies unregistered initializer results identify the cell and its name token.
3050    /// Retains initializer context and source span in a shape diagnostic.
3051    #[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    /// Preserves source declaration labels and name spans for initializer errors.
3073    /// Retains source-cell context and span in a shape diagnostic.
3074    #[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    /// Verifies registered type mismatches retain the ordinary out and output prefixes.
3104    /// Retains declaration context for registered leaf mismatches.
3105    #[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    /// Attributes a dependency-cycle validation error to the governed cell.
3121    #[test]
3122    fn dependency_cycle_validation_error_spans_the_governed_cell() {
3123        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
3124        // The branch writes the match cell `mode`, so whole-sheet validation returns
3125        // DependencyCycle on line 2 (`cell mode`), the governed cell that starts the cycle.
3126        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    // NOTE on coverage: `add_conditional`'s other `InvalidConditional` case with a
3132    // `Relationship`-only site list -- the same relationship reused across two branches (or a
3133    // branch and the default) -- is not reachable through adam-lang's own grammar today.
3134    // `parse_branch_relationships` calls `parse_relationship_decl`, which always creates a
3135    // *fresh* `RelationshipId` on `add_relationship`; adam-lang has no syntax for a `relationship`
3136    // to be *named* and then referenced again from a second branch. That case is exercised
3137    // directly against `Sheet::add_conditional` in
3138    // `add_conditional_returns_invalid_conditional_for_duplicate_relationship_across_branches`
3139    // (adam-rs/src/sheet.rs); it stays untested at the adam-lang parser level until adam-lang
3140    // grows named/referenceable relationships (tracked as a follow-up, not blocking this task).
3141    /// Renders invalid conditional relationships and cells as related labels.
3142    #[test]
3143    fn conditional_invalid_relationship_and_cell_sites_render_as_secondary_labels() {
3144        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
3145        // The governed branch output `a` reaches match cell `p` through the unconditional
3146        // relationship, so the diagnostic should use `a` as primary and attach the
3147        // remaining cycle sites as secondary labels.
3148        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    /// Attributes a duplicate output requirement to the repeated label.
3156    #[test]
3157    /// Locates duplicate labels at the repeated requirement, not the sheet opening.
3158    fn out_decl_duplicate_requirement_error_spans_the_duplicate_label() {
3159        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
3160        // The parser owns display-label validation, so it can identify the repeated label
3161        // itself rather than blaming runtime registration or the sheet's opening line.
3162        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}