Skip to main content

adam_lang/
ast.rs

1//! A span-carrying adam-lang structural AST, built by [`crate::AdamAstParser`] as an alternative to
2//! [`crate::AdamParser`]'s direct `adam_rs::Sheet` construction. Method bodies and cell
3//! initializers reference [`cel_parser::Expr`]/[`cel_parser::lex_lexer::Literal`] directly.
4//! Carries no resolved types, no `TypeRegistry` lookups, and never fails on semantic grounds
5//! (unknown type name, literal/type mismatch, undeclared cell, arity mismatch) — those checks
6//! are deferred to a later, separate compile-to-`Sheet` phase, mirroring
7//! [`cel_parser::AstContext`]'s design.
8
9pub use cel_parser::ExprSpan;
10use cel_parser::lex_lexer::Literal;
11
12pub use cel_parser::Comment;
13
14/// An ordered file of imports and sheet definitions, without resolved names or types.
15///
16/// Errors include malformed file items and errors recovered inside definitions.
17///
18/// # Examples
19///
20/// ```
21/// let module = adam_lang::AdamAstParser::new().parse_module_str(
22///     "use math::ops as operations;
23///      sheet component(input: i32, result: i32) {
24///          relationship { result := input; }
25///      }
26///      sheet root {
27///          source x: i32 = 1 filter 0..=10 require { @positive x > 0; };
28///          cell y: i32;
29///          cell z: i32;
30///          out pair: (i32, i32) := (x, y);
31///          relationship component(input: x, result: y);
32///          conditional true {
33///              true => { relationship { (y, z) := (x, x); } }
34///              _ => { relationship { y := x; } }
35///          }
36///      }"
37/// ).unwrap();
38/// assert!(module.errors.is_empty());
39/// assert_eq!(module.items.len(), 3);
40/// ```
41#[derive(Debug, Clone)]
42pub struct Module {
43    /// File items in source order, including recovery placeholders.
44    pub items: Vec<ModuleItem>,
45    /// Recovered syntax errors in source order.
46    pub errors: Vec<cel_parser::ParseError>,
47    /// Comments before each item and after the last item, populated by trivia attachment.
48    pub comments: Vec<Vec<Comment>>,
49}
50
51/// A file-level declaration or a recovered malformed declaration.
52///
53/// # Examples
54///
55/// See [`Module`] for a parsed file containing imports and sheet definitions.
56#[derive(Debug, Clone)]
57pub enum ModuleItem {
58    /// An explicit named import.
59    Use(UseDecl),
60    /// A root or parameterized definition.
61    Sheet(Sheet),
62    /// The span of a malformed file-level declaration.
63    Error(ExprSpan),
64}
65
66impl ModuleItem {
67    /// Returns the declaration or recovery span.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// let module = adam_lang::AdamAstParser::new().parse_module_str("sheet root {}").unwrap();
73    /// assert_eq!(module.items[0].span().start.start().line, 1);
74    /// ```
75    pub fn span(&self) -> ExprSpan {
76        match self {
77            Self::Use(item) => item.span,
78            Self::Sheet(item) => item.span,
79            Self::Error(span) => *span,
80        }
81    }
82}
83
84/// `use_decl = "use" identifier { "::" identifier } [ "as" identifier ] ";".`
85///
86/// # Examples
87///
88/// See [`Module`] for an aliased import in a parsed file.
89#[derive(Debug, Clone)]
90pub struct UseDecl {
91    /// The `use` keyword's span, excluding preceding documentation.
92    pub keyword_span: ExprSpan,
93    /// Leading inner documentation, if present.
94    pub doc_comment: Option<String>,
95    /// Unresolved path segments and their individual spans.
96    pub path: Vec<(String, ExprSpan)>,
97    /// The optional local alias and its span.
98    pub alias: Option<(String, ExprSpan)>,
99    /// The whole import's span.
100    pub span: ExprSpan,
101}
102
103/// A required typed formal parameter, without a default or an initializer.
104///
105/// # Examples
106///
107/// See [`Module`] for the component's `input` and `result` parameters.
108#[derive(Debug, Clone)]
109pub struct ParameterDecl {
110    /// The formal parameter's name.
111    pub name: String,
112    /// The name token's span.
113    pub name_span: ExprSpan,
114    /// The required unresolved type expression.
115    pub type_name: TypeExpr,
116}
117
118/// A named binding from a formal parameter to a caller's cell identifier.
119///
120/// # Examples
121///
122/// See [`Module`] for `input: x` and `result: y` actual arguments.
123#[derive(Debug, Clone)]
124pub struct NamedArgument {
125    /// The formal parameter's name.
126    pub name: String,
127    /// The formal name token's span.
128    pub name_span: ExprSpan,
129    /// The actual cell's name.
130    pub cell: String,
131    /// The actual cell token's span.
132    pub cell_span: ExprSpan,
133}
134
135/// `relationship_use = "relationship" path "(" [ named_arguments ] ")" ";".`
136///
137/// # Examples
138///
139/// See [`Module`] for a component use with named actual arguments.
140#[derive(Debug, Clone)]
141pub struct RelationshipUse {
142    /// The `relationship` keyword's span, excluding any preceding documentation.
143    pub keyword_span: ExprSpan,
144    /// Unresolved callee path segments and their spans.
145    pub path: Vec<(String, ExprSpan)>,
146    /// Named cell arguments in source order.
147    pub arguments: Vec<NamedArgument>,
148    /// Every comment in the opening-brace gap when this use is the first body item.
149    /// Empty for a later item; its ordinary sibling trivia uses `leading_comment` instead.
150    pub opening_comments: Vec<Comment>,
151    /// A recovered leading comment.
152    pub leading_comment: Option<Comment>,
153    /// A leading documentation comment.
154    pub doc_comment: Option<String>,
155    /// A comment on the declaration's last source line.
156    pub trailing_line_comment: Option<Comment>,
157    /// Whether a blank line precedes this declaration.
158    pub blank_line_before: bool,
159    /// The whole declaration's span.
160    pub span: ExprSpan,
161}
162
163/// A conditional branch item, retaining inline and included relationship order.
164///
165/// # Examples
166///
167/// See [`Module`] for inline relationships in named and default branches.
168#[derive(Debug, Clone)]
169pub enum BranchItem {
170    /// An ordinary inline relationship.
171    Relationship(RelationshipDecl),
172    /// A named component use.
173    RelationshipUse(RelationshipUse),
174}
175
176/// A parsed adam-lang sheet declaration, with source spans on every node.
177///
178/// Built by [`crate::AdamAstParser`]; consumed by the language server, the formatter,
179/// and [`crate::ModuleCompiler`].
180///
181/// # Examples
182///
183/// See [`Module`] for a root sheet and a parameterized definition.
184#[derive(Debug, Clone)]
185pub struct Sheet {
186    /// The `sheet` keyword's span, excluding any preceding documentation.
187    pub keyword_span: ExprSpan,
188    /// The sheet's declared name.
189    pub name: String,
190    /// The name token's span.
191    pub name_span: ExprSpan,
192    /// `None` for a root; `Some`, including an empty vector, for a component.
193    pub parameters: Option<Vec<ParameterDecl>>,
194    /// The sheet's items, in declaration order.
195    pub items: Vec<SheetItem>,
196    /// A leading `//`/`/* */` comment immediately preceding the `sheet` keyword (e.g. a file
197    /// header), if recovered by [`crate::trivia::attach_trivia`]. Unlike every other node's
198    /// `leading_comment`, this one has no enclosing sibling list to attach via — it covers the
199    /// gap between the start of the source and the sheet's own span.
200    pub leading_comment: Option<Comment>,
201    /// A leading `//!` doc comment immediately preceding the `sheet` keyword, if recovered by
202    /// [`crate::AdamAstParser`].
203    pub doc_comment: Option<String>,
204    /// A trailing comment immediately preceding the sheet's own closing `}`, if recovered. See
205    /// <https://github.com/stlab/cel-rs/issues/52>.
206    pub trailing_comment: Option<Comment>,
207    /// Whether a blank line preceded the sheet's own closing `}`, if recovered. See
208    /// <https://github.com/stlab/cel-rs/issues/52>.
209    pub blank_line_before_close: bool,
210    /// The span of the sheet's own opening `{`, used to recover trailing trivia when `items` is
211    /// empty. See <https://github.com/stlab/cel-rs/issues/52>.
212    pub open_brace_span: ExprSpan,
213    /// The span of the whole `sheet ... { ... }` construct.
214    pub span: ExprSpan,
215    /// Syntax errors recovered while parsing, in source order. Empty for a syntactically clean
216    /// sheet.
217    pub errors: Vec<cel_parser::ParseError>,
218}
219
220/// One top-level item inside a `sheet { ... }` body.
221///
222/// # Examples
223///
224/// See [`Module`] for source, cell, output, relationship-use and conditional items.
225#[derive(Debug, Clone)]
226pub enum SheetItem {
227    /// A `cell` declaration.
228    Cell(CellDecl),
229    /// A `relationship` declaration.
230    Relationship(RelationshipDecl),
231    /// A named component use.
232    RelationshipUse(RelationshipUse),
233    /// A `conditional` declaration.
234    Conditional(ConditionalDecl),
235    /// An `out` declaration.
236    Out(OutDecl),
237    /// A `source` declaration.
238    Source(SourceDecl),
239    /// A syntax error recovered at declaration granularity; `span` covers the skipped tokens.
240    Error {
241        /// The span of the skipped, malformed item.
242        span: ExprSpan,
243        /// A leading `//`/`/* */` comment immediately preceding this item, if recovered by
244        /// [`crate::trivia::attach_trivia`]. Preserved even though the item failed to parse, so
245        /// a comment explaining a broken declaration (e.g. `// TODO: fix this`) isn't silently
246        /// dropped.
247        leading_comment: Option<Comment>,
248        /// A leading `///` doc comment immediately preceding this item, if recovered by
249        /// [`crate::AdamAstParser`] before parsing failed.
250        doc_comment: Option<String>,
251        /// A `//`/`/* */` comment on the same source line as this item's own last token, if
252        /// recovered by [`crate::trivia::attach_trivia`]. Distinct from `leading_comment`, which
253        /// precedes an item on its own line. See <https://github.com/stlab/cel-rs/issues/59>.
254        trailing_line_comment: Option<Comment>,
255        /// Whether the gap before this item contained a blank line, if recovered by
256        /// [`crate::trivia::attach_trivia`].
257        blank_line_before: bool,
258    },
259}
260
261impl SheetItem {
262    /// Returns this item's source span.
263    ///
264    /// # Examples
265    ///
266    /// ```
267    /// let sheet = adam_lang::AdamAstParser::new()
268    ///     .parse_str("sheet s {\n    source x: i32;\n}").unwrap();
269    /// assert_eq!(sheet.items[0].span().start.start().line, 2);
270    /// assert_eq!(sheet.items[0].span().start.start().column, 4);
271    /// ```
272    pub fn span(&self) -> ExprSpan {
273        match self {
274            SheetItem::Cell(c) => c.span,
275            SheetItem::Relationship(r) => r.span,
276            SheetItem::RelationshipUse(r) => r.span,
277            SheetItem::Conditional(c) => c.span,
278            SheetItem::Out(o) => o.span,
279            SheetItem::Source(s) => s.span,
280            SheetItem::Error { span, .. } => *span,
281        }
282    }
283
284    /// Sets this item's leading comment.
285    pub(crate) fn set_leading_comment(&mut self, comment: Comment) {
286        match self {
287            SheetItem::Cell(c) => c.leading_comment = Some(comment),
288            SheetItem::Relationship(r) => r.leading_comment = Some(comment),
289            SheetItem::RelationshipUse(r) => r.leading_comment = Some(comment),
290            SheetItem::Conditional(c) => c.leading_comment = Some(comment),
291            SheetItem::Out(o) => o.leading_comment = Some(comment),
292            SheetItem::Source(s) => s.leading_comment = Some(comment),
293            SheetItem::Error {
294                leading_comment, ..
295            } => *leading_comment = Some(comment),
296        }
297    }
298
299    /// Sets a `//`/`/* */` comment recovered on the same source line as this item's own last
300    /// token. See <https://github.com/stlab/cel-rs/issues/59>.
301    pub(crate) fn set_trailing_line_comment(&mut self, comment: Comment) {
302        match self {
303            SheetItem::Cell(c) => c.trailing_line_comment = Some(comment),
304            SheetItem::Relationship(r) => r.trailing_line_comment = Some(comment),
305            SheetItem::RelationshipUse(r) => r.trailing_line_comment = Some(comment),
306            SheetItem::Conditional(c) => c.trailing_line_comment = Some(comment),
307            SheetItem::Out(o) => o.trailing_line_comment = Some(comment),
308            SheetItem::Source(s) => s.trailing_line_comment = Some(comment),
309            SheetItem::Error {
310                trailing_line_comment,
311                ..
312            } => *trailing_line_comment = Some(comment),
313        }
314    }
315
316    /// Sets whether a blank line preceded this item.
317    pub(crate) fn set_blank_line_before(&mut self, value: bool) {
318        match self {
319            SheetItem::Cell(c) => c.blank_line_before = value,
320            SheetItem::Relationship(r) => r.blank_line_before = value,
321            SheetItem::RelationshipUse(r) => r.blank_line_before = value,
322            SheetItem::Conditional(c) => c.blank_line_before = value,
323            SheetItem::Out(o) => o.blank_line_before = value,
324            SheetItem::Source(s) => s.blank_line_before = value,
325            SheetItem::Error {
326                blank_line_before, ..
327            } => *blank_line_before = value,
328        }
329    }
330
331    /// Sets this item's doc comment and widens its span to start at `start` (the doc comment's
332    /// own first token), so `trivia::attach_trivia`'s gap scan stops before the doc comment's
333    /// source text instead of misparsing it as a plain `//` comment.
334    pub(crate) fn set_doc_comment(&mut self, text: String, start: proc_macro2::Span) {
335        match self {
336            SheetItem::Cell(c) => {
337                c.doc_comment = Some(text);
338                c.span.start = start;
339            }
340            SheetItem::Relationship(r) => {
341                r.doc_comment = Some(text);
342                r.span.start = start;
343            }
344            SheetItem::RelationshipUse(r) => {
345                r.doc_comment = Some(text);
346                r.span.start = start;
347            }
348            SheetItem::Conditional(c) => {
349                c.doc_comment = Some(text);
350                c.span.start = start;
351            }
352            SheetItem::Out(o) => {
353                o.doc_comment = Some(text);
354                o.span.start = start;
355            }
356            SheetItem::Source(s) => {
357                s.doc_comment = Some(text);
358                s.span.start = start;
359            }
360            SheetItem::Error {
361                doc_comment, span, ..
362            } => {
363                *doc_comment = Some(text);
364                span.start = start;
365            }
366        }
367    }
368}
369
370/// Re-exports CEL's shared named, generic, tuple, array, and callable type expressions.
371///
372/// `()` is the empty tuple type; `(T)` is grouping; `(T,)` is a singleton tuple.
373/// Larger tuples prohibit trailing commas. Both languages use the same parser.
374///
375/// # Examples
376///
377/// See [`crate::TypeRegistry::resolve`] for parsing and resolving a nested array-and-tuple type.
378pub use cel_parser::TypeExpr;
379
380/// `cell_decl = "cell" identifier cell_type_init [ cell_filter ] [ "require" "{" { requirement }
381/// "}" ] ";".`
382///
383/// `type_name`/`initializer` are unresolved — no `TypeRegistry` lookup, no literal validation.
384/// Parsed declarations contain an annotation, an initializer, or both. Public field storage
385/// does not enforce this invariant for manually constructed nodes.
386///
387/// # Examples
388///
389/// See [`Module`] for annotated cells in a parsed sheet.
390#[derive(Debug, Clone)]
391pub struct CellDecl {
392    /// The cell's declared name.
393    pub name: String,
394    /// The name token's span.
395    pub name_span: ExprSpan,
396    /// The `: type_expr` annotation, if present.
397    pub type_name: Option<TypeExpr>,
398    /// The `= expression` initializer, if present. Unresolved and unevaluated here — see
399    /// `crate::parser::AdamParser` for the compile-to-`Sheet` phase, which parses this with no
400    /// cell scope pushed and evaluates it eagerly, once, at parse time.
401    pub initializer: Option<cel_parser::Expr>,
402    /// The `filter` clause, if present.
403    pub filter: Option<CellFilter>,
404    /// The `require { ... }` validation block, if present.
405    pub require: Option<RequireBlock>,
406    /// A leading `//`/`/* */` comment immediately preceding this declaration, if recovered by
407    /// [`crate::trivia::attach_trivia`].
408    pub leading_comment: Option<Comment>,
409    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
410    /// [`crate::AdamAstParser`].
411    pub doc_comment: Option<String>,
412    /// A `//`/`/* */` comment on the same source line as this declaration's own last token, if
413    /// recovered by [`crate::trivia::attach_trivia`]. See
414    /// <https://github.com/stlab/cel-rs/issues/59>.
415    pub trailing_line_comment: Option<Comment>,
416    /// Whether a blank line preceded this declaration, if recovered by
417    /// [`crate::trivia::attach_trivia`].
418    pub blank_line_before: bool,
419    /// The span of the whole `cell ...;` declaration.
420    pub span: ExprSpan,
421}
422
423/// `source_decl = "source" identifier cell_type_init [ cell_filter ] [ "require" "{" {
424/// requirement } "}" ] ";".`
425///
426/// Same shape as [`CellDecl`]: a `source` cell's initializer is a one-time expression exactly like a
427/// plain `cell`'s, and it supports the same `filter` clause and `require` block.
428///
429/// # Examples
430///
431/// See [`Module`] for a source with an initializer, range filter and requirement.
432#[derive(Debug, Clone)]
433pub struct SourceDecl {
434    /// The source cell's declared name.
435    pub name: String,
436    /// The name token's span.
437    pub name_span: ExprSpan,
438    /// The `: type_expr` annotation, if present.
439    pub type_name: Option<TypeExpr>,
440    /// The `= expression` initializer, if present. Unresolved and unevaluated here — see
441    /// `crate::parser::AdamParser` for the compile-to-`Sheet` phase, which parses this with no
442    /// cell scope pushed and evaluates it eagerly, once, at parse time.
443    pub initializer: Option<cel_parser::Expr>,
444    /// The `filter` clause, if present.
445    pub filter: Option<CellFilter>,
446    /// The `require { ... }` validation block, if present.
447    pub require: Option<RequireBlock>,
448    /// A leading `//`/`/* */` comment immediately preceding this declaration, if recovered by
449    /// [`crate::trivia::attach_trivia`].
450    pub leading_comment: Option<Comment>,
451    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
452    /// [`crate::AdamAstParser`].
453    pub doc_comment: Option<String>,
454    /// A `//`/`/* */` comment on the same source line as this declaration's own last token, if
455    /// recovered by [`crate::trivia::attach_trivia`]. See
456    /// <https://github.com/stlab/cel-rs/issues/59>.
457    pub trailing_line_comment: Option<Comment>,
458    /// Whether a blank line preceded this declaration, if recovered by
459    /// [`crate::trivia::attach_trivia`].
460    pub blank_line_before: bool,
461    /// The span of the whole `source ...;` declaration.
462    pub span: ExprSpan,
463}
464
465/// `cell_filter = "filter" expression.`
466///
467/// # Examples
468///
469/// See [`Module`] for an inclusive range filter.
470#[derive(Debug, Clone)]
471pub struct CellFilter {
472    /// The filter's body expression. `_` inside it denotes the candidate value being conformed;
473    /// every other identifier that names an already-declared cell is a deduced dependency.
474    pub body: cel_parser::Expr,
475    /// The span of the whole `filter ...` clause.
476    pub span: ExprSpan,
477}
478
479/// `relationship_decl = "relationship" "{" { binding } "}".`
480///
481/// # Examples
482///
483/// See [`Module`] for direct and destructuring bindings in relationships.
484#[derive(Debug, Clone)]
485pub struct RelationshipDecl {
486    /// The relationship block's bindings, in declaration order.
487    pub bindings: Vec<BindingDecl>,
488    /// A leading comment immediately preceding this declaration, if recovered.
489    pub leading_comment: Option<Comment>,
490    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
491    /// [`crate::AdamAstParser`].
492    pub doc_comment: Option<String>,
493    /// A `//`/`/* */` comment on the same source line as this declaration's own closing `}`, if
494    /// recovered. Distinct from `trailing_comment`, which precedes this declaration's own
495    /// closing `}` on an earlier line. See <https://github.com/stlab/cel-rs/issues/59>.
496    pub trailing_line_comment: Option<Comment>,
497    /// Whether a blank line preceded this declaration, if recovered.
498    pub blank_line_before: bool,
499    /// A trailing comment immediately preceding this declaration's own closing `}`, if
500    /// recovered. See <https://github.com/stlab/cel-rs/issues/52>.
501    pub trailing_comment: Option<Comment>,
502    /// Whether a blank line preceded this declaration's own closing `}`, if recovered. See
503    /// <https://github.com/stlab/cel-rs/issues/52>.
504    pub blank_line_before_close: bool,
505    /// The span of this declaration's own opening `{`, used to recover trailing trivia when
506    /// `bindings` is empty. See <https://github.com/stlab/cel-rs/issues/52>.
507    pub open_brace_span: ExprSpan,
508    /// The span of the whole `relationship { ... }` declaration.
509    pub span: ExprSpan,
510}
511
512/// `binding = binding_target ":=" expression ";".`
513/// `binding_target = identifier | "(" identifier "," [ identifier { "," identifier } ] ")".`
514///
515/// The compiler deduces inputs from resolved cell references in `body`. The structural
516/// AST records only the explicit output names, without resolving cell declarations.
517///
518/// Parenthesizing the left-hand side requests tuple destructuring, matching Rust's tuple-pattern
519/// syntax: `(a, b) := ...` and the single-element `(a,) := ...` (trailing comma mandatory,
520/// exactly as in a Rust 1-tuple pattern) both destructure `body`'s tuple result element-wise into
521/// `outputs`. A bare identifier binds `body`'s whole result directly to that one output instead.
522/// A parenthesized binding target always requires a comma; `(a) := ...` is invalid.
523/// See [`Self::destructure`].
524///
525/// # Examples
526///
527/// See [`Module`] for both direct and destructuring binding targets.
528#[derive(Debug, Clone)]
529pub struct BindingDecl {
530    /// The binding's output cell names (the left-hand side), in declaration order.
531    pub outputs: Vec<(String, ExprSpan)>,
532    /// Whether the left-hand side requests destructuring (`(a, b) := ...` or `(a,) := ...`) as
533    /// opposed to a direct bind (`a := ...`). Always
534    /// `true` when `outputs.len() > 1`; when `outputs.len() == 1`, distinguishes "destructure
535    /// this single-element tuple" from "bind this whole value directly" — a distinction the
536    /// parenthesized-or-not left-hand side is the only way to express, since both forms name
537    /// exactly one output.
538    pub destructure: bool,
539    /// The parsed right-hand-side expression.
540    pub body: cel_parser::Expr,
541    /// A leading comment immediately preceding this binding, if recovered by
542    /// [`crate::trivia::attach_trivia`].
543    pub leading_comment: Option<Comment>,
544    /// A `//`/`/* */` comment on the same source line as this binding's own last token, if
545    /// recovered by [`crate::trivia::attach_trivia`]. See
546    /// <https://github.com/stlab/cel-rs/issues/59>.
547    pub trailing_line_comment: Option<Comment>,
548    /// Whether a blank line preceded this binding, if recovered by
549    /// [`crate::trivia::attach_trivia`].
550    pub blank_line_before: bool,
551    /// The span of the whole `a := ...;` / `(a, b) := ...;` declaration.
552    pub span: ExprSpan,
553}
554
555/// `out_decl = "out" identifier [ ":" type_expr ] ":=" expression [ cell_filter ] [ "require"
556/// "{" { requirement } "}" ] ";".`
557///
558/// `type_expr` is unresolved here (no `TypeRegistry` lookup), matching `CellDecl`. When
559/// absent, the cell's type is inferred from `initializer`'s result type by the compile phase
560/// (`crate::parser::AdamParser`) — never here.
561///
562/// # Examples
563///
564/// See [`Module`] for a tuple-valued annotated output.
565#[derive(Debug, Clone)]
566pub struct OutDecl {
567    /// The declared cell's name.
568    pub name: String,
569    /// The name token's span.
570    pub name_span: ExprSpan,
571    /// The `: type_expr` annotation, if present.
572    pub type_name: Option<TypeExpr>,
573    /// The parsed initializer expression that computes this cell's value.
574    pub initializer: cel_parser::Expr,
575    /// The `filter` clause, if present.
576    pub filter: Option<CellFilter>,
577    /// The `require { ... }` validation block, if present.
578    pub require: Option<RequireBlock>,
579    /// A leading `//`/`/* */` comment immediately preceding this declaration, if recovered by
580    /// [`crate::trivia::attach_trivia`].
581    pub leading_comment: Option<Comment>,
582    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
583    /// [`crate::AdamAstParser`].
584    pub doc_comment: Option<String>,
585    /// A `//`/`/* */` comment on the same source line as this declaration's own last token, if
586    /// recovered by [`crate::trivia::attach_trivia`]. See
587    /// <https://github.com/stlab/cel-rs/issues/59>.
588    pub trailing_line_comment: Option<Comment>,
589    /// Whether a blank line preceded this declaration, if recovered by
590    /// [`crate::trivia::attach_trivia`].
591    pub blank_line_before: bool,
592    /// The span of the whole `out ... ;` declaration.
593    pub span: ExprSpan,
594}
595
596/// An optional `require { ... }` block on a cell, source or output declaration.
597///
598/// # Examples
599///
600/// See [`Module`] for a labeled requirement on a source.
601#[derive(Debug, Clone)]
602pub struct RequireBlock {
603    /// The block's requirements, in declaration order.
604    pub requirements: Vec<RequirementDecl>,
605    /// A trailing comment immediately preceding this block's own closing `}`, if recovered.
606    /// See <https://github.com/stlab/cel-rs/issues/52>.
607    pub trailing_comment: Option<Comment>,
608    /// Whether a blank line preceded this block's own closing `}`, if recovered. See
609    /// <https://github.com/stlab/cel-rs/issues/52>.
610    pub blank_line_before_close: bool,
611    /// The span of this block's own opening `{`, used to recover trailing trivia when
612    /// `requirements` is empty.
613    pub open_brace_span: ExprSpan,
614    /// The span of the whole `require { ... }` block.
615    pub span: ExprSpan,
616}
617
618/// `requirement = [ "@" identifier ] expression ";".`
619///
620/// `name`, when present, is a plain string label passed to `adam_rs::Sheet::add_requirement`,
621/// not a cell reference — it may coincide with a cell name declared elsewhere in the sheet but
622/// doesn't have to.
623///
624/// # Examples
625///
626/// See [`Module`] for an explicitly labeled Boolean requirement.
627#[derive(Debug, Clone)]
628pub struct RequirementDecl {
629    /// The requirement's declared label, if the `@identifier` marker was present.
630    pub name: Option<String>,
631    /// The `@identifier` marker's span, if present.
632    pub name_span: Option<ExprSpan>,
633    /// The parsed requirement body expression; must type-check as `bool`.
634    pub body: cel_parser::Expr,
635    /// A leading comment immediately preceding this requirement, if recovered by
636    /// [`crate::trivia::attach_trivia`].
637    pub leading_comment: Option<Comment>,
638    /// A `//`/`/* */` comment on the same source line as this requirement's own last token, if
639    /// recovered by [`crate::trivia::attach_trivia`]. See
640    /// <https://github.com/stlab/cel-rs/issues/59>.
641    pub trailing_line_comment: Option<Comment>,
642    /// Whether a blank line preceded this requirement, if recovered by
643    /// [`crate::trivia::attach_trivia`].
644    pub blank_line_before: bool,
645    /// The span of the whole `[ "@" identifier ] expr;` declaration.
646    pub span: ExprSpan,
647}
648
649/// `conditional_decl = "conditional" expression "{" { conditional_branch } "}".`
650///
651/// # Examples
652///
653/// See [`Module`] for a conditional with named and default branches.
654#[derive(Debug, Clone)]
655pub struct ConditionalDecl {
656    /// The match subject: an arbitrary expression over already-declared cells (a bare
657    /// identifier, e.g. `mode`, is the degenerate single-cell case).
658    pub match_expr: cel_parser::Expr,
659    /// The named (literal `=>`) branches, in declaration order.
660    pub branches: Vec<ConditionalBranch>,
661    /// The `_ => { ... }` default branch, if present.
662    pub default: Option<DefaultBranch>,
663    /// A leading comment immediately preceding this declaration, if recovered.
664    pub leading_comment: Option<Comment>,
665    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
666    /// [`crate::AdamAstParser`].
667    pub doc_comment: Option<String>,
668    /// A `//`/`/* */` comment on the same source line as this declaration's own closing `}`, if
669    /// recovered. Distinct from `trailing_comment`, which precedes this declaration's own
670    /// closing `}` on an earlier line. See <https://github.com/stlab/cel-rs/issues/59>.
671    pub trailing_line_comment: Option<Comment>,
672    /// Whether a blank line preceded this declaration, if recovered.
673    pub blank_line_before: bool,
674    /// A trailing comment immediately preceding this declaration's own closing `}`, if
675    /// recovered. See <https://github.com/stlab/cel-rs/issues/52>.
676    pub trailing_comment: Option<Comment>,
677    /// Whether a blank line preceded this declaration's own closing `}`, if recovered. See
678    /// <https://github.com/stlab/cel-rs/issues/52>.
679    pub blank_line_before_close: bool,
680    /// The span of this declaration's own opening `{`, used to recover trailing trivia when
681    /// there are no `branches`/`default`. See <https://github.com/stlab/cel-rs/issues/52>.
682    pub open_brace_span: ExprSpan,
683    /// The span of the whole `conditional ... { ... }` declaration.
684    pub span: ExprSpan,
685}
686
687/// The `_ => { ... }` default branch of a `conditional_decl`, mirroring `ConditionalBranch`'s
688/// shape (it has no match literal of its own).
689///
690/// # Examples
691///
692/// See [`Module`] for a conditional with a terminal default branch.
693#[derive(Debug, Clone)]
694pub struct DefaultBranch {
695    /// Comments before the default pattern, in source order.
696    pub leading_comment: Option<Comment>,
697    /// Whether a blank line precedes the default pattern.
698    pub blank_line_before: bool,
699    /// The default branch's relationships, in declaration order.
700    pub relationships: Vec<BranchItem>,
701    /// A trailing comment immediately preceding this branch's own closing `}`, if recovered.
702    /// See <https://github.com/stlab/cel-rs/issues/52>.
703    pub trailing_comment: Option<Comment>,
704    /// A `//`/`/* */` comment on the same source line as this branch's own closing `}` (between
705    /// it and the enclosing conditional's own closing `}`), if recovered. See
706    /// <https://github.com/stlab/cel-rs/issues/59>.
707    pub trailing_line_comment: Option<Comment>,
708    /// Whether a blank line preceded this branch's own closing `}`, if recovered.
709    pub blank_line_before_close: bool,
710    /// The span of this branch's own opening `{`, used to recover trailing trivia when
711    /// `relationships` is empty.
712    pub open_brace_span: ExprSpan,
713    /// The span from the `_` token through this branch's own closing `}`.
714    pub span: ExprSpan,
715}
716
717/// `conditional_branch = literal_pattern "=>" "{" { relationship_item } "}".`
718/// `literal_pattern = ["-"] literal.`
719///
720/// # Examples
721///
722/// See [`Module`] for a Boolean literal branch containing a destructuring binding.
723#[derive(Debug, Clone)]
724pub struct ConditionalBranch {
725    /// The branch's unresolved match literal, always stored unsigned; see [`Self::negated`]
726    /// for whether a leading `-` applies to it.
727    pub literal: Literal,
728    /// Whether the branch key is negated by a leading `-` (Rust's own `LiteralPattern` rule —
729    /// see <https://doc.rust-lang.org/reference/patterns.html#literal-patterns>). A `-`, if
730    /// present, precedes [`Self::literal_span`] and is covered by [`Self::span`]'s start, but
731    /// not by `literal_span` itself.
732    pub negated: bool,
733    /// The literal token's own span (never includes a leading `-`; see [`Self::negated`]).
734    pub literal_span: ExprSpan,
735    /// The branch's relationships, in declaration order.
736    pub relationships: Vec<BranchItem>,
737    /// A leading comment immediately preceding this branch, if recovered by
738    /// [`crate::trivia::attach_trivia`].
739    pub leading_comment: Option<Comment>,
740    /// A `//`/`/* */` comment on the same source line as this branch's own closing `}`, if
741    /// recovered. Distinct from `trailing_comment`, which precedes this branch's own closing
742    /// `}` on an earlier line. See <https://github.com/stlab/cel-rs/issues/59>.
743    pub trailing_line_comment: Option<Comment>,
744    /// Whether a blank line preceded this branch, if recovered by
745    /// [`crate::trivia::attach_trivia`].
746    pub blank_line_before: bool,
747    /// A trailing comment immediately preceding this branch's own closing `}`, if recovered.
748    /// See <https://github.com/stlab/cel-rs/issues/52>.
749    pub trailing_comment: Option<Comment>,
750    /// Whether a blank line preceded this branch's own closing `}`, if recovered. See
751    /// <https://github.com/stlab/cel-rs/issues/52>.
752    pub blank_line_before_close: bool,
753    /// The span of this branch's own opening `{`, used to recover trailing trivia when
754    /// `relationships` is empty. See <https://github.com/stlab/cel-rs/issues/52>.
755    pub open_brace_span: ExprSpan,
756    /// The span from the branch's literal through its closing `}`.
757    pub span: ExprSpan,
758}
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763    use proc_macro2::Span;
764
765    /// Checks nondefault source coordinates and independent trivia replacement for every variant.
766    #[test]
767    fn sheet_item_operations_preserve_unrelated_fields_for_every_variant() {
768        let source = "sheet s {\ncell a: i32;\nsource b: i32;\nout c := 1;\nrelationship {}\nrelationship component();\nconditional true {}\nbogus;\n}";
769        let mut sheet = crate::AdamAstParser::new().parse_str(source).unwrap();
770        assert_eq!(sheet.errors.len(), 1);
771        assert_eq!(sheet.items.len(), 7);
772        let replacement = "header"
773            .parse::<proc_macro2::TokenStream>()
774            .unwrap()
775            .into_iter()
776            .next()
777            .unwrap()
778            .span();
779        for (index, item) in sheet.items.iter_mut().enumerate() {
780            let original = item.span();
781            assert_eq!(original.start.start().line, index + 2);
782            assert_eq!(original.start.start().column, 0);
783            item.set_leading_comment(Comment::Line("old".into()));
784            item.set_leading_comment(Comment::Block("leading".into()));
785            item.set_trailing_line_comment(Comment::Line("trailing".into()));
786            item.set_blank_line_before(true);
787            item.set_blank_line_before(false);
788            item.set_doc_comment("documentation".into(), replacement);
789            assert_eq!(item.span().start.start(), replacement.start());
790            assert_eq!(item.span().end.end(), original.end.end());
791            let (leading, trailing, blank, doc) = item_trivia(item);
792            assert_eq!(leading, Some(&Comment::Block("leading".into())));
793            assert_eq!(trailing, Some(&Comment::Line("trailing".into())));
794            assert!(!blank);
795            assert_eq!(doc, Some("documentation"));
796        }
797    }
798
799    /// Borrows each variant's trivia without changing its span or contents.
800    fn item_trivia(item: &SheetItem) -> (Option<&Comment>, Option<&Comment>, bool, Option<&str>) {
801        match item {
802            SheetItem::Cell(c) => (
803                c.leading_comment.as_ref(),
804                c.trailing_line_comment.as_ref(),
805                c.blank_line_before,
806                c.doc_comment.as_deref(),
807            ),
808            SheetItem::Source(c) => (
809                c.leading_comment.as_ref(),
810                c.trailing_line_comment.as_ref(),
811                c.blank_line_before,
812                c.doc_comment.as_deref(),
813            ),
814            SheetItem::Out(c) => (
815                c.leading_comment.as_ref(),
816                c.trailing_line_comment.as_ref(),
817                c.blank_line_before,
818                c.doc_comment.as_deref(),
819            ),
820            SheetItem::Relationship(c) => (
821                c.leading_comment.as_ref(),
822                c.trailing_line_comment.as_ref(),
823                c.blank_line_before,
824                c.doc_comment.as_deref(),
825            ),
826            SheetItem::RelationshipUse(c) => (
827                c.leading_comment.as_ref(),
828                c.trailing_line_comment.as_ref(),
829                c.blank_line_before,
830                c.doc_comment.as_deref(),
831            ),
832            SheetItem::Conditional(c) => (
833                c.leading_comment.as_ref(),
834                c.trailing_line_comment.as_ref(),
835                c.blank_line_before,
836                c.doc_comment.as_deref(),
837            ),
838            SheetItem::Error {
839                leading_comment,
840                trailing_line_comment,
841                blank_line_before,
842                doc_comment,
843                ..
844            } => (
845                leading_comment.as_ref(),
846                trailing_line_comment.as_ref(),
847                *blank_line_before,
848                doc_comment.as_deref(),
849            ),
850        }
851    }
852
853    /// Wraps a token span as a point expression span.
854    fn point(span: Span) -> ExprSpan {
855        ExprSpan {
856            start: span,
857            end: span,
858        }
859    }
860
861    /// Returns the stored cell declaration span.
862    #[test]
863    fn sheet_item_span_reads_the_cell_variant() {
864        let span = point(Span::call_site());
865        let item = SheetItem::Cell(CellDecl {
866            name: "x".to_string(),
867            name_span: span,
868            type_name: None,
869            initializer: None,
870            filter: None,
871            require: None,
872            leading_comment: None,
873            doc_comment: None,
874            trailing_line_comment: None,
875            blank_line_before: false,
876            span,
877        });
878        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
879    }
880
881    /// Returns the stored inline relationship span.
882    #[test]
883    fn sheet_item_span_reads_the_relationship_variant() {
884        let span = point(Span::call_site());
885        let item = SheetItem::Relationship(RelationshipDecl {
886            bindings: Vec::new(),
887            leading_comment: None,
888            doc_comment: None,
889            trailing_line_comment: None,
890            blank_line_before: false,
891            trailing_comment: None,
892            blank_line_before_close: false,
893            open_brace_span: span,
894            span,
895        });
896        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
897    }
898
899    /// Returns the stored conditional declaration span.
900    #[test]
901    fn sheet_item_span_reads_the_conditional_variant() {
902        let span = point(Span::call_site());
903        let item = SheetItem::Conditional(ConditionalDecl {
904            match_expr: cel_parser::Expr::Ident {
905                name: "m".to_string(),
906                span,
907            },
908            branches: Vec::new(),
909            default: None,
910            leading_comment: None,
911            doc_comment: None,
912            trailing_line_comment: None,
913            blank_line_before: false,
914            trailing_comment: None,
915            blank_line_before_close: false,
916            open_brace_span: span,
917            span,
918        });
919        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
920    }
921
922    /// Returns the stored recovery range.
923    #[test]
924    fn sheet_item_span_reads_the_error_variant() {
925        let span = point(Span::call_site());
926        let item = SheetItem::Error {
927            span,
928            leading_comment: None,
929            doc_comment: None,
930            trailing_line_comment: None,
931            blank_line_before: false,
932        };
933        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
934    }
935
936    /// Attaches a leading comment to a cell.
937    #[test]
938    fn set_leading_comment_sets_the_cell_variant() {
939        let span = point(Span::call_site());
940        let mut item = SheetItem::Cell(CellDecl {
941            name: "x".to_string(),
942            name_span: span,
943            type_name: None,
944            initializer: None,
945            filter: None,
946            require: None,
947            leading_comment: None,
948            doc_comment: None,
949            trailing_line_comment: None,
950            blank_line_before: false,
951            span,
952        });
953        item.set_leading_comment(Comment::Line("hi".to_string()));
954        match item {
955            SheetItem::Cell(c) => {
956                assert_eq!(c.leading_comment, Some(Comment::Line("hi".to_string())))
957            }
958            other => panic!("expected Cell, got {other:?}"),
959        }
960    }
961
962    /// Attaches a leading comment to a recovery placeholder.
963    #[test]
964    fn set_leading_comment_sets_the_error_variant() {
965        let span = point(Span::call_site());
966        let mut item = SheetItem::Error {
967            span,
968            leading_comment: None,
969            doc_comment: None,
970            trailing_line_comment: None,
971            blank_line_before: false,
972        };
973        item.set_leading_comment(Comment::Line("hi".to_string()));
974        match item {
975            SheetItem::Error {
976                leading_comment, ..
977            } => {
978                assert_eq!(leading_comment, Some(Comment::Line("hi".to_string())))
979            }
980            other => panic!("expected Error, got {other:?}"),
981        }
982    }
983
984    /// Marks a cell as preceded by a blank line.
985    #[test]
986    fn set_blank_line_before_sets_the_cell_variant() {
987        let span = point(Span::call_site());
988        let mut item = SheetItem::Cell(CellDecl {
989            name: "x".to_string(),
990            name_span: span,
991            type_name: None,
992            initializer: None,
993            filter: None,
994            require: None,
995            leading_comment: None,
996            doc_comment: None,
997            trailing_line_comment: None,
998            blank_line_before: false,
999            span,
1000        });
1001        item.set_blank_line_before(true);
1002        match item {
1003            SheetItem::Cell(c) => assert!(c.blank_line_before),
1004            other => panic!("expected Cell, got {other:?}"),
1005        }
1006    }
1007
1008    /// Returns the stored output declaration span.
1009    #[test]
1010    fn sheet_item_span_reads_the_out_variant() {
1011        let span = point(Span::call_site());
1012        let item = SheetItem::Out(OutDecl {
1013            name: "o".to_string(),
1014            name_span: span,
1015            type_name: None,
1016            initializer: cel_parser::Expr::Ident {
1017                name: "x".to_string(),
1018                span,
1019            },
1020            filter: None,
1021            require: None,
1022            leading_comment: None,
1023            doc_comment: None,
1024            trailing_line_comment: None,
1025            blank_line_before: false,
1026            span,
1027        });
1028        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
1029    }
1030
1031    /// Attaches a leading comment to an output.
1032    #[test]
1033    fn set_leading_comment_sets_the_out_variant() {
1034        let span = point(Span::call_site());
1035        let mut item = SheetItem::Out(OutDecl {
1036            name: "o".to_string(),
1037            name_span: span,
1038            type_name: None,
1039            initializer: cel_parser::Expr::Ident {
1040                name: "x".to_string(),
1041                span,
1042            },
1043            filter: None,
1044            require: None,
1045            leading_comment: None,
1046            doc_comment: None,
1047            trailing_line_comment: None,
1048            blank_line_before: false,
1049            span,
1050        });
1051        item.set_leading_comment(Comment::Line("hi".to_string()));
1052        match item {
1053            SheetItem::Out(o) => {
1054                assert_eq!(o.leading_comment, Some(Comment::Line("hi".to_string())))
1055            }
1056            other => panic!("expected Out, got {other:?}"),
1057        }
1058    }
1059
1060    /// Accepts the CEL parser type expression without conversion.
1061    #[test]
1062    fn ast_type_expr_is_the_shared_cel_parser_type_expr() {
1063        /// Requires the expression's static type to match the shared parser type.
1064        fn assert_same_type<T>(_: &T) {}
1065        let span = cel_parser::ExprSpan {
1066            start: Span::call_site(),
1067            end: Span::call_site(),
1068        };
1069        let expr: TypeExpr = cel_parser::TypeExpr::Named {
1070            name: "i32".to_string(),
1071            args: Vec::new(),
1072            span,
1073        };
1074        assert_same_type::<cel_parser::TypeExpr>(&expr);
1075    }
1076
1077    /// Returns a named type expression's stored span.
1078    #[test]
1079    fn type_expr_named_span_is_its_own_span() {
1080        let span = point(Span::call_site());
1081        let expr = TypeExpr::Named {
1082            name: "i32".to_string(),
1083            args: Vec::new(),
1084            span,
1085        };
1086        assert_eq!(format!("{:?}", expr.span()), format!("{span:?}"));
1087    }
1088
1089    /// Returns a tuple type expression's stored span.
1090    #[test]
1091    fn type_expr_tuple_span_is_the_whole_parenthesized_span() {
1092        let span = point(Span::call_site());
1093        let expr = TypeExpr::Tuple {
1094            elements: Vec::new(),
1095            span,
1096        };
1097        assert_eq!(format!("{:?}", expr.span()), format!("{span:?}"));
1098    }
1099}