Skip to main content

adam_lang/
parser.rs

1//! adam-lang parser — grammar productions and sheet construction.
2//!
3//! See the crate root's [`# Grammar`](crate#grammar) section for the full EBNF.
4
5use std::any::{Any, TypeId};
6use std::cell::RefCell;
7use std::collections::{HashMap, HashSet};
8use std::str::FromStr;
9use std::sync::{Arc, Mutex};
10
11use indexmap::IndexMap;
12
13use adam_rs::{
14    CellId, ErrorSite, MatchExpr, Method, RelationshipId, Requirement, RequirementId, Sheet,
15};
16use cel_parser::lex_lexer::{HasSpan, LexLexer, Token};
17use cel_parser::{CELParser, OpLookup, ParseError, SourceSpan};
18use cel_runtime::DynSegment;
19use proc_macro2::{Span, TokenStream};
20
21use crate::TypeRegistry;
22use crate::cell_value::{CellValue, Frame, RawFrame};
23use crate::type_registry::{AddConditionalFn, TypeShape};
24
25/// Parser result type.
26pub type Result<T> = std::result::Result<T, ParseError>;
27
28/// A parsed identifier's resolved `(CellId, TypeShape)`, paired with its source name — used both
29/// for a construct's declared outputs and for the inputs [`AdamParser::parse_deduced_expr`]
30/// deduces from whichever already-declared cells an expression references.
31type NamedCells = Vec<(String, CellId, TypeShape)>;
32
33// ---------------------------------------------------------------------------
34// ParsedSheet
35// ---------------------------------------------------------------------------
36
37/// The result of [`AdamParser::parse_str`]: a live [`Sheet`] plus the declared
38/// cell names, in source declaration order.
39///
40/// Derefs to [`Sheet`] so callers that only need sheet methods (e.g.
41/// `propagate`) can use the result exactly as if it were a `Sheet`.
42///
43/// # Examples
44///
45/// ```
46/// use adam_lang::{AdamParser, TypeRegistry};
47/// use cel_parser::OpLookup;
48/// let parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
49///     .parse_str("sheet s { cell x: i32 = 1 require { @positive x > 0i32; }; }")?;
50/// let id = parsed.cell_requirements(parsed.cell_names["x"].0).unwrap()[0];
51/// assert_eq!(parsed.requirement_names[&id], "positive");
52/// # Ok::<(), cel_parser::ParseError>(())
53/// ```
54pub struct ParsedSheet {
55    /// The constructed sheet.
56    pub sheet: Sheet,
57    /// Cell name → `(CellId, TypeShape)`, in declaration order.
58    pub cell_names: IndexMap<String, (CellId, TypeShape)>,
59    /// Output name → `CellId`, in declaration order — parity with `cell_names`, for callers
60    /// that need to look up `Sheet::cell_requirements_valid`/`Sheet::violated_requirements` by
61    /// name.
62    pub output_names: IndexMap<String, CellId>,
63    /// Explicit `@name` labels keyed by requirement ID; unnamed requirements have no entry.
64    /// Lookup takes expected O(1) time. Iterate the sheet's requirement IDs, not this map,
65    /// to preserve attachment or violation order.
66    pub requirement_names: HashMap<RequirementId, String>,
67    /// `(RelationshipId, method index)` → the source span of that binding, populated for
68    /// every successfully-added relationship. Lets a caller translate an `adam_rs::Error`'s
69    /// `ErrorSite::Method` (raised well after parsing, e.g. from `Sheet::propagate`) back
70    /// to a source location.
71    ///
72    /// `out` declarations' internal writer relationships (created via `Sheet::add_out`, not
73    /// `parse_relationship_decl`) are not recorded here, since `add_out` returns only a
74    /// `CellId`, not the writer's `RelationshipId`. In practice this is narrow: an `out`
75    /// writer's body is always a CEL expression, so arithmetic failures already carry a
76    /// `cel_parser::SpanContext` regardless of this gap. A `MethodFailed`/`TypeMismatch` from
77    /// an `out` writer without a `SpanContext` falls back to `Display` instead of a
78    /// source-span diagnostic — never worse than resolving no site at all.
79    pub method_spans: HashMap<(RelationshipId, usize), SourceSpan>,
80    /// `RelationshipId` → the source span of that `relationship { ... }` block, populated for
81    /// every successfully-added relationship. Resolves an `adam_rs::Error`'s
82    /// `ErrorSite::Relationship` (e.g. from `Error::Conflict`) back to a source location.
83    pub relationship_spans: HashMap<RelationshipId, SourceSpan>,
84    /// `CellId` → the source span of that cell's declared name, populated for every declared
85    /// `cell`, `source`, and `out`. Resolves an `adam_rs::Error`'s `ErrorSite::Cell` back to a
86    /// source location.
87    pub cell_spans: HashMap<CellId, SourceSpan>,
88}
89
90impl std::fmt::Debug for ParsedSheet {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("ParsedSheet")
93            .field("cell_names", &self.cell_names)
94            .field("output_names", &self.output_names)
95            .field("requirement_names", &self.requirement_names)
96            .field("method_spans", &self.method_spans)
97            .field("relationship_spans", &self.relationship_spans)
98            .field("cell_spans", &self.cell_spans)
99            .finish()
100    }
101}
102
103impl ParsedSheet {
104    /// Clones a compound cell into its exact canonical witness representation.
105    ///
106    /// # Errors
107    /// Rejects unknown cells, concrete leaf storage, and recursive shape mismatches.
108    /// - Complexity: O(n + v) validation and cloning for n shape nodes and v values.
109    ///
110    /// # Examples
111    /// ```
112    /// use adam_lang::{AdamParser, TypeRegistry};
113    /// use cel_runtime::{ArrayOf, Leaf};
114    /// let sheet = AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
115    ///     .parse_str("sheet s { cell xs: [i32]; }").unwrap();
116    /// assert!(sheet.read_compound::<ArrayOf<Leaf<i32>>>(sheet.cell_names["xs"].0)?.is_empty());
117    /// # Ok::<(), anyhow::Error>(())
118    /// ```
119    pub fn read_compound<R: cel_runtime::ValueRepr>(&self, id: CellId) -> anyhow::Result<R::Value> {
120        let cell = self.sheet.read::<CellValue>(id)?;
121        anyhow::ensure!(
122            cell.0.value_type().same_shape(&R::value_type()),
123            "compound cell representation mismatch"
124        );
125        // SAFETY: The checked host boundary just validated the complete witness.
126        Ok(unsafe { cell.0.try_clone()?.into_typed_bound::<R>() })
127    }
128
129    /// Writes a compound aspiration after validating its complete witness identity.
130    ///
131    /// Existing cell capabilities, rather than witness defaults, govern propagation.
132    /// # Errors
133    /// Rejects unknown cells, leaf storage, or any recursive shape mismatch before writing.
134    /// - Complexity: O(n) validation and descriptor construction for n shape nodes.
135    ///
136    /// # Examples
137    /// ```
138    /// use adam_lang::{AdamParser, TypeRegistry};
139    /// use cel_runtime::{Array, ArrayOf, Leaf};
140    /// let mut sheet = AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
141    ///     .parse_str("sheet s { source xs: [i32]; }").unwrap();
142    /// sheet.write_compound::<ArrayOf<Leaf<i32>>>(sheet.cell_names["xs"].0, Array::from_vec(vec![1]))?;
143    /// # Ok::<(), anyhow::Error>(())
144    /// ```
145    pub fn write_compound<R: cel_runtime::ValueRepr>(
146        &mut self,
147        id: CellId,
148        value: R::Value,
149    ) -> anyhow::Result<()> {
150        let descriptor = self.sheet.read::<CellValue>(id)?.0.value_type().clone();
151        let value = cel_runtime::OwnedValue::new::<R>(value).with_value_type(descriptor)?;
152        self.sheet.write(id, CellValue(value))?;
153        Ok(())
154    }
155
156    /// Clones the original compound aspiration, independently of any filter result.
157    ///
158    /// # Errors
159    /// Rejects unknown cells, concrete leaf storage, or a mismatched recursive witness.
160    /// - Complexity: O(n + v) for n descriptor nodes and v cloned values.
161    ///
162    /// # Examples
163    /// ```
164    /// use adam_lang::{AdamParser, TypeRegistry};
165    /// use cel_runtime::{ArrayOf, Leaf};
166    /// let sheet = AdamParser::new(TypeRegistry::new(), cel_parser::OpLookup::new())
167    ///     .parse_str("sheet s { source xs: [i32]; }").unwrap();
168    /// assert!(sheet.source_compound::<ArrayOf<Leaf<i32>>>(sheet.cell_names["xs"].0)?.is_empty());
169    /// # Ok::<(), anyhow::Error>(())
170    /// ```
171    pub fn source_compound<R: cel_runtime::ValueRepr>(
172        &self,
173        id: CellId,
174    ) -> anyhow::Result<R::Value> {
175        let cell = self.sheet.source::<CellValue>(id)?;
176        anyhow::ensure!(
177            cell.0.value_type().same_shape(&R::value_type()),
178            "compound cell representation mismatch"
179        );
180        // SAFETY: The complete witness was just checked at this host boundary.
181        Ok(unsafe { cell.0.try_clone()?.into_typed_bound::<R>() })
182    }
183
184    /// Resolves each of `e`'s `ErrorSite`s to a source span and a human label, primary first.
185    ///
186    /// Sites whose span is not recorded are skipped, so the result may be shorter than
187    /// `e.sites()`; empty when none resolves (the caller then falls back to `Display`).
188    ///
189    /// - Complexity: O(s) in the number of sites.
190    ///
191    /// # Examples
192    ///
193    /// ```rust
194    /// use adam_lang::{AdamParser, TypeRegistry};
195    /// use cel_parser::OpLookup;
196    ///
197    /// let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
198    /// // Two relationships forming an algebraic loop with no external source for either
199    /// // cell: `x` needs `y` and `y` needs `x`, so `propagate` can't pick a valid method
200    /// // assignment and returns `Error::Cycle`.
201    /// let mut parsed = parser
202    ///     .parse_str(
203    ///         "sheet s { cell x: i32 = 0; cell y: i32 = 0; \
204    ///          relationship { x := y + 1i32; } relationship { y := x + 1i32; } }",
205    ///     )
206    ///     .unwrap();
207    /// let e = parsed.propagate().unwrap_err();
208    /// let located = parsed.locate_error(&e);
209    /// assert!(!located.is_empty());
210    /// assert!(!located[0].1.is_empty());
211    /// ```
212    pub fn locate_error(&self, e: &adam_rs::Error) -> Vec<(SourceSpan, String)> {
213        let by_id: HashMap<CellId, String> = self
214            .cell_names
215            .iter()
216            .map(|(n, (id, _))| (*id, n.clone()))
217            .collect();
218        let name = |id: CellId| by_id.get(&id).cloned();
219        let mut out = Vec::new();
220        for (i, site) in e.sites().iter().enumerate() {
221            let span = match site {
222                ErrorSite::Method(r, idx) => self.method_spans.get(&(*r, *idx)).copied(),
223                ErrorSite::Relationship(r) => self.relationship_spans.get(r).copied(),
224                ErrorSite::Cell(c) => self.cell_spans.get(c).copied(),
225                ErrorSite::MethodIndex(_) => None,
226                _ => None,
227            };
228            if let Some(span) = span {
229                out.push((span, crate::error_labels::site_label(e, i, &name)));
230            }
231        }
232        out
233    }
234}
235
236impl std::ops::Deref for ParsedSheet {
237    type Target = Sheet;
238
239    fn deref(&self) -> &Sheet {
240        &self.sheet
241    }
242}
243
244impl std::ops::DerefMut for ParsedSheet {
245    fn deref_mut(&mut self) -> &mut Sheet {
246        &mut self.sheet
247    }
248}
249
250// ---------------------------------------------------------------------------
251// ParseContext — mutable state for one parse_str call
252// ---------------------------------------------------------------------------
253
254/// Accumulates one sheet's declarations, display metadata, and source locations.
255struct ParseContext {
256    cursor: crate::token_cursor::TokenCursor,
257    sheet: Sheet,
258    /// Maps cell name → (CellId, TypeShape), in declaration order, for method and
259    /// conditional compilation and for exposing to callers via `ParsedSheet`.
260    cell_names: IndexMap<String, (CellId, TypeShape)>,
261    /// Maps output name → `CellId`, in declaration order, for exposing to callers via
262    /// `ParsedSheet`.
263    output_names: IndexMap<String, CellId>,
264    /// Accumulates only explicit labels against successfully registered requirement IDs.
265    requirement_names: HashMap<RequirementId, String>,
266    /// Accumulates spans for every successfully-added relationship's methods, for exposing to
267    /// callers via `ParsedSheet::method_spans`.
268    method_spans: HashMap<(RelationshipId, usize), SourceSpan>,
269    /// Accumulates spans for every successfully-added relationship block, for exposing to
270    /// callers via `ParsedSheet::relationship_spans`.
271    relationship_spans: HashMap<RelationshipId, SourceSpan>,
272    /// Accumulates spans for every declared cell's name, for exposing to callers via
273    /// `ParsedSheet::cell_spans`.
274    cell_spans: HashMap<CellId, SourceSpan>,
275    /// Accumulates proc-macro spans for every declared cell's name, for primary parser errors.
276    cell_token_spans: HashMap<CellId, Span>,
277}
278
279impl std::ops::Deref for ParseContext {
280    type Target = crate::token_cursor::TokenCursor;
281
282    fn deref(&self) -> &crate::token_cursor::TokenCursor {
283        &self.cursor
284    }
285}
286
287impl std::ops::DerefMut for ParseContext {
288    fn deref_mut(&mut self) -> &mut crate::token_cursor::TokenCursor {
289        &mut self.cursor
290    }
291}
292
293// ---------------------------------------------------------------------------
294// AdamParser
295// ---------------------------------------------------------------------------
296
297/// Parses adam-lang source strings into live [`ParsedSheet`]s (sheet + cell names).
298///
299/// # Example
300///
301/// ```rust
302/// use adam_lang::{AdamParser, TypeRegistry};
303/// use cel_parser::OpLookup;
304///
305/// let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
306/// let parsed = parser.parse_str("sheet s { cell x: i32 = 0; }").unwrap();
307/// ```
308pub struct AdamParser {
309    pub(crate) types: TypeRegistry,
310    pub(crate) cel: CELParser,
311}
312
313impl AdamParser {
314    /// Creates a parser with the given type registry and operation lookup.
315    ///
316    /// `op_lookup` is forwarded to the embedded [`CELParser`] when compiling method
317    /// body expressions. See
318    /// [`OpLookup::push_library_scope`](cel_parser::OpLookup::push_library_scope) for how to
319    /// install a function library (e.g. `cel-std`) before parsing. The embedded CEL parser also
320    /// receives a snapshot of `types`' registered scalar leaf names so typed array annotations
321    /// such as `[]: [Custom]` resolve custom Adam types during direct parsing.
322    pub fn new(types: TypeRegistry, op_lookup: OpLookup) -> Self {
323        let cel = CELParser::with_type_resolver(op_lookup, types.cel_type_resolver());
324        AdamParser { types, cel }
325    }
326
327    /// Returns a mutable reference to the embedded CEL operation lookup.
328    pub fn op_lookup_mut(&mut self) -> &mut OpLookup {
329        self.cel.op_lookup_mut()
330    }
331
332    /// Parses an adam-lang source string into a live [`ParsedSheet`].
333    ///
334    /// Resets internal parse state on each call.
335    ///
336    /// # Errors
337    ///
338    /// Returns `Err` on any syntax error, unknown type name, type mismatch between a
339    /// cell annotation and its initializer, undeclared cell name in a `relationship` binding's
340    /// output list, or a tuple arity/element-type mismatch between the output expression
341    /// and its declared outputs.
342    pub fn parse_str(&mut self, source: &str) -> Result<ParsedSheet> {
343        let stream =
344            TokenStream::from_str(source).map_err(|e| ParseError::from_lex_error(source, e))?;
345        let mut ctx = ParseContext {
346            cursor: crate::token_cursor::TokenCursor::new(
347                LexLexer::new(stream.into_iter()).peekable(),
348            ),
349            sheet: Sheet::new(),
350            cell_names: IndexMap::new(),
351            output_names: IndexMap::new(),
352            requirement_names: HashMap::new(),
353            method_spans: HashMap::new(),
354            relationship_spans: HashMap::new(),
355            cell_spans: HashMap::new(),
356            cell_token_spans: HashMap::new(),
357        };
358        let _ = ctx.consume_doc_comment_run(true); // sheet-level `//!` docs (ignored at runtime)
359        self.parse_sheet(&mut ctx)?;
360        if let Some(tok) = ctx.peek_token() {
361            return Err(ParseError::new("unexpected token", tok.span()));
362        }
363        ctx.sheet
364            .validate()
365            .map_err(|e| Self::validated_sheet_error(&ctx, e))?;
366        Ok(ParsedSheet {
367            sheet: ctx.sheet,
368            cell_names: ctx.cell_names,
369            output_names: ctx.output_names,
370            requirement_names: ctx.requirement_names,
371            method_spans: ctx.method_spans,
372            relationship_spans: ctx.relationship_spans,
373            cell_spans: ctx.cell_spans,
374        })
375    }
376
377    // -----------------------------------------------------------------------
378    // Grammar productions
379    // -----------------------------------------------------------------------
380
381    /// `sheet = "sheet" identifier "{" { sheet_item } "}".`
382    fn parse_sheet(&mut self, ctx: &mut ParseContext) -> Result<()> {
383        if !ctx.is_keyword("sheet") {
384            return Err(ctx.err_at("expected `sheet`"));
385        }
386        ctx.consume_ident()?; // sheet name (ignored at runtime)
387        ctx.expect_open_brace()?;
388        while !ctx.at_close_brace() {
389            self.parse_sheet_item(ctx)?;
390        }
391        ctx.expect_close_brace()?;
392        Ok(())
393    }
394
395    /// `sheet_item = [ doc_comment ] (cell_decl | relationship_decl | conditional_decl | out_decl
396    /// | source_decl).`
397    fn parse_sheet_item(&mut self, ctx: &mut ParseContext) -> Result<()> {
398        let _ = ctx.consume_doc_comment_run(false); // outer `///` docs (ignored at runtime)
399        match ctx.peek_token() {
400            Some(Token::Identifier(id)) if id == "cell" => self.parse_cell_decl(ctx),
401            Some(Token::Identifier(id)) if id == "relationship" => {
402                self.parse_relationship_decl(ctx).map(|_| ())
403            }
404            Some(Token::Identifier(id)) if id == "conditional" => self.parse_conditional_decl(ctx),
405            Some(Token::Identifier(id)) if id == "out" => self.parse_out_decl(ctx),
406            Some(Token::Identifier(id)) if id == "source" => self.parse_source_decl(ctx),
407            Some(tok) => Err(ParseError::new(
408                "expected `cell`, `relationship`, `conditional`, `out`, or `source`",
409                tok.span(),
410            )),
411            None => Err(ParseError::new(
412                "unexpected end of input",
413                Span::call_site(),
414            )),
415        }
416    }
417
418    /// `cell_decl = "cell" identifier cell_type_init [ cell_filter ] [ "require" "{" {
419    /// requirement } "}" ] ";".`
420    ///
421    /// `cell_type_init = (":" type_expr ["=" expression]) | ("=" expression).`
422    fn parse_cell_decl(&mut self, ctx: &mut ParseContext) -> Result<()> {
423        ctx.is_keyword("cell"); // consume
424        let (name, name_span) = ctx.consume_ident()?;
425        if ctx.cell_names.contains_key(&name) {
426            return Err(ParseError::new(
427                format!("duplicate cell `{name}`"),
428                name_span,
429            ));
430        }
431
432        let declared_shape: Option<TypeShape> = if ctx.consume_punct(":") {
433            let type_expr = self.parse_type_expr(ctx)?;
434            Some(
435                self.types
436                    .resolve(&type_expr)
437                    .map_err(|(msg, span)| ParseError::new(msg, span))?,
438            )
439        } else {
440            None
441        };
442
443        let has_initializer = ctx.consume_punct("=");
444        let (shape, cell_id) = if has_initializer {
445            let segment = self.parse_cel_expression(ctx)?;
446            self.validate_initializer(&segment, declared_shape.as_ref(), "cell", &name, name_span)?;
447            let (actual_shape, cell_id) = self
448                .build_cell_from_segment(segment, ctx)
449                .map_err(|e| ParseError::new(e.message(), name_span))?;
450            (actual_shape, cell_id)
451        } else {
452            let declared = declared_shape.ok_or_else(|| {
453                ParseError::new("expected `:` or `=` in cell declaration", name_span)
454            })?;
455            let cell_id = self.build_default_cell(&declared, name_span, ctx)?;
456            (declared, cell_id)
457        };
458        ctx.cell_spans
459            .insert(cell_id, SourceSpan::from_proc_macro2(name_span));
460        ctx.cell_token_spans.insert(cell_id, name_span);
461
462        let filter = if ctx.is_keyword("filter") {
463            Some(self.parse_cell_filter(ctx, &name, name_span, &shape)?)
464        } else {
465            None
466        };
467
468        // Inserted before the `require` block is parsed (unlike `filter`, parsed just above with
469        // `name` not yet in `ctx.cell_names`) so a requirement can reference the cell's own name
470        // — e.g. `require { @positive x > 0; }` — exactly as `parse_out_decl`'s `require` block
471        // can already reference its own out cell's name.
472        ctx.cell_names.insert(name.clone(), (cell_id, shape));
473
474        let require_names_and_reqs: Vec<(Option<String>, Requirement)> =
475            if ctx.is_keyword("require") {
476                self.parse_requirement_block(ctx)?
477            } else {
478                Vec::new()
479            };
480
481        ctx.expect_punct(";")?;
482        if let Some(filter) = filter {
483            ctx.sheet
484                .add_filter(cell_id, filter)
485                .map_err(|e| Self::sheet_error(ctx, name_span, e))?;
486        }
487        for (req_name, requirement) in require_names_and_reqs {
488            Self::register_requirement(ctx, cell_id, req_name, requirement, name_span)?;
489        }
490        Ok(())
491    }
492
493    /// `source_decl = "source" identifier cell_type_init [ cell_filter ] [ "require" "{" {
494    /// requirement } "}" ] ";".`
495    ///
496    /// `cell_type_init = (":" type_expr ["=" expression]) | ("=" expression).`
497    ///
498    /// Mirrors [`Self::parse_cell_decl`] exactly — the declared cell is added via
499    /// [`Sheet::add_source`] instead of [`Sheet::add_cell`], so it is fixed as a planner source
500    /// and can never be claimed as a relationship's output.
501    fn parse_source_decl(&mut self, ctx: &mut ParseContext) -> Result<()> {
502        ctx.is_keyword("source"); // consume
503        let (name, name_span) = ctx.consume_ident()?;
504        if ctx.cell_names.contains_key(&name) {
505            return Err(ParseError::new(
506                format!("duplicate cell `{name}`"),
507                name_span,
508            ));
509        }
510
511        let declared_shape: Option<TypeShape> = if ctx.consume_punct(":") {
512            let type_expr = self.parse_type_expr(ctx)?;
513            Some(
514                self.types
515                    .resolve(&type_expr)
516                    .map_err(|(msg, span)| ParseError::new(msg, span))?,
517            )
518        } else {
519            None
520        };
521
522        let has_initializer = ctx.consume_punct("=");
523        let (shape, cell_id) = if has_initializer {
524            let segment = self.parse_cel_expression(ctx)?;
525            self.validate_initializer(
526                &segment,
527                declared_shape.as_ref(),
528                "source",
529                &name,
530                name_span,
531            )?;
532            let (actual_shape, cell_id) = self
533                .build_source_cell_from_segment(segment, ctx)
534                .map_err(|e| ParseError::new(e.message(), name_span))?;
535            (actual_shape, cell_id)
536        } else {
537            let declared = declared_shape.ok_or_else(|| {
538                ParseError::new("expected `:` or `=` in source declaration", name_span)
539            })?;
540            let cell_id = self.build_default_source_cell(&declared, name_span, ctx)?;
541            (declared, cell_id)
542        };
543        ctx.cell_spans
544            .insert(cell_id, SourceSpan::from_proc_macro2(name_span));
545        ctx.cell_token_spans.insert(cell_id, name_span);
546
547        let filter = if ctx.is_keyword("filter") {
548            Some(self.parse_cell_filter(ctx, &name, name_span, &shape)?)
549        } else {
550            None
551        };
552
553        // Inserted before the `require` block is parsed so a requirement can reference the
554        // cell's own name — e.g. `require { @positive x > 0; }` — exactly as `parse_cell_decl`.
555        ctx.cell_names.insert(name.clone(), (cell_id, shape));
556
557        let require_names_and_reqs: Vec<(Option<String>, Requirement)> =
558            if ctx.is_keyword("require") {
559                self.parse_requirement_block(ctx)?
560            } else {
561                Vec::new()
562            };
563
564        ctx.expect_punct(";")?;
565        if let Some(filter) = filter {
566            ctx.sheet
567                .add_filter(cell_id, filter)
568                .map_err(|e| Self::sheet_error(ctx, name_span, e))?;
569        }
570        for (req_name, requirement) in require_names_and_reqs {
571            Self::register_requirement(ctx, cell_id, req_name, requirement, name_span)?;
572        }
573        Ok(())
574    }
575
576    /// `cell_filter = "filter" expression.`
577    ///
578    /// Builds an [`adam_rs::Filter`] from a single deduced expression: `_` denotes the candidate
579    /// value being conformed (of `declared_shape`'s type); every other identifier that names an
580    /// already-declared cell is a deduced dependency, exactly as [`Self::parse_deduced_expr`]
581    /// resolves them for a `relationship` binding or `out` declaration — see
582    /// [`Self::parse_filter_expr`]. `cell_name`/`cell_span`/`declared_shape` describe the
583    /// *filtered cell* (for error-message context and the candidate value's type), already
584    /// resolved by the caller — [`Self::parse_cell_decl`], [`Self::parse_source_decl`], or
585    /// [`Self::parse_out_decl`]. The filtered cell's own `CellId` is not needed
586    /// here: the caller attaches the returned `Filter` to it afterwards, via `Sheet::add_filter`.
587    ///
588    /// # Errors
589    /// Returns `Err` if an identifier inside the expression names neither `_` nor an
590    /// already-declared cell, if `_`
591    /// is never referenced, if the expression's inferred type doesn't match `declared_shape`, or,
592    /// if the expression is `RangeInclusive`-typed, if its element type doesn't match
593    /// `declared_shape`.
594    ///
595    /// - Complexity: O(m) in the number of distinct cell identifiers the expression references,
596    ///   for this method's own bookkeeping (on top of the expression's own parse/compile cost).
597    fn parse_cell_filter(
598        &mut self,
599        ctx: &mut ParseContext,
600        cell_name: &str,
601        cell_span: Span,
602        declared_shape: &TypeShape,
603    ) -> Result<adam_rs::Filter> {
604        let (segment, inputs, underscore_used) = self.parse_filter_expr(ctx, declared_shape)?;
605
606        let value_type_id = cell_type_id(declared_shape);
607        let output_type_id = segment.peek_output_type_id().ok_or_else(|| {
608            ParseError::new(
609                format!("cell `{cell_name}`: filter produced no value"),
610                cell_span,
611            )
612        })?;
613
614        let arg_ids: Vec<CellId> = inputs.iter().map(|(_, id, _)| *id).collect();
615        let arg_type_ids: Vec<TypeId> = inputs
616            .iter()
617            .map(|(_, _, shape)| cell_type_id(shape))
618            .collect();
619        let mut input_shapes = vec![declared_shape.clone()];
620        input_shapes.extend(inputs.iter().map(|(_, _, shape)| shape.clone()));
621
622        // A `RangeInclusive<T>`-shaped output is checked and built independently of
623        // `underscore_used`: a genuine range filter body (`lo..=hi`) essentially never references
624        // `_`, so gating this branch on `underscore_used` would make the more specific "range
625        // bounds must be" error unreachable for the primary mistake it exists to catch.
626        if let Some(range_shape) = self.types.range_entry(output_type_id) {
627            if range_shape.element_type_id != value_type_id {
628                return Err(ParseError::new(
629                    format!(
630                        "cell `{cell_name}`: filter range bounds must be `{}`",
631                        self.types.display_name(declared_shape)
632                    ),
633                    cell_span,
634                ));
635            }
636
637            let default_fn = self
638                .types
639                .entry_by_type_id(value_type_id)
640                .expect("declared cell type registered")
641                .default_fn
642                .expect("numeric range-filter cell type has a default");
643            let placeholder = default_fn();
644            let result = segment.peek_stack_infos(1)[0].value_type.clone();
645            let segment = std::rc::Rc::new(RefCell::new(
646                RawFrame::new(segment, &input_shapes, &result, &self.types)
647                    .map_err(|e| ParseError::new(e.to_string(), cell_span))?,
648            ));
649            let clamp_segment = std::rc::Rc::clone(&segment);
650            let bounds_segment = std::rc::Rc::clone(&segment);
651            let clamp_fn = range_shape.clamp_fn;
652            let bounds_fn = range_shape.bounds_fn;
653
654            return Ok(adam_rs::Filter::range(
655                value_type_id,
656                arg_ids,
657                arg_type_ids,
658                move |value, args| {
659                    clamp_fn(
660                        clamp_segment.borrow_mut().call_with_prefix(value, args)?,
661                        value,
662                    )
663                },
664                move |args| {
665                    bounds_fn(
666                        bounds_segment
667                            .borrow_mut()
668                            .call_checked_with_prefix(placeholder.as_ref(), args)
669                            .ok()?,
670                    )
671                    .ok()
672                },
673            ));
674        }
675
676        if !underscore_used {
677            return Err(ParseError::new(
678                "filter must reference `_` (the value being filtered)",
679                cell_span,
680            ));
681        }
682
683        if !segment.peek_stack_infos(1)[0]
684            .value_type
685            .same_shape(&self.types.value_type(declared_shape))
686        {
687            return Err(ParseError::new(
688                format!(
689                    "cell `{cell_name}`: filter must produce `{}`",
690                    self.types.display_name(declared_shape)
691                ),
692                cell_span,
693            ));
694        }
695
696        let frame = RefCell::new(
697            Frame::new(
698                segment,
699                &input_shapes,
700                std::slice::from_ref(declared_shape),
701                false,
702                &self.types,
703            )
704            .map_err(|e| ParseError::new(e.to_string(), cell_span))?,
705        );
706
707        Ok(adam_rs::Filter::new(
708            value_type_id,
709            arg_ids,
710            arg_type_ids,
711            move |value, args| {
712                Ok(frame
713                    .borrow_mut()
714                    .call_with_prefix(value, args)?
715                    .pop()
716                    .expect("one result"))
717            },
718        ))
719    }
720
721    /// Prepares and evaluates a nullary expression into concrete leaf or canonical ownership.
722    ///
723    /// - Precondition: `segment` requires no pre-loaded arguments (a `cell` initializer never
724    ///   has an input-cell scope pushed, unlike a `relationship` binding's, `out` declaration's, or
725    ///   `require`ment's body).
726    ///
727    /// # Errors
728    /// Rejects unregistered leaves, missing cell capabilities, or evaluation errors.
729    /// - Complexity: O(n) preparation for n descriptor nodes, plus execution costs.
730    fn eval_segment_boxed(&self, segment: DynSegment) -> Result<(TypeShape, Box<dyn Any>)> {
731        let shape = self.segment_shape(&segment, Span::call_site(), format_args!(""))?;
732        let mut frame = Frame::new(
733            segment,
734            &[],
735            std::slice::from_ref(&shape),
736            false,
737            &self.types,
738        )
739        .map_err(|e| ParseError::new(e.to_string(), Span::call_site()))?;
740        let value = frame
741            .call(&[])
742            .map_err(|e| ParseError::new(e.to_string(), Span::call_site()))?
743            .pop()
744            .expect("one result");
745        Ok((shape, value))
746    }
747
748    /// Infers complete recursive identity without executing the expression.
749    ///
750    /// # Errors
751    /// Rejects absent results or unregistered leaves at `span`, prefixed by `context`.
752    /// - Complexity: O(n) in result descriptor nodes.
753    fn segment_shape(
754        &self,
755        segment: &DynSegment,
756        span: Span,
757        context: std::fmt::Arguments<'_>,
758    ) -> Result<TypeShape> {
759        let info = segment.peek_stack_infos(1).first().ok_or_else(|| {
760            ParseError::new(format!("{context}expression produced no value"), span)
761        })?;
762        self.types
763            .shape_of(&info.value_type)
764            .map_err(|message| ParseError::new(format!("{context}{message}"), span))
765    }
766
767    /// Validates an initializer's complete shape before any initializer effects.
768    ///
769    /// # Errors
770    /// Reports a ranged shape-inference error, type mismatch, or missing cell capability.
771    /// Shape-inference and mismatch errors identify `construct` and `name`.
772    /// - Complexity: O(n) in result descriptor nodes.
773    fn validate_initializer(
774        &self,
775        segment: &DynSegment,
776        declared: Option<&TypeShape>,
777        construct: &str,
778        name: &str,
779        span: Span,
780    ) -> Result<()> {
781        let actual = self.segment_shape(segment, span, format_args!("{construct} `{name}`: "))?;
782        if let Some(expected) = declared
783            && expected != &actual
784        {
785            return Err(ParseError::new(
786                format!(
787                    "{construct} `{name}`: type mismatch: expected `{}`, got `{}`",
788                    self.types.display_name(expected),
789                    self.types.display_name(&actual)
790                ),
791                span,
792            ));
793        }
794        if !matches!(actual, TypeShape::Named(_)) {
795            CellValue::validate(&self.types.value_type(&actual))
796                .map_err(|e| ParseError::new(e.to_string(), span))?;
797        }
798        Ok(())
799    }
800
801    /// Adds a prepared initializer as concrete leaf storage or private canonical ownership.
802    ///
803    /// - Precondition: see [`eval_segment_boxed`](Self::eval_segment_boxed).
804    ///
805    /// # Errors
806    /// See [`eval_segment_boxed`](Self::eval_segment_boxed).
807    fn build_cell_from_segment(
808        &self,
809        segment: DynSegment,
810        ctx: &mut ParseContext,
811    ) -> Result<(TypeShape, CellId)> {
812        let (shape, boxed) = self.eval_segment_boxed(segment)?;
813        let cell_id = match &shape {
814            TypeShape::Named(type_id) => {
815                let entry = self.types.entry_by_type_id(*type_id).expect("registered");
816                (entry.add_cell_fn)(&mut ctx.sheet, boxed)
817            }
818            _ => {
819                let value = *boxed.downcast::<CellValue>().expect("canonical compound");
820                ctx.sheet.add_cell(value)
821            }
822        };
823        Ok((shape, cell_id))
824    }
825
826    /// Returns one live tuple element's type name in the same registered naming scheme
827    /// [`TypeRegistry::display_name`] uses, recursing into nested tuples. Falls back to the
828    /// value's own Rust type path for an unregistered leaf, which has no registered name.
829    ///
830    /// - Complexity: O(n) in the number of (nested) elements.
831    fn associated_display_name(&self, elem: &cel_runtime::AssociatedType) -> String {
832        self.types
833            .shape_of(&elem.value_type)
834            .map(|shape| self.types.display_name(&shape))
835            .unwrap_or_else(|_| elem.value_type.type_name().to_string())
836    }
837
838    /// Validates an entire default cell before invoking any registered defaults.
839    ///
840    /// Missing defaults take precedence over missing clone/equality capabilities.
841    /// # Errors
842    /// Names the first missing default in source order, or an unavailable cell capability.
843    /// - Complexity: O(n) in shape nodes.
844    fn validate_default_cell(&self, shape: &TypeShape, span: Span) -> Result<()> {
845        self.types
846            .validate_default(shape)
847            .and_then(|()| CellValue::validate(&self.types.value_type(shape)))
848            .map_err(|error| ParseError::new(error.to_string(), span))
849    }
850
851    /// Builds a default-valued cell for `shape` (scalar or compound, recursively), adding it to
852    /// `ctx.sheet`.
853    ///
854    /// - Complexity: O(n) in shape nodes, plus registered default construction costs.
855    ///
856    /// # Errors
857    /// Rejects missing defaults or cell capabilities before invoking any defaults or adding a cell.
858    fn build_default_cell(
859        &self,
860        shape: &TypeShape,
861        span: Span,
862        ctx: &mut ParseContext,
863    ) -> Result<CellId> {
864        self.validate_default_cell(shape, span)?;
865        match shape {
866            TypeShape::Named(type_id) => {
867                let entry = self
868                    .types
869                    .entry_by_type_id(*type_id)
870                    .expect("build_default_cell: type registered (resolved via TypeRegistry)");
871                let default_fn = entry.default_fn.ok_or_else(|| {
872                    ParseError::new(
873                        format!("type `{}` has no default; provide `= ...`", entry.type_name),
874                        span,
875                    )
876                })?;
877                Ok((entry.add_cell_fn)(&mut ctx.sheet, default_fn()))
878            }
879            _ => {
880                let mut body = DynSegment::new::<()>();
881                self.types
882                    .emit_default(shape, &mut body)
883                    .map_err(|e| ParseError::new(e.to_string(), span))?;
884                self.build_cell_from_segment(body, ctx)
885                    .map(|(_, id)| id)
886                    .map_err(|error| ParseError::new(error.message(), span))
887            }
888        }
889    }
890
891    /// Evaluates `segment` and adds concrete leaf or canonical compound source storage.
892    /// Mirrors
893    /// [`build_cell_from_segment`](Self::build_cell_from_segment) exactly, routed through
894    /// `Sheet::add_source` instead of `Sheet::add_cell`.
895    ///
896    /// - Complexity: O(n) in shape nodes, plus registered default construction costs.
897    ///
898    /// - Precondition: see [`eval_segment_boxed`](Self::eval_segment_boxed).
899    ///
900    /// # Errors
901    /// See [`eval_segment_boxed`](Self::eval_segment_boxed).
902    fn build_source_cell_from_segment(
903        &self,
904        segment: DynSegment,
905        ctx: &mut ParseContext,
906    ) -> Result<(TypeShape, CellId)> {
907        let (shape, boxed) = self.eval_segment_boxed(segment)?;
908        let cell_id = match &shape {
909            TypeShape::Named(type_id) => {
910                let entry = self.types.entry_by_type_id(*type_id).expect("registered");
911                (entry.add_source_fn)(&mut ctx.sheet, boxed)
912            }
913            _ => {
914                let value = *boxed.downcast::<CellValue>().expect("canonical compound");
915                ctx.sheet.add_source(value)
916            }
917        };
918        Ok((shape, cell_id))
919    }
920
921    /// Builds a default-valued *source* cell for `shape` (scalar or compound, recursively), adding
922    /// it to `ctx.sheet` (see [`Sheet::add_source`]). Mirrors
923    /// [`build_default_cell`](Self::build_default_cell) exactly, routed through
924    /// `Sheet::add_source` instead of `Sheet::add_cell`.
925    ///
926    /// # Errors
927    /// Rejects missing defaults or cell capabilities before invoking any defaults or adding a cell.
928    fn build_default_source_cell(
929        &self,
930        shape: &TypeShape,
931        span: Span,
932        ctx: &mut ParseContext,
933    ) -> Result<CellId> {
934        self.validate_default_cell(shape, span)?;
935        match shape {
936            TypeShape::Named(type_id) => {
937                let entry = self.types.entry_by_type_id(*type_id).expect(
938                    "build_default_source_cell: type registered (resolved via TypeRegistry)",
939                );
940                let default_fn = entry.default_fn.ok_or_else(|| {
941                    ParseError::new(
942                        format!("type `{}` has no default; provide `= ...`", entry.type_name),
943                        span,
944                    )
945                })?;
946                Ok((entry.add_source_fn)(&mut ctx.sheet, default_fn()))
947            }
948            _ => {
949                let mut body = DynSegment::new::<()>();
950                self.types
951                    .emit_default(shape, &mut body)
952                    .map_err(|e| ParseError::new(e.to_string(), span))?;
953                self.build_source_cell_from_segment(body, ctx)
954                    .map(|(_, id)| id)
955                    .map_err(|error| ParseError::new(error.message(), span))
956            }
957        }
958    }
959
960    /// Delegates one `type_expr` to CELParser, sharing the token stream.
961    fn parse_type_expr(&mut self, ctx: &mut ParseContext) -> Result<crate::ast::TypeExpr> {
962        let tokens = ctx.cursor.take_tokens().expect("tokens present");
963        self.cel.set_lex_tokens(tokens);
964        let result = self.cel.parse_type_expression();
965        ctx.cursor
966            .set_tokens(self.cel.take_lex_tokens().expect("tokens set"));
967        result
968    }
969
970    /// `relationship_decl = "relationship" "{" { binding } "}".`
971    ///
972    /// - Postcondition: the returned `RelationshipId` identifies the relationship just added to
973    ///   `ctx.sheet`.
974    /// - Postcondition: on success, `ctx.method_spans` gains one entry per parsed binding,
975    ///   keyed by `(rel_id, binding_index)`, mapping to that binding's source span, and
976    ///   `ctx.relationship_spans` gains one entry mapping `rel_id` to the whole block's span.
977    /// - Postcondition: on failure, the returned error's span covers the first binding it
978    ///   implicates (or the whole block, if none), with every other resolvable
979    ///   binding/cell attached as a secondary label (see `error_labels::site_label`).
980    fn parse_relationship_decl(&mut self, ctx: &mut ParseContext) -> Result<RelationshipId> {
981        let block_start = ctx.peek_span();
982        ctx.is_keyword("relationship"); // consume
983        ctx.expect_open_brace()?;
984        let mut methods = Vec::new();
985        let mut spans: Vec<(Span, Span)> = Vec::new();
986        while !ctx.at_close_brace() {
987            let (method, start, end) = self.parse_binding(ctx)?;
988            methods.push(method);
989            spans.push((start, end));
990        }
991        let close_span = ctx.expect_close_brace()?;
992        match ctx.sheet.add_relationship(methods) {
993            Ok(rel_id) => {
994                ctx.relationship_spans.insert(
995                    rel_id,
996                    SourceSpan::from_proc_macro2_range(block_start, close_span),
997                );
998                for (idx, (start, end)) in spans.into_iter().enumerate() {
999                    ctx.method_spans.insert(
1000                        (rel_id, idx),
1001                        SourceSpan::from_proc_macro2_range(start, end),
1002                    );
1003                }
1004                Ok(rel_id)
1005            }
1006            Err(e) => {
1007                let by_id: HashMap<CellId, String> = ctx
1008                    .cell_names
1009                    .iter()
1010                    .map(|(n, (id, _))| (*id, n.clone()))
1011                    .collect();
1012                let name = |id: CellId| by_id.get(&id).cloned();
1013
1014                // The primary site is the first binding this error implicates; every other
1015                // resolvable site (another binding, or a cell recorded so far) becomes a
1016                // secondary label. Falls back to the whole block when no site resolves — e.g.
1017                // `InvalidMethod` for an empty relationship body carries no sites at all.
1018                //
1019                // Resolved strictly from `sites().first()`, not by scanning for the first
1020                // `MethodIndex` anywhere in the list: every `Error` variant `add_relationship`
1021                // currently returns with a `MethodIndex` identifying the primary offending
1022                // binding. Making that dependence explicit means variants that lead with some
1023                // other site kind fail safe here (fall back to the whole block) instead of this
1024                // code silently treating a later, unrelated `MethodIndex` site as primary.
1025                let primary_idx = match e.sites().first() {
1026                    Some(ErrorSite::MethodIndex(i)) => Some(*i),
1027                    _ => None,
1028                };
1029                let (start, end) = primary_idx
1030                    .and_then(|i| spans.get(i).copied())
1031                    .unwrap_or((block_start, close_span));
1032                let secondary: Vec<cel_parser::SpanLabel> = e
1033                    .sites()
1034                    .iter()
1035                    .enumerate()
1036                    .filter_map(|(i, s)| {
1037                        let sp = match s {
1038                            ErrorSite::MethodIndex(idx) if Some(*idx) != primary_idx => spans
1039                                .get(*idx)
1040                                .map(|(st, en)| SourceSpan::from_proc_macro2_range(*st, *en)),
1041                            ErrorSite::Cell(c) => ctx.cell_spans.get(c).copied(),
1042                            _ => None,
1043                        }?;
1044                        Some(cel_parser::SpanLabel {
1045                            span: sp,
1046                            label: crate::error_labels::site_label(&e, i, &name),
1047                        })
1048                    })
1049                    .collect();
1050                Err(ParseError::new_range(e.to_string(), start, end).with_secondary(secondary))
1051            }
1052        }
1053    }
1054
1055    /// `binding = binding_target ":=" expression ";".`
1056    ///
1057    /// - Postcondition: the returned `Span`s bound the binding's full source range, from its
1058    ///   first token through the terminating `;`, for use in error-location resolution.
1059    fn parse_binding(&mut self, ctx: &mut ParseContext) -> Result<(Method, Span, Span)> {
1060        let start_span = ctx.peek_span();
1061        let (names, destructure) = parse_binding_target(ctx)?;
1062        let mut outputs: NamedCells = Vec::with_capacity(names.len());
1063        for (name, span) in names {
1064            let (cell_id, shape) = ctx
1065                .cell_names
1066                .get(&name)
1067                .cloned()
1068                .ok_or_else(|| ParseError::new(format!("undeclared cell `{name}`"), span))?;
1069            outputs.push((name, cell_id, shape));
1070        }
1071        ctx.expect_punct(":=")?;
1072        let (segment, inputs) = self.parse_deduced_expr(ctx)?;
1073        let end_span = ctx.expect_punct(";")?;
1074        let compiled = self.compile_outputs(ctx, &segment, &outputs, destructure, start_span)?;
1075        Ok((
1076            build_method(inputs, outputs, segment, compiled, &self.types)?,
1077            start_span,
1078            end_span,
1079        ))
1080    }
1081
1082    /// Parses an `expression` whose input cells are deduced from whichever already-declared
1083    /// cell identifiers it references, rather than an explicit `cell_list` — the mechanism
1084    /// shared by a conditional's match-subject expression ([`Self::parse_match_expr`]), a
1085    /// `relationship` binding's right-hand side, an `out` declaration's initializer, and a
1086    /// `require`ment body.
1087    ///
1088    /// Each 0-arity identifier lookup that names an already-declared cell is assigned the
1089    /// next argument index on first reference within this expression and reuses it on repeat
1090    /// reference (e.g. `a && a` allocates one argument slot, not two), via a scope pushed
1091    /// onto the CEL operation lookup for the duration of this parse.
1092    ///
1093    /// # Errors
1094    /// Returns `Err` if the expression fails to parse.
1095    ///
1096    /// - Complexity: O(k) in the number of distinct cell identifiers referenced, for this
1097    ///   method's own bookkeeping (on top of `cel-parser`'s own parse cost).
1098    fn parse_deduced_expr(&mut self, ctx: &mut ParseContext) -> Result<(DynSegment, NamedCells)> {
1099        self.parse_deduced_expr_restricted(ctx, cel_parser::ExpressionRestriction::None)
1100    }
1101
1102    /// Compiles a dependency-deducing expression with the enclosing header's brace reservation.
1103    ///
1104    /// - Postcondition: temporary dependency scope is removed on success and error.
1105    /// - Complexity: O(k + n) for k declared cells and n expression tokens.
1106    ///
1107    /// # Errors
1108    /// Returns expression syntax, unknown cell, or operation/type errors.
1109    fn parse_deduced_expr_restricted(
1110        &mut self,
1111        ctx: &mut ParseContext,
1112        restriction: cel_parser::ExpressionRestriction,
1113    ) -> Result<(DynSegment, NamedCells)> {
1114        // Precompute how to push each currently-declared cell, keyed by name. Built before
1115        // the scope closure captures anything, since `push_scope` requires `'static` (the
1116        // closure can't borrow `self.types`).
1117        let push_table: std::collections::HashMap<String, (CellId, TypeShape, InputPush)> = ctx
1118            .cell_names
1119            .iter()
1120            .map(|(name, (cell_id, shape))| {
1121                let push = self.types.value_type(shape);
1122                (name.clone(), (*cell_id, shape.clone(), push))
1123            })
1124            .collect();
1125
1126        let accumulator: Arc<Mutex<NamedCells>> = Arc::new(Mutex::new(Vec::new()));
1127        let scope_accumulator = Arc::clone(&accumulator);
1128
1129        self.cel
1130            .op_lookup_mut()
1131            .push_scope(move |name, segment, arity, _span| {
1132                if arity != 0 {
1133                    return Ok(false);
1134                }
1135                let Some((cell_id, shape, push)) = push_table.get(name) else {
1136                    return Ok(false);
1137                };
1138                let idx = {
1139                    let mut acc = scope_accumulator.lock().expect("scope mutex not poisoned");
1140                    match acc.iter().position(|(n, ..)| n == name) {
1141                        Some(pos) => pos,
1142                        None => {
1143                            acc.push((name.to_string(), *cell_id, shape.clone()));
1144                            acc.len() - 1
1145                        }
1146                    }
1147                };
1148                segment.push_argument(idx, push.clone())?;
1149                Ok(true)
1150            });
1151
1152        let result = self.parse_cel_expression_restricted(ctx, restriction);
1153        self.cel.op_lookup_mut().pop_scope();
1154        let segment = result?;
1155
1156        let inputs = accumulator
1157            .lock()
1158            .expect("scope mutex not poisoned")
1159            .clone();
1160        Ok((segment, inputs))
1161    }
1162
1163    /// Parses a `filter` clause's body expression, deducing its dependencies exactly as
1164    /// [`Self::parse_deduced_expr`] does, plus one reserved identifier: `_` always resolves to
1165    /// argument slot 0 (the candidate value being conformed, of `declared_shape`'s type), ahead
1166    /// of any cell-derived slots, which start at slot 1. Returns whether `_` was referenced at
1167    /// least once, alongside the compiled segment and its deduced cell inputs — the caller
1168    /// decides whether that occurrence count is acceptable.
1169    ///
1170    /// # Errors
1171    /// Returns `Err` if the expression fails to parse.
1172    ///
1173    /// - Complexity: O(k) in the number of distinct cell identifiers referenced, for this
1174    ///   method's own bookkeeping (on top of `cel-parser`'s own parse cost).
1175    fn parse_filter_expr(
1176        &mut self,
1177        ctx: &mut ParseContext,
1178        declared_shape: &TypeShape,
1179    ) -> Result<(DynSegment, NamedCells, bool)> {
1180        let push_table: std::collections::HashMap<String, (CellId, TypeShape, InputPush)> = ctx
1181            .cell_names
1182            .iter()
1183            .map(|(name, (cell_id, shape))| {
1184                let push = self.types.value_type(shape);
1185                (name.clone(), (*cell_id, shape.clone(), push))
1186            })
1187            .collect();
1188
1189        let value_push = self.types.value_type(declared_shape);
1190
1191        let accumulator: Arc<Mutex<NamedCells>> = Arc::new(Mutex::new(Vec::new()));
1192        let scope_accumulator = Arc::clone(&accumulator);
1193        let underscore_used: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
1194        let scope_underscore_used = Arc::clone(&underscore_used);
1195
1196        self.cel
1197            .op_lookup_mut()
1198            .push_scope(move |name, segment, arity, _span| {
1199                if arity != 0 {
1200                    return Ok(false);
1201                }
1202                if name == "_" {
1203                    *scope_underscore_used
1204                        .lock()
1205                        .expect("scope mutex not poisoned") = true;
1206                    segment.push_argument(0, value_push.clone())?;
1207                    return Ok(true);
1208                }
1209                let Some((cell_id, shape, push)) = push_table.get(name) else {
1210                    return Ok(false);
1211                };
1212                let idx = {
1213                    let mut acc = scope_accumulator.lock().expect("scope mutex not poisoned");
1214                    match acc.iter().position(|(n, ..)| n == name) {
1215                        Some(pos) => pos + 1,
1216                        None => {
1217                            acc.push((name.to_string(), *cell_id, shape.clone()));
1218                            acc.len()
1219                        }
1220                    }
1221                };
1222                segment.push_argument(idx, push.clone())?;
1223                Ok(true)
1224            });
1225
1226        let result = self.parse_cel_expression(ctx);
1227        self.cel.op_lookup_mut().pop_scope();
1228        let segment = result?;
1229
1230        let inputs = accumulator
1231            .lock()
1232            .expect("scope mutex not poisoned")
1233            .clone();
1234        let used = *underscore_used.lock().expect("scope mutex not poisoned");
1235        Ok((segment, inputs, used))
1236    }
1237
1238    /// Compiles a conditional's match-subject expression — a bare identifier (`mode`) is the
1239    /// degenerate single-cell case; anything more (`a && b`) draws on however many
1240    /// already-declared cells it references, via [`Self::parse_deduced_expr`].
1241    ///
1242    /// `match_span` is used to report errors raised by this method or the shape inference it
1243    /// delegates to; the caller already has it (from before parsing the expression) for its own
1244    /// error reporting, so it's threaded through rather than recomputed.
1245    ///
1246    /// # Errors
1247    /// Returns `Err` if the expression fails to parse, produces no value, or contains a leaf
1248    /// type that isn't registered in the `TypeRegistry`.
1249    fn parse_match_expr(
1250        &mut self,
1251        ctx: &mut ParseContext,
1252        match_span: proc_macro2::Span,
1253    ) -> Result<(TypeShape, MatchExpr)> {
1254        let (segment, inputs) = self.parse_deduced_expr_restricted(
1255            ctx,
1256            cel_parser::ExpressionRestriction::NoStructLiteral,
1257        )?;
1258        self.build_match_expr(segment, inputs, match_span)
1259    }
1260
1261    /// Builds a `(TypeShape, MatchExpr)` from a compiled match-expression segment and its
1262    /// deduced input cells, dispatching on the segment's inferred output shape — mirrors
1263    /// `build_method`'s single-output dispatch (`CompiledOutputs::Single`/`SingleTuple`), but
1264    /// for a match value read repeatedly across `propagate()` calls rather than written once
1265    /// per method call.
1266    ///
1267    /// - Precondition: `segment` was compiled with no pre-loaded arguments (`push_arg`-based),
1268    ///   matching every input in `inputs` by index.
1269    ///
1270    /// # Errors
1271    /// Returns `Err` at `match_span` if the segment produces no value or contains a leaf type
1272    /// that isn't registered in the `TypeRegistry`, prefixed by `match expression:`.
1273    fn build_match_expr(
1274        &self,
1275        segment: DynSegment,
1276        inputs: NamedCells,
1277        match_span: proc_macro2::Span,
1278    ) -> Result<(TypeShape, MatchExpr)> {
1279        let input_ids: Vec<CellId> = inputs.iter().map(|(_, id, _)| *id).collect();
1280        let input_types: Vec<TypeId> = inputs
1281            .iter()
1282            .map(|(_, _, shape)| cell_type_id(shape))
1283            .collect();
1284
1285        let shape = self.segment_shape(&segment, match_span, format_args!("match expression: "))?;
1286        let eq_fn = match &shape {
1287            TypeShape::Named(id) => {
1288                self.types
1289                    .entry_by_type_id(*id)
1290                    .expect("registered")
1291                    .eq_dyn_fn
1292            }
1293            _ => compound_eq,
1294        };
1295        let frame = RefCell::new(
1296            Frame::new(
1297                segment,
1298                &inputs
1299                    .iter()
1300                    .map(|(_, _, shape)| shape.clone())
1301                    .collect::<Vec<_>>(),
1302                std::slice::from_ref(&shape),
1303                false,
1304                &self.types,
1305            )
1306            .map_err(|e| ParseError::new(e.to_string(), match_span))?,
1307        );
1308        let match_expr = MatchExpr::new(
1309            input_ids,
1310            input_types,
1311            cell_type_id(&shape),
1312            eq_fn,
1313            move |args| Ok(frame.borrow_mut().call(args)?.pop().expect("one result")),
1314        );
1315        Ok((shape, match_expr))
1316    }
1317
1318    /// `conditional_decl = "conditional" expression "{" { conditional_branch } "}".`
1319    fn parse_conditional_decl(&mut self, ctx: &mut ParseContext) -> Result<()> {
1320        ctx.is_keyword("conditional"); // consume
1321        let match_span = ctx.peek_span();
1322        let (match_shape, match_expr) = self.parse_match_expr(ctx, match_span)?;
1323        ctx.expect_open_brace()?;
1324
1325        let mut branches: Vec<(Box<dyn Any>, Vec<RelationshipId>)> = Vec::new();
1326        let mut default_rel_ids: Vec<RelationshipId> = Vec::new();
1327
1328        while !ctx.at_close_brace() {
1329            // Check for default branch `_ => { ... }`
1330            if matches!(ctx.peek_token(), Some(Token::Identifier(id)) if id == "_") {
1331                ctx.advance(); // consume `_`
1332                ctx.expect_punct("=>")?;
1333                ctx.expect_open_brace()?;
1334                let rel_ids = self.parse_branch_relationships(ctx)?;
1335                ctx.expect_close_brace()?;
1336                ctx.consume_punct(",");
1337                default_rel_ids = rel_ids;
1338                break; // default branch is always last
1339            }
1340
1341            // Named branch: `literal_pattern "=>" "{" ... "}"` — a bare literal, optionally
1342            // negated by a leading `-`, matching Rust's own `LiteralPattern` rule. CEL has no
1343            // constant-expression syntax in pattern position (see
1344            // https://doc.rust-lang.org/reference/patterns.html), so `0i32 + 1 =>` is rejected
1345            // here exactly as it already is by the CST parser's `consume_literal_pattern`.
1346            let branch_span = ctx.peek_span();
1347            let segment = self.parse_literal_pattern(ctx)?;
1348            let (branch_shape, branch_val) = self.eval_segment_boxed(segment)?;
1349            if branch_shape != match_shape {
1350                return Err(ParseError::new(
1351                    format!(
1352                        "conditional branch: type mismatch: expected `{}`, got `{}`",
1353                        self.types.display_name(&match_shape),
1354                        self.types.display_name(&branch_shape)
1355                    ),
1356                    branch_span,
1357                ));
1358            }
1359            ctx.expect_punct("=>")?;
1360            ctx.expect_open_brace()?;
1361            let rel_ids = self.parse_branch_relationships(ctx)?;
1362            ctx.expect_close_brace()?;
1363            ctx.consume_punct(",");
1364            branches.push((branch_val, rel_ids));
1365        }
1366        ctx.expect_close_brace()?;
1367
1368        match &match_shape {
1369            TypeShape::Named(type_id) => {
1370                let add_cond_fn: AddConditionalFn = self
1371                    .types
1372                    .entry_by_type_id(*type_id)
1373                    .ok_or_else(|| {
1374                        ParseError::new("match cell type not in TypeRegistry", match_span)
1375                    })?
1376                    .add_conditional_fn;
1377                add_cond_fn(&mut ctx.sheet, match_expr, branches, default_rel_ids)
1378                    .map_err(|e| Self::sheet_error(ctx, match_span, e))?;
1379            }
1380            _ => {
1381                let typed_branches: Vec<(Vec<CellValue>, Vec<RelationshipId>)> = branches
1382                    .into_iter()
1383                    .map(|(val, rel_ids)| {
1384                        let seq = *val.downcast::<CellValue>().expect("canonical compound");
1385                        (vec![seq], rel_ids)
1386                    })
1387                    .collect();
1388                ctx.sheet
1389                    .add_conditional::<CellValue>(match_expr, typed_branches, default_rel_ids)
1390                    .map_err(|e| Self::sheet_error(ctx, match_span, e))?;
1391            }
1392        }
1393
1394        Ok(())
1395    }
1396
1397    /// Turns a `Sheet` construction failure into a `ParseError` with `primary` as the primary
1398    /// span and every one of `e`'s `Relationship`/`Cell` sites that resolves against `ctx`'s
1399    /// span tables as a secondary label.
1400    ///
1401    /// - Complexity: O(s) in the number of `e`'s sites.
1402    fn sheet_error(ctx: &ParseContext, primary: Span, e: adam_rs::Error) -> ParseError {
1403        let by_id: HashMap<CellId, String> = ctx
1404            .cell_names
1405            .iter()
1406            .map(|(n, (id, _))| (*id, n.clone()))
1407            .collect();
1408        let name = |id: CellId| by_id.get(&id).cloned();
1409        let secondary: Vec<cel_parser::SpanLabel> = e
1410            .sites()
1411            .iter()
1412            .enumerate()
1413            .filter_map(|(i, s)| {
1414                let sp = match s {
1415                    ErrorSite::Relationship(r) => ctx.relationship_spans.get(r).copied(),
1416                    ErrorSite::Cell(c) => ctx.cell_spans.get(c).copied(),
1417                    _ => None,
1418                }?;
1419                Some(cel_parser::SpanLabel {
1420                    span: sp,
1421                    label: crate::error_labels::site_label(&e, i, &name),
1422                })
1423            })
1424            .collect();
1425        let message = e.to_string();
1426        ParseError::new(message, primary).with_secondary(secondary)
1427    }
1428
1429    /// Turns a post-construction validation failure into a `ParseError`.
1430    ///
1431    /// The first resolvable site is the primary label, which for `DependencyCycle` is the
1432    /// governed cell declaration. Remaining resolvable sites become secondary labels.
1433    ///
1434    /// - Complexity: O(C + s), where C is the number of declared cells (each name is
1435    ///   copied into a lookup map) and s is the number of `e`'s sites.
1436    fn validated_sheet_error(ctx: &ParseContext, e: adam_rs::Error) -> ParseError {
1437        let by_id: HashMap<CellId, String> = ctx
1438            .cell_names
1439            .iter()
1440            .map(|(n, (id, _))| (*id, n.clone()))
1441            .collect();
1442        let name = |id: CellId| by_id.get(&id).cloned();
1443        let primary_index = e.sites().iter().position(|s| match s {
1444            ErrorSite::Cell(c) => ctx.cell_token_spans.contains_key(c),
1445            _ => false,
1446        });
1447        let primary = primary_index
1448            .and_then(|i| match e.sites()[i] {
1449                ErrorSite::Cell(c) => ctx.cell_token_spans.get(&c).copied(),
1450                _ => None,
1451            })
1452            .unwrap_or_else(Span::call_site);
1453        let secondary: Vec<cel_parser::SpanLabel> = e
1454            .sites()
1455            .iter()
1456            .enumerate()
1457            .filter(|(i, _)| Some(*i) != primary_index)
1458            .filter_map(|(i, s)| {
1459                let span = match s {
1460                    ErrorSite::Relationship(r) => ctx.relationship_spans.get(r).copied(),
1461                    ErrorSite::Cell(c) => ctx.cell_spans.get(c).copied(),
1462                    _ => None,
1463                }?;
1464                Some(cel_parser::SpanLabel {
1465                    span,
1466                    label: crate::error_labels::site_label(&e, i, &name),
1467                })
1468            })
1469            .collect();
1470        ParseError::new(e.to_string(), primary).with_secondary(secondary)
1471    }
1472
1473    /// Parses one `conditional_branch`/`default_branch`'s shared body: `"{" { relationship_decl }
1474    /// "}"`, up to (not including) the closing `}`.
1475    fn parse_branch_relationships(
1476        &mut self,
1477        ctx: &mut ParseContext,
1478    ) -> Result<Vec<RelationshipId>> {
1479        let mut rel_ids = Vec::new();
1480        while !ctx.at_close_brace() {
1481            if !matches!(ctx.peek_token(), Some(Token::Identifier(id)) if id == "relationship") {
1482                return Err(ctx.err_at("expected `relationship`"));
1483            }
1484            rel_ids.push(self.parse_relationship_decl(ctx)?);
1485        }
1486        Ok(rel_ids)
1487    }
1488
1489    /// `out_decl = "out" identifier [ ":" type_expr ] ":=" expression [ cell_filter ] [ "require"
1490    /// "{" { requirement } "}" ] ";".`
1491    fn parse_out_decl(&mut self, ctx: &mut ParseContext) -> Result<()> {
1492        ctx.is_keyword("out"); // consume
1493        let (name, name_span) = ctx.consume_ident()?;
1494        if ctx.cell_names.contains_key(&name) {
1495            return Err(ParseError::new(
1496                format!("duplicate cell `{name}`"),
1497                name_span,
1498            ));
1499        }
1500
1501        let declared_shape: Option<TypeShape> = if ctx.consume_punct(":") {
1502            let type_expr = self.parse_type_expr(ctx)?;
1503            Some(
1504                self.types
1505                    .resolve(&type_expr)
1506                    .map_err(|(msg, span)| ParseError::new(msg, span))?,
1507            )
1508        } else {
1509            None
1510        };
1511
1512        ctx.expect_punct(":=")?;
1513        let (segment, inputs) = self.parse_deduced_expr(ctx)?;
1514
1515        // Unlike a `cell` initializer's segment (zero-argument, safe to evaluate once eagerly
1516        // via `eval_segment_boxed`/`build_cell_from_segment`), an `out` writer's segment takes
1517        // real cell inputs (via `push_arg`) and must stay live for repeated re-evaluation by the
1518        // `Method` built below on every `Sheet::propagate` — so only its *shape* is inferred
1519        // here, from stack info, never actually executed.
1520        let actual_shape =
1521            self.segment_shape(&segment, name_span, format_args!("out `{name}`: "))?;
1522
1523        let out_shape = match &declared_shape {
1524            Some(declared) => {
1525                if declared != &actual_shape {
1526                    return Err(ctx.err_at(format!(
1527                        "out `{name}`: type mismatch: expected `{}`, got `{}`",
1528                        self.types.display_name(declared),
1529                        self.types.display_name(&actual_shape)
1530                    )));
1531                }
1532                declared.clone()
1533            }
1534            None => actual_shape,
1535        };
1536
1537        if !matches!(out_shape, TypeShape::Named(_)) {
1538            CellValue::validate(&self.types.value_type(&out_shape))
1539                .map_err(|error| ParseError::new(error.to_string(), name_span))?;
1540        }
1541        let cell_id = self.build_default_cell(&out_shape, name_span, ctx)?;
1542        ctx.cell_spans
1543            .insert(cell_id, SourceSpan::from_proc_macro2(name_span));
1544        ctx.cell_token_spans.insert(cell_id, name_span);
1545
1546        let filter = if ctx.is_keyword("filter") {
1547            Some(self.parse_cell_filter(ctx, &name, name_span, &out_shape)?)
1548        } else {
1549            None
1550        };
1551
1552        ctx.cell_names
1553            .insert(name.clone(), (cell_id, out_shape.clone()));
1554
1555        let compiled = CompiledOutputs::Single;
1556        let writer = build_method(
1557            inputs,
1558            vec![(name.clone(), cell_id, out_shape)],
1559            segment,
1560            compiled,
1561            &self.types,
1562        )?;
1563
1564        let requirements = if ctx.is_keyword("require") {
1565            self.parse_requirement_block(ctx)?
1566        } else {
1567            Vec::new()
1568        };
1569
1570        ctx.expect_punct(";")?;
1571
1572        let out_cell = ctx
1573            .sheet
1574            .add_out(writer, Vec::new())
1575            .map_err(|e| ParseError::new(e.to_string(), name_span))?;
1576        for (req_name, requirement) in requirements {
1577            Self::register_requirement(ctx, out_cell, req_name, requirement, name_span)?;
1578        }
1579        if let Some(filter) = filter {
1580            ctx.sheet
1581                .add_filter(out_cell, filter)
1582                .map_err(|e| Self::sheet_error(ctx, name_span, e))?;
1583        }
1584        ctx.output_names.insert(name, out_cell);
1585
1586        Ok(())
1587    }
1588
1589    /// `requirement_block = "{" { requirement } "}".`
1590    ///
1591    /// # Errors
1592    /// Returns a parse error for invalid requirements or repeated explicit labels within
1593    /// this declaration; unnamed requirements do not participate in duplicate detection.
1594    ///
1595    /// - Complexity: Expected O(n) bookkeeping for n requirements, plus compilation cost.
1596    fn parse_requirement_block(
1597        &mut self,
1598        ctx: &mut ParseContext,
1599    ) -> Result<Vec<(Option<String>, Requirement)>> {
1600        ctx.expect_open_brace()?;
1601        let mut requirements = Vec::new();
1602        let mut names = HashSet::new();
1603        while !ctx.at_close_brace() {
1604            let span = ctx
1605                .peek_token()
1606                .ok_or_else(|| ParseError::new("unexpected end of input", Span::call_site()))?
1607                .span();
1608            let (name, requirement) = self.parse_requirement(ctx)?;
1609            if let Some(label) = &name
1610                && !names.insert(label.clone())
1611            {
1612                return Err(ParseError::new(
1613                    format!("duplicate requirement `{label}`"),
1614                    span,
1615                ));
1616            }
1617            requirements.push((name, requirement));
1618        }
1619        ctx.expect_close_brace()?;
1620        Ok(requirements)
1621    }
1622
1623    /// Attaches a requirement and records its explicit label against the returned ID.
1624    ///
1625    /// # Errors
1626    /// Returns a parse error at `span` when sheet registration fails.
1627    ///
1628    /// - Complexity: The cost of `Sheet::add_requirement`, plus expected O(1) map insertion.
1629    fn register_requirement(
1630        ctx: &mut ParseContext,
1631        cell: CellId,
1632        name: Option<String>,
1633        requirement: Requirement,
1634        span: Span,
1635    ) -> Result<()> {
1636        let id = ctx
1637            .sheet
1638            .add_requirement(cell, requirement)
1639            .map_err(|e| ParseError::new(e.to_string(), span))?;
1640        if let Some(name) = name {
1641            ctx.requirement_names.insert(id, name);
1642        }
1643        Ok(())
1644    }
1645
1646    /// `requirement = [ "@" identifier ] expression ";".`
1647    fn parse_requirement(
1648        &mut self,
1649        ctx: &mut ParseContext,
1650    ) -> Result<(Option<String>, Requirement)> {
1651        let name = if ctx.consume_punct("@") {
1652            let (name, _name_span) = ctx.consume_ident()?;
1653            Some(name)
1654        } else {
1655            None
1656        };
1657        let (segment, inputs) = self.parse_deduced_expr(ctx)?;
1658        ctx.expect_punct(";")?;
1659
1660        let bool_type_id = TypeId::of::<bool>();
1661        let actual_type_id = segment.peek_output_type_id().ok_or_else(|| {
1662            ctx.err_at(match &name {
1663                Some(name) => format!("requirement `{name}`: expression produced no value"),
1664                None => "requirement: expression produced no value".to_string(),
1665            })
1666        })?;
1667        if actual_type_id != bool_type_id {
1668            let got = self
1669                .types
1670                .entry_by_type_id(actual_type_id)
1671                .map(|e| e.type_name)
1672                .unwrap_or("?");
1673            return Err(ctx.err_at(match &name {
1674                Some(name) => format!("requirement `{name}`: expected `bool`, got `{got}`"),
1675                None => format!("requirement: expected `bool`, got `{got}`"),
1676            }));
1677        }
1678
1679        let input_ids: Vec<CellId> = inputs.iter().map(|(_, id, _)| *id).collect();
1680        let input_types: Vec<TypeId> = inputs
1681            .iter()
1682            .map(|(_, _, shape)| cell_type_id(shape))
1683            .collect();
1684        let frame = RefCell::new(
1685            RawFrame::new(
1686                segment,
1687                &inputs
1688                    .iter()
1689                    .map(|(_, _, shape)| shape.clone())
1690                    .collect::<Vec<_>>(),
1691                &cel_runtime::ValueType::leaf::<bool>(),
1692                &self.types,
1693            )
1694            .map_err(|e| ctx.err_at(e.to_string()))?,
1695        );
1696        let requirement = Requirement::new(input_ids, input_types, move |args| {
1697            let value = frame.borrow_mut().call(args)?;
1698            // SAFETY: The complete result was bound as the concrete bool leaf.
1699            Ok(unsafe { value.into_typed_bound::<cel_runtime::Leaf<bool>>() })
1700        });
1701
1702        Ok((name, requirement))
1703    }
1704
1705    /// Determines how to split a compiled body segment's result across `outputs`, given their
1706    /// declared shapes — used by `parse_binding` to dispatch a `relationship` binding's direct-bind vs.
1707    /// destructuring cases against a single compiled `expression`. Written generically so any
1708    /// future N-output construct can reuse it; `out` declarations are always single-output,
1709    /// never destructuring, and currently use their own simpler, separate dispatch instead.
1710    ///
1711    /// Direct bindings move the whole result, including source unit. Destructuring requires
1712    /// a tuple of exactly matching recursive field shapes and moves its field owners.
1713    /// Shape-inference errors identify the output at `binding_span`.
1714    ///
1715    /// # Errors
1716    /// Returns `Err` if any output's declared shape doesn't structurally match the body's actual
1717    /// result (scalar type mismatch, tuple arity mismatch, or tuple element shape mismatch, at
1718    /// any nesting depth), or if a single non-destructuring output's
1719    /// expression produced no value or contains an unregistered leaf type.
1720    fn compile_outputs(
1721        &self,
1722        ctx: &mut ParseContext,
1723        segment: &DynSegment,
1724        outputs: &[(String, CellId, TypeShape)],
1725        destructure: bool,
1726        binding_span: Span,
1727    ) -> Result<CompiledOutputs> {
1728        if outputs.len() == 1 && !destructure {
1729            let (out_name, _, out_shape) = &outputs[0];
1730            let actual =
1731                self.segment_shape(segment, binding_span, format_args!("output `{out_name}`: "))?;
1732            if &actual != out_shape {
1733                return Err(ctx.err_at(format!(
1734                    "output `{out_name}`: type mismatch: expected `{}`, got `{}`",
1735                    self.types.display_name(out_shape),
1736                    self.types.display_name(&actual)
1737                )));
1738            }
1739            Ok(CompiledOutputs::Single)
1740        } else {
1741            let arity = segment.peek_tuple_arity().unwrap_or(0);
1742            if arity != outputs.len() {
1743                return Err(ctx.err_at(format!(
1744                    "output expression has arity {arity} but {} output(s) declared",
1745                    outputs.len()
1746                )));
1747            }
1748            let associated = segment.peek_stack_infos(1)[0]
1749                .value_type
1750                .tuple_elements()
1751                .map(<[_]>::to_vec)
1752                .unwrap_or_default();
1753            for (i, ((out_name, _, out_shape), elem)) in outputs.iter().zip(&associated).enumerate()
1754            {
1755                if !self
1756                    .types
1757                    .value_type(out_shape)
1758                    .same_shape(&elem.value_type)
1759                {
1760                    return Err(ctx.err_at(format!(
1761                        "output {i} `{out_name}`: type mismatch: expected `{}`, got `{}`",
1762                        self.types.display_name(out_shape),
1763                        self.associated_display_name(elem)
1764                    )));
1765                }
1766            }
1767            Ok(CompiledOutputs::Tuple)
1768        }
1769    }
1770
1771    /// Delegates one `expression` to CELParser, sharing the token stream.
1772    fn parse_cel_expression(&mut self, ctx: &mut ParseContext) -> Result<DynSegment> {
1773        self.parse_cel_expression_restricted(ctx, cel_parser::ExpressionRestriction::None)
1774    }
1775
1776    /// Delegates a header expression while leaving its body brace in the shared token stream.
1777    ///
1778    /// - Complexity: O(n) in expression tokens.
1779    ///
1780    /// # Errors
1781    /// Returns the embedded CEL expression's syntax or type error.
1782    fn parse_cel_expression_restricted(
1783        &mut self,
1784        ctx: &mut ParseContext,
1785        restriction: cel_parser::ExpressionRestriction,
1786    ) -> Result<DynSegment> {
1787        let tokens = ctx.cursor.take_tokens().expect("tokens present");
1788        self.cel.set_lex_tokens(tokens);
1789        let result = self.cel.parse_expression_restricted(restriction);
1790        ctx.cursor
1791            .set_tokens(self.cel.take_lex_tokens().expect("tokens set"));
1792        let mut segment = result?;
1793        if let Some(info) = segment.peek_stack_infos(1).first() {
1794            let enriched = self
1795                .types
1796                .enrich_value_type(&info.value_type)
1797                .map_err(|error| ParseError::new(error.to_string(), Span::call_site()))?;
1798            segment
1799                .annotate_top(enriched)
1800                .map_err(|error| ParseError::new(error.to_string(), Span::call_site()))?;
1801        }
1802        Ok(segment)
1803    }
1804
1805    /// Delegates one `literal_pattern` (a bare literal, optionally negated by a leading `-`) to
1806    /// CELParser, sharing the token stream.
1807    fn parse_literal_pattern(&mut self, ctx: &mut ParseContext) -> Result<DynSegment> {
1808        let tokens = ctx.cursor.take_tokens().expect("tokens present");
1809        self.cel.set_lex_tokens(tokens);
1810        let result = self.cel.parse_literal_pattern();
1811        ctx.cursor
1812            .set_tokens(self.cel.take_lex_tokens().expect("tokens set"));
1813        result
1814    }
1815}
1816
1817// ---------------------------------------------------------------------------
1818// Free functions
1819// ---------------------------------------------------------------------------
1820
1821/// Complete metadata for an input cell's canonical borrowed argument read.
1822type InputPush = cel_runtime::ValueType;
1823
1824/// Compares private compound conditional keys at an already validated boundary.
1825///
1826/// - Complexity: O(v) for v contained values.
1827fn compound_eq(a: &dyn Any, b: &dyn Any) -> bool {
1828    a.downcast_ref::<CellValue>() == b.downcast_ref::<CellValue>()
1829}
1830
1831/// How to turn one compiled `expression`'s result into per-output values.
1832enum CompiledOutputs {
1833    /// Moves the segment's whole result into one cell.
1834    Single,
1835    /// Moves the segment's tuple fields into source-ordered output cells.
1836    Tuple,
1837}
1838
1839/// Builds a [`Method`] from parsed inputs, outputs, the compiled body segment, and how
1840/// to split its result across `outputs`.
1841fn build_method(
1842    inputs: NamedCells,
1843    outputs: NamedCells,
1844    segment: DynSegment,
1845    compiled: CompiledOutputs,
1846    types: &TypeRegistry,
1847) -> Result<Method> {
1848    let input_ids: Vec<CellId> = inputs.iter().map(|(_, id, _)| *id).collect();
1849    let output_ids: Vec<CellId> = outputs.iter().map(|(_, id, _)| *id).collect();
1850    let input_types: Vec<TypeId> = inputs
1851        .iter()
1852        .map(|(_, _, shape)| cell_type_id(shape))
1853        .collect();
1854    let output_types: Vec<TypeId> = outputs
1855        .iter()
1856        .map(|(_, _, shape)| cell_type_id(shape))
1857        .collect();
1858
1859    let frame = RefCell::new(
1860        Frame::new(
1861            segment,
1862            &inputs.iter().map(|(_, _, s)| s.clone()).collect::<Vec<_>>(),
1863            &outputs
1864                .iter()
1865                .map(|(_, _, s)| s.clone())
1866                .collect::<Vec<_>>(),
1867            matches!(compiled, CompiledOutputs::Tuple),
1868            types,
1869        )
1870        .map_err(|e| ParseError::new(e.to_string(), Span::call_site()))?,
1871    );
1872    Ok(Method::new(
1873        input_ids,
1874        output_ids,
1875        input_types,
1876        output_types,
1877        move |inputs| frame.borrow_mut().call(inputs),
1878    ))
1879}
1880
1881/// Flattens a declared cell's `TypeShape` to the single `TypeId` `adam_rs` itself needs for its
1882/// own (CEL-agnostic) storage bookkeeping. Compound shape identity remains separate from
1883/// the private common storage carrier and is validated before constructing any adapter.
1884fn cell_type_id(shape: &TypeShape) -> TypeId {
1885    match shape {
1886        TypeShape::Named(type_id) => *type_id,
1887        _ => TypeId::of::<CellValue>(),
1888    }
1889}
1890
1891/// `binding_target = identifier | "(" identifier { "," identifier } [ "," ] ")".`
1892///
1893/// Returns the output names in declaration order alongside whether the left-hand side requests
1894/// destructuring: `false` for a bare identifier or a single parenthesized identifier with no
1895/// comma (mere grouping, matching Rust's `(a)` pattern); `true` for `(a,)` (a 1-tuple pattern,
1896/// trailing comma mandatory) or `(a, b, ...)`.
1897fn parse_binding_target(ctx: &mut ParseContext) -> Result<(Vec<(String, Span)>, bool)> {
1898    if !ctx.at_open_paren() {
1899        let (name, span) = ctx.consume_ident()?;
1900        return Ok((vec![(name, span)], false));
1901    }
1902
1903    ctx.expect_open_paren()?;
1904    let (first_name, first_span) = ctx.consume_ident()?;
1905    if ctx.at_close_paren() {
1906        // Grouping: exactly one identifier, no comma -- same as the bare form.
1907        ctx.expect_close_paren()?;
1908        return Ok((vec![(first_name, first_span)], false));
1909    }
1910    if !ctx.consume_punct(",") {
1911        return Err(ctx.err_at("expected ',' or closing parenthesis"));
1912    }
1913    if ctx.at_close_paren() {
1914        // Single identifier + trailing comma: destructures a 1-tuple.
1915        ctx.expect_close_paren()?;
1916        return Ok((vec![(first_name, first_span)], true));
1917    }
1918    let mut outputs = vec![(first_name, first_span)];
1919    loop {
1920        let (name, span) = ctx.consume_ident()?;
1921        outputs.push((name, span));
1922        if ctx.at_close_paren() {
1923            break;
1924        }
1925        if !ctx.consume_punct(",") {
1926            return Err(ctx.err_at("expected ',' or closing parenthesis"));
1927        }
1928    }
1929    ctx.expect_close_paren()?;
1930    Ok((outputs, true))
1931}
1932
1933// ---------------------------------------------------------------------------
1934// Tests
1935// ---------------------------------------------------------------------------
1936
1937#[cfg(test)]
1938mod tests {
1939    use super::*;
1940    use crate::TypeRegistry;
1941    use cel_parser::OpLookup;
1942    use cel_runtime::{ArrayOf, Leaf};
1943
1944    fn parser() -> AdamParser {
1945        AdamParser::new(TypeRegistry::new(), OpLookup::new())
1946    }
1947
1948    #[derive(Clone, Debug, PartialEq)]
1949    struct Custom(i32);
1950
1951    #[derive(Clone)]
1952    struct CountFn;
1953
1954    fn custom_lookup() -> OpLookup {
1955        let mut lookup = OpLookup::new();
1956        lookup.push_scope(|name, segment, arity, _span| match name {
1957            "left" if arity == 0 => {
1958                segment.op0(|| Custom(1));
1959                Ok(true)
1960            }
1961            "right" if arity == 0 => {
1962                segment.op0(|| Custom(2));
1963                Ok(true)
1964            }
1965            "count" if arity == 0 => {
1966                segment.op0(|| CountFn);
1967                Ok(true)
1968            }
1969            "()" if arity == 2 => {
1970                if segment.peek_stack_infos(1)[0]
1971                    .value_type
1972                    .array_element()
1973                    .is_some_and(|element| element.array_element().is_some())
1974                {
1975                    segment
1976                        .op2_repr::<Leaf<CountFn>, ArrayOf<ArrayOf<Leaf<Custom>>>, Leaf<i32>, _>(
1977                            |_callee, array| array.len() as i32,
1978                        )?;
1979                } else {
1980                    segment.op2_repr::<Leaf<CountFn>, ArrayOf<Leaf<Custom>>, Leaf<i32>, _>(
1981                        |_callee, array| array.len() as i32,
1982                    )?;
1983                }
1984                Ok(true)
1985            }
1986            _ => Ok(false),
1987        });
1988        lookup
1989    }
1990
1991    #[test]
1992    fn parse_empty_sheet() {
1993        let _sheet = parser().parse_str("sheet empty {}").unwrap();
1994    }
1995
1996    #[test]
1997    fn parse_cell_with_annotation_and_initializer() {
1998        let _sheet = parser()
1999            .parse_str("sheet s { cell width: f64 = 1920.0; }")
2000            .unwrap();
2001    }
2002
2003    #[test]
2004    fn parse_cell_annotation_only_uses_default() {
2005        let _sheet = parser().parse_str("sheet s { cell area: f64; }").unwrap();
2006    }
2007
2008    #[test]
2009    fn parse_cell_initializer_infers_type() {
2010        let _sheet = parser().parse_str("sheet s { cell mode = 0i32; }").unwrap();
2011    }
2012
2013    #[test]
2014    fn parse_cell_unknown_type_is_error() {
2015        let result = parser().parse_str("sheet s { cell x: unknown_type; }");
2016        assert!(result.is_err());
2017        let err = result.expect_err("expected Err");
2018        let msg = err.message().to_lowercase();
2019        assert!(
2020            msg.contains("unknown type") || msg.contains("unknown_type"),
2021            "{msg}"
2022        );
2023    }
2024
2025    #[test]
2026    fn parse_cell_missing_default_is_error() {
2027        #[derive(PartialEq, Clone, Debug)]
2028        struct NoDef(i32);
2029        let mut reg = TypeRegistry::new();
2030        reg.register_no_default::<NoDef>("NoDef");
2031        let mut p = AdamParser::new(reg, OpLookup::new());
2032        let result = p.parse_str("sheet s { cell x: NoDef; }");
2033        assert!(result.is_err());
2034    }
2035
2036    #[test]
2037    fn parse_cell_initializer_supports_registry_typed_arrays() {
2038        let mut reg = TypeRegistry::new();
2039        reg.register_no_default::<Custom>("Custom");
2040        let mut parser = AdamParser::new(reg, custom_lookup());
2041
2042        let parsed = parser
2043            .parse_str(
2044                "sheet s { \
2045                    cell values: i32 = count([left, right]: [Custom]); \
2046                    cell empty: i32 = count([]: [Custom]); \
2047                    cell nested: i32 = count([[]: [Custom]]: [[Custom]]); \
2048                }",
2049            )
2050            .unwrap();
2051
2052        let (values_id, _) = parsed.cell_names["values"];
2053        assert_eq!(*parsed.read::<i32>(values_id).unwrap(), 2);
2054
2055        let (empty_id, _) = parsed.cell_names["empty"];
2056        assert_eq!(*parsed.read::<i32>(empty_id).unwrap(), 0);
2057
2058        let (nested_id, _) = parsed.cell_names["nested"];
2059        assert_eq!(*parsed.read::<i32>(nested_id).unwrap(), 1);
2060    }
2061
2062    #[test]
2063    fn parse_cell_initializer_rejects_unknown_registry_typed_arrays() {
2064        let mut parser = AdamParser::new(TypeRegistry::new(), custom_lookup());
2065        let err = parser
2066            .parse_str("sheet s { cell values = []: [Custom]; }")
2067            .expect_err("unknown custom array annotations must fail");
2068
2069        assert!(
2070            err.message().contains("unknown type `Custom`"),
2071            "got: {}",
2072            err.message()
2073        );
2074    }
2075
2076    #[test]
2077    fn parse_cell_initializer_preserves_persistent_array_cells() {
2078        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2079        let parsed = parser
2080            .parse_str("sheet s { cell values = []: [(i32, f64)]; }")
2081            .unwrap();
2082        type Pairs = cel_runtime::ArrayOf<
2083            cel_runtime::Cons<
2084                cel_runtime::Leaf<i32>,
2085                cel_runtime::Cons<cel_runtime::Leaf<f64>, cel_runtime::Nil>,
2086            >,
2087        >;
2088        assert!(
2089            parsed
2090                .read_compound::<Pairs>(parsed.cell_names["values"].0)
2091                .unwrap()
2092                .is_empty()
2093        );
2094    }
2095
2096    #[test]
2097    fn parse_source_decl_registers_a_source_kind_cell() {
2098        let sheet = parser()
2099            .parse_str("sheet s { source width: i32 = 4; }")
2100            .unwrap();
2101        let (width, _) = sheet.cell_names["width"];
2102        assert_eq!(sheet.cell_kind(width), Some(adam_rs::CellKind::Source));
2103    }
2104
2105    #[test]
2106    fn parse_source_decl_requires_colon_or_equals() {
2107        let result = parser().parse_str("sheet s { source width; }");
2108        assert!(result.is_err());
2109    }
2110
2111    #[test]
2112    fn parse_cell_decl_with_a_require_block_attaches_requirements() {
2113        let sheet = parser()
2114            .parse_str("sheet s { cell x: i32 = 5 require { @positive x > 0; }; }")
2115            .unwrap();
2116        let (x, _) = sheet.cell_names["x"];
2117        assert_eq!(sheet.cell_requirements(x).unwrap().len(), 1);
2118    }
2119
2120    #[test]
2121    fn parse_source_decl_with_a_require_block_attaches_requirements() {
2122        let sheet = parser()
2123            .parse_str("sheet s { source x: i32 = 5 require { @positive x > 0; }; }")
2124            .unwrap();
2125        let (x, _) = sheet.cell_names["x"];
2126        assert_eq!(sheet.cell_requirements(x).unwrap().len(), 1);
2127    }
2128
2129    #[test]
2130    fn parse_source_decl_with_a_filter_clause_attaches_a_filter() {
2131        let sheet = parser()
2132            .parse_str("sheet s { source x: i32 = 5 filter 0..=10; }")
2133            .unwrap();
2134        let (x, _) = sheet.cell_names["x"];
2135        assert!(sheet.filter_kind(x).is_some());
2136    }
2137
2138    #[test]
2139    fn parse_out_decl_with_a_filter_clause_attaches_a_filter() {
2140        let sheet = parser()
2141            .parse_str("sheet s { cell width: i32 = 4; out area := width filter 0..=100; }")
2142            .unwrap();
2143        let area = sheet.output_names["area"];
2144        assert!(sheet.filter_kind(area).is_some());
2145    }
2146
2147    #[test]
2148    fn cell_filter_with_no_named_dependency_clamps_on_write() {
2149        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2150        let mut parsed = parser
2151            .parse_str(
2152                "sheet s { cell a: i32 filter if _ < 1 { 1 } else if _ > 100 { 100 } else { _ }; }",
2153            )
2154            .unwrap();
2155        let (cell_id, _) = parsed.cell_names["a"];
2156        parsed.sheet.write(cell_id, 500i32).unwrap();
2157        parsed.sheet.propagate().unwrap();
2158        assert_eq!(*parsed.sheet.read::<i32>(cell_id).unwrap(), 100);
2159    }
2160
2161    #[test]
2162    fn cell_filter_referencing_a_cell_tracks_its_current_value() {
2163        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2164        let mut parsed = parser
2165            .parse_str(
2166                "sheet s { \
2167                     cell hi: i32 = 100; \
2168                     cell a: i32 filter if _ < 1 { 1 } else if _ > hi { hi } else { _ }; \
2169                 }",
2170            )
2171            .unwrap();
2172        let (a_id, _) = parsed.cell_names["a"];
2173        let (hi_id, _) = parsed.cell_names["hi"];
2174
2175        parsed.sheet.write(a_id, 500i32).unwrap();
2176        parsed.sheet.propagate().unwrap();
2177        assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 100);
2178
2179        parsed.sheet.write(hi_id, 10i32).unwrap();
2180        parsed.sheet.write(a_id, 500i32).unwrap();
2181        parsed.sheet.propagate().unwrap();
2182        assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 10);
2183    }
2184
2185    #[test]
2186    fn cell_filter_referencing_the_same_value_twice_is_idempotent() {
2187        // Snap-to-grid: `_ - (_ % step)` — `_` referenced twice must denote the same value both
2188        // times, not two independent parameters.
2189        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2190        let mut parsed = parser
2191            .parse_str("sheet s { cell step: i32 = 10; cell a: i32 filter _ - (_ % step); }")
2192            .unwrap();
2193        let (a_id, _) = parsed.cell_names["a"];
2194        parsed.sheet.write(a_id, 27i32).unwrap();
2195        parsed.sheet.propagate().unwrap();
2196        assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 20);
2197    }
2198
2199    #[test]
2200    fn filter_dependency_cycle_reported_with_secondary_labels() {
2201        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2202        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}";
2203        assert!(parser.parse_str(ok_source).is_ok());
2204
2205        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2206        // The parser only lets a filter reference already-declared cells, so the cycle becomes
2207        // visible when the later relationship adds the reverse dependency edge.
2208        let source = "sheet s {\n    cell b: i32 = 0;\n    cell a: i32 = 5 filter _ + b - b;\n    relationship { b := a; }\n}";
2209        let err = parser.parse_str(source).unwrap_err();
2210        assert_eq!(
2211            err.message(),
2212            "a filter or conditional depends on a cell it governs (dependency cycle)"
2213        );
2214        assert!(!err.secondary().is_empty());
2215    }
2216
2217    #[test]
2218    fn cell_filter_without_underscore_is_a_parse_error() {
2219        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2220        let err = parser.parse_str("sheet s { cell a: i32 filter 1; }");
2221        assert!(err.is_err());
2222    }
2223
2224    #[test]
2225    fn cell_filter_body_type_mismatch_is_a_parse_error() {
2226        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2227        let err = parser.parse_str("sheet s { cell a: i32 filter _ > 0; }");
2228        assert!(err.is_err());
2229    }
2230
2231    #[test]
2232    fn cell_filter_undeclared_identifier_is_a_parse_error() {
2233        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2234        let err = parser.parse_str("sheet s { cell a: i32 filter _ + nope; }");
2235        assert!(err.is_err());
2236    }
2237
2238    #[test]
2239    fn cell_filter_with_a_range_inclusive_body_clamps_on_write() {
2240        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2241        let mut parsed = parser
2242            .parse_str("sheet s { cell a: i32 filter 0..=100; }")
2243            .unwrap();
2244        let (a_id, _) = parsed.cell_names["a"];
2245        assert!(matches!(
2246            parsed.sheet.filter_kind(a_id),
2247            Some(adam_rs::FilterKind::Range { .. })
2248        ));
2249        parsed.sheet.write(a_id, 500i32).unwrap();
2250        parsed.sheet.propagate().unwrap();
2251        assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 100);
2252    }
2253
2254    /// Rejects invalid public erased inputs on every call without poisoning later bounds.
2255    #[test]
2256    fn public_range_bounds_rejects_invalid_scalar_inputs() {
2257        let parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
2258            .parse_str("sheet s { cell hi: f64 = 10.0; cell value: f64 filter 0.0..=hi; }")
2259            .unwrap();
2260        let Some(adam_rs::FilterKind::Range { bounds }) =
2261            parsed.sheet.filter_kind(parsed.cell_names["value"].0)
2262        else {
2263            panic!("expected range filter");
2264        };
2265        let invalid: [&[&dyn Any]; 4] = [&[&42u64], &[&1u8], &[], &[&10.0f64, &20.0f64]];
2266        for _ in 0..2 {
2267            for args in invalid {
2268                let (lo, hi) = bounds(&[&10.0f64]).unwrap();
2269                assert_eq!(*lo.downcast::<f64>().unwrap(), 0.0);
2270                assert_eq!(*hi.downcast::<f64>().unwrap(), 10.0);
2271                // The first mismatch has f64's size and alignment, so the pre-fix
2272                // regression fails by accepting it, without an undersized raw read.
2273                assert!(bounds(args).is_none(), "invalid scalar inputs accepted");
2274                let (_, hi) = bounds(&[&25.0f64]).unwrap();
2275                assert_eq!(*hi.downcast::<f64>().unwrap(), 25.0);
2276            }
2277        }
2278    }
2279
2280    /// Checks complete compound shapes, carrier identity, arity, and recovery.
2281    #[test]
2282    fn public_range_bounds_rejects_invalid_compound_inputs() {
2283        use cel_runtime::{Cons, IntoCStackList, Leaf, Nil, OwnedValue};
2284
2285        let parsed = AdamParser::new(TypeRegistry::new(), OpLookup::new())
2286            .parse_str(
2287                "sheet s { cell limits: (f64, f64) = (0.0, 10.0); \
2288                 cell value: f64 filter limits.0..=limits.1; }",
2289            )
2290            .unwrap();
2291        let Some(adam_rs::FilterKind::Range { bounds }) =
2292            parsed.sheet.filter_kind(parsed.cell_names["value"].0)
2293        else {
2294            panic!("expected range filter");
2295        };
2296        type Limits = Cons<Leaf<f64>, Cons<Leaf<f64>, Nil>>;
2297        type WrongLimits = Cons<Leaf<f64>, Cons<Leaf<u64>, Nil>>;
2298        let valid = CellValue(OwnedValue::new::<Limits>((0.0, 10.0).into_c_stack_list()));
2299        // Identical field sizes and valid f64 bits keep this regression safe pre-fix.
2300        let wrong_shape = CellValue(OwnedValue::new::<WrongLimits>(
2301            (0.0, 10.0f64.to_bits()).into_c_stack_list(),
2302        ));
2303        let invalid: [&[&dyn Any]; 4] = [&[&wrong_shape], &[&false], &[], &[&valid, &valid]];
2304        for _ in 0..2 {
2305            for args in invalid {
2306                let (lo, hi) = bounds(&[&valid]).unwrap();
2307                assert_eq!(*lo.downcast::<f64>().unwrap(), 0.0);
2308                assert_eq!(*hi.downcast::<f64>().unwrap(), 10.0);
2309                assert!(bounds(args).is_none(), "invalid compound inputs accepted");
2310                assert!(bounds(&[&valid]).is_some());
2311            }
2312        }
2313    }
2314
2315    #[test]
2316    fn cell_filter_range_does_not_require_underscore() {
2317        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2318        let result = parser.parse_str("sheet s { cell a: i32 filter 0..=100; }");
2319        assert!(result.is_ok());
2320    }
2321
2322    #[test]
2323    fn cell_filter_range_bounds_track_cell_dependencies_live() {
2324        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2325        let mut parsed = parser
2326            .parse_str(
2327                "sheet s { cell lo: i32 = 0; cell hi: i32 = 100; cell a: i32 filter lo..=hi; }",
2328            )
2329            .unwrap();
2330        let (a_id, _) = parsed.cell_names["a"];
2331        let (hi_id, _) = parsed.cell_names["hi"];
2332        assert_eq!(parsed.sheet.filter_range::<i32>(a_id), Some((0, 100)));
2333        parsed.sheet.write(hi_id, 10i32).unwrap();
2334        assert_eq!(parsed.sheet.filter_range::<i32>(a_id), Some((0, 10)));
2335        parsed.sheet.write(a_id, 500i32).unwrap();
2336        parsed.sheet.propagate().unwrap();
2337        assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 10);
2338    }
2339
2340    #[test]
2341    fn cell_filter_range_with_float_cell_type_works() {
2342        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2343        let mut parsed = parser
2344            .parse_str("sheet s { cell a: f64 filter 0.0..=100.0; }")
2345            .unwrap();
2346        let (a_id, _) = parsed.cell_names["a"];
2347        parsed.sheet.write(a_id, 500.0f64).unwrap();
2348        parsed.sheet.propagate().unwrap();
2349        assert_eq!(*parsed.sheet.read::<f64>(a_id).unwrap(), 100.0);
2350    }
2351
2352    #[test]
2353    fn cell_filter_range_with_mismatched_element_type_is_a_parse_error() {
2354        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2355        let err = parser
2356            .parse_str("sheet s { cell a: f64 filter 0..=100; }")
2357            .expect_err("expected Err");
2358        assert!(
2359            err.message().contains("filter range bounds must be"),
2360            "{}",
2361            err.message()
2362        );
2363    }
2364
2365    #[test]
2366    fn cell_filter_range_with_mismatched_element_type_and_underscore_is_still_a_range_error() {
2367        // Confirms the "range bounds must be" check fires unconditionally on a recognized
2368        // RangeInclusive-shaped output, even though `_` is referenced here (cast to `i32`, the
2369        // range's own element type, rather than left at the cell's declared `f64`) — it must not
2370        // be swallowed by the opaque `underscore_used` path.
2371        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2372        let err = parser
2373            .parse_str("sheet s { cell a: f64 filter (_ as i32)..=100; }")
2374            .expect_err("expected Err");
2375        assert!(
2376            err.message().contains("filter range bounds must be"),
2377            "{}",
2378            err.message()
2379        );
2380    }
2381
2382    #[test]
2383    fn cell_filter_range_returns_none_instead_of_panicking_when_a_bound_expression_fails_to_evaluate()
2384     {
2385        // `100 / hi` is a registered CEL builtin (`/` on `i32`, see
2386        // `cel_parser::op_table::OpLookup`) that returns `Err` at runtime on division by zero
2387        // rather than panicking or wrapping — `parse_cell_filter` only validates this range
2388        // filter's *type* at parse time, so evaluating the bound expression with `hi == 0` must
2389        // still be handled at runtime. Before this fix, `FilterKind::Range::bounds` was
2390        // infallible and `parse_cell_filter`'s wrapping closure used `.expect(..)`, which would
2391        // panic here instead of degrading to `None`.
2392        // `hi` starts at a non-zero value so `add_filter`'s own immediate clamp of `a`'s default
2393        // value (0) succeeds during parsing (`0..=(100 / 1)`); the failing evaluation is
2394        // triggered afterwards, purely through `filter_range`, by writing `hi` to 0.
2395        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2396        let mut parsed = parser
2397            .parse_str("sheet s { cell hi: i32 = 1; cell a: i32 filter 0..=(100 / hi); }")
2398            .unwrap();
2399        let (a_id, _) = parsed.cell_names["a"];
2400        let (hi_id, _) = parsed.cell_names["hi"];
2401
2402        assert_eq!(parsed.sheet.filter_range::<i32>(a_id), Some((0, 100)));
2403
2404        // Puts the range expression's own evaluation into a failing state (division by zero)
2405        // without going through `add_filter`'s clamp path at all — this is exactly the "some
2406        // other write puts an endpoint's dependency into a bad state" scenario from the review
2407        // finding, and must degrade to `None` rather than panicking.
2408        parsed.sheet.write(hi_id, 0i32).unwrap();
2409        assert_eq!(parsed.sheet.filter_range::<i32>(a_id), None);
2410    }
2411
2412    #[test]
2413    fn cell_filter_general_expression_still_compiles_to_opaque_kind() {
2414        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2415        let parsed = parser
2416            .parse_str("sheet s { cell a: i32 filter if _ < 0 { 0 } else { _ }; }")
2417            .unwrap();
2418        let (a_id, _) = parsed.cell_names["a"];
2419        assert!(matches!(
2420            parsed.sheet.filter_kind(a_id),
2421            Some(adam_rs::FilterKind::Opaque)
2422        ));
2423    }
2424
2425    #[test]
2426    fn filter_tracks_a_tuple_typed_range_cell_dynamically() {
2427        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2428        let mut parsed = parser
2429            .parse_str(
2430                "sheet s { \
2431                     cell a_range: (i32, i32) = (1, 100); \
2432                     cell max: i32 = 100; \
2433                     relationship { a_range := (1, max); } \
2434                     cell a: i32 filter if _ < a_range.0 { a_range.0 } \
2435                         else if _ > a_range.1 { a_range.1 } else { _ }; \
2436                 }",
2437            )
2438            .unwrap();
2439        let (a_id, _) = parsed.cell_names["a"];
2440        let (max_id, _) = parsed.cell_names["max"];
2441
2442        parsed.sheet.write(a_id, 500i32).unwrap();
2443        parsed.sheet.propagate().unwrap();
2444        assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 100);
2445
2446        parsed.sheet.write(max_id, 10i32).unwrap();
2447        parsed.sheet.propagate().unwrap();
2448        parsed.sheet.write(a_id, 500i32).unwrap();
2449        parsed.sheet.propagate().unwrap();
2450        assert_eq!(*parsed.sheet.read::<i32>(a_id).unwrap(), 10);
2451    }
2452
2453    #[test]
2454    fn cell_filter_on_a_tuple_typed_cell_is_supported() {
2455        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2456        let sheet = parser
2457            .parse_str("sheet s { cell a: (i32, i32) filter (_.0, _.1); }")
2458            .unwrap();
2459        assert!(sheet.filter_kind(sheet.cell_names["a"].0).is_some());
2460    }
2461
2462    #[test]
2463    fn parse_filter_clause_attaches_a_filter() {
2464        let sheet = parser()
2465            .parse_str("sheet s { cell x: i32 = 0 filter 0..=10; }")
2466            .unwrap();
2467        let (x, _) = sheet.cell_names["x"];
2468        assert!(sheet.filter_kind(x).is_some());
2469    }
2470
2471    #[test]
2472    fn parse_filter_clause_with_a_label_is_now_a_parse_error() {
2473        let result = parser().parse_str("sheet s { cell x: i32 = 0 filter clamp: 0..=10; }");
2474        assert!(result.is_err());
2475    }
2476
2477    #[test]
2478    fn parse_multiple_cells() {
2479        let _sheet = parser()
2480            .parse_str(
2481                r#"
2482            sheet image_resize {
2483                cell width:  f64 = 1920.0;
2484                cell height: f64 = 1080.0;
2485                cell area:   f64;
2486                cell mode:   i32 = 0;
2487            }
2488        "#,
2489            )
2490            .unwrap();
2491    }
2492
2493    #[test]
2494    fn parse_relationship_with_a_single_binding() {
2495        let mut sheet = parser()
2496            .parse_str(
2497                r#"
2498                sheet s {
2499                    cell a: i32 = 2;
2500                    cell b: i32 = 0;
2501                    relationship {
2502                        b := a;
2503                    }
2504                }
2505            "#,
2506            )
2507            .unwrap();
2508        sheet.propagate().unwrap();
2509        let (b_id, _) = sheet.cell_names["b"].clone();
2510        assert_eq!(*sheet.read::<i32>(b_id).unwrap(), 2);
2511    }
2512
2513    #[test]
2514    fn parse_relationship_deduces_inputs_from_referenced_identifiers() {
2515        let mut sheet = parser()
2516            .parse_str(
2517                r#"
2518                sheet s {
2519                    cell a: i32 = 2;
2520                    cell b: i32 = 3;
2521                    cell c: i32 = 0;
2522                    relationship {
2523                        c := a * b;
2524                    }
2525                }
2526            "#,
2527            )
2528            .unwrap();
2529        sheet.propagate().unwrap();
2530        let (c_id, _) = sheet.cell_names["c"].clone();
2531        assert_eq!(*sheet.read::<i32>(c_id).unwrap(), 6);
2532    }
2533
2534    /// Verifies member labels never become dependencies or resolve same-named cells.
2535    #[test]
2536    fn member_labels_are_not_cell_dependencies() {
2537        let mut lookup = OpLookup::new();
2538        lookup.push_scope(|name, segment, arity, _| {
2539            if matches!(name, "x" | "width") && arity == 1 {
2540                segment.op1(|value: f64| value * 2.0)?;
2541                return Ok(true);
2542            }
2543            Ok(false)
2544        });
2545        let mut sheet = AdamParser::new(TypeRegistry::new(), lookup)
2546            .parse_str(
2547                "sheet s { cell input: f64 = 3.0; cell x: bool = true; \
2548                 cell width: bool = false; cell result: f64 = 0.0; \
2549                 relationship { result := input.x().width; } }",
2550            )
2551            .unwrap();
2552        let input = sheet.cell_names["input"].0;
2553        let result = sheet.cell_names["result"].0;
2554        let (&(relationship, method), _) = sheet.method_spans.iter().next().unwrap();
2555        assert_eq!(
2556            sheet.method_inputs(relationship, method),
2557            Some([input].as_slice())
2558        );
2559        sheet.propagate().unwrap();
2560        assert_eq!(*sheet.read::<f64>(result).unwrap(), 12.0);
2561        sheet.write(input, 5.0f64).unwrap();
2562        sheet.propagate().unwrap();
2563        assert_eq!(*sheet.read::<f64>(result).unwrap(), 20.0);
2564    }
2565
2566    /// Tracks only value expressions in a record-member binding.
2567    #[test]
2568    fn field_labels_and_member_names_are_not_cell_dependencies() {
2569        use cel_parser::{OperationSignature, RecordField, RecordSchema, ResolvedLeafType};
2570        use cel_runtime::ValueType;
2571        /// A record independent of any geometry library.
2572        #[derive(Clone, Debug, PartialEq)]
2573        struct TestPoint {
2574            x: f64,
2575            y: f64,
2576        }
2577        let point = ResolvedLeafType::new("TestPoint", ValueType::leaf::<TestPoint>());
2578        let scalar = ResolvedLeafType::new("f64", ValueType::leaf::<f64>());
2579        let mut lookup = OpLookup::new();
2580        lookup
2581            .register_record(RecordSchema {
2582                ty: point.clone(),
2583                fields: vec![
2584                    RecordField {
2585                        name: "x",
2586                        ty: scalar.clone().into(),
2587                    },
2588                    RecordField {
2589                        name: "y",
2590                        ty: scalar.clone().into(),
2591                    },
2592                ],
2593                emit: Box::new(|s, order, _, _| {
2594                    assert_eq!(order, [0, 1]);
2595                    s.op2(|x: f64, y: f64| TestPoint { x, y })
2596                }),
2597            })
2598            .unwrap();
2599        lookup
2600            .register_operation(OperationSignature {
2601                name: "x",
2602                operands: vec![point.into()],
2603                result: scalar.into(),
2604                emit: Box::new(|s, _| s.op1(|p: TestPoint| p.x)),
2605            })
2606            .unwrap();
2607        let mut sheet = AdamParser::new(TypeRegistry::new(), lookup)
2608            .parse_str(
2609                "sheet s { cell a: f64 = 2.0; cell b: f64 = 3.0; \
2610             cell x: bool = false; cell y: bool = false; cell TestPoint: bool = true; \
2611             cell result: f64 = 0.0; \
2612             relationship { result := TestPoint { x: a, y: b }.x; } }",
2613            )
2614            .unwrap();
2615        let a = sheet.cell_names["a"].0;
2616        let b = sheet.cell_names["b"].0;
2617        let result = sheet.cell_names["result"].0;
2618        let (&(relationship, method), _) = sheet.method_spans.iter().next().unwrap();
2619        assert_eq!(
2620            sheet.method_inputs(relationship, method),
2621            Some([a, b].as_slice())
2622        );
2623        sheet.propagate().unwrap();
2624        assert_eq!(*sheet.read::<f64>(result).unwrap(), 2.0);
2625        sheet.write(a, 7.0f64).unwrap();
2626        sheet.propagate().unwrap();
2627        assert_eq!(*sheet.read::<f64>(result).unwrap(), 7.0);
2628    }
2629
2630    #[test]
2631    fn parse_relationship_with_multiple_bindings_lets_the_planner_pick_a_direction() {
2632        // `c` is declared first (and so has the weakest cell strength — `adam_rs`'s
2633        // planner processes cells in *descending* strength order, preferentially
2634        // leaving the strongest cells as sources, so the earliest-declared cell is the
2635        // one left to be computed): with `a`/`b` fixed as sources, the only
2636        // self-consistent choice among these three mutually-referencing bindings is to
2637        // compute `c := a * b`.
2638        let mut sheet = parser()
2639            .parse_str(
2640                r#"
2641                sheet s {
2642                    cell c: i32 = 0;
2643                    cell a: i32 = 2;
2644                    cell b: i32 = 3;
2645                    relationship {
2646                        c := a * b;
2647                        a := c / b;
2648                        b := c / a;
2649                    }
2650                }
2651            "#,
2652            )
2653            .unwrap();
2654        sheet.propagate().unwrap();
2655        let (c_id, _) = sheet.cell_names["c"].clone();
2656        assert_eq!(*sheet.read::<i32>(c_id).unwrap(), 6);
2657    }
2658
2659    #[test]
2660    fn mismatched_method_cells_error_spans_the_relationship_block_not_the_sheet() {
2661        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2662        let source = "sheet s {\n    cell a: i32;\n    cell b: i32;\n\n    relationship {\n        a := b;\n        b := 42;\n    }\n}";
2663        let err = parser.parse_str(source).err().unwrap();
2664        // The mismatched binding (`b := 42;`, method index 1) is on line 7; the sheet's
2665        // opening line (1) must not be reported instead. `ParseError::span()` returns a
2666        // `proc_macro2::Span`, so `.start()` is a method call here, not a field access (unlike
2667        // `cel_parser::SourceSpan`, whose `start`/`end` are public `LineColumn` fields).
2668        assert_eq!(err.span().start().line, 7);
2669    }
2670
2671    #[test]
2672    fn successful_relationship_parse_populates_method_spans() {
2673        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2674        let source = "sheet s {\n    cell a: i32;\n    cell b: i32;\n\n    relationship {\n        a := b;\n    }\n}";
2675        let parsed = parser.parse_str(source).unwrap();
2676        assert_eq!(parsed.method_spans.len(), 1);
2677        let ((_, idx), span) = parsed.method_spans.iter().next().unwrap();
2678        assert_eq!(*idx, 0);
2679        assert_eq!(span.start.line, 6);
2680    }
2681
2682    #[test]
2683    fn parse_populates_relationship_and_cell_spans() {
2684        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2685        let parsed = parser
2686            .parse_str("sheet s { cell a: i32 = 0; cell b: i32; relationship { b := a; } }")
2687            .unwrap();
2688        assert_eq!(parsed.cell_spans.len(), 2);
2689        assert_eq!(parsed.relationship_spans.len(), 1);
2690        // every declared cell id has a span
2691        for (_, (id, _)) in &parsed.cell_names {
2692            assert!(parsed.cell_spans.contains_key(id));
2693        }
2694    }
2695
2696    #[test]
2697    fn locate_error_resolves_a_cycle_to_multiple_ordered_spans() {
2698        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2699        let mut parsed = parser
2700            .parse_str(
2701                "sheet s { cell x: i32 = 0; cell y: i32 = 0; relationship { x := y + 1i32; } \
2702                 relationship { y := x + 1i32; } }",
2703            )
2704            .unwrap();
2705        let err = parsed.propagate().unwrap_err();
2706        assert!(matches!(err, adam_rs::Error::Cycle { .. }));
2707        let located = parsed.locate_error(&err);
2708        // both relationship blocks resolve to spans
2709        let rel_spans = located.len();
2710        assert!(
2711            rel_spans >= 2,
2712            "expected >=2 spans, got {rel_spans}: {located:?}"
2713        );
2714    }
2715
2716    #[test]
2717    fn duplicate_output_set_error_underlines_both_bindings() {
2718        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
2719        let source =
2720            "sheet s { cell a: i32 = 0; cell b: i32; relationship { b := a; b := a + 1i32; } }";
2721        let err = parser.parse_str(source).unwrap_err();
2722        let out =
2723            err.format_rustc_style(source, "t.adm2", 1, &annotate_snippets::Renderer::plain());
2724        // Primary caret on the later binding, secondary carets on the earlier binding and the
2725        // shared output cell's own declaration.
2726        assert!(out.contains("outputs"), "{out}"); // message text
2727        assert!(out.contains("collides with this earlier method"), "{out}");
2728        assert!(
2729            out.contains("output cell `b` is claimed more than once"),
2730            "{out}"
2731        );
2732    }
2733
2734    #[test]
2735    fn parse_binding_undeclared_output_is_an_error() {
2736        let result = parser().parse_str(
2737            r#"
2738            sheet s {
2739                cell a: i32 = 1;
2740                relationship {
2741                    missing := a;
2742                }
2743            }
2744        "#,
2745        );
2746        assert!(result.is_err());
2747    }
2748
2749    #[test]
2750    fn parse_binding_multi_output_tuple_matches_existing_tuple_shape_rules() {
2751        let mut sheet = parser()
2752            .parse_str(
2753                r#"
2754                sheet s {
2755                    cell w: i32 = 4;
2756                    cell x: i32 = 0;
2757                    cell y: i32 = 0;
2758                    relationship {
2759                        (x, y) := (w, w * 2);
2760                    }
2761                }
2762            "#,
2763            )
2764            .unwrap();
2765        sheet.propagate().unwrap();
2766        let (x_id, _) = sheet.cell_names["x"].clone();
2767        let (y_id, _) = sheet.cell_names["y"].clone();
2768        assert_eq!(*sheet.read::<i32>(x_id).unwrap(), 4);
2769        assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 8);
2770    }
2771
2772    #[test]
2773    fn parse_binding_multi_output_arity_mismatch_is_an_error() {
2774        let result = parser().parse_str(
2775            r#"
2776            sheet s {
2777                cell w: i32 = 4;
2778                cell x: i32 = 0;
2779                cell y: i32 = 0;
2780                relationship {
2781                    (x, y) := w;
2782                }
2783            }
2784        "#,
2785        );
2786        assert!(result.is_err());
2787    }
2788
2789    #[test]
2790    fn parse_binding_multi_output_without_parens_is_a_parse_error() {
2791        let result = parser().parse_str(
2792            r#"
2793            sheet s {
2794                cell w: i32 = 4;
2795                cell x: i32 = 0;
2796                cell y: i32 = 0;
2797                relationship {
2798                    x, y := (w, w * 2);
2799                }
2800            }
2801        "#,
2802        );
2803        assert!(result.is_err());
2804    }
2805
2806    #[test]
2807    fn parse_binding_single_element_tuple_destructure_extracts_the_element() {
2808        let mut sheet = parser()
2809            .parse_str(
2810                r#"
2811                sheet s {
2812                    cell w: i32 = 4;
2813                    cell x: i32 = 0;
2814                    relationship {
2815                        (x,) := (w,);
2816                    }
2817                }
2818            "#,
2819            )
2820            .unwrap();
2821        sheet.propagate().unwrap();
2822        let (x_id, _) = sheet.cell_names["x"].clone();
2823        assert_eq!(*sheet.read::<i32>(x_id).unwrap(), 4);
2824    }
2825
2826    #[test]
2827    fn parse_binding_single_parenthesized_identifier_without_comma_is_a_direct_bind() {
2828        let mut sheet = parser()
2829            .parse_str(
2830                r#"
2831                sheet s {
2832                    cell w: i32 = 4;
2833                    cell x: i32 = 0;
2834                    relationship {
2835                        (x) := w;
2836                    }
2837                }
2838            "#,
2839            )
2840            .unwrap();
2841        sheet.propagate().unwrap();
2842        let (x_id, _) = sheet.cell_names["x"].clone();
2843        assert_eq!(*sheet.read::<i32>(x_id).unwrap(), 4);
2844    }
2845
2846    #[test]
2847    fn parse_out_with_direct_initializer_and_no_require_block() {
2848        let mut sheet = parser()
2849            .parse_str(
2850                r#"
2851                sheet s {
2852                    cell a: i32 = 3;
2853                    cell b: i32 = 4;
2854                    out area: i32 := a * b;
2855                }
2856            "#,
2857            )
2858            .unwrap();
2859        sheet.propagate().unwrap();
2860        let cell_id = sheet.output_names["area"];
2861        assert_eq!(*sheet.sheet.read::<i32>(cell_id).unwrap(), 12);
2862    }
2863
2864    #[test]
2865    fn parse_out_with_no_type_annotation_infers_from_initializer() {
2866        let sheet = parser()
2867            .parse_str(
2868                r#"
2869                sheet s {
2870                    cell a: i32 = 3;
2871                    out doubled := a * 2;
2872                }
2873            "#,
2874            )
2875            .unwrap();
2876        let (_, shape) = sheet.cell_names["doubled"].clone();
2877        assert_eq!(
2878            shape,
2879            crate::type_registry::TypeShape::Named(std::any::TypeId::of::<i32>())
2880        );
2881    }
2882
2883    #[test]
2884    fn parse_out_with_a_require_block_registers_named_requirements() {
2885        let mut sheet = parser()
2886            .parse_str(
2887                r#"
2888                sheet s {
2889                    cell a: i32 = 3;
2890                    cell b: i32 = 4;
2891                    out area: i32 := a * b require {
2892                        @positive area > 0;
2893                        @small area < 1000;
2894                    };
2895                }
2896            "#,
2897            )
2898            .unwrap();
2899        sheet.propagate().unwrap();
2900        let output_id = sheet.output_names["area"];
2901        assert!(sheet.sheet.cell_requirements(output_id).unwrap().len() == 2);
2902        assert!(
2903            sheet
2904                .sheet
2905                .violated_requirements(output_id)
2906                .next()
2907                .is_none()
2908        );
2909    }
2910
2911    #[test]
2912    fn parse_out_require_block_requirement_can_violate() {
2913        let mut sheet = parser()
2914            .parse_str(
2915                r#"
2916                sheet s {
2917                    cell a: i32 = 3;
2918                    cell b: i32 = 4;
2919                    out area: i32 := a * b require {
2920                        @too_small area > 1000;
2921                    };
2922                }
2923            "#,
2924            )
2925            .unwrap();
2926        sheet.propagate().unwrap();
2927        let output_id = sheet.output_names["area"];
2928        assert_eq!(sheet.sheet.violated_requirements(output_id).count(), 1);
2929    }
2930
2931    #[test]
2932    fn parse_requirement_non_bool_body_is_an_error() {
2933        let result = parser().parse_str(
2934            r#"
2935            sheet s {
2936                cell a: i32 = 3;
2937                out x: i32 := a require {
2938                    @bad a;
2939                };
2940            }
2941        "#,
2942        );
2943        assert!(result.is_err());
2944    }
2945
2946    #[test]
2947    fn parses_a_sheet_with_an_outer_doc_comment_on_a_cell() {
2948        let parsed = parser()
2949            .parse_str("sheet s {\n    /// the total\n    cell x: i32 = 1;\n}")
2950            .unwrap();
2951        assert_eq!(parsed.cell_names.len(), 1);
2952    }
2953
2954    #[test]
2955    fn parses_a_sheet_with_an_inner_doc_comment() {
2956        let parsed = parser()
2957            .parse_str("//! module docs\nsheet s {\n    cell x: i32 = 1;\n}")
2958            .unwrap();
2959        assert_eq!(parsed.cell_names.len(), 1);
2960    }
2961
2962    #[test]
2963    fn parses_a_sheet_with_doc_comments_on_every_declaration_kind() {
2964        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}";
2965        let parsed = parser().parse_str(source).unwrap();
2966        assert_eq!(parsed.cell_names.len(), 2);
2967    }
2968
2969    #[test]
2970    fn parse_binding_undeclared_input_is_error() {
2971        let result = parser().parse_str(
2972            r#"
2973            sheet s {
2974                cell x: f64 = 1.0;
2975                relationship { x := bogus; }
2976            }
2977        "#,
2978        );
2979        assert!(result.is_err());
2980        let err = result.expect_err("expected Err");
2981        let msg = err.message().to_lowercase();
2982        assert!(msg.contains("bogus") || msg.contains("undefined"), "{msg}");
2983    }
2984
2985    #[test]
2986    fn parse_method_output_type_mismatch_is_error() {
2987        let result = parser().parse_str(
2988            r#"
2989            sheet s {
2990                cell x: f64 = 0.0;
2991                cell n: i32 = 0;
2992                relationship { n := x; }
2993            }
2994        "#,
2995        );
2996        assert!(result.is_err(), "f64 body for i32 output must be an error");
2997    }
2998
2999    #[test]
3000    fn parse_and_propagate_multi_output_tuple_sheet() {
3001        let mut sheet = parser()
3002            .parse_str(
3003                r#"
3004            sheet s {
3005                cell a:    i32 = 3;
3006                cell b:    i32 = 4;
3007                cell sum:  i32;
3008                cell diff: i32;
3009                relationship { (sum, diff) := (a + b, a - b); }
3010            }
3011        "#,
3012            )
3013            .unwrap();
3014
3015        // sum = a + b = 7, diff = a - b = -1. This exercises the CompiledOutputs::Tuple
3016        // runtime path end to end (RefCell wrapping + seg.call_dyn_tuple); see
3017        // `existing_multi_output_scalar_methods_still_work_unchanged` for the value-level
3018        // assertion of the same mechanism.
3019        sheet.propagate().unwrap();
3020        let _ = sheet; // sheet is live and propagated
3021    }
3022
3023    #[test]
3024    fn parse_method_output_tuple_arity_mismatch_is_error() {
3025        let result = parser().parse_str(
3026            r#"
3027            sheet s {
3028                cell a: i32 = 1;
3029                cell b: i32 = 2;
3030                cell x: i32;
3031                cell y: i32;
3032                cell z: i32;
3033                relationship { (x, y, z) := (a + b, a - b); }
3034            }
3035        "#,
3036        );
3037        assert!(
3038            result.is_err(),
3039            "2-tuple body for 3 declared outputs must be an error"
3040        );
3041        let err = result.expect_err("expected Err");
3042        let msg = err.message().to_lowercase();
3043        assert!(msg.contains("arity"), "{msg}");
3044    }
3045
3046    #[test]
3047    fn parse_method_output_type_mismatch_names_the_registered_type() {
3048        let result = parser().parse_str(
3049            r#"
3050            sheet s {
3051                cell a: String = "x";
3052                cell x: i32;
3053                relationship { x := a; }
3054            }
3055        "#,
3056        );
3057        let err = result.expect_err("a String body for an i32 output must be an error");
3058        let msg = err.message();
3059        assert!(msg.contains("got `String`"), "{msg}");
3060        assert!(!msg.contains("::"), "no Rust type path may leak: {msg}");
3061    }
3062
3063    #[test]
3064    fn parse_method_destructured_output_type_mismatch_names_the_registered_type() {
3065        let result = parser().parse_str(
3066            r#"
3067            sheet s {
3068                cell a: String = "x";
3069                cell b: i32 = 1;
3070                cell x: i32;
3071                cell y: i32;
3072                relationship { (x, y) := (a, b); }
3073            }
3074        "#,
3075        );
3076        let err = result.expect_err("a String tuple element for an i32 output must be an error");
3077        let msg = err.message();
3078        assert!(msg.contains("got `String`"), "{msg}");
3079        assert!(!msg.contains("::"), "no Rust type path may leak: {msg}");
3080    }
3081
3082    #[test]
3083    fn parse_method_output_tuple_element_type_mismatch_is_error() {
3084        let result = parser().parse_str(
3085            r#"
3086            sheet s {
3087                cell a: i32 = 1;
3088                cell b: f64 = 2.0;
3089                cell x: i32;
3090                cell y: i32;
3091                relationship { (x, y) := (a, b); }
3092            }
3093        "#,
3094        );
3095        assert!(
3096            result.is_err(),
3097            "f64 tuple element for an i32 output must be an error"
3098        );
3099        let err = result.expect_err("expected Err");
3100        let msg = err.message().to_lowercase();
3101        assert!(msg.contains("type mismatch"), "{msg}");
3102    }
3103
3104    #[test]
3105    fn parse_method_single_output_rejects_tuple_body() {
3106        let result = parser().parse_str(
3107            r#"
3108            sheet s {
3109                cell x: i32 = 1;
3110                cell y: i32;
3111                relationship { y := (x,); }
3112            }
3113        "#,
3114        );
3115        assert!(
3116            result.is_err(),
3117            "1-tuple body for a single declared output must be an error"
3118        );
3119    }
3120
3121    #[test]
3122    fn parse_method_single_tuple_typed_output() {
3123        let mut sheet = parser()
3124            .parse_str(
3125                r#"
3126                sheet s {
3127                    cell a: i32 = 3;
3128                    cell b: i32 = 4;
3129                    cell pair: (i32, i32);
3130                    relationship { pair := (a, b); }
3131                }
3132            "#,
3133            )
3134            .unwrap();
3135        sheet.propagate().unwrap();
3136        let (cell_id, _) = sheet.cell_names["pair"].clone();
3137        let value = sheet
3138            .read_compound::<cel_runtime::Cons<
3139                cel_runtime::Leaf<i32>,
3140                cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Nil>,
3141            >>(cell_id)
3142            .unwrap();
3143        let (a, b) = (value.1, value.0.1);
3144        assert_eq!((a, b), (3, 4));
3145    }
3146
3147    #[test]
3148    fn parse_method_tuple_typed_output_among_several() {
3149        let mut sheet = parser()
3150            .parse_str(
3151                r#"
3152                sheet s {
3153                    cell a: i32 = 3;
3154                    cell b: i32 = 4;
3155                    cell pair: (i32, i32);
3156                    cell extra: i32;
3157                    relationship { (pair, extra) := ((a, b), a); }
3158                }
3159            "#,
3160            )
3161            .unwrap();
3162        sheet.propagate().unwrap();
3163        let (pair_id, _) = sheet.cell_names["pair"].clone();
3164        let (extra_id, _) = sheet.cell_names["extra"].clone();
3165        let pair = sheet
3166            .read_compound::<cel_runtime::Cons<
3167                cel_runtime::Leaf<i32>,
3168                cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Nil>,
3169            >>(pair_id)
3170            .unwrap();
3171        let (a, b) = (pair.1, pair.0.1);
3172        assert_eq!((a, b), (3, 4));
3173        assert_eq!(*sheet.read::<i32>(extra_id).unwrap(), 3);
3174    }
3175
3176    #[test]
3177    fn parse_method_with_tuple_typed_input_supports_field_indexing() {
3178        let mut sheet = parser()
3179            .parse_str(
3180                r#"
3181                sheet s {
3182                    cell pair: (i32, i32) = (10, 20);
3183                    cell sum: i32;
3184                    relationship { sum := pair.0 + pair.1; }
3185                }
3186            "#,
3187            )
3188            .unwrap();
3189        sheet.propagate().unwrap();
3190        let (sum_id, _) = sheet.cell_names["sum"].clone();
3191        assert_eq!(*sheet.read::<i32>(sum_id).unwrap(), 30);
3192    }
3193
3194    #[test]
3195    fn parse_method_tuple_output_shape_mismatch_is_an_error() {
3196        let result = parser().parse_str(
3197            r#"
3198            sheet s {
3199                cell a: i32 = 1;
3200                cell b: f64 = 2.0;
3201                cell pair: (i32, i32);
3202                relationship { pair := (a, b); }
3203            }
3204        "#,
3205        );
3206        assert!(result.is_err());
3207    }
3208
3209    #[test]
3210    fn existing_multi_output_scalar_methods_still_work_unchanged() {
3211        // Regression: today's N-scalar-outputs mechanism must still behave identically after the
3212        // CompiledOutputs refactor.
3213        let mut sheet = parser()
3214            .parse_str(
3215                r#"
3216                sheet s {
3217                    cell a: i32 = 3;
3218                    cell b: i32 = 4;
3219                    cell sum: i32;
3220                    cell diff: i32;
3221                    relationship { (sum, diff) := (a + b, a - b); }
3222                }
3223            "#,
3224            )
3225            .unwrap();
3226        sheet.propagate().unwrap();
3227        let (sum_id, _) = sheet.cell_names["sum"].clone();
3228        let (diff_id, _) = sheet.cell_names["diff"].clone();
3229        assert_eq!(*sheet.read::<i32>(sum_id).unwrap(), 7);
3230        assert_eq!(*sheet.read::<i32>(diff_id).unwrap(), -1);
3231    }
3232
3233    #[test]
3234    fn parse_method_single_empty_tuple_typed_output() {
3235        let mut sheet = parser()
3236            .parse_str(
3237                r#"
3238                sheet s {
3239                    cell x: i32 = 1;
3240                    cell nothing: ();
3241                    // `()` alone references no cell, and adam_rs rejects a method with no
3242                    // inputs -- reference `x` via an if/else whose branches both still
3243                    // evaluate to `()`, so the binding has a deduced input.
3244                    relationship { nothing := if x > 0 { () } else { () }; }
3245                }
3246            "#,
3247            )
3248            .unwrap();
3249        sheet.propagate().unwrap();
3250        let (cell_id, _) = sheet.cell_names["nothing"].clone();
3251        sheet.read_compound::<cel_runtime::Nil>(cell_id).unwrap();
3252    }
3253
3254    #[test]
3255    fn parse_conditional_decl() {
3256        let _sheet = parser()
3257            .parse_str(
3258                r#"
3259            sheet image_resize {
3260                cell width:  f64 = 1920.0;
3261                cell height: f64 = 1080.0;
3262                cell ratio:  f64 = 1.0;
3263                cell mode:   i32 = 0;
3264                conditional mode {
3265                    0i32 => {
3266                        relationship { height := width; }
3267                    },
3268                    1i32 => {
3269                        relationship { height := width * ratio; }
3270                    },
3271                    _ => {
3272                        relationship { height := width; }
3273                    },
3274                }
3275            }
3276        "#,
3277            )
3278            .unwrap();
3279    }
3280
3281    #[test]
3282    fn parse_conditional_branch_with_multiple_relationships() {
3283        let mut sheet = parser()
3284            .parse_str(
3285                r#"
3286            sheet s {
3287                cell mode: i32 = 0;
3288                cell a: f64 = 2.0;
3289                cell b: f64 = 3.0;
3290                cell c: f64;
3291                cell d: f64 = 4.0;
3292                cell e: f64 = 5.0;
3293                cell f: f64;
3294                conditional mode {
3295                    0i32 => {
3296                        relationship { c := a * b; }
3297                        relationship { f := d * e; }
3298                    }
3299                }
3300            }
3301        "#,
3302            )
3303            .unwrap();
3304        sheet.propagate().unwrap();
3305    }
3306
3307    #[test]
3308    fn conditional_branch_bare_binding_without_relationship_wrapper_is_error() {
3309        let result = parser().parse_str(
3310            r#"
3311            sheet s {
3312                cell x: i32 = 0;
3313                conditional x { 0i32 => { x := x; } }
3314            }
3315        "#,
3316        );
3317        assert!(
3318            result.is_err(),
3319            "a conditional_branch body now requires relationship_decl, not a bare binding"
3320        );
3321    }
3322
3323    #[test]
3324    fn conditional_undeclared_match_cell_is_error() {
3325        let result = parser().parse_str(
3326            r#"
3327            sheet s {
3328                cell x: i32 = 0;
3329                conditional bogus { 0i32 => { relationship { x := x; } } }
3330            }
3331        "#,
3332        );
3333        assert!(result.is_err());
3334        let err = result.expect_err("expected Err");
3335        let msg = err.message().to_lowercase();
3336        assert!(msg.contains("bogus") || msg.contains("undeclared"), "{msg}");
3337    }
3338
3339    #[test]
3340    fn conditional_branch_literal_type_mismatch_is_error() {
3341        let result = parser().parse_str(
3342            r#"
3343            sheet s {
3344                cell mode: i32 = 0;
3345                cell x:    f64 = 0.0;
3346                conditional mode { 1.0 => { relationship { x := x; } } }
3347            }
3348        "#,
3349        );
3350        assert!(
3351            result.is_err(),
3352            "float literal for i32 match cell must be an error"
3353        );
3354    }
3355
3356    #[test]
3357    fn conditional_branch_negative_literal_key_matches() {
3358        let mut sheet = parser()
3359            .parse_str(
3360                r#"
3361                sheet s {
3362                    cell mode: i32 = -1;
3363                    cell x: i32 = 1;
3364                    cell y: i32 = 0;
3365                    conditional mode {
3366                        -1i32 => { relationship { y := x; } },
3367                    }
3368                }
3369            "#,
3370            )
3371            .unwrap();
3372        sheet.propagate().unwrap();
3373        let (y_id, _) = sheet.cell_names["y"].clone();
3374        assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
3375    }
3376
3377    #[test]
3378    fn conditional_branch_negating_unsigned_literal_key_is_error() {
3379        let result = parser().parse_str(
3380            r#"
3381            sheet s {
3382                cell mode: u32 = 0;
3383                cell x: i32 = 1;
3384                cell y: i32 = 0;
3385                conditional mode {
3386                    -1u32 => { relationship { y := x; } },
3387                }
3388            }
3389        "#,
3390        );
3391        assert!(
3392            result.is_err(),
3393            "negating an unsigned literal must be a parse error"
3394        );
3395    }
3396
3397    #[test]
3398    fn conditional_with_tuple_typed_match_cell_and_default_only_still_works() {
3399        // A tuple isn't a `literal_pattern`, so a tuple-typed match cell can no longer pair with
3400        // a named branch key — only the `_` default branch remains reachable.
3401        let mut sheet = parser()
3402            .parse_str(
3403                r#"
3404                sheet s {
3405                    cell mode: (i32, i32) = (0, 0);
3406                    cell x: f64 = 1.0;
3407                    cell y: f64;
3408                    conditional mode {
3409                        _ => { relationship { y := x * 2.0; } },
3410                    }
3411                }
3412            "#,
3413            )
3414            .unwrap();
3415        sheet.propagate().unwrap();
3416        let (y_id, _) = sheet.cell_names["y"].clone();
3417        assert_eq!(*sheet.read::<f64>(y_id).unwrap(), 2.0);
3418    }
3419
3420    #[test]
3421    fn conditional_branch_tuple_literal_key_is_error() {
3422        let result = parser().parse_str(
3423            r#"
3424            sheet s {
3425                cell mode: (i32, i32) = (0, 0);
3426                cell x: f64 = 1.0;
3427                cell y: f64;
3428                conditional mode {
3429                    (0, 0) => { relationship { y := x; } },
3430                    _ => { relationship { y := x * 2.0; } },
3431                }
3432            }
3433        "#,
3434        );
3435        assert!(result.is_err(), "tuple branch key must be a parse error");
3436    }
3437
3438    #[test]
3439    fn conditional_on_a_two_cell_boolean_expression_activates_and_reacts_to_writes() {
3440        let mut sheet = parser()
3441            .parse_str(
3442                r#"
3443                sheet s {
3444                    cell a: bool = false;
3445                    cell b: bool = false;
3446                    cell x: i32 = 1;
3447                    cell y: i32 = 0;
3448                    conditional a && b {
3449                        true => { relationship { y := x; } },
3450                    }
3451                }
3452            "#,
3453            )
3454            .unwrap();
3455        let (a_id, _) = sheet.cell_names["a"].clone();
3456        let (b_id, _) = sheet.cell_names["b"].clone();
3457        let (y_id, _) = sheet.cell_names["y"].clone();
3458
3459        sheet.write(a_id, true).unwrap();
3460        sheet.write(b_id, false).unwrap();
3461        sheet.propagate().unwrap();
3462        assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 0);
3463
3464        sheet.write(b_id, true).unwrap();
3465        sheet.propagate().unwrap();
3466        assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
3467    }
3468
3469    #[test]
3470    fn conditional_expression_referencing_the_same_cell_twice_compiles_and_evaluates() {
3471        let mut sheet = parser()
3472            .parse_str(
3473                r#"
3474                sheet s {
3475                    cell a: bool = true;
3476                    cell x: i32 = 1;
3477                    cell y: i32 = 0;
3478                    conditional a && a {
3479                        true => { relationship { y := x; } },
3480                    }
3481                }
3482            "#,
3483            )
3484            .unwrap();
3485        sheet.propagate().unwrap();
3486        let (y_id, _) = sheet.cell_names["y"].clone();
3487        assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
3488    }
3489
3490    #[test]
3491    fn conditional_bare_identifier_match_subject_still_works() {
3492        let mut sheet = parser()
3493            .parse_str(
3494                r#"
3495                sheet s {
3496                    cell mode: i32 = 0;
3497                    cell x: i32 = 1;
3498                    cell y: i32 = 0;
3499                    conditional mode {
3500                        1i32 => { relationship { y := x; } },
3501                    }
3502                }
3503            "#,
3504            )
3505            .unwrap();
3506        let (mode_id, _) = sheet.cell_names["mode"].clone();
3507        let (y_id, _) = sheet.cell_names["y"].clone();
3508
3509        sheet.propagate().unwrap();
3510        assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 0);
3511
3512        sheet.write(mode_id, 1_i32).unwrap();
3513        sheet.propagate().unwrap();
3514        assert_eq!(*sheet.read::<i32>(y_id).unwrap(), 1);
3515    }
3516
3517    /// Verifies runtime conditional headers accept grouped records and deduce field dependencies.
3518    #[test]
3519    fn adam_registered_match_header_reserves_braces() {
3520        let mut parser = parser();
3521        parser
3522            .op_lookup_mut()
3523            .register_record(cel_parser::RecordSchema {
3524                ty: cel_parser::ResolvedLeafType::new(
3525                    "Gate",
3526                    cel_runtime::ValueType::leaf::<bool>(),
3527                ),
3528                fields: vec![cel_parser::RecordField {
3529                    name: "value",
3530                    ty: cel_parser::ResolvedLeafType::new(
3531                        "bool",
3532                        cel_runtime::ValueType::leaf::<bool>(),
3533                    )
3534                    .into(),
3535                }],
3536                emit: Box::new(|segment, _, _, _| segment.op1(|value: bool| value)),
3537            })
3538            .unwrap();
3539        let mut sheet = parser
3540            .parse_str(
3541                "sheet s {
3542                cell flag: bool = true;
3543                cell input: i32 = 7;
3544                cell output: i32 = 0;
3545                conditional (Gate { value: flag }) {
3546                    true => { relationship { output := if flag { input } else { 0 }; } }
3547                    _ => { relationship { output := 0; } }
3548                }
3549            }",
3550            )
3551            .unwrap();
3552        let output = sheet.cell_names["output"].0;
3553        let flag = sheet.cell_names["flag"].0;
3554        sheet.propagate().unwrap();
3555        assert_eq!(*sheet.read::<i32>(output).unwrap(), 7);
3556        sheet.write(flag, false).unwrap();
3557        sheet.propagate().unwrap();
3558        assert_eq!(*sheet.read::<i32>(output).unwrap(), 0);
3559    }
3560
3561    #[test]
3562    fn conditional_branch_bare_identifier_key_is_error() {
3563        // A bare identifier isn't a `literal_pattern` either, even when it resolves (as a
3564        // client-registered constant would) to a value of the match subject's own type.
3565        let result = parser().parse_str(
3566            r#"
3567            sheet s {
3568                cell mode: i32 = 0;
3569                cell x: i32 = 1;
3570                cell y: i32 = 0;
3571                conditional mode {
3572                    mode => { relationship { y := x; } },
3573                }
3574            }
3575        "#,
3576        );
3577        assert!(
3578            result.is_err(),
3579            "bare identifier branch key must be a parse error"
3580        );
3581    }
3582
3583    #[test]
3584    fn conditional_expression_referencing_an_undeclared_identifier_is_a_parse_error() {
3585        let result = parser().parse_str(
3586            r#"
3587            sheet s {
3588                cell a: bool = true;
3589                conditional a && nope {
3590                    true => { relationship { a := a; } },
3591                }
3592            }
3593        "#,
3594        );
3595        assert!(result.is_err());
3596    }
3597
3598    #[test]
3599    fn parse_cell_literal_type_mismatch_is_error() {
3600        // Float literal for an i32 cell should be a parse error.
3601        let result = parser().parse_str("sheet s { cell x: i32 = 1.0; }");
3602        assert!(
3603            result.is_err(),
3604            "float literal for i32 annotation must be an error"
3605        );
3606    }
3607
3608    #[test]
3609    fn parse_and_propagate_sheet() {
3610        let mut p = AdamParser::new(TypeRegistry::new(), OpLookup::new());
3611        let mut sheet = p
3612            .parse_str(
3613                r#"
3614            sheet image_resize {
3615                cell width:  f64 = 4.0;
3616                cell height: f64 = 3.0;
3617                cell area:   f64;
3618                relationship {
3619                    area := width * height;
3620                    width := area / height;
3621                    height := area / width;
3622                }
3623            }
3624        "#,
3625            )
3626            .unwrap();
3627
3628        // The relationship should compute area = width * height = 4.0 * 3.0 = 12.0.
3629        // We can't read cells by name yet (no name→CellId API on Sheet), so we verify
3630        // the sheet propagated without error, which exercises the full call_dyn path.
3631        // A future test can assert specific cell values once a name-lookup API exists.
3632        sheet.propagate().unwrap();
3633        let _ = sheet; // sheet is live and propagated
3634    }
3635
3636    #[test]
3637    fn parse_duplicate_cell_is_error() {
3638        let result = parser().parse_str("sheet s { cell x: i32; cell x: f64; }");
3639        match result {
3640            Ok(_) => panic!("expected error for duplicate cell name"),
3641            Err(e) => {
3642                let msg = e.to_string();
3643                assert!(
3644                    msg.contains("duplicate cell `x`"),
3645                    "error mentions duplicate name: {msg}"
3646                );
3647            }
3648        }
3649    }
3650
3651    #[test]
3652    fn parse_str_returns_cell_names_in_declaration_order() {
3653        let parsed = parser()
3654            .parse_str("sheet s { cell z: i32 = 1; cell a: i32 = 2; cell m: i32 = 3; }")
3655            .unwrap();
3656        let names: Vec<&str> = parsed.cell_names.keys().map(String::as_str).collect();
3657        assert_eq!(names, vec!["z", "a", "m"]);
3658    }
3659
3660    #[test]
3661    fn parsed_sheet_derefs_to_sheet_for_propagate() {
3662        let mut parsed = parser().parse_str("sheet s { cell x: i32 = 1; }").unwrap();
3663        // Deref/DerefMut must make Sheet's methods directly callable.
3664        parsed.propagate().unwrap();
3665    }
3666
3667    #[test]
3668    fn parse_out_with_explicit_type_propagates_correctly() {
3669        let mut sheet = parser()
3670            .parse_str(
3671                r#"
3672                sheet s {
3673                    cell width: f64 = 4.0;
3674                    cell height: f64 = 3.0;
3675                    out area: f64 := width * height;
3676                }
3677            "#,
3678            )
3679            .unwrap();
3680        sheet.propagate().unwrap();
3681        let cell_id = *sheet.output_names.get("area").expect("area registered");
3682        assert_eq!(*sheet.read::<f64>(cell_id).unwrap(), 12.0);
3683    }
3684
3685    #[test]
3686    fn parse_out_with_tuple_type_value_debug_formats_correctly() {
3687        let mut sheet = parser()
3688            .parse_str(
3689                r#"
3690                sheet s {
3691                    cell x: i32 = 3;
3692                    out pair: (i32, i32) := (x, x);
3693                }
3694            "#,
3695            )
3696            .unwrap();
3697        sheet.propagate().unwrap();
3698        let cell_id = *sheet.output_names.get("pair").unwrap();
3699        let value = sheet.read::<CellValue>(cell_id).unwrap();
3700        assert_eq!(format!("{value:?}"), "(3, 3)");
3701    }
3702
3703    #[test]
3704    fn parse_out_with_no_annotation_infers_type_from_writer_body() {
3705        let sheet = parser()
3706            .parse_str(
3707                r#"
3708                sheet s {
3709                    cell width: f64 = 4.0;
3710                    out doubled := width + width;
3711                }
3712            "#,
3713            )
3714            .unwrap();
3715        let (_, shape) = sheet.cell_names.get("doubled").unwrap().clone();
3716        assert_eq!(shape, TypeShape::Named(std::any::TypeId::of::<f64>()));
3717    }
3718
3719    #[test]
3720    fn parse_out_type_mismatch_is_error() {
3721        let result = parser().parse_str(
3722            r#"
3723            sheet s {
3724                cell width: f64 = 4.0;
3725                out area: i32 := width;
3726            }
3727        "#,
3728        );
3729        assert!(
3730            result.is_err(),
3731            "f64 body for an i32 annotation must be an error"
3732        );
3733    }
3734
3735    #[test]
3736    fn parse_out_with_requirements_reports_output_valid_and_violated() {
3737        let mut sheet = parser()
3738            .parse_str(
3739                r#"
3740                sheet s {
3741                    cell width: f64 = 4.0;
3742                    cell height: f64 = 3.0;
3743                    cell max_area: f64 = 100.0;
3744                    out area: f64 := width * height require {
3745                        @max_area width * height <= max_area;
3746                    };
3747                }
3748            "#,
3749            )
3750            .unwrap();
3751        sheet.propagate().unwrap();
3752        let output_id = *sheet.output_names.get("area").unwrap();
3753        assert!(sheet.cell_requirements_valid(output_id));
3754        assert_eq!(sheet.violated_requirements(output_id).count(), 0);
3755    }
3756
3757    #[test]
3758    fn parse_out_requirement_violation_is_reported_after_propagate() {
3759        let mut sheet = parser()
3760            .parse_str(
3761                r#"
3762                sheet s {
3763                    cell width: f64 = 40.0;
3764                    cell height: f64 = 30.0;
3765                    cell max_area: f64 = 100.0;
3766                    out area: f64 := width * height require {
3767                        @max_area width * height <= max_area;
3768                    };
3769                }
3770            "#,
3771            )
3772            .unwrap();
3773        sheet.propagate().unwrap();
3774        let output_id = *sheet.output_names.get("area").unwrap();
3775        assert!(!sheet.cell_requirements_valid(output_id));
3776        assert_eq!(sheet.violated_requirements(output_id).count(), 1);
3777    }
3778
3779    #[test]
3780    /// Resolves explicit labels by returned IDs while preserving unnamed checks and order.
3781    fn requirement_names_are_metadata_for_explicit_labels_only() {
3782        let mut parsed = parser()
3783            .parse_str(
3784                "sheet s {
3785                    cell x: i32 = 1 require { @same x > 0i32; x > 0i32; x < 9i32; };
3786                    source y: i32 = 2 require { @same y > 0i32; y > 0i32; };
3787                    out z := x require { @last z < 0i32; z < 0i32; @first z > 9i32; };
3788                }",
3789            )
3790            .unwrap();
3791        let x = parsed.cell_names["x"].0;
3792        let y = parsed.cell_names["y"].0;
3793        let z = parsed.output_names["z"];
3794        assert_eq!(parsed.requirement_names.len(), 4);
3795        for cell in [x, y] {
3796            let ids = parsed.cell_requirements(cell).unwrap();
3797            assert_eq!(parsed.requirement_names[&ids[0]], "same");
3798            assert!(!parsed.requirement_names.contains_key(&ids[1]));
3799        }
3800        parsed.propagate().unwrap();
3801        let violated: Vec<_> = parsed.violated_requirements(z).collect();
3802        assert_eq!(violated, parsed.cell_requirements(z).unwrap());
3803        assert_eq!(parsed.requirement_names[&violated[0]], "last");
3804        assert!(!parsed.requirement_names.contains_key(&violated[1]));
3805        assert_eq!(parsed.requirement_names[&violated[2]], "first");
3806    }
3807
3808    #[test]
3809    /// Rejects repeated explicit labels locally for every declaration kind.
3810    fn duplicate_requirement_labels_are_rejected_per_declaration() {
3811        for declaration in [
3812            "cell x: i32 = 1",
3813            "source x: i32 = 1",
3814            "cell y: i32 = 1; out x := y",
3815        ] {
3816            let source =
3817                format!("sheet s {{ {declaration} require {{ @dup x > 0i32; @dup x < 9i32; }}; }}");
3818            assert!(parser().parse_str(&source).is_err(), "{source}");
3819        }
3820    }
3821
3822    #[test]
3823    fn parse_out_duplicate_requirement_names_is_error() {
3824        let result = parser().parse_str(
3825            r#"
3826            sheet s {
3827                cell width: f64 = 4.0;
3828                out area: f64 := width require {
3829                    @dup width <= 10.0;
3830                    @dup width >= 0.0;
3831                };
3832            }
3833        "#,
3834        );
3835        assert!(
3836            result.is_err(),
3837            "two requirements sharing a name must be an error"
3838        );
3839    }
3840
3841    #[test]
3842    fn parse_out_with_an_unlabeled_requirement_is_enforced() {
3843        let mut sheet = parser()
3844            .parse_str(
3845                r#"
3846                sheet s {
3847                    cell width: f64 = 40.0;
3848                    cell height: f64 = 30.0;
3849                    cell max_area: f64 = 100.0;
3850                    out area: f64 := width * height require {
3851                        width * height <= max_area;
3852                    };
3853                }
3854            "#,
3855            )
3856            .unwrap();
3857        sheet.propagate().unwrap();
3858        let output_id = *sheet.output_names.get("area").unwrap();
3859        assert_eq!(sheet.violated_requirements(output_id).count(), 1);
3860    }
3861
3862    #[test]
3863    fn parse_out_with_a_bare_identifier_unlabeled_requirement_is_enforced() {
3864        let mut sheet = parser()
3865            .parse_str(
3866                r#"
3867                sheet s {
3868                    cell is_valid: bool = false;
3869                    out area: i32 := 4 require {
3870                        is_valid;
3871                    };
3872                }
3873            "#,
3874            )
3875            .unwrap();
3876        sheet.propagate().unwrap();
3877        let output_id = *sheet.output_names.get("area").unwrap();
3878        assert_eq!(sheet.violated_requirements(output_id).count(), 1);
3879    }
3880
3881    #[test]
3882    fn parse_out_cell_referenced_elsewhere_succeeds() {
3883        // `out` cells are no longer terminal (see `adam_rs::CellKind`): referencing an `out`
3884        // cell as another relationship's input is legal.
3885        let result = parser().parse_str(
3886            r#"
3887            sheet s {
3888                cell width: f64 = 4.0;
3889                cell height: f64 = 3.0;
3890                out area: f64 := width * height;
3891                relationship { width := area; }
3892            }
3893        "#,
3894        );
3895        assert!(
3896            result.is_ok(),
3897            "referencing an out cell as another relationship's input must not be an error"
3898        );
3899    }
3900
3901    #[test]
3902    fn parse_out_infers_structural_array_without_registration() {
3903        let result = parser().parse_str(
3904            r#"
3905            sheet s {
3906                cell dummy: i32 = 0;
3907                out x := []: [i32];
3908            }
3909        "#,
3910        );
3911        let mut sheet = result.unwrap();
3912        sheet.propagate().unwrap();
3913        assert!(
3914            sheet
3915                .read_compound::<cel_runtime::ArrayOf<cel_runtime::Leaf<i32>>>(
3916                    sheet.cell_names["x"].0
3917                )
3918                .unwrap()
3919                .is_empty()
3920        );
3921    }
3922
3923    /// Empty tuple writers execute their initializer and propagate runtime errors.
3924    #[test]
3925    fn empty_tuple_output_executes_its_writer() {
3926        let mut lookup = OpLookup::new();
3927        lookup.push_scope(|name, segment, arity, _| {
3928            if name == "bad" && arity == 0 {
3929                segment.op0r(|| -> anyhow::Result<i32> { anyhow::bail!("unit writer failed") });
3930                return Ok(true);
3931            }
3932            Ok(false)
3933        });
3934        let mut sheet = AdamParser::new(TypeRegistry::new(), lookup)
3935            .parse_str("sheet s { cell input: i32 = 0; cell nothing: (); relationship { nothing := (input, bad, ()).2; } }")
3936            .unwrap();
3937        let error = sheet.propagate().unwrap_err();
3938        assert!(error.to_string().contains("unit writer failed"), "{error}");
3939    }
3940
3941    #[test]
3942    fn parse_out_cell_referenced_in_conditional_succeeds() {
3943        // `out` cells are no longer terminal (see `adam_rs::CellKind`): referencing an `out`
3944        // cell as a relationship input inside a conditional branch is legal.
3945        let result = parser().parse_str(
3946            r#"
3947            sheet s {
3948                cell width: f64 = 4.0;
3949                cell height: f64 = 3.0;
3950                cell mode: i32 = 0;
3951                out area: f64 := width * height;
3952                conditional mode {
3953                    0i32 => { relationship { width := area; } }
3954                }
3955            }
3956        "#,
3957        );
3958        assert!(
3959            result.is_ok(),
3960            "referencing an out cell as a conditional branch relationship's input must not be an error"
3961        );
3962    }
3963
3964    #[test]
3965    fn parse_cell_with_explicit_tuple_type_and_initializer() {
3966        let mut p = parser();
3967        let parsed = p
3968            .parse_str("sheet s { cell a: (i32, f64) = (1, 2.5); }")
3969            .unwrap();
3970        let (cell_id, shape) = parsed.cell_names["a"].clone();
3971        assert_eq!(
3972            shape,
3973            TypeShape::Tuple(vec![
3974                TypeShape::Named(std::any::TypeId::of::<i32>()),
3975                TypeShape::Named(std::any::TypeId::of::<f64>()),
3976            ])
3977        );
3978        let value = parsed
3979            .read_compound::<cel_runtime::Cons<
3980                cel_runtime::Leaf<i32>,
3981                cel_runtime::Cons<cel_runtime::Leaf<f64>, cel_runtime::Nil>,
3982            >>(cell_id)
3983            .unwrap();
3984        let (a, b) = (value.1, value.0.1);
3985        assert_eq!((a, b), (1, 2.5));
3986    }
3987
3988    #[test]
3989    fn parse_cell_with_tuple_type_and_no_initializer_uses_recursive_default() {
3990        let parsed = parser()
3991            .parse_str("sheet s { cell a: (i32, f64); }")
3992            .unwrap();
3993        let (cell_id, _) = parsed.cell_names["a"].clone();
3994        let value = parsed
3995            .read_compound::<cel_runtime::Cons<
3996                cel_runtime::Leaf<i32>,
3997                cel_runtime::Cons<cel_runtime::Leaf<f64>, cel_runtime::Nil>,
3998            >>(cell_id)
3999            .unwrap();
4000        let (a, b) = (value.1, value.0.1);
4001        assert_eq!((a, b), (0, 0.0));
4002    }
4003
4004    #[test]
4005    fn parse_cell_with_tuple_initializer_arity_mismatch_is_an_error() {
4006        let result = parser().parse_str("sheet s { cell a: (i32, f64, i32) = (1, 2.5); }");
4007        assert!(result.is_err());
4008    }
4009
4010    #[test]
4011    fn parse_cell_with_nested_tuple_type_round_trips() {
4012        let parsed = parser()
4013            .parse_str("sheet s { cell a: (i32, (f64, String)) = (1, (2.5, \"x\")); }")
4014            .unwrap();
4015        let (cell_id, _) = parsed.cell_names["a"].clone();
4016        type Inner = cel_runtime::Cons<
4017            cel_runtime::Leaf<f64>,
4018            cel_runtime::Cons<cel_runtime::Leaf<String>, cel_runtime::Nil>,
4019        >;
4020        type Outer =
4021            cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Cons<Inner, cel_runtime::Nil>>;
4022        let value = parsed.read_compound::<Outer>(cell_id).unwrap();
4023        assert_eq!(value.1, 1);
4024        let (b, c) = (value.0.1.1, value.0.1.0.1);
4025        assert_eq!((b, c), (2.5, "x".to_string()));
4026    }
4027
4028    #[test]
4029    fn parse_out_with_explicit_tuple_type_infers_and_stores_correctly() {
4030        let mut sheet = parser()
4031            .parse_str(
4032                r#"
4033                sheet s {
4034                    cell x: i32 = 3;
4035                    out pair: (i32, i32) := (x, x);
4036                }
4037            "#,
4038            )
4039            .unwrap();
4040        sheet.propagate().unwrap();
4041        let cell_id = *sheet.output_names.get("pair").unwrap();
4042        let value = sheet
4043            .read_compound::<cel_runtime::Cons<
4044                cel_runtime::Leaf<i32>,
4045                cel_runtime::Cons<cel_runtime::Leaf<i32>, cel_runtime::Nil>,
4046            >>(cell_id)
4047            .unwrap();
4048        let (a, b) = (value.1, value.0.1);
4049        assert_eq!((a, b), (3, 3));
4050    }
4051
4052    #[test]
4053    fn parse_cell_range_initializer_fails_cleanly_at_type_inference_not_grammar() {
4054        // `Range<i32>` isn't a registered adam-lang type — this must still fail today, but only
4055        // at `eval_segment_boxed`'s existing "cannot infer a type" check, proving the CEL-level
4056        // range parsing itself succeeded (rather than failing as "unexpected token" or similar
4057        // at the grammar level, which would indicate the entry-point swap didn't take effect).
4058        let result = parser().parse_str("sheet s { cell x = 1i32..5i32; }");
4059        assert!(result.is_err());
4060        let err = result.expect_err("expected Err");
4061        assert!(err.message().contains("cannot infer a type"));
4062    }
4063
4064    /// Verifies unregistered out results identify the declaration and its name token.
4065    #[test]
4066    fn shape_error_out_preserves_context_and_span() {
4067        for expression in ["1i32..5i32", "(1i32, 1i32..5i32)"] {
4068            let source = format!("sheet s {{\nout result := {expression};\n}}");
4069            let err = parser().parse_str(&source).unwrap_err();
4070            assert!(err.message().contains("cannot infer a type"));
4071            assert_eq!(
4072                err.span().start(),
4073                proc_macro2::LineColumn { line: 2, column: 4 }
4074            );
4075            assert_eq!(
4076                err.span().end(),
4077                proc_macro2::LineColumn {
4078                    line: 2,
4079                    column: 10
4080                }
4081            );
4082            assert!(err.message().starts_with("out `result`:"));
4083        }
4084    }
4085
4086    /// Verifies unregistered match results identify the expression and its opening token.
4087    #[test]
4088    fn shape_error_match_preserves_context_and_span() {
4089        for (expression, end_column) in [("1i32..5i32", 16), ("(1i32, 1i32..5i32)", 30)] {
4090            let source = format!("sheet s {{\nconditional {expression} {{ _ => {{}} }}\n}}");
4091            let err = parser().parse_str(&source).unwrap_err();
4092            assert!(err.message().contains("cannot infer a type"));
4093            assert_eq!(
4094                err.span().start(),
4095                proc_macro2::LineColumn {
4096                    line: 2,
4097                    column: 12
4098                }
4099            );
4100            assert_eq!(
4101                err.span().end(),
4102                proc_macro2::LineColumn {
4103                    line: 2,
4104                    column: end_column
4105                }
4106            );
4107            assert!(err.message().starts_with("match expression:"));
4108        }
4109    }
4110
4111    /// Verifies unregistered binding results identify the output and its binding token.
4112    #[test]
4113    fn shape_error_output_preserves_context_and_span() {
4114        for (shape, expression) in [("i32", "1i32..5i32"), ("(i32, i32)", "(1i32, 1i32..5i32)")] {
4115            let source = format!(
4116                "sheet s {{\ncell result: {shape};\nrelationship {{\nresult := {expression};\n}}\n}}"
4117            );
4118            let err = parser().parse_str(&source).unwrap_err();
4119            assert!(err.message().contains("cannot infer a type"));
4120            assert_eq!(
4121                err.span().start(),
4122                proc_macro2::LineColumn { line: 4, column: 0 }
4123            );
4124            assert_eq!(
4125                err.span().end(),
4126                proc_macro2::LineColumn { line: 4, column: 6 }
4127            );
4128            assert!(err.message().starts_with("output `result`:"));
4129        }
4130    }
4131
4132    /// Verifies unregistered initializer results identify the cell and its name token.
4133    #[test]
4134    fn shape_error_initializer_preserves_context_and_span() {
4135        for expression in ["1i32..5i32", "(1i32, 1i32..5i32)"] {
4136            let source = format!("sheet s {{\ncell result = {expression};\n}}");
4137            let err = parser().parse_str(&source).unwrap_err();
4138            assert!(err.message().contains("cannot infer a type"));
4139            assert_eq!(
4140                err.span().start(),
4141                proc_macro2::LineColumn { line: 2, column: 5 }
4142            );
4143            assert_eq!(
4144                err.span().end(),
4145                proc_macro2::LineColumn {
4146                    line: 2,
4147                    column: 11
4148                }
4149            );
4150            assert!(err.message().starts_with("cell `result`:"));
4151        }
4152    }
4153
4154    /// Preserves source declaration labels and name spans for initializer errors.
4155    #[test]
4156    fn shape_error_source_preserves_context_and_span() {
4157        for (declaration, message) in [
4158            ("source result: i32 = true;", "type mismatch"),
4159            ("source result = 1i32..5i32;", "cannot infer a type"),
4160            ("source result = (1i32, 1i32..5i32);", "cannot infer a type"),
4161        ] {
4162            let source = format!("sheet s {{\n{declaration}\n}}");
4163            let err = parser().parse_str(&source).unwrap_err();
4164            assert!(err.message().contains(message));
4165            assert!(
4166                err.message().starts_with("source `result`:"),
4167                "{}",
4168                err.message()
4169            );
4170            assert_eq!(
4171                err.span().start(),
4172                proc_macro2::LineColumn { line: 2, column: 7 }
4173            );
4174            assert_eq!(
4175                err.span().end(),
4176                proc_macro2::LineColumn {
4177                    line: 2,
4178                    column: 13
4179                }
4180            );
4181        }
4182    }
4183
4184    /// Verifies registered type mismatches retain the ordinary out and output prefixes.
4185    #[test]
4186    fn shape_error_registered_mismatches_preserve_context() {
4187        for (source, prefix) in [
4188            ("sheet s { out result: i32 := true; }", "out `result`:"),
4189            (
4190                "sheet s { cell result: i32; relationship { result := true; } }",
4191                "output `result`:",
4192            ),
4193        ] {
4194            let err = parser().parse_str(source).unwrap_err();
4195            assert!(err.message().contains("type mismatch"));
4196            assert!(err.message().starts_with(prefix));
4197        }
4198    }
4199
4200    #[test]
4201    fn dependency_cycle_validation_error_spans_the_governed_cell() {
4202        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
4203        // The branch writes the match cell `mode`, so whole-sheet validation returns
4204        // DependencyCycle on line 2 (`cell mode`), the governed cell that starts the cycle.
4205        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}";
4206        let err = parser.parse_str(source).unwrap_err();
4207        assert_eq!(err.span().start().line, 2);
4208    }
4209
4210    // NOTE on coverage: `add_conditional`'s other `InvalidConditional` case with a
4211    // `Relationship`-only site list -- the same relationship reused across two branches (or a
4212    // branch and the default) -- is not reachable through adam-lang's own grammar today.
4213    // `parse_branch_relationships` calls `parse_relationship_decl`, which always creates a
4214    // *fresh* `RelationshipId` on `add_relationship`; adam-lang has no syntax for a `relationship`
4215    // to be *named* and then referenced again from a second branch. That case is exercised
4216    // directly against `Sheet::add_conditional` in
4217    // `add_conditional_returns_invalid_conditional_for_duplicate_relationship_across_branches`
4218    // (adam-rs/src/sheet.rs); it stays untested at the adam-lang parser level until adam-lang
4219    // grows named/referenceable relationships (tracked as a follow-up, not blocking this task).
4220    #[test]
4221    fn conditional_invalid_relationship_and_cell_sites_render_as_secondary_labels() {
4222        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
4223        // The governed branch output `a` reaches match cell `p` through the unconditional
4224        // relationship, so the diagnostic should use `a` as primary and attach the
4225        // remaining cycle sites as secondary labels.
4226        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}";
4227        let err = parser.parse_str(source).unwrap_err();
4228        let out =
4229            err.format_rustc_style(source, "t.adm2", 1, &annotate_snippets::Renderer::plain());
4230        assert!(out.contains("cell `p` is part of the cycle"), "{out}");
4231    }
4232
4233    #[test]
4234    /// Locates duplicate labels at the repeated requirement, not the sheet opening.
4235    fn out_decl_duplicate_requirement_error_spans_the_duplicate_label() {
4236        let mut parser = AdamParser::new(TypeRegistry::new(), OpLookup::new());
4237        // The parser owns display-label validation, so it can identify the repeated label
4238        // itself rather than blaming runtime registration or the sheet's opening line.
4239        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}";
4240        let err = parser.parse_str(source).unwrap_err();
4241        assert_eq!(err.span().start().line, 5);
4242        assert!(err.to_string().contains("duplicate requirement `pos`"));
4243    }
4244}