Skip to main content

caixa_ast/
node.rs

1use crate::span::Span;
2use crate::trivia::Trivia;
3
4/// A parsed Lisp node with span + attached trivia.
5#[derive(Debug, Clone, PartialEq)]
6pub struct Node {
7    pub kind: NodeKind,
8    pub span: Span,
9    /// Comments / blank lines immediately before this node.
10    pub leading: Vec<Trivia>,
11    /// For a compound node: trivia sitting between its last child and its
12    /// closing delimiter, with no child to attach to. Emitted INSIDE the
13    /// form, before the `)`.
14    ///
15    /// Note this slot is overloaded relative to its original meaning
16    /// ("trailing on the same line"); `sequence()` claimed it for the
17    /// dangling case. That is why [`Self::after`] exists rather than this
18    /// being reused again.
19    pub trailing: Vec<Trivia>,
20    /// Trivia that follows this node at its own level — OUTSIDE any
21    /// delimiter it owns.
22    ///
23    /// The distinction from [`Self::trailing`] is load-bearing, not
24    /// pedantry: `(define x 1) ; why` and `(define x 1 ; why\n)` are
25    /// different documents, and a single slot cannot represent both. With
26    /// only the two original slots the top-level case had nowhere to go
27    /// and was DISCARDED at EOF — measurably: one mass-format destroyed 44
28    /// trailing comments in `pleme-io/actions` alone.
29    pub after: Vec<Trivia>,
30}
31
32/// The typed variant discriminator on the caixa-ast surface — every
33/// [`Node`]'s carrying-shape (atom family, compound family, quote family)
34/// projects through this closed thirteen-arm partition.
35///
36/// The [`gen_platform::IsVariant`] derive emits per-arm arm-discriminator
37/// predicates — [`Self::is_nil`], [`Self::is_symbol`], [`Self::is_keyword`],
38/// [`Self::is_str`], [`Self::is_int`], [`Self::is_float`], [`Self::is_bool`],
39/// [`Self::is_list`], [`Self::is_map`], [`Self::is_vector`], [`Self::is_quote`],
40/// [`Self::is_quasiquote`], [`Self::is_unquote`], [`Self::is_unquote_splice`]
41/// — so every downstream consumer that only needs the arm-discriminator
42/// projection (not the borrowed field value) reaches for one typed dispatch
43/// on the substrate primitive rather than a hand-rolled
44/// `matches!(x.kind, NodeKind::X(_))` literal. Peer of the caixa-core
45/// [`caixa_core::CaixaKind`] / [`caixa_core::CaixaDialeto`] /
46/// [`caixa_core::DepList`] / [`caixa_core::UpgradeInstruction`] /
47/// caixa-lint / caixa-arch / caixa-provedor / caixa-theme sibling enums
48/// that already carry the `gen_platform::IsVariant` discipline — the first
49/// closed-set-typed-enum lift on the caixa-ast surface, extending the
50/// discipline onto the AST-node-family axis every downstream authoring
51/// consumer (`caixa-fmt`, `caixa-lint`, `caixa-lsp`) partitions on.
52#[derive(Debug, Clone, PartialEq, gen_platform::IsVariant)]
53pub enum NodeKind {
54    Nil,
55    Symbol(String),
56    Keyword(String),
57    Str(String),
58    Int(i64),
59    Float(f64),
60    Bool(bool),
61    List(Vec<Node>),
62    /// `{ :k v … }` — the brace dialect. REAL SYNTAX per
63    /// theory/TATARA-LISP-CONSOLIDATION.md D4; 62 live caixa.lisp
64    /// manifests author nested maps and are consumed today.
65    Map(Vec<Node>),
66    /// `[ a b … ]` — the vector dialect, D4's sibling.
67    Vector(Vec<Node>),
68    Quote(Box<Node>),
69    Quasiquote(Box<Node>),
70    Unquote(Box<Node>),
71    UnquoteSplice(Box<Node>),
72}
73
74impl NodeKind {
75    /// Substrate-canonical projection onto the [`Self::Keyword`] arm's
76    /// borrowed scalar payload — returns `Some(&str)` byte-borrowed from
77    /// the arm's own [`String`] storage, and [`None`] on every other arm
78    /// of the closed fourteen-arm [`NodeKind`] variant set.
79    ///
80    /// Six production consumers today across two caixa-monorepo crates
81    /// — the [`Node::kwarg`] pair-loop `:key value` alternator, and the
82    /// caixa-lint rule surface's [`caixa_lint::rules`]::`check_keyword_kebab`
83    /// walker, `check_enum_pascal` kwarg-loop filter, `keyword_present`
84    /// walker, `matches_kwarg` kwarg-loop filter, and `items_has_key`
85    /// kwarg-loop filter — which previously reached the underlying
86    /// keyword-name scalar through six raw `if let NodeKind::Keyword(k)
87    /// = &n.kind` (or `matches!(&n.kind, NodeKind::Keyword(k) if k ==
88    /// key)`) open-coded per-arm pattern-matches that expressed no
89    /// compile-time link back to the substrate primitive's typed
90    /// scalar-arm projection. A future `NodeKind` arm addition (a
91    /// `TaggedKeyword(String, KeywordTag)` shape once the tatara-lisp
92    /// reader grows a per-keyword scope tag, a `NamespacedKeyword(String,
93    /// String)` shape once the sexp→JSON bridge stabilizes the
94    /// `::ns/key` sugar theory/TATARA-LISP-CONSOLIDATION.md D6 sketches)
95    /// reaches every downstream per-`Keyword`-arm consumer through this
96    /// one dispatch by construction — no coordinated six-way rewrite
97    /// across every per-rule projection site.
98    ///
99    /// Zero-copy — the returned `&str` borrows from the arm's own
100    /// [`String`] storage (pinned by the
101    /// `as_keyword_is_by_borrow_pointer_identity` test), the same
102    /// discipline as the sibling [`caixa_teia::TeiaValue::as_str`]
103    /// (7304ffe) / [`caixa_teia::TeiaValue::as_object`] (7304ffe)
104    /// outer-`TeiaValue` sum-type per-arm projections and the sibling
105    /// [`caixa_teia::TeiaRefRepr::tipo`] (a856d67) /
106    /// [`caixa_teia::TeiaRefRepr::nome`] (15bcdef) /
107    /// [`caixa_teia::TeiaRefRepr::atributo`] outer-`TeiaRefRepr` scalar
108    /// accessors on the substrate's IaC-side per-`(ref …)` reference
109    /// carrier — one axis level up on the sibling AST-side per-`NodeKind`
110    /// sum-type projection surface.
111    ///
112    /// First `Option<&<payload>>` projection accessor on the outer
113    /// [`NodeKind`] sum-type — opens the `as_<variant>` typed projection
114    /// family the sibling per-arm [`Self::as_symbol`] (e96eea1) and
115    /// [`Self::as_str`] projections fold on the same shape at their
116    /// consumer surfaces, extending the discipline onto the caixa-ast
117    /// per-AST-node-family arm-set every downstream authoring consumer
118    /// (`caixa-fmt`, `caixa-lint`, `caixa-lsp`) partitions on.
119    ///
120    /// `pub const fn` — the body reads the arm discriminant through a
121    /// per-arm pattern-match on `&self` that binds `k: &String` on the
122    /// `Keyword` arm and projects onto its byte-borrowed `&str` view via
123    /// [`String::as_str`] (`pub const fn` since Rust 1.87, well before
124    /// this workspace's 1.89 MSRV floor); no arm-storage owning-borrow is
125    /// taken, no drop is invoked on any arm's `String` / `Vec<Node>` /
126    /// `Box<Node>` payload. Extends the const-eval discipline the sibling
127    /// caixa-ast source-position primitive family (`Span::new` /
128    /// `Span::point` / `Span::contains` / `Span::union` / `Span::len` /
129    /// `Span::is_empty`, `Position::new` / `Position::origin`,
130    /// `line_column`) and the paired [`Self::seq_delims`] /
131    /// [`Self::reader_macro_prefix`] writer-half siblings on the
132    /// compound-arm / reader-macro-arm sets already carry onto the
133    /// caixa-ast [`NodeKind`] outer-sum-type's per-`Keyword`-arm
134    /// borrowed-scalar-projection axis. Every downstream reader that
135    /// wants a compile-time keyword-name-arm identity fixture (a `const
136    /// LOOKUP: Option<&str> = NodeKind::Keyword(…).as_keyword();` future
137    /// caixa-lint kwarg-key const-lookup table, a per-arm identity oracle
138    /// a future caixa-lsp writer const-registry consults at compile time,
139    /// a compile-time keyword-arm-set partition truth table the caixa-fmt
140    /// writer keys off) now reads through one substrate-primitive const
141    /// dispatch rather than being forced onto the runtime code path.
142    #[must_use]
143    pub const fn as_keyword(&self) -> Option<&str> {
144        match self {
145            Self::Keyword(k) => Some(k.as_str()),
146            _ => None,
147        }
148    }
149
150    /// Substrate-canonical projection onto the [`Self::Symbol`] arm's
151    /// borrowed scalar payload — returns `Some(&str)` byte-borrowed from
152    /// the arm's own [`String`] storage, and [`None`] on every other arm
153    /// of the closed fourteen-arm [`NodeKind`] variant set.
154    ///
155    /// Eight production consumers today across four caixa-monorepo crates
156    /// — the caixa-ast [`Node::head_symbol`] list-head projection, the
157    /// caixa-fmt printer's `special_head_arity` head-lookup and
158    /// `head_symbol_is_command` command-head gate, the caixa-lint rule
159    /// surface's `check_paired_kwargs` positional-KW-head skip,
160    /// `check_aplicacao_timeout` `:kind Aplicacao` match,
161    /// `check_git_pin` `:tipo git` `matches_kwarg`-predicate closure,
162    /// and `check_consistent_quote` `(quote …)`-form detector, and the
163    /// caixa-teia `is_ref_form` `(ref …)`-form detector — which
164    /// previously reached the underlying symbol-name scalar through
165    /// eight raw `if let NodeKind::Symbol(s) = &n.kind` /
166    /// `matches!(&n.kind, NodeKind::Symbol(s) if s == "…")` /
167    /// `matches!(items.first().map(|n| &n.kind), Some(NodeKind::Symbol(s))
168    /// if s == "…")` open-coded per-arm pattern-matches that expressed
169    /// no compile-time link back to the substrate primitive's typed
170    /// scalar-arm projection.
171    ///
172    /// Zero-copy — the returned `&str` borrows from the arm's own
173    /// [`String`] storage (pinned by the
174    /// `as_symbol_is_by_borrow_pointer_identity` test), the same
175    /// discipline as the sibling [`Self::as_keyword`] (6804427) /
176    /// [`caixa_teia::TeiaValue::as_str`] (7304ffe) /
177    /// [`caixa_teia::TeiaValue::as_object`] (7304ffe) outer-sum-type
178    /// per-arm projections. Second `Option<&<payload>>` projection
179    /// accessor on the outer [`NodeKind`] sum-type — extends the
180    /// `as_<variant>` typed projection family the sibling
181    /// [`Self::as_keyword`] opened onto the second load-bearing scalar-
182    /// arm axis (symbol names — every head symbol lookup, every enum
183    /// variant match, every form-head-tag detector) across the
184    /// caixa-ast/caixa-fmt/caixa-lint/caixa-teia consumer surface.
185    ///
186    /// `pub const fn` — sibling in `const`-eval posture to the peer
187    /// [`Self::as_keyword`] promotion on the same substrate-primitive
188    /// per-arm scalar-projection family; the body is body-preserving
189    /// verbatim (same `match &self { Self::Symbol(s) => Some(s.as_str()),
190    /// _ => None }` shape, same [`String::as_str`] `pub const fn` const-
191    /// stable since Rust 1.87 the [`Self::as_keyword`] promotion routes
192    /// through), so the promotion extends the const-eval discipline onto
193    /// the second per-arm borrowed-scalar-projection axis of the closed
194    /// three-arm `Keyword` / `Symbol` / `Str` per-`String`-arm family the
195    /// sibling [`Self::as_str`] promotion closes onto the third arm.
196    #[must_use]
197    pub const fn as_symbol(&self) -> Option<&str> {
198        match self {
199            Self::Symbol(s) => Some(s.as_str()),
200            _ => None,
201        }
202    }
203
204    /// Substrate-canonical projection onto the [`Self::Str`] arm's
205    /// borrowed scalar payload — returns `Some(&str)` byte-borrowed from
206    /// the arm's own [`String`] storage, and [`None`] on every other arm
207    /// of the closed fourteen-arm [`NodeKind`] variant set.
208    ///
209    /// Three production consumers today across two caixa-monorepo crates
210    /// — the caixa-lint rule surface's [`caixa_lint::rules`]::
211    /// `check_nome_kebab` `:nome`-value kebab-case gate,
212    /// `check_no_fixme` `:descricao`-value FIXME-placeholder gate, and
213    /// the caixa-fmt printer's `is_flag_token` `-flag`/`--flag`
214    /// string-literal command-argument-group detector — which previously
215    /// reached the underlying string-literal scalar through three raw
216    /// `if let NodeKind::Str(s) = &n.kind` / `matches!(&n.kind,
217    /// NodeKind::Str(s) if …)` open-coded per-arm pattern-matches that
218    /// expressed no compile-time link back to the substrate primitive's
219    /// typed scalar-arm projection.
220    ///
221    /// Zero-copy — the returned `&str` borrows from the arm's own
222    /// [`String`] storage (pinned by the
223    /// `as_str_is_by_borrow_pointer_identity` test), the same discipline
224    /// as the sibling [`Self::as_keyword`] (6804427) / [`Self::as_symbol`]
225    /// (e96eea1) outer-`NodeKind` sum-type per-arm projections and the
226    /// peer [`caixa_teia::TeiaValue::as_str`] (7304ffe) outer-`TeiaValue`
227    /// sum-type per-arm projection. Third `Option<&<payload>>` projection
228    /// accessor on the outer [`NodeKind`] sum-type — closes the
229    /// `as_<variant>` typed projection family the sibling [`Self::as_keyword`]
230    /// and [`Self::as_symbol`] opened onto the third and final load-
231    /// bearing scalar-arm axis (string-literal payloads — every
232    /// `:nome` / `:descricao` value gate, every `-flag` token detector,
233    /// every quoted-string `:kind` mis-authoring diagnostic) across the
234    /// caixa-lint/caixa-fmt consumer surface.
235    ///
236    /// `pub const fn` — closes the const-eval-surface promotion the
237    /// paired [`Self::as_keyword`] / [`Self::as_symbol`] siblings opened
238    /// onto the third and last per-`String`-arm axis of the closed
239    /// three-arm `Keyword` / `Symbol` / `Str` per-arm scalar-projection
240    /// family. Body-preserving verbatim (same `match &self { Self::Str(s)
241    /// => Some(s.as_str()), _ => None }` shape, same [`String::as_str`]
242    /// `pub const fn` const-stable since Rust 1.87 the two sibling
243    /// promotions route through), so every downstream consumer that
244    /// wants a compile-time string-literal-arm identity fixture (a
245    /// `const IS_FLAG: Option<&str> = NodeKind::Str("--flag".into())
246    /// .as_str();`-shaped future caixa-fmt writer const-lookup table over
247    /// a `const` fixture that widens `String::from` to `const` once that
248    /// lands upstream, a per-arm identity oracle a future caixa-lint
249    /// no-string-literal-in-numeric-position rule consults at compile
250    /// time, a compile-time Str-arm-set partition truth table the
251    /// caixa-lsp writer keys off) now reads through one substrate-
252    /// primitive const dispatch rather than being forced onto the runtime
253    /// code path.
254    #[must_use]
255    pub const fn as_str(&self) -> Option<&str> {
256        match self {
257            Self::Str(s) => Some(s.as_str()),
258            _ => None,
259        }
260    }
261
262    /// Substrate-canonical projection onto the disjunctive
263    /// [`Self::Symbol`] | [`Self::Str`] | [`Self::Keyword`] atom-string-
264    /// carrying arm-set — returns `Some(&str)` byte-borrowed from the
265    /// matched arm's own [`String`] storage, and [`None`] on every other
266    /// arm of the closed fourteen-arm [`NodeKind`] variant set.
267    ///
268    /// Two production consumers today across the caixa-teia manifest
269    /// parser — the `kwarg_symbol` `:tipo` / `:nome` value-shape gate
270    /// (`(defteia :tipo aws/vpc :nome main …)` — accepts a bare symbol,
271    /// a quoted `"aws/vpc"` string, or a `:aws/vpc` keyword form
272    /// depending on author preference) and the `build_ref` `:atributo`
273    /// slot (`(ref aws/vpc main id)` / `(ref aws/vpc main :id)` — the
274    /// third position accepts any of the three atom-string-carrying arm
275    /// shapes, error-messaged as "must be a symbol/keyword/string").
276    /// Both previously reached the underlying scalar through a raw
277    /// three-arm `NodeKind::Symbol(s) | NodeKind::Str(s) |
278    /// NodeKind::Keyword(s) => s.clone()` open-coded per-arm disjunctive
279    /// pattern-match that expressed no compile-time link back to the
280    /// substrate primitive's typed atom-string-carrying arm-set.
281    ///
282    /// Semantically distinct from the sibling per-arm [`Self::as_symbol`]
283    /// / [`Self::as_str`] / [`Self::as_keyword`] projections — each of
284    /// those returns `Some(&str)` on exactly one arm; this one returns
285    /// `Some(&str)` on the three-arm disjunction of atom-string-carrying
286    /// arms. A future `NodeKind` arm addition that carries a `String`
287    /// payload usable as a name-slot value (a hypothetical
288    /// `NodeKind::TaggedSymbol(String, SymbolTag)` once the tatara-lisp
289    /// reader grows a per-symbol scope tag, a `NodeKind::NamespacedSymbol
290    /// (String, String)` shape once the sexp→JSON bridge stabilizes the
291    /// `ns/sym` sugar) folds into this projection by extending the
292    /// accessor's arm-set once at the substrate primitive, rather than a
293    /// two-way rewrite across every caixa-teia manifest-parser call site.
294    ///
295    /// Zero-copy — the returned `&str` borrows from the matched arm's
296    /// own [`String`] storage (pinned by the
297    /// `as_atom_string_is_by_borrow_pointer_identity` test), the same
298    /// discipline as the sibling per-arm [`Self::as_keyword`] (6804427)
299    /// / [`Self::as_symbol`] (e96eea1) / [`Self::as_str`] (55b2909)
300    /// scalar-arm projections and the peer [`caixa_teia::TeiaValue::as_str`]
301    /// (7304ffe) outer-sum-type projection. Fourth `Option<&<payload>>`
302    /// projection accessor on the outer [`NodeKind`] sum-type — the
303    /// first *disjunctive* accessor on the projection family the three
304    /// sibling per-arm accessors already opened, extending the discipline
305    /// onto the atom-string-carrying arm-set every caixa-teia name-slot
306    /// gate partitions on.
307    ///
308    /// `pub const fn` — extends the caixa-ast const-eval-surface family
309    /// (`Span::new` / `Span::point` / `Span::contains` / `Span::union` /
310    /// `Span::len` / `Span::is_empty` / `Position::new` / `Position::origin`
311    /// / `line_column`; the writer-half `NodeKind::seq_delims` /
312    /// `NodeKind::reader_macro_prefix`; the sibling per-arm
313    /// [`Self::as_keyword`] / [`Self::as_symbol`] / [`Self::as_str`]
314    /// scalar-projection triple; the compound-arm [`Self::as_list`]) onto
315    /// the outer-`NodeKind` sum-type's *disjunctive* atom-string-carrying
316    /// three-arm projection axis. Body reads the arm discriminant through a
317    /// `const`-friendly `match` and returns `Some(&str)` through
318    /// [`String::as_str`] (`pub const fn` since Rust 1.87, well before this
319    /// promotion) — no interior heap traffic, no trait dispatch, no runtime
320    /// operation on the accessor path. A future compile-time caixa-fmt
321    /// writer-side per-arm-identity truth-table / caixa-lint keyword-key
322    /// const-lookup / caixa-lsp writer const-registry that keys off the
323    /// atom-string-carrying three-arm disjunction lands directly on this
324    /// accessor without a runtime-context escape hatch.
325    #[must_use]
326    pub const fn as_atom_string(&self) -> Option<&str> {
327        match self {
328            Self::Symbol(s) | Self::Str(s) | Self::Keyword(s) => Some(s.as_str()),
329            _ => None,
330        }
331    }
332
333    /// Substrate-canonical projection onto the disjunctive
334    /// [`Self::Symbol`] | [`Self::Str`] atom-name-carrying arm-set —
335    /// returns `Some(&str)` byte-borrowed from the matched arm's own
336    /// [`String`] storage, and [`None`] on every other arm of the closed
337    /// fourteen-arm [`NodeKind`] variant set.
338    ///
339    /// Two production consumers today across two caixa-monorepo crates
340    /// — the caixa-teia `build_ref` `:nome` slot (`(ref aws/vpc main id)`
341    /// — the second position accepts either a bare symbol `main` or a
342    /// quoted `"main"` string, error-messaged as "must be a symbol or
343    /// string") and the caixa-lsp `document_symbol` `:nome`-detail
344    /// projection (the `DocumentSymbol.detail` field takes the
345    /// `:nome value` from every top-level `(defX …)` form, where authors
346    /// spell the name as either a bare `nome-slug` symbol or a quoted
347    /// `"nome-slug"` string). Both previously reached the underlying
348    /// scalar through a raw two-arm `NodeKind::Symbol(s) |
349    /// NodeKind::Str(s) => s.clone()` open-coded per-arm disjunctive
350    /// pattern-match that expressed no compile-time link back to the
351    /// substrate primitive's typed atom-name-carrying arm-set.
352    ///
353    /// Semantically distinct from the sibling three-arm
354    /// [`Self::as_atom_string`] (3c3ca48) projection — that one accepts
355    /// the full atom-string-carrying arm-set including `Keyword` (a `:foo`
356    /// keyword literal counts as an atom-string in caixa-teia's
357    /// `:tipo`/`:nome` / `:atributo` slot); this one accepts only the
358    /// two-arm subset that excludes `Keyword`, matching the caixa-teia
359    /// `build_ref` `:nome` gate's "must be a symbol or string" contract
360    /// and the caixa-lsp `document_symbol` detail's "bare-symbol or
361    /// quoted-string name" author-facing shape.
362    ///
363    /// Zero-copy — the returned `&str` borrows from the matched arm's
364    /// own [`String`] storage (pinned by the
365    /// `as_symbol_or_str_is_by_borrow_pointer_identity` test), the same
366    /// discipline as the sibling per-arm [`Self::as_keyword`] (6804427)
367    /// / [`Self::as_symbol`] (e96eea1) / [`Self::as_str`] (55b2909) /
368    /// three-arm [`Self::as_atom_string`] (3c3ca48) projections and the
369    /// peer [`caixa_teia::TeiaValue::as_str`] (7304ffe) outer-sum-type
370    /// projection. Fifth `Option<&<payload>>` projection accessor on the
371    /// outer [`NodeKind`] sum-type — the second *disjunctive* accessor
372    /// on the projection family the three sibling per-arm accessors
373    /// opened, extending the discipline onto the two-arm atom-name-
374    /// carrying subset every caixa-teia `build_ref` `:nome` / caixa-lsp
375    /// `document_symbol` `:nome`-detail site partitions on.
376    ///
377    /// `pub const fn` — sibling in `const`-eval posture to the paired
378    /// three-arm [`Self::as_atom_string`] disjunctive projection, extending
379    /// the caixa-ast const-eval-surface family onto the strict-subset
380    /// two-arm atom-name-carrying axis. Same `match` + [`String::as_str`]
381    /// (`pub const fn` since Rust 1.87) shape as every other borrowed-`&str`
382    /// projection on the [`NodeKind`] outer sum-type — no interior heap
383    /// traffic, no trait dispatch, no runtime operation on the accessor
384    /// path. Pairs with [`Self::as_atom_string`]'s three-arm promotion so
385    /// the two disjunctive-arm-set consumer axes on the substrate primitive
386    /// (`kwarg_symbol` `:tipo`/`:nome`; `build_ref` `:nome`; `document_symbol`
387    /// `:nome`-detail) route through one const-dispatch pair.
388    #[must_use]
389    pub const fn as_symbol_or_str(&self) -> Option<&str> {
390        match self {
391            Self::Symbol(s) | Self::Str(s) => Some(s.as_str()),
392            _ => None,
393        }
394    }
395
396    /// Substrate-canonical projection onto the [`Self::List`] arm's
397    /// borrowed compound payload — returns `Some(&[Node])` byte-borrowed
398    /// from the arm's own [`Vec<Node>`] storage, and [`None`] on every
399    /// other arm of the closed fourteen-arm [`NodeKind`] variant set.
400    ///
401    /// Eleven production consumers today across four caixa-monorepo crates
402    /// — the caixa-ast [`Node::head_symbol`] list-head projection and
403    /// [`Node::kwarg`] `:key value` pair-loop; the caixa-lint rule
404    /// surface's `check_enum_pascal`, `check_paired_kwargs`, and
405    /// `check_git_pin` per-list walkers; the caixa-teia manifest parser's
406    /// `instance_from_node` `:atributos` kwargs-list gate; the caixa-fmt
407    /// `commented_head_degrades_out_of_the_plist_shape` printer test's
408    /// list-shape unwrap; the caixa-fmt `float_roundtrip` reparse test's
409    /// list-shape unwrap; and the caixa-ast `parse_list`,
410    /// `parse_map_and_vector`, and `parse_reader_macros` parser tests'
411    /// list-shape unwraps — which previously reached the underlying
412    /// [`Vec<Node>`] child sequence through eleven raw `let NodeKind::
413    /// List(items) = &X.kind else { … }` open-coded per-arm let-else
414    /// pattern-matches that expressed no compile-time link back to the
415    /// substrate primitive's typed compound-arm projection.
416    ///
417    /// Semantically distinct from the sibling `Map` and `Vector`
418    /// compound-arm carrying shapes — every one of those arms also
419    /// carries a `Vec<Node>` payload (`Map(Vec<Node>)` is the brace
420    /// dialect's `{ :k v … }`; `Vector(Vec<Node>)` is the bracket
421    /// dialect's `[ a b … ]`), so a hand-rolled `matches!(&_.kind,
422    /// NodeKind::List(_))` gate has to keep the strict-`List`-only
423    /// boundary in view at every site. This projection pins the boundary
424    /// on the substrate primitive — the sibling `Map` / `Vector` arms
425    /// return `None` even though they carry the same shape payload —
426    /// which is why the seven caixa-lint / caixa-teia / caixa-fmt /
427    /// caixa-ast production consumers all reach for the strict `List`-arm
428    /// gate rather than any compound-arm disjunction (a `(defcaixa …)`
429    /// form is a `List`, not a `Map` or `Vector`; a `:atributos` kwargs
430    /// slot is a `List`, not a `Map`; the parser's positional-run detection
431    /// runs on `List`, not the D4 brace/bracket dialect).
432    ///
433    /// Zero-copy — the returned `&[Node]` borrows from the arm's own
434    /// [`Vec<Node>`] storage (pinned by the
435    /// `as_list_is_by_borrow_pointer_identity` test), the same discipline
436    /// as the sibling per-arm [`Self::as_keyword`] (6804427) /
437    /// [`Self::as_symbol`] (e96eea1) / [`Self::as_str`] (55b2909) scalar
438    /// projections and the sibling disjunctive [`Self::as_atom_string`]
439    /// (3c3ca48) / [`Self::as_symbol_or_str`] (fd39cea) two-/three-arm
440    /// atom-name projections. Sixth `Option<&<payload>>` projection
441    /// accessor on the outer [`NodeKind`] sum-type — the first accessor
442    /// on the *compound*-arm axis (Map/List/Vector all carry
443    /// `Vec<Node>`), extending the projection family from the three
444    /// scalar-arm accessors and two disjunctive-scalar-arm accessors onto
445    /// the compound-arm axis every downstream authoring-tool and
446    /// manifest-parser walker partitions on.
447    ///
448    /// `pub const fn` — extends the caixa-ast const-eval-surface family
449    /// ([`Self::as_keyword`] / [`Self::as_symbol`] / [`Self::as_str`] on
450    /// the per-arm scalar axis, [`Self::seq_delims`] /
451    /// [`Self::reader_macro_prefix`] on the outer-`NodeKind` writer-half
452    /// projection axis, [`crate::Span::new`] / [`crate::Span::point`] /
453    /// [`crate::Span::len`] / [`crate::Span::is_empty`] /
454    /// [`crate::Span::contains`] / [`crate::Span::union`] on the
455    /// byte-offset axis, [`crate::Position::new`] /
456    /// [`crate::Position::origin`] / [`crate::Position::line_column`] on
457    /// the 1-indexed line/column axis, [`crate::Trivia::comment_text`]
458    /// on the trivia-envelope-scoped projection axis) onto the
459    /// compound-arm projection axis. The body reaches for
460    /// [`Vec::as_slice`] on the [`Self::List`] borrowed-`Vec<Node>` slot
461    /// — const-stable since Rust 1.7, well before this workspace's 1.89
462    /// MSRV floor — so the promotion is a body-preserving type-signature
463    /// widening. Every downstream authoring consumer that wants a
464    /// compile-time list-body fixture (a `const HEAD: Option<&[Node]> =
465    /// KIND.as_list();` compile-time oracle a caixa-lint arity gate keys
466    /// off, a per-lint const-context list-shape probe an admission
467    /// webhook consults) now reads through one substrate-primitive const
468    /// dispatch rather than being forced onto the runtime code path.
469    #[must_use]
470    pub const fn as_list(&self) -> Option<&[Node]> {
471        match self {
472            Self::List(items) => Some(items.as_slice()),
473            _ => None,
474        }
475    }
476
477    /// Substrate-canonical projection onto the disjunctive
478    /// [`Self::Quote`] | [`Self::Quasiquote`] | [`Self::Unquote`] |
479    /// [`Self::UnquoteSplice`] reader-macro arm-set — returns
480    /// `Some(&Node)` byte-borrowed from the matched arm's own
481    /// [`Box<Node>`] storage, and [`None`] on every other arm of the
482    /// closed fourteen-arm [`NodeKind`] variant set.
483    ///
484    /// Four production consumers today across four caixa-monorepo crates
485    /// — the caixa-ast [`crate::visit::walk`] visitor recursion, the
486    /// caixa-fmt printer's `contains_comment` sub-tree comment probe,
487    /// the caixa-teia `node_to_value` manifest lowerer, and the caixa-
488    /// lint per-file `walk` traversal — which previously reached the
489    /// underlying inner-node payload through four raw `NodeKind::Quote
490    /// (inner) | NodeKind::Quasiquote(inner) | NodeKind::Unquote(inner)
491    /// | NodeKind::UnquoteSplice(inner) => recurse(inner)` open-coded
492    /// four-arm disjunctive pattern-matches that expressed no compile-
493    /// time link back to the substrate primitive's typed reader-macro-
494    /// carrying arm-set. Each is a transparent-wrapper unwrap: the
495    /// consumer does not care WHICH reader macro variant it is, only
496    /// that the wrapper hides an inner [`Node`] whose recursion is the
497    /// whole traversal.
498    ///
499    /// Semantically distinct from the peer per-arm compound projections
500    /// [`Self::as_list`] (48efa3b) and from the sibling per-arm scalar
501    /// projections [`Self::as_keyword`] (6804427) / [`Self::as_symbol`]
502    /// (e96eea1) / [`Self::as_str`] (55b2909) / disjunctive scalar
503    /// [`Self::as_atom_string`] (3c3ca48) / [`Self::as_symbol_or_str`]
504    /// (fd39cea) — those project onto borrowed scalars or borrowed
505    /// [`Vec<Node>`] slices; this one projects onto a borrowed inner
506    /// [`Node`], the first projection accessor on the outer-`NodeKind`
507    /// sum-type to reach across the [`Box<Node>`] indirection every
508    /// reader-macro arm carries. A future reader-macro arm addition (a
509    /// hypothetical `NodeKind::Splice(Box<Node>)` shape once the
510    /// tatara-lisp reader grows a splicing-quote variant, a
511    /// `NodeKind::TaggedQuote(Box<Node>, QuoteTag)` shape once the
512    /// sexp→JSON bridge stabilizes tagged-quote sugar) folds into this
513    /// projection by extending the accessor's arm-set once at the
514    /// substrate primitive, rather than a four-way coordinated rewrite
515    /// across every downstream walker.
516    ///
517    /// Zero-copy — the returned `&Node` borrows from the matched arm's
518    /// own [`Box<Node>`] storage (pinned by the
519    /// `as_reader_macro_inner_is_by_borrow_pointer_identity` test), the
520    /// same discipline as the sibling per-arm scalar and compound
521    /// projections. Seventh `Option<&<payload>>` projection accessor on
522    /// the outer [`NodeKind`] sum-type — the first accessor on the
523    /// *reader-macro* arm-family (Quote/Quasiquote/Unquote/UnquoteSplice
524    /// all carry `Box<Node>`), extending the projection family from the
525    /// three scalar-arm, two disjunctive-scalar-arm, and one compound-
526    /// arm accessors onto the fourth arm-family every downstream
527    /// authoring-tool walker and manifest-lowerer partitions on.
528    ///
529    /// `pub const fn` — closes the const-eval discipline the sibling
530    /// caixa-ast source-position primitive family (`Span::new` /
531    /// `Span::point` / `Span::contains` / `Span::union` / `Span::len` /
532    /// `Span::is_empty`, `Position::new` / `Position::origin`,
533    /// `line_column`), the outer-`NodeKind` per-arm scalar-projection
534    /// family (`as_keyword` / `as_symbol` / `as_str`), the two
535    /// disjunctive-scalar accessors (`as_atom_string` /
536    /// `as_symbol_or_str`), the single-arm compound projection
537    /// (`as_list`), and the paired writer-half projections (`seq_delims`
538    /// / `reader_macro_prefix`) already carry onto the caixa-ast
539    /// [`NodeKind`] outer-sum-type's per-`Box<Node>`-arm reader-half
540    /// projection axis. Body is preserved verbatim: the four-arm
541    /// disjunctive `match &self` binds `inner: &Box<Node>` and returns
542    /// `Some(inner)`, where the compiler's built-in `&Box<T>` → `&T`
543    /// return-position coercion sidesteps the `Deref` trait-dispatch
544    /// path (const-stable in Rust since long before this workspace's 1.89
545    /// MSRV floor), so no arm-storage owning-borrow is taken and no drop
546    /// is invoked on any arm's `Box<Node>` payload. Every downstream
547    /// walker that wants a compile-time reader-macro-inner fixture (a
548    /// `const INNER: Option<&Node> = KIND.as_reader_macro_inner();`
549    /// compile-time oracle a caixa-lint reader-macro-arity gate keys off,
550    /// a per-arm identity oracle a future caixa-lsp writer const-
551    /// registry consults at compile time) now reads through one substrate-
552    /// primitive const dispatch rather than being forced onto the runtime
553    /// code path.
554    #[must_use]
555    pub const fn as_reader_macro_inner(&self) -> Option<&Node> {
556        match self {
557            Self::Quote(inner)
558            | Self::Quasiquote(inner)
559            | Self::Unquote(inner)
560            | Self::UnquoteSplice(inner) => Some(inner),
561            _ => None,
562        }
563    }
564
565    /// Substrate-canonical projection onto the disjunctive
566    /// [`Self::List`] | [`Self::Map`] | [`Self::Vector`] compound-body
567    /// arm-set — returns `Some(&[Node])` byte-borrowed from the matched
568    /// arm's own [`Vec<Node>`] storage on any of the three D4-dialect
569    /// compound-carrying arms (parenthesized list, brace map, bracket
570    /// vector), and [`None`] on every other arm of the closed
571    /// fourteen-arm [`NodeKind`] variant set.
572    ///
573    /// Three production consumers today across two caixa-monorepo crates
574    /// — the caixa-ast [`crate::visit::walk`] visitor recursion (which
575    /// treats every compound as a child-bearing container to descend
576    /// into), the caixa-fmt printer's `emit_header_operand` inlineable
577    /// gate (which flattens any D4-dialect compound header operand that
578    /// fits the width budget), and the caixa-fmt printer's `is_atom`
579    /// grid-cell classifier (which refuses any D4-dialect compound as a
580    /// grid cell because it carries its own layout) — which previously
581    /// reached the underlying `Vec<Node>` child sequence through three
582    /// raw `NodeKind::List(items) | NodeKind::Map(items) |
583    /// NodeKind::Vector(items) => …` open-coded three-arm disjunctive
584    /// pattern-matches that expressed no compile-time link back to the
585    /// substrate primitive's typed compound-body arm-set. Each site is a
586    /// compound-shape-agnostic projection: the consumer does not care
587    /// WHICH D4-dialect compound arm it is (the delimiter distinction
588    /// matters to per-arm emit sites, not to walkers), only that the
589    /// arm hides a `Vec<Node>` child sequence whose iteration is the
590    /// whole traversal.
591    ///
592    /// Semantically distinct from the sibling [`Self::as_list`] (48efa3b)
593    /// single-arm compound projection — that one pins the strict-`List`-
594    /// only boundary the manifest-parser and positional-classifier sites
595    /// gate on (a `(defcaixa …)` form is a `List`, not a `Map` or
596    /// `Vector`); this one lifts the disjunctive three-arm compound-body
597    /// arm-set every compound-shape-agnostic walker / header-inliner /
598    /// grid-cell classifier reaches for. A future D4-adjacent compound
599    /// arm addition (a hypothetical `NodeKind::Set(Vec<Node>)` shape
600    /// once the tatara-lisp reader grows a `#{…}` set literal, a
601    /// `NodeKind::Tuple(Vec<Node>)` shape once the sexp→JSON bridge
602    /// stabilizes fixed-arity tuple sugar) folds into this projection
603    /// by extending the accessor's arm-set once at the substrate
604    /// primitive, rather than a three-way coordinated rewrite across
605    /// every downstream walker.
606    ///
607    /// Zero-copy — the returned `&[Node]` borrows from the matched arm's
608    /// own [`Vec<Node>`] storage (pinned by the
609    /// `as_seq_body_is_by_borrow_pointer_identity` test), the same
610    /// discipline as the sibling per-arm [`Self::as_list`] (48efa3b)
611    /// compound and [`Self::as_reader_macro_inner`] (20be266) reader-
612    /// macro projections. Eighth `Option<&<payload>>` projection
613    /// accessor on the outer [`NodeKind`] sum-type — extends the
614    /// projection family from the two disjunctive-scalar and one
615    /// reader-macro-arm-set accessors onto the disjunctive-compound-arm
616    /// axis every downstream compound-shape-agnostic walker partitions
617    /// on.
618    ///
619    /// `pub const fn` — sibling in `const`-eval posture to the paired
620    /// single-arm [`Self::as_list`] compound projection, extending the
621    /// caixa-ast const-eval-surface family onto the disjunctive three-
622    /// arm D4-dialect compound-body axis. Body-preserving verbatim
623    /// (same `match &self { Self::List(items) | Self::Map(items) |
624    /// Self::Vector(items) => Some(items.as_slice()), _ => None }`
625    /// shape, same [`Vec::as_slice`] `pub const fn` const-stable since
626    /// Rust 1.7 the [`Self::as_list`] promotion routes through), so the
627    /// promotion is a type-signature widening — no arm-storage owning-
628    /// borrow is taken, no drop is invoked on any arm's `Vec<Node>`
629    /// payload. Closes the const-eval discipline the sibling caixa-ast
630    /// source-position primitive family, the outer-`NodeKind` per-arm
631    /// scalar-projection family (`as_keyword` / `as_symbol` / `as_str`),
632    /// the two disjunctive-scalar accessors (`as_atom_string` /
633    /// `as_symbol_or_str`), the single-arm compound projection
634    /// (`as_list`), the paired writer-half projections (`seq_delims` /
635    /// `reader_macro_prefix`), and the sibling reader-macro-arm reader-
636    /// half projection ([`Self::as_reader_macro_inner`]) already carry
637    /// onto the last remaining `Option<&_>` projection axis of the
638    /// outer-`NodeKind` sum-type — the disjunctive three-arm compound-
639    /// body axis every compound-shape-agnostic walker / header-inliner /
640    /// grid-cell classifier partitions on. Pairs with the sibling
641    /// writer-half [`Self::seq_delims`] promotion so the reader/writer-
642    /// duality's two halves on the D4-dialect compound-arm-set (the
643    /// body slice the arm carries, and the two delimiter bytes the arm
644    /// reads back as) now BOTH dispatch through one substrate-primitive
645    /// const accessor rather than a runtime-context escape hatch on
646    /// either half.
647    #[must_use]
648    pub const fn as_seq_body(&self) -> Option<&[Node]> {
649        match self {
650            Self::List(items) | Self::Map(items) | Self::Vector(items) => Some(items.as_slice()),
651            _ => None,
652        }
653    }
654
655    /// Substrate-canonical projection onto the delimiter-pair `(open, close)`
656    /// that the tatara-lisp reader consumes to build the three D4-dialect
657    /// compound arms — `Self::List` reads as `('(', ')')`, `Self::Map` as
658    /// `('{', '}')`, `Self::Vector` as `('[', ']')` — and returns [`None`]
659    /// on every other arm of the closed fourteen-arm [`NodeKind`] variant
660    /// set. Companion to the sibling [`Self::as_seq_body`] compound-body
661    /// projection: the reader/writer duality's two halves — the body slice
662    /// the arm carries, and the two delimiter bytes the arm reads back as
663    /// — now each dispatch through one substrate accessor rather than a
664    /// three-way per-arm pattern-match repeated at every writer site.
665    ///
666    /// Four production consumer sites today in caixa-fmt/src/printer.rs
667    /// — the top-level `emit` main compound-arm dispatch, the
668    /// `emit_header_operand` inlineable branch (previously guarded by
669    /// [`Self::as_seq_body`] then re-matched with a `_ => unreachable!`
670    /// trap), the `render_node_inline` inline-render main compound-arm
671    /// dispatch, and the `classify_is_total_and_names_the_expected_shape`
672    /// test helper — which previously reached the delimiter pair through
673    /// three raw `NodeKind::List(_) => Delims::PAREN | NodeKind::Map(_)
674    /// => Delims::BRACE | NodeKind::Vector(_) => Delims::BRACKET`
675    /// open-coded per-arm pattern-matches. The doc-comment on caixa-fmt's
676    /// own `Delims` type already names the invariant this accessor lifts:
677    /// "`(…)`, `{…}` and `[…]` differ ONLY in these two bytes."
678    ///
679    /// Contract with [`Self::as_seq_body`]: for every variant, either both
680    /// return [`Some`] (the three D4-dialect compound arms) or both return
681    /// [`None`] (every other arm) — pinned by
682    /// `seq_delims_partitions_the_same_arm_set_as_as_seq_body`. A future
683    /// D4-adjacent compound arm addition (a hypothetical
684    /// `NodeKind::Set(Vec<Node>)` shape once the tatara-lisp reader grows
685    /// a `#{…}` set literal) folds onto both accessors at exactly the
686    /// substrate — the writer half by extending this partition once, the
687    /// walker half by extending [`Self::as_seq_body`]'s — rather than a
688    /// coordinated rewrite across every per-consumer writer site.
689    ///
690    /// `pub const fn` — the body reads the arm discriminant through a
691    /// `_`-binding pattern-match that projects onto a `Copy`-shaped
692    /// `Option<(char, char)>` return, no arm-storage borrow is taken, and
693    /// no drop is invoked on any arm's `Vec<Node>` payload (the `_`
694    /// pattern binds nothing). Pattern matching on `&Self` where the
695    /// enum carries drop-typed arms has been const-stable in Rust since
696    /// long before this workspace's 1.89 MSRV floor. Extends the
697    /// const-eval discipline the sibling caixa-ast source-position
698    /// primitive family (`Span::new` / `Span::point` / `Span::contains`
699    /// / `Span::union` / `Span::len` / `Span::is_empty`, `Position::new`
700    /// / `Position::origin`, `line_column`) and the paired
701    /// [`Self::reader_macro_prefix`] writer-half sibling on the reader-
702    /// macro-arm-set already carry onto the caixa-ast [`NodeKind`] outer-
703    /// sum-type's per-arm identity-projection axis. Every downstream
704    /// writer that wants a compile-time delimiter-pair fixture (a
705    /// `const PAREN: Option<(char, char)> =
706    /// NodeKind::List(…).seq_delims();` future caixa-fmt writer
707    /// const-lookup table, a per-arm identity oracle a future caixa-lint
708    /// no-delimiter-in-non-compound-context rule consults at compile
709    /// time, a compile-time compound-arm-set partition truth table the
710    /// caixa-lsp writer keys off) now reads through one substrate-
711    /// primitive const dispatch rather than being forced onto the
712    /// runtime code path.
713    #[must_use]
714    pub const fn seq_delims(&self) -> Option<(char, char)> {
715        match self {
716            Self::List(_) => Some(('(', ')')),
717            Self::Map(_) => Some(('{', '}')),
718            Self::Vector(_) => Some(('[', ']')),
719            _ => None,
720        }
721    }
722
723    /// Substrate-canonical projection onto the writer-side sigil prefix
724    /// that the tatara-lisp reader consumes to build the four reader-macro
725    /// arms — `Self::Quote` reads as one apostrophe byte, `Self::Quasiquote`
726    /// as one backtick byte, `Self::Unquote` as one comma byte,
727    /// `Self::UnquoteSplice` as two bytes (comma then at-sign) — and
728    /// returns [`None`] on every other arm of the closed fourteen-arm
729    /// [`NodeKind`] variant set. Companion to the sibling
730    /// [`Self::as_reader_macro_inner`] reader-macro-inner projection: the
731    /// reader/writer duality's two halves on the reader-macro-arm-set —
732    /// the boxed inner node the arm carries, and the sigil bytes the arm
733    /// reads back as — now each dispatch through one substrate accessor
734    /// rather than a four-arm per-arm pattern-match repeated at every
735    /// writer site.
736    ///
737    /// Two production consumer sites today in caixa-fmt/src/printer.rs
738    /// — the top-level `Printer::emit` main reader-macro-arm dispatch, and
739    /// the peer `render_node_inline` inline-render main reader-macro-arm
740    /// dispatch — which previously reached the sigil bytes through four
741    /// raw per-arm pattern-matches (`Quote` pushing the apostrophe byte,
742    /// `Quasiquote` pushing the backtick byte, `Unquote` pushing the
743    /// comma byte, `UnquoteSplice` pushing the two-byte comma-then-at-
744    /// sign) restated at each site. The fact that the four reader-macro
745    /// arms differ ONLY in these one-or-two sigil bytes had no home in
746    /// the type system — it lived as prose in the printer's own arm
747    /// dispatch, and a copy-paste that flipped one arm's sigil (routed
748    /// `Quote` through the backtick byte and `Quasiquote` through the
749    /// apostrophe byte) would silently rewrite every quote as a
750    /// quasiquote and vice versa at every writer site.
751    ///
752    /// Contract with [`Self::as_reader_macro_inner`]: for every variant,
753    /// either both accessors return [`Some`] (the four reader-macro arms)
754    /// or both return [`None`] (every other arm) — pinned by
755    /// `reader_macro_prefix_partitions_the_same_arm_set_as_as_reader_macro_inner`.
756    /// A future reader-macro arm addition (a hypothetical
757    /// `NodeKind::Splice(Box<Node>)` shape once the tatara-lisp reader
758    /// grows a splicing-quote variant, a `NodeKind::TaggedQuote(Box<Node>,
759    /// QuoteTag)` shape once the sexp→JSON bridge stabilizes tagged-quote
760    /// sugar — the same extension axis the sibling
761    /// [`Self::as_reader_macro_inner`] doc-comment names) folds onto both
762    /// accessors at exactly the substrate — the writer half by extending
763    /// this partition once, the walker half by extending
764    /// [`Self::as_reader_macro_inner`]'s — rather than a coordinated
765    /// rewrite across every per-consumer writer site.
766    ///
767    /// Tenth `Option<&<payload>>`-shaped projection accessor on the
768    /// outer-`NodeKind` sum-type — the writer-half sibling to
769    /// [`Self::as_reader_macro_inner`] on the reader-macro-arm-set,
770    /// mirroring the shape of the paired
771    /// [`Self::seq_delims`] / [`Self::as_seq_body`] compound-arm-set
772    /// accessors on the sibling D4-dialect compound axis. Returns
773    /// `&'static str` rather than a borrow into arm storage because the
774    /// sigil bytes are the arm's IDENTITY (like `seq_delims`'s
775    /// `(char, char)` pair) rather than a payload the arm carries.
776    ///
777    /// `pub const fn` — the body reads the arm discriminant through a
778    /// `_`-binding pattern-match that projects onto a `Copy`-shaped
779    /// `Option<&'static str>` return, no arm-storage borrow is taken, and
780    /// no drop is invoked on any arm's `Box<Node>` payload (the `_`
781    /// pattern binds nothing). Pattern matching on `&Self` where the
782    /// enum carries drop-typed arms has been const-stable in Rust since
783    /// long before this workspace's 1.89 MSRV floor. Extends the
784    /// const-eval discipline the sibling caixa-ast source-position
785    /// primitive family (`Span::new` / `Span::point` / `Span::contains`
786    /// / `Span::union` / `Span::len` / `Span::is_empty`, `Position::new`
787    /// / `Position::origin`, `line_column`) already carries onto the
788    /// caixa-ast [`NodeKind`] outer-sum-type's per-arm identity-projection
789    /// axis. Every downstream writer that wants a compile-time reader-
790    /// macro-sigil fixture (a `const QUOTE: Option<&'static str> =
791    /// NodeKind::Quote(…).reader_macro_prefix();` future caixa-fmt writer
792    /// const-lookup table, a per-arm identity oracle a future caixa-lint
793    /// no-quote-sigil-in-non-reader-macro-context rule consults at
794    /// compile time, a compile-time reader-macro-arm-set partition truth
795    /// table the caixa-lsp writer keys off) now reads through one
796    /// substrate-primitive const dispatch rather than being forced onto
797    /// the runtime code path.
798    #[must_use]
799    pub const fn reader_macro_prefix(&self) -> Option<&'static str> {
800        match self {
801            Self::Quote(_) => Some("'"),
802            Self::Quasiquote(_) => Some("`"),
803            Self::Unquote(_) => Some(","),
804            Self::UnquoteSplice(_) => Some(",@"),
805            _ => None,
806        }
807    }
808}
809
810impl Node {
811    #[must_use]
812    pub fn new(kind: NodeKind, span: Span) -> Self {
813        Self {
814            kind,
815            span,
816            leading: Vec::new(),
817            trailing: Vec::new(),
818            after: Vec::new(),
819        }
820    }
821
822    /// Drop all spans + trivia, lowering into the plain `tatara_lisp::Sexp`
823    /// used by the compile pipeline.
824    ///
825    /// Route the D4-dialect compound-body lowering through the lifted
826    /// [`NodeKind::as_seq_body`] `Option<&[Node]>` accessor rather than
827    /// the raw four-arm `NodeKind::List(items) => Sexp::List(…) |
828    /// NodeKind::Map(items) | NodeKind::Vector(items) => Sexp::List(…)`
829    /// open-coded per-arm dispatch — sibling in shape to the peer
830    /// [`crate::visit::walk`], `caixa-fmt::Printer::emit`,
831    /// `caixa-fmt::render_node_inline`, `caixa-fmt::emit_header_operand`,
832    /// `caixa-fmt::is_atom`, and `caixa-teia::node_to_value` compound-body
833    /// sites that all key off the same substrate-canonical accessor. The
834    /// three D4-dialect compound arms produce IDENTICAL `Sexp::List`
835    /// bodies (only the brace-ness is dropped — see the historical note
836    /// below), so the pre-lift shape was two match arms restating the
837    /// same `Sexp::List(items.iter().map(Node::to_tatara_sexp).collect())`
838    /// body; the lift folds both onto one dispatch that gates on the
839    /// substrate-primitive compound-body-carrying arm-set.
840    ///
841    /// `tatara_lisp::Sexp` has no Map/Vector variant yet — adding them is
842    /// a LANGUAGE change, sequenced as Phase 2 of
843    /// theory/TATARA-LISP-CONSOLIDATION.md D4 and gated on its own
844    /// differential run over the 1,123-file corpus (correction C4). Until
845    /// that lands, all three compound arms lower to a plain list: the
846    /// elements survive in order, only the brace-ness is dropped. That is
847    /// strictly closer to intent than the pre-D4 behaviour, where the
848    /// delimiters lowered as literal `{` / `}` SYMBOLS inside the list.
849    /// This projection is used only by the round-trip equivalence tests,
850    /// which stay honest because formatting re-emits the delimiters and
851    /// re-parsing recovers the node.
852    #[must_use]
853    pub fn to_tatara_sexp(&self) -> tatara_lisp::Sexp {
854        use tatara_lisp::{Atom, Sexp};
855        if let Some(items) = self.kind.as_seq_body() {
856            return Sexp::List(items.iter().map(Node::to_tatara_sexp).collect());
857        }
858        match &self.kind {
859            NodeKind::Nil => Sexp::Nil,
860            NodeKind::Symbol(s) => Sexp::Atom(Atom::Symbol(s.clone())),
861            NodeKind::Keyword(s) => Sexp::Atom(Atom::Keyword(s.clone())),
862            NodeKind::Str(s) => Sexp::Atom(Atom::Str(s.clone())),
863            NodeKind::Int(i) => Sexp::Atom(Atom::Int(*i)),
864            NodeKind::Float(f) => Sexp::Atom(Atom::Float(*f)),
865            NodeKind::Bool(b) => Sexp::Atom(Atom::Bool(*b)),
866            NodeKind::Quote(inner) => Sexp::Quote(Box::new(inner.to_tatara_sexp())),
867            NodeKind::Quasiquote(inner) => Sexp::Quasiquote(Box::new(inner.to_tatara_sexp())),
868            NodeKind::Unquote(inner) => Sexp::Unquote(Box::new(inner.to_tatara_sexp())),
869            NodeKind::UnquoteSplice(inner) => Sexp::UnquoteSplice(Box::new(inner.to_tatara_sexp())),
870            NodeKind::List(_) | NodeKind::Map(_) | NodeKind::Vector(_) => {
871                unreachable!("compound-arm-set routed through NodeKind::as_seq_body above")
872            }
873        }
874    }
875
876    /// Head symbol for a list node like `(defX ...)`. Returns None unless this
877    /// is a `List` whose first element is a `Symbol`.
878    ///
879    /// Routes the head-slot symbol-name projection through the lifted
880    /// [`NodeKind::as_symbol`] `Option<&str>` accessor rather than the
881    /// raw `let NodeKind::Symbol(s) = &items.first()?.kind else …`
882    /// open-coded per-arm pattern-match — sibling in shape to the
883    /// caixa-fmt `special_head_arity` / `head_symbol_is_command`,
884    /// caixa-lint `check_paired_kwargs` / `check_aplicacao_timeout` /
885    /// `check_git_pin` / `check_consistent_quote`, and caixa-teia
886    /// `is_ref_form` sites (all converged in this run) that partition
887    /// on the outer-`NodeKind` `Symbol` arm through the same substrate-
888    /// canonical accessor.
889    #[must_use]
890    pub fn head_symbol(&self) -> Option<&str> {
891        self.kind.as_list()?.first()?.kind.as_symbol()
892    }
893
894    /// For a list formatted as alternating `:key value :key value`, returns
895    /// the matching value node for `key` (without the leading colon).
896    #[must_use]
897    pub fn kwarg(&self, key: &str) -> Option<&Node> {
898        let items = self.kind.as_list()?;
899        let start = usize::from(items.first().is_some_and(|n| n.kind.is_symbol()));
900        let mut i = start;
901        while i + 1 < items.len() {
902            // Route the per-`:key value` pair-loop's per-item keyword-
903            // name scalar projection through the lifted
904            // [`NodeKind::as_keyword`] `Option<&str>` accessor rather
905            // than the raw `if let NodeKind::Keyword(k) = &items[i]
906            // .kind` open-coded per-arm pattern-match — the per-
907            // `Keyword`-arm scalar projection now keys off the
908            // substrate-canonical sum-type per-arm accessor every
909            // downstream caixa-ast/caixa-lint per-`Keyword`-arm consumer
910            // (`caixa_lint::rules::check_keyword_kebab`,
911            // `check_enum_pascal`, `keyword_present`, `matches_kwarg`,
912            // `items_has_key`) routes through, so any future
913            // `NodeKind::Keyword`-adjacent arm extension picks up this
914            // dispatch through exactly one edit on the substrate primitive.
915            if let Some(k) = items[i].kind.as_keyword()
916                && k == key
917            {
918                return Some(&items[i + 1]);
919            }
920            i += 2;
921        }
922        None
923    }
924}
925
926#[cfg(test)]
927mod is_variant_tests {
928    use super::*;
929
930    fn all_variants() -> Vec<(NodeKind, &'static str)> {
931        vec![
932            (NodeKind::Nil, "Nil"),
933            (NodeKind::Symbol("x".into()), "Symbol"),
934            (NodeKind::Keyword("k".into()), "Keyword"),
935            (NodeKind::Str("s".into()), "Str"),
936            (NodeKind::Int(0), "Int"),
937            (NodeKind::Float(0.0), "Float"),
938            (NodeKind::Bool(false), "Bool"),
939            (NodeKind::List(Vec::new()), "List"),
940            (NodeKind::Map(Vec::new()), "Map"),
941            (NodeKind::Vector(Vec::new()), "Vector"),
942            (
943                NodeKind::Quote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
944                "Quote",
945            ),
946            (
947                NodeKind::Quasiquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
948                "Quasiquote",
949            ),
950            (
951                NodeKind::Unquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
952                "Unquote",
953            ),
954            (
955                NodeKind::UnquoteSplice(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
956                "UnquoteSplice",
957            ),
958        ]
959    }
960
961    fn predicate_row(k: &NodeKind) -> [bool; 14] {
962        [
963            k.is_nil(),
964            k.is_symbol(),
965            k.is_keyword(),
966            k.is_str(),
967            k.is_int(),
968            k.is_float(),
969            k.is_bool(),
970            k.is_list(),
971            k.is_map(),
972            k.is_vector(),
973            k.is_quote(),
974            k.is_quasiquote(),
975            k.is_unquote(),
976            k.is_unquote_splice(),
977        ]
978    }
979
980    // Fail-before-pass-after pin on the [`gen_platform::IsVariant`]
981    // derive-generated per-arm predicate partition — for every variant
982    // in `all_variants()`, the observed 14-slot predicate row must
983    // equal a one-hot row with the `true` at exactly the same index as
984    // the variant's declaration order. Expected rows are generated
985    // live from the enumeration rather than transcribed by hand, so a
986    // copy-paste flip that reroutes one arm through the wrong
987    // predicate lane trips at the identity-diagonal assertion the way
988    // every peer `CaixaKind` / `CaixaDialeto` / `DepList` /
989    // `PathShapeViolation` / `RestartStrategy` partition pin already
990    // does on the sibling caixa-core surface.
991    #[test]
992    fn node_kind_is_variant_predicates_partition_the_arm_set() {
993        let variants = all_variants();
994        for (idx, (variant, name)) in variants.iter().enumerate() {
995            let observed = predicate_row(variant);
996            let mut expected = [false; 14];
997            expected[idx] = true;
998            assert_eq!(
999                observed, expected,
1000                "NodeKind::{name} at declaration-order slot {idx} must \
1001                 satisfy exactly one is_* predicate (its own); observed \
1002                 row must equal the one-hot expected row"
1003            );
1004        }
1005    }
1006
1007    // Byte-parity pin on the two field-agnostic `matches!` shapes this
1008    // lift replaces at production call sites: the `NodeKind::Symbol(_)`
1009    // gate (caixa-ast/src/node.rs `kwarg` head-skip, caixa-fmt/src/
1010    // printer.rs `kwargs_head_len` take-while) and the
1011    // `NodeKind::Keyword(_)` gate (caixa-fmt/src/printer.rs
1012    // `kwargs_head_len` pair-check + `inline_slot_count` pair-detect,
1013    // caixa-lint/src/rules.rs `paired-kwargs` first-arg + second-arg
1014    // gates). Refuses a future accidental split between the derived
1015    // predicate and its pre-lift `matches!` shape (a hand-rolled
1016    // shadow `impl` that overrides one path, an accidental rebrand of
1017    // one converged call site back to the `matches!` form) on the two
1018    // load-bearing arm-discriminator axes every downstream authoring
1019    // consumer (caixa-fmt, caixa-lint) partitions on.
1020    #[test]
1021    fn node_kind_is_symbol_and_is_keyword_byte_equal_pre_lift_matches_shape() {
1022        for (variant, name) in all_variants() {
1023            let via_matches_symbol = matches!(variant, NodeKind::Symbol(_));
1024            let via_predicate_symbol = variant.is_symbol();
1025            assert_eq!(
1026                via_predicate_symbol, via_matches_symbol,
1027                "NodeKind::{name}.is_symbol() must byte-equal \
1028                 matches!(_, NodeKind::Symbol(_)) — otherwise the \
1029                 converged call sites in caixa-ast/caixa-fmt would \
1030                 silently disagree with their pre-lift shape"
1031            );
1032            let via_matches_keyword = matches!(variant, NodeKind::Keyword(_));
1033            let via_predicate_keyword = variant.is_keyword();
1034            assert_eq!(
1035                via_predicate_keyword, via_matches_keyword,
1036                "NodeKind::{name}.is_keyword() must byte-equal \
1037                 matches!(_, NodeKind::Keyword(_)) — otherwise the \
1038                 converged call sites in caixa-fmt/caixa-lint would \
1039                 silently disagree with their pre-lift shape"
1040            );
1041        }
1042    }
1043
1044    // Projection contract on the outer-`NodeKind` sum-type's `Keyword`
1045    // scalar-arm accessor: exactly the `Keyword` arm returns
1046    // `Some(&str)` byte-borrowed from the arm's own [`String`] storage;
1047    // every other arm returns `None`. Pins the "one canonical
1048    // projection dispatch per typed arm on the substrate primitive"
1049    // discipline the six per-`Keyword`-arm consumer sites (caixa-ast
1050    // [`Node::kwarg`] pair-loop; caixa-lint `check_keyword_kebab`,
1051    // `check_enum_pascal`, `keyword_present`, `matches_kwarg`,
1052    // `items_has_key`) route through via
1053    // `.as_keyword()`/`.and_then(NodeKind::as_keyword)`. A regression
1054    // that admitted a non-`Keyword` arm through this projection would
1055    // silently classify a bare symbol or string literal as a keyword
1056    // at the six per-consumer sites — `kwarg` would return the wrong
1057    // pair value, `keyword_present` would find phantom `:timeout`s in
1058    // string literals, `matches_kwarg`/`items_has_key` would treat
1059    // positional args as keyword pairs. This test guards that surface
1060    // across every arm of the closed fourteen-arm partition.
1061    #[test]
1062    fn as_keyword_projects_only_keyword_arm() {
1063        let variants = all_variants();
1064        for (variant, name) in &variants {
1065            let projected = variant.as_keyword();
1066            if matches!(variant, NodeKind::Keyword(_)) {
1067                let NodeKind::Keyword(k) = variant else {
1068                    unreachable!("guarded by matches! above");
1069                };
1070                assert_eq!(
1071                    projected,
1072                    Some(k.as_str()),
1073                    "NodeKind::{name} is the Keyword arm — as_keyword() \
1074                     must project onto its own String payload"
1075                );
1076            } else {
1077                assert_eq!(
1078                    projected, None,
1079                    "NodeKind::{name} is not the Keyword arm — \
1080                     as_keyword() must return None"
1081                );
1082            }
1083        }
1084        // Empty keyword — the accessor is a projection, not a gate; an
1085        // empty-`:` keyword (author-declared or parser-produced) round-
1086        // trips as `Some("")`, not `None`.
1087        assert_eq!(NodeKind::Keyword(String::new()).as_keyword(), Some(""));
1088    }
1089
1090    // Zero-copy pin — `k.as_keyword()` must borrow from the `Keyword`
1091    // arm's own [`String`] storage, not clone into a fresh buffer.
1092    // Fails at build time if a future rewrite regresses to
1093    // `Some(k.clone().leak())` or any other detour that silently
1094    // allocates on every call (the same shape as the sibling
1095    // [`caixa_teia::TeiaValue::as_str_is_by_borrow_pointer_identity`]
1096    // pin on the outer-`TeiaValue` scalar accessor).
1097    #[test]
1098    fn as_keyword_is_by_borrow_pointer_identity() {
1099        let k = NodeKind::Keyword("timeout".into());
1100        let via_accessor: &str = k.as_keyword().unwrap();
1101        let NodeKind::Keyword(ref inner) = k else {
1102            unreachable!("constructed above as NodeKind::Keyword");
1103        };
1104        assert_eq!(
1105            via_accessor.as_ptr(),
1106            inner.as_ptr(),
1107            "NodeKind::as_keyword must borrow from the Keyword arm's \
1108             String backing storage (zero-copy projection)",
1109        );
1110        assert_eq!(
1111            via_accessor.len(),
1112            inner.len(),
1113            "NodeKind::as_keyword and inner.as_str() must byte-equal \
1114             in length (same slice)",
1115        );
1116    }
1117
1118    // Byte-parity pin on the pre-lift `if let NodeKind::Keyword(k) =
1119    // &n.kind { … if k == key … }` shape the six caixa-ast/caixa-lint
1120    // consumer sites route through today via
1121    // `n.kind.as_keyword() == Some(key)` (or `if let Some(k) =
1122    // n.kind.as_keyword() { if k == key … }`). Refuses a future
1123    // accidental split between the accessor's return contract and its
1124    // pre-lift `matches!`/`if let` shape (a hand-rolled shadow
1125    // `impl` that overrides one path, an accidental rebrand of one
1126    // converged call site back to the raw `NodeKind::Keyword(k)`
1127    // form) on the load-bearing keyword-name-projection axis every
1128    // downstream caixa-lint rule's `:key value` pair-loop / walker
1129    // partitions on.
1130    #[test]
1131    fn as_keyword_byte_equal_pre_lift_pattern_match_shape() {
1132        for (variant, name) in all_variants() {
1133            let via_pattern: Option<&str> = match &variant {
1134                NodeKind::Keyword(k) => Some(k.as_str()),
1135                _ => None,
1136            };
1137            let via_accessor = variant.as_keyword();
1138            assert_eq!(
1139                via_accessor, via_pattern,
1140                "NodeKind::{name}.as_keyword() must byte-equal \
1141                 `match &_ {{ NodeKind::Keyword(k) => Some(k.as_str()), \
1142                 _ => None }}` — otherwise the six converged caixa-ast/ \
1143                 caixa-lint call sites would silently disagree with \
1144                 their pre-lift shape"
1145            );
1146        }
1147    }
1148
1149    // Projection contract on the outer-`NodeKind` sum-type's `Symbol`
1150    // scalar-arm accessor: exactly the `Symbol` arm returns
1151    // `Some(&str)` byte-borrowed from the arm's own [`String`] storage;
1152    // every other arm returns `None`. Pins the "one canonical
1153    // projection dispatch per typed arm on the substrate primitive"
1154    // discipline the eight per-`Symbol`-arm consumer sites (caixa-ast
1155    // [`Node::head_symbol`]; caixa-fmt `special_head_arity`,
1156    // `head_symbol_is_command`; caixa-lint `check_paired_kwargs`,
1157    // `check_aplicacao_timeout`, `check_git_pin`,
1158    // `check_consistent_quote`; caixa-teia `is_ref_form`) route
1159    // through via `.as_symbol()` / `.as_symbol() == Some("…")` /
1160    // `.and_then(|n| n.kind.as_symbol())`. A regression that admitted
1161    // a non-`Symbol` arm through this projection would silently
1162    // classify a keyword or string literal as a symbol at the eight
1163    // per-consumer sites — `head_symbol` would return names of quoted
1164    // keyword forms, `head_symbol_is_command` would fire on `:tag`
1165    // literals, `check_git_pin`'s `:tipo git` gate would accept
1166    // `"git"` strings, `is_ref_form` would classify `("ref" …)` as a
1167    // ref. This test guards that surface across every arm of the
1168    // closed fourteen-arm partition.
1169    #[test]
1170    fn as_symbol_projects_only_symbol_arm() {
1171        let variants = all_variants();
1172        for (variant, name) in &variants {
1173            let projected = variant.as_symbol();
1174            if matches!(variant, NodeKind::Symbol(_)) {
1175                let NodeKind::Symbol(s) = variant else {
1176                    unreachable!("guarded by matches! above");
1177                };
1178                assert_eq!(
1179                    projected,
1180                    Some(s.as_str()),
1181                    "NodeKind::{name} is the Symbol arm — as_symbol() \
1182                     must project onto its own String payload"
1183                );
1184            } else {
1185                assert_eq!(
1186                    projected, None,
1187                    "NodeKind::{name} is not the Symbol arm — \
1188                     as_symbol() must return None"
1189                );
1190            }
1191        }
1192        // Empty symbol — the accessor is a projection, not a gate; an
1193        // empty-name symbol (parser-produced from a stray reader edge
1194        // case) round-trips as `Some("")`, not `None`.
1195        assert_eq!(NodeKind::Symbol(String::new()).as_symbol(), Some(""));
1196    }
1197
1198    // Zero-copy pin — `k.as_symbol()` must borrow from the `Symbol`
1199    // arm's own [`String`] storage, not clone into a fresh buffer.
1200    // Fails at build time if a future rewrite regresses to
1201    // `Some(s.clone().leak())` or any other detour that silently
1202    // allocates on every call (the same shape as the sibling
1203    // [`as_keyword_is_by_borrow_pointer_identity`] pin on the peer
1204    // outer-`NodeKind` `Keyword`-arm scalar accessor and the
1205    // [`caixa_teia::TeiaValue::as_str_is_by_borrow_pointer_identity`]
1206    // pin on the outer-`TeiaValue` `Str`-arm scalar accessor).
1207    #[test]
1208    fn as_symbol_is_by_borrow_pointer_identity() {
1209        let k = NodeKind::Symbol("defcaixa".into());
1210        let via_accessor: &str = k.as_symbol().unwrap();
1211        let NodeKind::Symbol(ref inner) = k else {
1212            unreachable!("constructed above as NodeKind::Symbol");
1213        };
1214        assert_eq!(
1215            via_accessor.as_ptr(),
1216            inner.as_ptr(),
1217            "NodeKind::as_symbol must borrow from the Symbol arm's \
1218             String backing storage (zero-copy projection)",
1219        );
1220        assert_eq!(
1221            via_accessor.len(),
1222            inner.len(),
1223            "NodeKind::as_symbol and inner.as_str() must byte-equal \
1224             in length (same slice)",
1225        );
1226    }
1227
1228    // Byte-parity pin on the pre-lift `let NodeKind::Symbol(s) = &n.kind
1229    // else …` / `matches!(&n.kind, NodeKind::Symbol(s) if s == "…")`
1230    // shape the eight caixa-ast/caixa-fmt/caixa-lint/caixa-teia
1231    // consumer sites route through today via `.as_symbol()` /
1232    // `.as_symbol() == Some("…")`. Refuses a future accidental split
1233    // between the accessor's return contract and its pre-lift shape
1234    // (a hand-rolled shadow `impl` that overrides one path, an
1235    // accidental rebrand of one converged call site back to the raw
1236    // `NodeKind::Symbol(s)` form) on the load-bearing symbol-name-
1237    // projection axis every downstream head-symbol / form-tag /
1238    // enum-variant partition keys off.
1239    #[test]
1240    fn as_symbol_byte_equal_pre_lift_pattern_match_shape() {
1241        for (variant, name) in all_variants() {
1242            let via_pattern: Option<&str> = match &variant {
1243                NodeKind::Symbol(s) => Some(s.as_str()),
1244                _ => None,
1245            };
1246            let via_accessor = variant.as_symbol();
1247            assert_eq!(
1248                via_accessor, via_pattern,
1249                "NodeKind::{name}.as_symbol() must byte-equal \
1250                 `match &_ {{ NodeKind::Symbol(s) => Some(s.as_str()), \
1251                 _ => None }}` — otherwise the eight converged caixa-ast/ \
1252                 caixa-fmt/caixa-lint/caixa-teia call sites would silently \
1253                 disagree with their pre-lift shape"
1254            );
1255        }
1256    }
1257
1258    // Projection contract on the outer-`NodeKind` sum-type's `Str`
1259    // scalar-arm accessor: exactly the `Str` arm returns `Some(&str)`
1260    // byte-borrowed from the arm's own [`String`] storage; every other
1261    // arm returns `None`. Pins the "one canonical projection dispatch
1262    // per typed arm on the substrate primitive" discipline the three
1263    // per-`Str`-arm consumer sites (caixa-lint `check_nome_kebab`,
1264    // `check_no_fixme`; caixa-fmt `is_flag_token`) route through via
1265    // `.as_str()` / `.as_str().is_some_and(…)`. A regression that
1266    // admitted a non-`Str` arm through this projection would silently
1267    // classify a bare symbol or keyword literal as a string at the three
1268    // per-consumer sites — `check_nome_kebab` would flag `:nome` bare-
1269    // symbol values as non-kebab, `check_no_fixme` would scan symbol
1270    // names for FIXME, `is_flag_token` would layout-align symbols as
1271    // `-flag` tokens. This test guards that surface across every arm of
1272    // the closed fourteen-arm partition.
1273    #[test]
1274    fn as_str_projects_only_str_arm() {
1275        let variants = all_variants();
1276        for (variant, name) in &variants {
1277            let projected = variant.as_str();
1278            if matches!(variant, NodeKind::Str(_)) {
1279                let NodeKind::Str(s) = variant else {
1280                    unreachable!("guarded by matches! above");
1281                };
1282                assert_eq!(
1283                    projected,
1284                    Some(s.as_str()),
1285                    "NodeKind::{name} is the Str arm — as_str() \
1286                     must project onto its own String payload"
1287                );
1288            } else {
1289                assert_eq!(
1290                    projected, None,
1291                    "NodeKind::{name} is not the Str arm — \
1292                     as_str() must return None"
1293                );
1294            }
1295        }
1296        // Empty string — the accessor is a projection, not a gate; an
1297        // empty-`""` string literal (author-declared or parser-produced)
1298        // round-trips as `Some("")`, not `None`.
1299        assert_eq!(NodeKind::Str(String::new()).as_str(), Some(""));
1300    }
1301
1302    // Zero-copy pin — `k.as_str()` must borrow from the `Str` arm's own
1303    // [`String`] storage, not clone into a fresh buffer. Fails at build
1304    // time if a future rewrite regresses to `Some(s.clone().leak())` or
1305    // any other detour that silently allocates on every call (the same
1306    // shape as the sibling [`as_keyword_is_by_borrow_pointer_identity`]
1307    // / [`as_symbol_is_by_borrow_pointer_identity`] pins on the peer
1308    // outer-`NodeKind` `Keyword`- / `Symbol`-arm scalar accessors and
1309    // the [`caixa_teia::TeiaValue::as_str_is_by_borrow_pointer_identity`]
1310    // pin on the outer-`TeiaValue` `Str`-arm scalar accessor).
1311    #[test]
1312    fn as_str_is_by_borrow_pointer_identity() {
1313        let k = NodeKind::Str("demo".into());
1314        let via_accessor: &str = k.as_str().unwrap();
1315        let NodeKind::Str(ref inner) = k else {
1316            unreachable!("constructed above as NodeKind::Str");
1317        };
1318        assert_eq!(
1319            via_accessor.as_ptr(),
1320            inner.as_ptr(),
1321            "NodeKind::as_str must borrow from the Str arm's \
1322             String backing storage (zero-copy projection)",
1323        );
1324        assert_eq!(
1325            via_accessor.len(),
1326            inner.len(),
1327            "NodeKind::as_str and inner.as_str() must byte-equal \
1328             in length (same slice)",
1329        );
1330    }
1331
1332    // Byte-parity pin on the pre-lift `if let NodeKind::Str(s) = &n.kind
1333    // { … }` / `matches!(&n.kind, NodeKind::Str(s) if …)` shape the
1334    // three caixa-lint/caixa-fmt consumer sites route through today via
1335    // `.as_str()` / `.as_str().is_some_and(…)`. Refuses a future
1336    // accidental split between the accessor's return contract and its
1337    // pre-lift shape (a hand-rolled shadow `impl` that overrides one
1338    // path, an accidental rebrand of one converged call site back to
1339    // the raw `NodeKind::Str(s)` form) on the load-bearing string-
1340    // literal-projection axis every downstream `:nome`/`:descricao`
1341    // value-shape / flag-token layout partition keys off.
1342    #[test]
1343    fn as_str_byte_equal_pre_lift_pattern_match_shape() {
1344        for (variant, name) in all_variants() {
1345            let via_pattern: Option<&str> = match &variant {
1346                NodeKind::Str(s) => Some(s.as_str()),
1347                _ => None,
1348            };
1349            let via_accessor = variant.as_str();
1350            assert_eq!(
1351                via_accessor, via_pattern,
1352                "NodeKind::{name}.as_str() must byte-equal \
1353                 `match &_ {{ NodeKind::Str(s) => Some(s.as_str()), \
1354                 _ => None }}` — otherwise the three converged \
1355                 caixa-lint/caixa-fmt call sites would silently disagree \
1356                 with their pre-lift shape"
1357            );
1358        }
1359    }
1360
1361    // Pin the const-eval surface: the substrate-primitive per-arm
1362    // scalar-projection family — [`NodeKind::as_keyword`],
1363    // [`NodeKind::as_symbol`], [`NodeKind::as_str`] — reaches into `const`
1364    // context, so a future compile-time caixa-fmt writer-side per-arm-
1365    // identity truth-table / caixa-lint keyword-key const-lookup /
1366    // caixa-lsp writer const-registry can key off the three sibling
1367    // borrowed-scalar-projection accessors without being forced onto the
1368    // runtime code path. The `const _: () = assert!(…)` bindings resolve
1369    // each projection at compile time on the non-`String`-payload arm-set
1370    // (the three `String`-carrying arms themselves cannot be constructed
1371    // in `const` context yet — `String::new` widens the const-eval surface
1372    // once `String::from` / `From<&str>` reach `const`, sibling to the
1373    // same rust-lang/rust #143874 tracking issue the peer `Span::union`
1374    // open-codes `Ord::min` / `Ord::max` around), pinning the four `Nil` /
1375    // `Int` / `Bool` / `Float` non-`String`-payload atom arms — each of
1376    // which is `const`-constructible in-place through its raw arm ctor
1377    // and each of which must fall through the accessor's `_ => None` arm
1378    // on all three projections. Any regression that drops `pub const fn`
1379    // back to `pub fn` on any of the three (a body edit that reaches for
1380    // a non-const operation on the accessor path) fails this test at
1381    // compile time rather than at runtime, matching the sibling caixa-ast
1382    // source-position primitive family's `Span::new` / `Span::point` /
1383    // `Span::contains` / `Span::union` / `Span::len` / `Span::is_empty` /
1384    // `Position::new` / `Position::origin` / `line_column` `pub const fn`
1385    // shape's const-eval discipline and the paired [`NodeKind::seq_delims`]
1386    // / [`NodeKind::reader_macro_prefix`] writer-half sibling promotions
1387    // on the compound-arm / reader-macro-arm sets — extended onto the
1388    // caixa-ast [`NodeKind`] outer-sum-type's per-`String`-arm borrowed-
1389    // scalar-projection axis.
1390    #[test]
1391    fn as_keyword_as_symbol_as_str_are_const() {
1392        const NIL: NodeKind = NodeKind::Nil;
1393        const NIL_KEYWORD: Option<&str> = NIL.as_keyword();
1394        const NIL_SYMBOL: Option<&str> = NIL.as_symbol();
1395        const NIL_STR: Option<&str> = NIL.as_str();
1396        const _: () = assert!(NIL_KEYWORD.is_none());
1397        const _: () = assert!(NIL_SYMBOL.is_none());
1398        const _: () = assert!(NIL_STR.is_none());
1399
1400        const INT: NodeKind = NodeKind::Int(0);
1401        const INT_KEYWORD: Option<&str> = INT.as_keyword();
1402        const INT_SYMBOL: Option<&str> = INT.as_symbol();
1403        const INT_STR: Option<&str> = INT.as_str();
1404        const _: () = assert!(INT_KEYWORD.is_none());
1405        const _: () = assert!(INT_SYMBOL.is_none());
1406        const _: () = assert!(INT_STR.is_none());
1407
1408        const BOOL: NodeKind = NodeKind::Bool(false);
1409        const BOOL_KEYWORD: Option<&str> = BOOL.as_keyword();
1410        const BOOL_SYMBOL: Option<&str> = BOOL.as_symbol();
1411        const BOOL_STR: Option<&str> = BOOL.as_str();
1412        const _: () = assert!(BOOL_KEYWORD.is_none());
1413        const _: () = assert!(BOOL_SYMBOL.is_none());
1414        const _: () = assert!(BOOL_STR.is_none());
1415
1416        const FLOAT: NodeKind = NodeKind::Float(0.0);
1417        const FLOAT_KEYWORD: Option<&str> = FLOAT.as_keyword();
1418        const FLOAT_SYMBOL: Option<&str> = FLOAT.as_symbol();
1419        const FLOAT_STR: Option<&str> = FLOAT.as_str();
1420        const _: () = assert!(FLOAT_KEYWORD.is_none());
1421        const _: () = assert!(FLOAT_SYMBOL.is_none());
1422        const _: () = assert!(FLOAT_STR.is_none());
1423    }
1424
1425    // Pin the const-eval surface on the paired *disjunctive* projections
1426    // that extend the sibling per-arm [`NodeKind::as_keyword`] /
1427    // [`NodeKind::as_symbol`] / [`NodeKind::as_str`] scalar-projection
1428    // family onto the disjunctive atom-string-carrying arm-sets:
1429    // [`NodeKind::as_atom_string`] (three-arm Symbol|Str|Keyword) and
1430    // [`NodeKind::as_symbol_or_str`] (two-arm Symbol|Str strict subset).
1431    // Both reach into `const` context so a future compile-time caixa-teia
1432    // `kwarg_symbol` / `build_ref` const-lookup, or caixa-lsp
1433    // `document_symbol` writer const-registry, can key off the disjunctive
1434    // projections without escape onto the runtime path. The `const _: ()
1435    // = assert!(…)` bindings resolve both projections at compile time on
1436    // the four non-`String`-payload atom arms (Nil / Int / Bool / Float),
1437    // each of which is `const`-constructible in-place through its raw arm
1438    // ctor and each of which must fall through to `None` on both
1439    // projections. A regression that drops `pub const fn` back to `pub fn`
1440    // on either (a body edit that reaches for a non-const operation on the
1441    // accessor path) fails this test at compile time rather than at runtime,
1442    // matching the sibling per-arm scalar-projection triple's
1443    // `as_keyword_as_symbol_as_str_are_const` pin and the writer-half
1444    // `NodeKind::seq_delims` / `NodeKind::reader_macro_prefix` /
1445    // compound-arm `NodeKind::as_list` const-eval discipline — extended
1446    // onto the outer-`NodeKind` disjunctive-arm-set projection axis.
1447    #[test]
1448    fn as_atom_string_and_as_symbol_or_str_are_const() {
1449        const NIL: NodeKind = NodeKind::Nil;
1450        const NIL_ATOM_STRING: Option<&str> = NIL.as_atom_string();
1451        const NIL_SYMBOL_OR_STR: Option<&str> = NIL.as_symbol_or_str();
1452        const _: () = assert!(NIL_ATOM_STRING.is_none());
1453        const _: () = assert!(NIL_SYMBOL_OR_STR.is_none());
1454
1455        const INT: NodeKind = NodeKind::Int(0);
1456        const INT_ATOM_STRING: Option<&str> = INT.as_atom_string();
1457        const INT_SYMBOL_OR_STR: Option<&str> = INT.as_symbol_or_str();
1458        const _: () = assert!(INT_ATOM_STRING.is_none());
1459        const _: () = assert!(INT_SYMBOL_OR_STR.is_none());
1460
1461        const BOOL: NodeKind = NodeKind::Bool(false);
1462        const BOOL_ATOM_STRING: Option<&str> = BOOL.as_atom_string();
1463        const BOOL_SYMBOL_OR_STR: Option<&str> = BOOL.as_symbol_or_str();
1464        const _: () = assert!(BOOL_ATOM_STRING.is_none());
1465        const _: () = assert!(BOOL_SYMBOL_OR_STR.is_none());
1466
1467        const FLOAT: NodeKind = NodeKind::Float(0.0);
1468        const FLOAT_ATOM_STRING: Option<&str> = FLOAT.as_atom_string();
1469        const FLOAT_SYMBOL_OR_STR: Option<&str> = FLOAT.as_symbol_or_str();
1470        const _: () = assert!(FLOAT_ATOM_STRING.is_none());
1471        const _: () = assert!(FLOAT_SYMBOL_OR_STR.is_none());
1472    }
1473
1474    // Projection contract on the outer-`NodeKind` sum-type's disjunctive
1475    // `Symbol` | `Str` | `Keyword` atom-string-carrying accessor: exactly
1476    // the three atom-string-carrying arms return `Some(&str)` byte-
1477    // borrowed from the matched arm's own [`String`] storage; every
1478    // other arm returns `None`. Pins the "one canonical projection
1479    // dispatch per typed arm-set on the substrate primitive" discipline
1480    // the two per-disjunctive-arm-set consumer sites (caixa-teia
1481    // `kwarg_symbol` `:tipo`/`:nome` value-shape gate; `build_ref`
1482    // `:atributo`-slot projection) route through via
1483    // `.as_atom_string().map(str::to_owned)` /
1484    // `.as_atom_string().map(str::to_owned).ok_or(…)`. A regression
1485    // that admitted a non-atom-string-carrying arm (a numeric literal,
1486    // a list, a quote) through this projection would silently classify
1487    // a bare `42` or `(a b)` as a `:tipo`/`:nome` name at the caixa-teia
1488    // manifest parser — `kwarg_symbol` would surface a stringified
1489    // integer as an instance name, `build_ref` would let `(ref aws/vpc
1490    // main (a b))` slip past its "must be a symbol/keyword/string"
1491    // guard. This test guards that surface across every arm of the
1492    // closed fourteen-arm partition.
1493    #[test]
1494    fn as_atom_string_projects_only_symbol_str_keyword_arms() {
1495        let variants = all_variants();
1496        for (variant, name) in &variants {
1497            let projected = variant.as_atom_string();
1498            let is_atom_string_arm = matches!(
1499                variant,
1500                NodeKind::Symbol(_) | NodeKind::Str(_) | NodeKind::Keyword(_)
1501            );
1502            if is_atom_string_arm {
1503                let expected = match variant {
1504                    NodeKind::Symbol(s) | NodeKind::Str(s) | NodeKind::Keyword(s) => s.as_str(),
1505                    _ => unreachable!("guarded by is_atom_string_arm above"),
1506                };
1507                assert_eq!(
1508                    projected,
1509                    Some(expected),
1510                    "NodeKind::{name} is an atom-string-carrying arm — \
1511                     as_atom_string() must project onto its own String \
1512                     payload"
1513                );
1514            } else {
1515                assert_eq!(
1516                    projected, None,
1517                    "NodeKind::{name} is not an atom-string-carrying \
1518                     arm — as_atom_string() must return None"
1519                );
1520            }
1521        }
1522        // Empty payload on each of the three carrying arms — the
1523        // accessor is a projection, not a gate; a nameless
1524        // symbol/string/keyword round-trips as `Some("")`, not `None`.
1525        assert_eq!(
1526            NodeKind::Symbol(String::new()).as_atom_string(),
1527            Some(""),
1528            "empty-payload Symbol arm must project to Some(\"\")"
1529        );
1530        assert_eq!(
1531            NodeKind::Str(String::new()).as_atom_string(),
1532            Some(""),
1533            "empty-payload Str arm must project to Some(\"\")"
1534        );
1535        assert_eq!(
1536            NodeKind::Keyword(String::new()).as_atom_string(),
1537            Some(""),
1538            "empty-payload Keyword arm must project to Some(\"\")"
1539        );
1540    }
1541
1542    // Zero-copy pin — `k.as_atom_string()` must borrow from the matched
1543    // arm's own [`String`] storage, not clone into a fresh buffer. Fails
1544    // at build time if a future rewrite regresses to `Some(s.clone()
1545    // .leak())` or any other detour that silently allocates on every
1546    // call. Pinned across all three atom-string-carrying arms
1547    // (Symbol/Str/Keyword) — a per-arm-specific regression would slip
1548    // past a single-arm pin — the same shape as the sibling
1549    // [`as_symbol_is_by_borrow_pointer_identity`] /
1550    // [`as_str_is_by_borrow_pointer_identity`] /
1551    // [`as_keyword_is_by_borrow_pointer_identity`] pins on the peer
1552    // outer-`NodeKind` per-arm scalar accessors.
1553    #[test]
1554    fn as_atom_string_is_by_borrow_pointer_identity() {
1555        for (constructor, ctor_name) in [
1556            (
1557                (|s: String| NodeKind::Symbol(s)) as fn(String) -> NodeKind,
1558                "Symbol",
1559            ),
1560            (
1561                (|s: String| NodeKind::Str(s)) as fn(String) -> NodeKind,
1562                "Str",
1563            ),
1564            (
1565                (|s: String| NodeKind::Keyword(s)) as fn(String) -> NodeKind,
1566                "Keyword",
1567            ),
1568        ] {
1569            let payload = format!("aws/vpc-{ctor_name}");
1570            let payload_len = payload.len();
1571            let k = constructor(payload);
1572            let via_accessor: &str = k.as_atom_string().unwrap();
1573            let inner_ptr = match &k {
1574                NodeKind::Symbol(inner) | NodeKind::Str(inner) | NodeKind::Keyword(inner) => {
1575                    inner.as_ptr()
1576                }
1577                _ => unreachable!("constructed above via atom-string arm ctor"),
1578            };
1579            assert_eq!(
1580                via_accessor.as_ptr(),
1581                inner_ptr,
1582                "NodeKind::as_atom_string on the {ctor_name} arm must \
1583                 borrow from that arm's String backing storage \
1584                 (zero-copy projection)",
1585            );
1586            assert_eq!(
1587                via_accessor.len(),
1588                payload_len,
1589                "NodeKind::as_atom_string on the {ctor_name} arm must \
1590                 byte-equal in length to the arm's payload",
1591            );
1592        }
1593    }
1594
1595    // Byte-parity pin on the pre-lift three-arm disjunctive
1596    // `match &v.kind { NodeKind::Symbol(s) | NodeKind::Str(s) |
1597    // NodeKind::Keyword(s) => Some(s.clone()), _ => None }` /
1598    // `NodeKind::Symbol(s) | NodeKind::Str(s) | NodeKind::Keyword(s)
1599    // => s.clone()` shape the two caixa-teia manifest-parser consumer
1600    // sites (`kwarg_symbol` :tipo/:nome value-shape gate, `build_ref`
1601    // :atributo-slot projection) route through today via
1602    // `.as_atom_string().map(str::to_owned)`. Refuses a future
1603    // accidental split between the accessor's return contract and its
1604    // pre-lift disjunctive shape (a hand-rolled shadow `impl` that
1605    // overrides one path, an accidental rebrand of one converged call
1606    // site back to the raw three-arm `NodeKind::Symbol(s) | …` form)
1607    // on the load-bearing atom-string-carrying disjunctive axis every
1608    // caixa-teia name-slot gate keys off.
1609    #[test]
1610    fn as_atom_string_byte_equal_pre_lift_pattern_match_shape() {
1611        for (variant, name) in all_variants() {
1612            let via_pattern: Option<&str> = match &variant {
1613                NodeKind::Symbol(s) | NodeKind::Str(s) | NodeKind::Keyword(s) => Some(s.as_str()),
1614                _ => None,
1615            };
1616            let via_accessor = variant.as_atom_string();
1617            assert_eq!(
1618                via_accessor, via_pattern,
1619                "NodeKind::{name}.as_atom_string() must byte-equal \
1620                 `match &_ {{ NodeKind::Symbol(s) | NodeKind::Str(s) | \
1621                 NodeKind::Keyword(s) => Some(s.as_str()), _ => None \
1622                 }}` — otherwise the two converged caixa-teia \
1623                 manifest-parser call sites would silently disagree \
1624                 with their pre-lift shape"
1625            );
1626        }
1627    }
1628
1629    // Projection contract on the outer-`NodeKind` sum-type's disjunctive
1630    // two-arm `Symbol` | `Str` atom-name-carrying accessor: exactly the
1631    // two atom-name-carrying arms return `Some(&str)` byte-borrowed from
1632    // the matched arm's own [`String`] storage; every other arm (including
1633    // the `Keyword` arm — the strict-subset boundary vs the sibling
1634    // three-arm [`NodeKind::as_atom_string`] projection) returns `None`.
1635    // Pins the "one canonical projection dispatch per typed arm-set on
1636    // the substrate primitive" discipline the two per-two-arm-disjunction
1637    // consumer sites (caixa-teia `build_ref` `:nome`-slot projection;
1638    // caixa-lsp `document_symbol` `:nome`-detail projection) route through
1639    // via `.as_symbol_or_str().map(str::to_owned)`. A regression that
1640    // admitted a non-atom-name-carrying arm (a keyword literal, a numeric,
1641    // a list, a quote) through this projection would silently classify
1642    // `:foo` / `42` / `(a b)` as a `:nome` name at the caixa-teia manifest
1643    // parser and the caixa-lsp document-symbol pane — `build_ref` would
1644    // let `(ref aws/vpc :foo id)` slip past its "must be a symbol or
1645    // string" guard, `document_symbol.detail` would surface a stringified
1646    // keyword or integer as an instance name. This test guards that
1647    // surface across every arm of the closed fourteen-arm partition.
1648    #[test]
1649    fn as_symbol_or_str_projects_only_symbol_and_str_arms() {
1650        let variants = all_variants();
1651        for (variant, name) in &variants {
1652            let projected = variant.as_symbol_or_str();
1653            let is_symbol_or_str_arm = matches!(variant, NodeKind::Symbol(_) | NodeKind::Str(_));
1654            if is_symbol_or_str_arm {
1655                let expected = match variant {
1656                    NodeKind::Symbol(s) | NodeKind::Str(s) => s.as_str(),
1657                    _ => unreachable!("guarded by is_symbol_or_str_arm above"),
1658                };
1659                assert_eq!(
1660                    projected,
1661                    Some(expected),
1662                    "NodeKind::{name} is an atom-name-carrying arm — \
1663                     as_symbol_or_str() must project onto its own String \
1664                     payload"
1665                );
1666            } else {
1667                assert_eq!(
1668                    projected, None,
1669                    "NodeKind::{name} is not an atom-name-carrying arm \
1670                     (Symbol|Str) — as_symbol_or_str() must return None"
1671                );
1672            }
1673        }
1674        // Empty payload on each of the two carrying arms — the accessor
1675        // is a projection, not a gate; a nameless symbol/string round-
1676        // trips as `Some("")`, not `None`.
1677        assert_eq!(
1678            NodeKind::Symbol(String::new()).as_symbol_or_str(),
1679            Some(""),
1680            "empty-payload Symbol arm must project to Some(\"\")"
1681        );
1682        assert_eq!(
1683            NodeKind::Str(String::new()).as_symbol_or_str(),
1684            Some(""),
1685            "empty-payload Str arm must project to Some(\"\")"
1686        );
1687        // Strict-subset boundary vs the sibling three-arm
1688        // [`NodeKind::as_atom_string`] (3c3ca48): the `Keyword` arm is
1689        // in the three-arm superset but NOT in this two-arm subset — a
1690        // future refactor that widened this accessor to admit `Keyword`
1691        // (silently collapsing the semantic distinction between the
1692        // two accessors) trips at this pin before the caixa-teia
1693        // `build_ref` `:nome` guard and the caixa-lsp `document_symbol`
1694        // detail silently start accepting `:keyword` literals.
1695        assert_eq!(
1696            NodeKind::Keyword("k".into()).as_symbol_or_str(),
1697            None,
1698            "Keyword arm is in as_atom_string's three-arm set but MUST NOT \
1699             be in as_symbol_or_str's two-arm subset — the strict-subset \
1700             boundary is the whole point of a distinct accessor"
1701        );
1702    }
1703
1704    // Zero-copy pin — `k.as_symbol_or_str()` must borrow from the matched
1705    // arm's own [`String`] storage, not clone into a fresh buffer. Fails
1706    // at build time if a future rewrite regresses to `Some(s.clone()
1707    // .leak())` or any other detour that silently allocates on every call.
1708    // Pinned across both atom-name-carrying arms (Symbol/Str) — a per-
1709    // arm-specific regression would slip past a single-arm pin — the same
1710    // shape as the sibling [`as_atom_string_is_by_borrow_pointer_identity`]
1711    // pin on the peer three-arm outer-`NodeKind` disjunctive projection.
1712    #[test]
1713    fn as_symbol_or_str_is_by_borrow_pointer_identity() {
1714        for (constructor, ctor_name) in [
1715            (
1716                (|s: String| NodeKind::Symbol(s)) as fn(String) -> NodeKind,
1717                "Symbol",
1718            ),
1719            (
1720                (|s: String| NodeKind::Str(s)) as fn(String) -> NodeKind,
1721                "Str",
1722            ),
1723        ] {
1724            let payload = format!("aws-vpc-{ctor_name}");
1725            let payload_len = payload.len();
1726            let k = constructor(payload);
1727            let via_accessor: &str = k.as_symbol_or_str().unwrap();
1728            let inner_ptr = match &k {
1729                NodeKind::Symbol(inner) | NodeKind::Str(inner) => inner.as_ptr(),
1730                _ => unreachable!("constructed above via atom-name arm ctor"),
1731            };
1732            assert_eq!(
1733                via_accessor.as_ptr(),
1734                inner_ptr,
1735                "NodeKind::as_symbol_or_str on the {ctor_name} arm must \
1736                 borrow from that arm's String backing storage \
1737                 (zero-copy projection)",
1738            );
1739            assert_eq!(
1740                via_accessor.len(),
1741                payload_len,
1742                "NodeKind::as_symbol_or_str on the {ctor_name} arm must \
1743                 byte-equal in length to the arm's payload",
1744            );
1745        }
1746    }
1747
1748    // Byte-parity pin on the pre-lift two-arm disjunctive
1749    // `match &_.kind { NodeKind::Symbol(s) | NodeKind::Str(s) => s.clone(),
1750    // _ => … }` shape the two caixa-teia `build_ref` `:nome`-slot and
1751    // caixa-lsp `document_symbol` `:nome`-detail consumer sites route
1752    // through today via `.as_symbol_or_str().map(str::to_owned)`. Refuses
1753    // a future accidental split between the accessor's return contract
1754    // and its pre-lift disjunctive shape (a hand-rolled shadow `impl` that
1755    // overrides one path, an accidental rebrand of one converged call site
1756    // back to the raw two-arm `NodeKind::Symbol(s) | NodeKind::Str(s)`
1757    // form, or a widening of the accessor to admit `Keyword`) on the
1758    // load-bearing atom-name-carrying disjunctive axis every `:nome` slot
1759    // partitions on.
1760    #[test]
1761    fn as_symbol_or_str_byte_equal_pre_lift_pattern_match_shape() {
1762        for (variant, name) in all_variants() {
1763            let via_pattern: Option<&str> = match &variant {
1764                NodeKind::Symbol(s) | NodeKind::Str(s) => Some(s.as_str()),
1765                _ => None,
1766            };
1767            let via_accessor = variant.as_symbol_or_str();
1768            assert_eq!(
1769                via_accessor, via_pattern,
1770                "NodeKind::{name}.as_symbol_or_str() must byte-equal \
1771                 `match &_ {{ NodeKind::Symbol(s) | NodeKind::Str(s) => \
1772                 Some(s.as_str()), _ => None }}` — otherwise the two \
1773                 converged caixa-teia/caixa-lsp `:nome` call sites would \
1774                 silently disagree with their pre-lift shape"
1775            );
1776        }
1777    }
1778
1779    // Byte-parity pin on the disjunctive `NodeKind::Int(_) |
1780    // NodeKind::Float(_)` shape the caixa-fmt/src/printer.rs
1781    // `is_numeric` grid-column right-align gate keys off. Refuses a
1782    // future arm addition (a hypothetical `NodeKind::Rational(_)` /
1783    // `NodeKind::Ratio(_)` when the reader grows a rational literal)
1784    // that lands on the disjunction without a matching predicate
1785    // extension — the pin trips at build time before the fmt printer
1786    // silently miscategorises the new numeric arm.
1787    #[test]
1788    fn node_kind_is_int_or_is_float_byte_equal_pre_lift_numeric_matches_shape() {
1789        for (variant, name) in all_variants() {
1790            let via_matches = matches!(variant, NodeKind::Int(_) | NodeKind::Float(_));
1791            let via_predicate = variant.is_int() || variant.is_float();
1792            assert_eq!(
1793                via_predicate, via_matches,
1794                "NodeKind::{name}.is_int() || .is_float() must \
1795                 byte-equal matches!(_, NodeKind::Int(_) | \
1796                 NodeKind::Float(_)) — otherwise caixa-fmt's grid-column \
1797                 numeric-right-align gate would silently disagree with \
1798                 its pre-lift shape"
1799            );
1800        }
1801    }
1802
1803    // Projection contract on the outer-`NodeKind` sum-type's `List`
1804    // compound-arm accessor: exactly the `List` arm returns `Some(&[Node])`
1805    // byte-borrowed from the arm's own [`Vec<Node>`] storage; every other
1806    // arm — INCLUDING the sibling `Map` and `Vector` arms that also carry
1807    // `Vec<Node>` payloads — returns `None`. Pins the "one canonical
1808    // projection dispatch per typed arm on the substrate primitive"
1809    // discipline the eleven per-`List`-arm consumer sites (caixa-ast
1810    // [`Node::head_symbol`], [`Node::kwarg`], `parse_list`,
1811    // `parse_map_and_vector`, `parse_reader_macros`; caixa-lint
1812    // `check_enum_pascal`, `check_paired_kwargs`, `check_git_pin`;
1813    // caixa-teia `instance_from_node` `:atributos` gate; caixa-fmt
1814    // `commented_head_degrades_out_of_the_plist_shape`, `float_roundtrip`)
1815    // route through via `.as_list()`. A regression that admitted a `Map`
1816    // or `Vector` arm through this projection would silently classify
1817    // brace-dialect `{ :k v … }` forms and bracket-dialect `[ a b … ]`
1818    // forms as ordinary parenthesized `(a b …)` lists at the eleven
1819    // per-consumer sites — `head_symbol` would surface the first element
1820    // of a brace map as a form-head, `kwarg` would scan bracket vectors
1821    // for `:key value` pairs, `check_enum_pascal` would flag Vec elements
1822    // as enum-variant mis-authorings, `instance_from_node`'s `:atributos`
1823    // gate would accept a hoisted `{ :k v }` shape it explicitly refuses.
1824    // This test guards that surface across every arm of the closed
1825    // fourteen-arm partition, with the `Map` / `Vector` non-`List`-arm
1826    // gates called out explicitly as the strict-`List`-only boundary the
1827    // whole point of a per-arm accessor turns on.
1828    #[test]
1829    fn as_list_projects_only_list_arm() {
1830        let variants = all_variants();
1831        for (variant, name) in &variants {
1832            let projected = variant.as_list();
1833            if matches!(variant, NodeKind::List(_)) {
1834                let NodeKind::List(items) = variant else {
1835                    unreachable!("guarded by matches! above");
1836                };
1837                assert_eq!(
1838                    projected,
1839                    Some(items.as_slice()),
1840                    "NodeKind::{name} is the List arm — as_list() must \
1841                     project onto its own Vec<Node> payload"
1842                );
1843            } else {
1844                assert_eq!(
1845                    projected, None,
1846                    "NodeKind::{name} is not the List arm — as_list() \
1847                     must return None"
1848                );
1849            }
1850        }
1851        // Empty list — the accessor is a projection, not a gate; an
1852        // empty-`()` list (author-declared or parser-produced from a stray
1853        // reader edge case) round-trips as `Some(&[])`, not `None`.
1854        assert_eq!(NodeKind::List(Vec::new()).as_list(), Some(&[] as &[Node]));
1855        // Strict-`List`-only boundary vs the sibling compound arms `Map`
1856        // and `Vector` — both also carry `Vec<Node>` payloads, so a hand-
1857        // rolled `matches!(_.kind, NodeKind::List(_))` gate has to keep
1858        // the boundary in view at every site. A future refactor that
1859        // widened this accessor to admit `Map` or `Vector` (silently
1860        // collapsing the semantic distinction between the parenthesized-
1861        // list, brace-map, and bracket-vector dialects) trips at these
1862        // pins before the eleven per-consumer sites silently start
1863        // accepting brace / bracket forms in `(defX …)` / `:atributos` /
1864        // `:key value` slots.
1865        assert_eq!(
1866            NodeKind::Map(Vec::new()).as_list(),
1867            None,
1868            "Map arm carries Vec<Node> but MUST NOT project through \
1869             as_list — the strict-List-only boundary is the whole point \
1870             of a distinct compound-arm accessor"
1871        );
1872        assert_eq!(
1873            NodeKind::Vector(Vec::new()).as_list(),
1874            None,
1875            "Vector arm carries Vec<Node> but MUST NOT project through \
1876             as_list — the strict-List-only boundary is the whole point \
1877             of a distinct compound-arm accessor"
1878        );
1879    }
1880
1881    // Zero-copy pin — `k.as_list()` must borrow from the `List` arm's
1882    // own [`Vec<Node>`] storage, not clone into a fresh Vec. Fails at
1883    // build time if a future rewrite regresses to `Some(items.clone()
1884    // .as_slice().to_vec().leak())` or any other detour that silently
1885    // allocates on every call (the same shape as the sibling
1886    // [`as_keyword_is_by_borrow_pointer_identity`] /
1887    // [`as_symbol_is_by_borrow_pointer_identity`] /
1888    // [`as_str_is_by_borrow_pointer_identity`] /
1889    // [`as_atom_string_is_by_borrow_pointer_identity`] /
1890    // [`as_symbol_or_str_is_by_borrow_pointer_identity`] pins on the
1891    // peer outer-`NodeKind` scalar and disjunctive-scalar accessors).
1892    #[test]
1893    fn as_list_is_by_borrow_pointer_identity() {
1894        let k = NodeKind::List(vec![
1895            Node::new(NodeKind::Symbol("defcaixa".into()), Span::new(0, 8)),
1896            Node::new(NodeKind::Symbol("demo".into()), Span::new(9, 13)),
1897        ]);
1898        let via_accessor: &[Node] = k.as_list().unwrap();
1899        let NodeKind::List(ref inner) = k else {
1900            unreachable!("constructed above as NodeKind::List");
1901        };
1902        assert_eq!(
1903            via_accessor.as_ptr(),
1904            inner.as_ptr(),
1905            "NodeKind::as_list must borrow from the List arm's \
1906             Vec<Node> backing storage (zero-copy projection)",
1907        );
1908        assert_eq!(
1909            via_accessor.len(),
1910            inner.len(),
1911            "NodeKind::as_list and inner.as_slice() must byte-equal \
1912             in length (same slice)",
1913        );
1914    }
1915
1916    // Byte-parity pin on the pre-lift `let NodeKind::List(items) = &X.kind
1917    // else { … }` shape the eleven caixa-ast/caixa-lint/caixa-teia/
1918    // caixa-fmt consumer sites route through today via `.as_list()`.
1919    // Refuses a future accidental split between the accessor's return
1920    // contract and its pre-lift shape (a hand-rolled shadow `impl` that
1921    // overrides one path, an accidental rebrand of one converged call
1922    // site back to the raw `NodeKind::List(items)` form, a widening to
1923    // admit `Map` or `Vector`) on the load-bearing list-shape-projection
1924    // axis every downstream form walker / `:atributos` gate / positional
1925    // classifier partitions on.
1926    #[test]
1927    fn as_list_byte_equal_pre_lift_pattern_match_shape() {
1928        for (variant, name) in all_variants() {
1929            let via_pattern: Option<&[Node]> = match &variant {
1930                NodeKind::List(items) => Some(items.as_slice()),
1931                _ => None,
1932            };
1933            let via_accessor = variant.as_list();
1934            assert_eq!(
1935                via_accessor, via_pattern,
1936                "NodeKind::{name}.as_list() must byte-equal \
1937                 `match &_ {{ NodeKind::List(items) => \
1938                 Some(items.as_slice()), _ => None }}` — otherwise the \
1939                 eleven converged caixa-ast/caixa-lint/caixa-teia/ \
1940                 caixa-fmt call sites would silently disagree with their \
1941                 pre-lift shape"
1942            );
1943        }
1944    }
1945
1946    // Fail-before-pass-after pin on [`NodeKind::as_list`]'s
1947    // `const`-eval-surface posture. The projection routes the
1948    // [`NodeKind::List`] arm's borrowed-`Vec<Node>` slot through the
1949    // `pub const fn` [`Vec::as_slice`] (const-stable since Rust 1.7, well
1950    // within the workspace's 1.89 MSRV floor) — any future accidental
1951    // downgrade to non-`const` fails `as_list_via_const_fn` at caixa-ast
1952    // build time with E0015 (`cannot call non-const method`), strictly
1953    // stronger than a runtime `assert!`. Sibling of the peer
1954    // per-source-position-primitive `const`-eval-surface passes on the
1955    // caixa-ast surface ([`crate::Span::new`] / [`crate::Span::point`] /
1956    // [`crate::Span::len`] / [`crate::Span::is_empty`] /
1957    // [`crate::Span::contains`] / [`crate::Span::union`] on the
1958    // byte-offset axis, [`crate::Position::new`] /
1959    // [`crate::Position::origin`] / [`crate::Position::line_column`] on
1960    // the 1-indexed line/column axis, [`crate::Trivia::comment_text`] on
1961    // the trivia-envelope-scoped projection axis, [`NodeKind::seq_delims`]
1962    // / [`NodeKind::reader_macro_prefix`] / [`NodeKind::as_keyword`] /
1963    // [`NodeKind::as_symbol`] / [`NodeKind::as_str`] on the
1964    // outer-`NodeKind` writer-half + per-arm scalar projection axes) —
1965    // the first `const`-eval-surface pin on the outer-`NodeKind` sum-type
1966    // compound-arm projection axis. The sweep exercises the
1967    // [`NodeKind::List`] projecting-arm alongside two non-projecting
1968    // sibling arms (the sibling compound arm [`NodeKind::Map`] to pin the
1969    // strict-`List`-only compound-axis boundary, and the atom-arm
1970    // [`NodeKind::Nil`] to pin the non-projecting-arm floor) so a
1971    // copy-paste flip that widened the projecting-arm-set or reroute
1972    // through a non-`const` detour (a hand-rolled `Some(items.clone()
1973    // .as_slice().to_vec().leak())` shape, an inner `.iter().collect()`
1974    // that allocates) trips at caixa-ast test time under `PartialEq` on
1975    // the `Option<&[Node]>` return shape rather than at a downstream
1976    // caixa-fmt / caixa-lint / caixa-teia / caixa-ast consumer-observable
1977    // drift.
1978    #[test]
1979    fn as_list_projection_is_const_fn() {
1980        const fn as_list_via_const_fn(k: &NodeKind) -> Option<&[Node]> {
1981            k.as_list()
1982        }
1983        let empty_list = NodeKind::List(Vec::new());
1984        let empty_map = NodeKind::Map(Vec::new());
1985        let nil = NodeKind::Nil;
1986        assert_eq!(as_list_via_const_fn(&empty_list), Some(&[] as &[Node]));
1987        assert_eq!(as_list_via_const_fn(&empty_map), None);
1988        assert_eq!(as_list_via_const_fn(&nil), None);
1989        // Round-trip cross-check against the sibling runtime dispatch —
1990        // any future divergence between the `const fn` path and the
1991        // runtime path (a hand-rolled shadow `impl` overriding one side,
1992        // a `#[cfg(...)]`-gated body that shipped only one lane) trips
1993        // here under `PartialEq` on the `Option<&[Node]>` return shape.
1994        assert_eq!(as_list_via_const_fn(&empty_list), empty_list.as_list());
1995        assert_eq!(as_list_via_const_fn(&empty_map), empty_map.as_list());
1996        assert_eq!(as_list_via_const_fn(&nil), nil.as_list());
1997    }
1998
1999    // Projection contract on the outer-`NodeKind` sum-type's disjunctive
2000    // four-arm `Quote` | `Quasiquote` | `Unquote` | `UnquoteSplice`
2001    // reader-macro-carrying accessor: exactly the four reader-macro-
2002    // carrying arms return `Some(&Node)` byte-borrowed from the matched
2003    // arm's own [`Box<Node>`] storage; every other arm of the closed
2004    // fourteen-arm partition returns `None`. Pins the "one canonical
2005    // projection dispatch per typed arm-set on the substrate primitive"
2006    // discipline the four per-disjunctive-arm-set consumer sites (caixa-
2007    // ast [`crate::visit::walk`] visitor recursion, caixa-fmt printer
2008    // `contains_comment` sub-tree probe, caixa-teia `node_to_value`
2009    // manifest lowerer, caixa-lint per-file `walk` traversal) route
2010    // through via `.as_reader_macro_inner().map(recurse)`. A regression
2011    // that admitted a non-reader-macro arm (a bare List, an atom) through
2012    // this projection would silently reroute the walker's recursion into
2013    // a compound-body traversal at every reader-macro site — the four-
2014    // arm walker converges would stop descending into unquoted forms and
2015    // start descending twice into every list.
2016    #[test]
2017    fn as_reader_macro_inner_projects_only_reader_macro_arms() {
2018        let variants = all_variants();
2019        for (variant, name) in &variants {
2020            let projected = variant.as_reader_macro_inner();
2021            let is_reader_macro_arm = matches!(
2022                variant,
2023                NodeKind::Quote(_)
2024                    | NodeKind::Quasiquote(_)
2025                    | NodeKind::Unquote(_)
2026                    | NodeKind::UnquoteSplice(_)
2027            );
2028            if is_reader_macro_arm {
2029                let expected: &Node = match variant {
2030                    NodeKind::Quote(inner)
2031                    | NodeKind::Quasiquote(inner)
2032                    | NodeKind::Unquote(inner)
2033                    | NodeKind::UnquoteSplice(inner) => inner.as_ref(),
2034                    _ => unreachable!("guarded by is_reader_macro_arm above"),
2035                };
2036                assert_eq!(
2037                    projected,
2038                    Some(expected),
2039                    "NodeKind::{name} is a reader-macro-carrying arm — \
2040                     as_reader_macro_inner() must project onto its own \
2041                     Box<Node> inner payload"
2042                );
2043            } else {
2044                assert_eq!(
2045                    projected, None,
2046                    "NodeKind::{name} is not a reader-macro-carrying arm \
2047                     — as_reader_macro_inner() must return None"
2048                );
2049            }
2050        }
2051        // Strict-boundary vs the sibling compound-carrying arms — both
2052        // `List` and `Map` and `Vector` carry compound child sequences,
2053        // but reader-macros carry a single boxed inner node; a future
2054        // refactor that widened this accessor to admit the compound arms
2055        // (silently collapsing the semantic distinction between a
2056        // reader-macro wrapper `'x` and a container `(x)`) trips at
2057        // these pins before the four per-consumer walker sites silently
2058        // start unwrapping list bodies as reader-macro inners.
2059        assert_eq!(
2060            NodeKind::List(Vec::new()).as_reader_macro_inner(),
2061            None,
2062            "List arm carries a Vec<Node> body but MUST NOT project through \
2063             as_reader_macro_inner — the reader-macro-only boundary is the \
2064             whole point of a distinct projection"
2065        );
2066        assert_eq!(
2067            NodeKind::Map(Vec::new()).as_reader_macro_inner(),
2068            None,
2069            "Map arm carries a Vec<Node> body but MUST NOT project through \
2070             as_reader_macro_inner — the reader-macro-only boundary is the \
2071             whole point of a distinct projection"
2072        );
2073        assert_eq!(
2074            NodeKind::Vector(Vec::new()).as_reader_macro_inner(),
2075            None,
2076            "Vector arm carries a Vec<Node> body but MUST NOT project through \
2077             as_reader_macro_inner — the reader-macro-only boundary is the \
2078             whole point of a distinct projection"
2079        );
2080    }
2081
2082    // Zero-copy pin — `k.as_reader_macro_inner()` must borrow from the
2083    // matched arm's own [`Box<Node>`] storage, not clone into a fresh
2084    // Node. Fails at build time if a future rewrite regresses to
2085    // `Some(Box::leak(inner.clone()))` or any other detour that silently
2086    // allocates on every call. Pinned across all four reader-macro-
2087    // carrying arms (Quote/Quasiquote/Unquote/UnquoteSplice) — a per-
2088    // arm-specific regression would slip past a single-arm pin — the
2089    // same shape as the sibling
2090    // [`as_atom_string_is_by_borrow_pointer_identity`] /
2091    // [`as_symbol_or_str_is_by_borrow_pointer_identity`] pins on the
2092    // peer disjunctive scalar accessors, extended onto the Box<Node>
2093    // indirection every reader-macro arm carries.
2094    #[test]
2095    fn as_reader_macro_inner_is_by_borrow_pointer_identity() {
2096        for (constructor, ctor_name) in [
2097            (
2098                (|n: Node| NodeKind::Quote(Box::new(n))) as fn(Node) -> NodeKind,
2099                "Quote",
2100            ),
2101            (
2102                (|n: Node| NodeKind::Quasiquote(Box::new(n))) as fn(Node) -> NodeKind,
2103                "Quasiquote",
2104            ),
2105            (
2106                (|n: Node| NodeKind::Unquote(Box::new(n))) as fn(Node) -> NodeKind,
2107                "Unquote",
2108            ),
2109            (
2110                (|n: Node| NodeKind::UnquoteSplice(Box::new(n))) as fn(Node) -> NodeKind,
2111                "UnquoteSplice",
2112            ),
2113        ] {
2114            let inner = Node::new(NodeKind::Symbol(format!("s-{ctor_name}")), Span::new(0, 8));
2115            let k = constructor(inner);
2116            let via_accessor: &Node = k.as_reader_macro_inner().unwrap();
2117            let inner_ref: &Node = match &k {
2118                NodeKind::Quote(b)
2119                | NodeKind::Quasiquote(b)
2120                | NodeKind::Unquote(b)
2121                | NodeKind::UnquoteSplice(b) => b.as_ref(),
2122                _ => unreachable!("constructed above via reader-macro arm ctor"),
2123            };
2124            assert!(
2125                std::ptr::eq(via_accessor, inner_ref),
2126                "NodeKind::as_reader_macro_inner on the {ctor_name} arm \
2127                 must borrow from that arm's Box<Node> heap allocation \
2128                 (zero-copy projection through the boxed indirection)",
2129            );
2130        }
2131    }
2132
2133    // Byte-parity pin on the pre-lift four-arm disjunctive
2134    // `match &_.kind { NodeKind::Quote(inner) | NodeKind::Quasiquote(inner)
2135    // | NodeKind::Unquote(inner) | NodeKind::UnquoteSplice(inner) =>
2136    // recurse(inner), _ => … }` shape the four caixa-ast/caixa-fmt/
2137    // caixa-teia/caixa-lint consumer sites route through today via
2138    // `.as_reader_macro_inner()`. Refuses a future accidental split
2139    // between the accessor's return contract and its pre-lift disjunctive
2140    // shape (a hand-rolled shadow `impl` that overrides one path, an
2141    // accidental rebrand of one converged call site back to the raw
2142    // four-arm `NodeKind::Quote(inner) | …` form, a widening to admit a
2143    // compound arm) on the load-bearing reader-macro-carrying disjunctive
2144    // axis every downstream walker recursion partitions on.
2145    #[test]
2146    fn as_reader_macro_inner_byte_equal_pre_lift_pattern_match_shape() {
2147        for (variant, name) in all_variants() {
2148            let via_pattern: Option<&Node> = match &variant {
2149                NodeKind::Quote(inner)
2150                | NodeKind::Quasiquote(inner)
2151                | NodeKind::Unquote(inner)
2152                | NodeKind::UnquoteSplice(inner) => Some(inner.as_ref()),
2153                _ => None,
2154            };
2155            let via_accessor = variant.as_reader_macro_inner();
2156            assert_eq!(
2157                via_accessor, via_pattern,
2158                "NodeKind::{name}.as_reader_macro_inner() must byte-equal \
2159                 `match &_ {{ NodeKind::Quote(inner) | \
2160                 NodeKind::Quasiquote(inner) | NodeKind::Unquote(inner) | \
2161                 NodeKind::UnquoteSplice(inner) => Some(inner.as_ref()), \
2162                 _ => None }}` — otherwise the four converged caixa-ast/ \
2163                 caixa-fmt/caixa-teia/caixa-lint walker sites would \
2164                 silently disagree with their pre-lift shape"
2165            );
2166        }
2167    }
2168
2169    // Pin the const-eval surface: the substrate-primitive reader-half-of-
2170    // the-reader/writer-duality projection accessor on the reader-macro-
2171    // arm-set reaches into `const` context, so a future compile-time
2172    // caixa-lint reader-macro-arity truth-table / caixa-lsp writer const-
2173    // registry / caixa-teia manifest-lowerer const-fixture that keys off
2174    // `NodeKind::as_reader_macro_inner` lands directly on the accessor
2175    // without a runtime-context escape hatch. The `const _: () = assert!(…)`
2176    // bindings resolve the projection at compile time on the non-reader-
2177    // macro arm-set (the reader-macro arms themselves carry a `Box<Node>`
2178    // payload with an `impl Drop` whose const-drop stability is behind
2179    // the same rust-lang/rust #143874 tracking issue the sibling
2180    // `Span::union` open-codes `Ord::min` / `Ord::max` around), pinning
2181    // the four `Nil` / `Int` / `Bool` / `Float` non-compound atom arms —
2182    // each `const`-constructible in-place through its raw arm ctor and
2183    // each of which must fall through the accessor's `_ => None` arm.
2184    // Any regression that drops `pub const fn` back to `pub fn` (a body
2185    // edit that reaches for a non-const operation on the accessor path
2186    // — e.g. a `Some(Box::leak(inner.clone()))` regression, an inner
2187    // `Deref::deref` trait-dispatch call that opts out of the compiler's
2188    // built-in `&Box<T>` → `&T` return-position coercion) fails this
2189    // test at compile time rather than at runtime, matching the sibling
2190    // `reader_macro_prefix_is_const` pin on the paired writer-half of
2191    // the reader-macro-arm-set and the caixa-ast source-position primitive
2192    // family's `Span::new` / `Span::point` / `Span::contains` /
2193    // `Span::union` / `Span::len` / `Span::is_empty` / `Position::new` /
2194    // `Position::origin` / `line_column` `pub const fn` shape's const-
2195    // eval discipline extended onto the caixa-ast [`NodeKind`] outer-
2196    // sum-type's per-`Box<Node>`-arm reader-half projection axis.
2197    #[test]
2198    fn as_reader_macro_inner_is_const() {
2199        const NIL: NodeKind = NodeKind::Nil;
2200        const NIL_INNER: Option<&Node> = NIL.as_reader_macro_inner();
2201        const _: () = assert!(NIL_INNER.is_none());
2202
2203        const INT: NodeKind = NodeKind::Int(0);
2204        const INT_INNER: Option<&Node> = INT.as_reader_macro_inner();
2205        const _: () = assert!(INT_INNER.is_none());
2206
2207        const BOOL: NodeKind = NodeKind::Bool(false);
2208        const BOOL_INNER: Option<&Node> = BOOL.as_reader_macro_inner();
2209        const _: () = assert!(BOOL_INNER.is_none());
2210
2211        const FLOAT: NodeKind = NodeKind::Float(0.0);
2212        const FLOAT_INNER: Option<&Node> = FLOAT.as_reader_macro_inner();
2213        const _: () = assert!(FLOAT_INNER.is_none());
2214
2215        // Runtime cross-check against the sibling runtime dispatch — any
2216        // future divergence between the `const fn` path and the runtime
2217        // path (a hand-rolled shadow `impl` overriding one side, a
2218        // `#[cfg(...)]`-gated body that shipped only one lane) trips
2219        // here under `PartialEq` on the `Option<&Node>` return shape.
2220        const fn inner_via_const_fn(k: &NodeKind) -> Option<&Node> {
2221            k.as_reader_macro_inner()
2222        }
2223        let nil = NodeKind::Nil;
2224        assert_eq!(inner_via_const_fn(&nil), nil.as_reader_macro_inner());
2225    }
2226
2227    // Projection contract on the outer-`NodeKind` sum-type's disjunctive
2228    // three-arm `List` | `Map` | `Vector` D4-dialect compound-body arm-
2229    // set accessor: exactly the three compound-carrying arms return
2230    // `Some(&[Node])` byte-borrowed from the matched arm's own
2231    // [`Vec<Node>`] storage; every other arm of the closed fourteen-arm
2232    // partition returns `None`. Pins the "one canonical projection
2233    // dispatch per typed arm-set on the substrate primitive" discipline
2234    // the three per-disjunctive-arm-set consumer sites (caixa-ast
2235    // [`crate::visit::walk`] visitor recursion, caixa-fmt printer
2236    // `emit_header_operand` inlineable gate, caixa-fmt printer `is_atom`
2237    // grid-cell classifier) route through via `.as_seq_body()`. A
2238    // regression that admitted a non-compound arm (an atom, a reader-
2239    // macro wrapper) through this projection would silently reroute the
2240    // walker's recursion into an atom-body traversal at every compound
2241    // site — the three-arm walker converges would start descending into
2242    // atoms and stop descending into brace/bracket dialects.
2243    #[test]
2244    fn as_seq_body_projects_only_compound_arms() {
2245        let variants = all_variants();
2246        for (variant, name) in &variants {
2247            let projected = variant.as_seq_body();
2248            let is_compound_arm = matches!(
2249                variant,
2250                NodeKind::List(_) | NodeKind::Map(_) | NodeKind::Vector(_)
2251            );
2252            if is_compound_arm {
2253                let expected: &[Node] = match variant {
2254                    NodeKind::List(items) | NodeKind::Map(items) | NodeKind::Vector(items) => {
2255                        items.as_slice()
2256                    }
2257                    _ => unreachable!("guarded by is_compound_arm above"),
2258                };
2259                assert_eq!(
2260                    projected,
2261                    Some(expected),
2262                    "NodeKind::{name} is a compound-carrying arm — \
2263                     as_seq_body() must project onto its own Vec<Node> \
2264                     body"
2265                );
2266            } else {
2267                assert_eq!(
2268                    projected, None,
2269                    "NodeKind::{name} is not a compound-carrying arm — \
2270                     as_seq_body() must return None"
2271                );
2272            }
2273        }
2274        // Empty compound — the accessor is a projection, not a gate; an
2275        // empty-`()` list, empty-`{}` map, and empty-`[]` vector each
2276        // round-trip as `Some(&[])`, not `None`, because the arm still
2277        // carries a (zero-length) `Vec<Node>` body a walker is entitled
2278        // to iterate over.
2279        assert_eq!(
2280            NodeKind::List(Vec::new()).as_seq_body(),
2281            Some(&[] as &[Node])
2282        );
2283        assert_eq!(
2284            NodeKind::Map(Vec::new()).as_seq_body(),
2285            Some(&[] as &[Node])
2286        );
2287        assert_eq!(
2288            NodeKind::Vector(Vec::new()).as_seq_body(),
2289            Some(&[] as &[Node])
2290        );
2291        // Strict-compound-body-only boundary vs the sibling reader-macro-
2292        // carrying arms — all four reader-macro arms carry a boxed inner
2293        // `Node`, not a `Vec<Node>` body, so a widening to admit a
2294        // reader-macro arm through `as_seq_body` (silently collapsing the
2295        // semantic distinction between a container `(x y)` and a wrapper
2296        // `'x`) would break every walker that gates on the compound-body
2297        // arm-set. Pinned across all four reader-macro arms — a per-arm-
2298        // specific regression would slip past a single-arm pin.
2299        assert_eq!(
2300            NodeKind::Quote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))).as_seq_body(),
2301            None,
2302            "Quote arm carries a Box<Node> inner but MUST NOT project \
2303             through as_seq_body — the compound-body-only boundary is \
2304             the whole point of a distinct projection"
2305        );
2306        assert_eq!(
2307            NodeKind::Quasiquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))).as_seq_body(),
2308            None,
2309            "Quasiquote arm carries a Box<Node> inner but MUST NOT \
2310             project through as_seq_body — the compound-body-only \
2311             boundary is the whole point of a distinct projection"
2312        );
2313        assert_eq!(
2314            NodeKind::Unquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))).as_seq_body(),
2315            None,
2316            "Unquote arm carries a Box<Node> inner but MUST NOT project \
2317             through as_seq_body — the compound-body-only boundary is \
2318             the whole point of a distinct projection"
2319        );
2320        assert_eq!(
2321            NodeKind::UnquoteSplice(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0))))
2322                .as_seq_body(),
2323            None,
2324            "UnquoteSplice arm carries a Box<Node> inner but MUST NOT \
2325             project through as_seq_body — the compound-body-only \
2326             boundary is the whole point of a distinct projection"
2327        );
2328    }
2329
2330    // Zero-copy pin — `k.as_seq_body()` must borrow from the matched
2331    // arm's own [`Vec<Node>`] storage, not clone into a fresh Vec.
2332    // Fails at build time if a future rewrite regresses to
2333    // `Some(items.clone().as_slice().to_vec().leak())` or any other
2334    // detour that silently allocates on every call. Pinned across all
2335    // three compound-carrying arms (List/Map/Vector) — a per-arm-specific
2336    // regression would slip past a single-arm pin — the same shape as
2337    // the sibling
2338    // [`as_reader_macro_inner_is_by_borrow_pointer_identity`] pin on
2339    // the peer disjunctive reader-macro accessor, extended onto the
2340    // Vec<Node> compound-body indirection every D4-dialect arm carries.
2341    #[test]
2342    fn as_seq_body_is_by_borrow_pointer_identity() {
2343        for (constructor, ctor_name) in [
2344            (
2345                (|items: Vec<Node>| NodeKind::List(items)) as fn(Vec<Node>) -> NodeKind,
2346                "List",
2347            ),
2348            (
2349                (|items: Vec<Node>| NodeKind::Map(items)) as fn(Vec<Node>) -> NodeKind,
2350                "Map",
2351            ),
2352            (
2353                (|items: Vec<Node>| NodeKind::Vector(items)) as fn(Vec<Node>) -> NodeKind,
2354                "Vector",
2355            ),
2356        ] {
2357            let payload = vec![
2358                Node::new(NodeKind::Symbol(format!("s-{ctor_name}")), Span::new(0, 8)),
2359                Node::new(NodeKind::Int(42), Span::new(9, 11)),
2360            ];
2361            let k = constructor(payload);
2362            let via_accessor: &[Node] = k.as_seq_body().unwrap();
2363            let inner: &Vec<Node> = match &k {
2364                NodeKind::List(items) | NodeKind::Map(items) | NodeKind::Vector(items) => items,
2365                _ => unreachable!("constructed above via compound arm ctor"),
2366            };
2367            assert_eq!(
2368                via_accessor.as_ptr(),
2369                inner.as_ptr(),
2370                "NodeKind::as_seq_body on the {ctor_name} arm must borrow \
2371                 from that arm's Vec<Node> backing storage (zero-copy \
2372                 projection)",
2373            );
2374            assert_eq!(
2375                via_accessor.len(),
2376                inner.len(),
2377                "NodeKind::as_seq_body and inner.as_slice() must byte-equal \
2378                 in length (same slice) on the {ctor_name} arm",
2379            );
2380        }
2381    }
2382
2383    // Byte-parity pin on the pre-lift three-arm disjunctive
2384    // `match &_.kind { NodeKind::List(items) | NodeKind::Map(items) |
2385    // NodeKind::Vector(items) => Some(items.as_slice()), _ => None }`
2386    // shape the three caixa-ast/caixa-fmt consumer sites route through
2387    // today via `.as_seq_body()`. Refuses a future accidental split
2388    // between the accessor's return contract and its pre-lift
2389    // disjunctive shape (a hand-rolled shadow `impl` that overrides
2390    // one path, an accidental rebrand of one converged call site back
2391    // to the raw three-arm `NodeKind::List(items) | …` form, a
2392    // widening to admit a reader-macro arm) on the load-bearing
2393    // compound-body-carrying disjunctive axis every downstream walker
2394    // recursion / header-inliner / grid-cell classifier partitions on.
2395    #[test]
2396    fn as_seq_body_byte_equal_pre_lift_pattern_match_shape() {
2397        for (variant, name) in all_variants() {
2398            let via_pattern: Option<&[Node]> = match &variant {
2399                NodeKind::List(items) | NodeKind::Map(items) | NodeKind::Vector(items) => {
2400                    Some(items.as_slice())
2401                }
2402                _ => None,
2403            };
2404            let via_accessor = variant.as_seq_body();
2405            assert_eq!(
2406                via_accessor, via_pattern,
2407                "NodeKind::{name}.as_seq_body() must byte-equal \
2408                 `match &_ {{ NodeKind::List(items) | NodeKind::Map(items) \
2409                 | NodeKind::Vector(items) => Some(items.as_slice()), _ \
2410                 => None }}` — otherwise the three converged caixa-ast/ \
2411                 caixa-fmt walker / header-inliner / grid-cell classifier \
2412                 sites would silently disagree with their pre-lift shape"
2413            );
2414        }
2415    }
2416
2417    // Pin the const-eval surface: the substrate-primitive reader-half-of-
2418    // the-reader/writer-duality projection accessor on the D4-dialect
2419    // compound-body arm-set reaches into `const` context, so a future
2420    // compile-time caixa-fmt writer-side compound-body-lookup truth-table
2421    // / caixa-lint no-empty-compound-in-header-position rule / caixa-lsp
2422    // writer const-registry / caixa-teia manifest-lowerer const-fixture
2423    // that keys off `NodeKind::as_seq_body` lands directly on the accessor
2424    // without a runtime-context escape hatch. Pairs with the sibling
2425    // writer-half `seq_delims_is_const` pin so BOTH halves of the
2426    // reader/writer-duality on the D4-dialect compound-arm-set now sit in
2427    // `const` context — the body slice the arm carries AND the two
2428    // delimiter bytes the arm reads back as. The `const _: () = assert!(…)`
2429    // bindings resolve the projection at compile time on the non-compound
2430    // arm-set (the compound arms themselves carry a `Vec<Node>` payload
2431    // with an `impl Drop` whose const-drop stability is behind the same
2432    // rust-lang/rust #143874 tracking issue the sibling `Span::union`
2433    // open-codes `Ord::min` / `Ord::max` around), pinning the four `Nil` /
2434    // `Int` / `Bool` / `Float` non-compound atom arms — each of which is
2435    // `const`-constructible in-place through its raw arm ctor
2436    // (`NodeKind::Nil` a unit ctor; `NodeKind::Int(i64)` / `Bool(bool)` /
2437    // `Float(f64)` all one-slot tuple-newtype ctors on `Copy` scalar
2438    // payloads) and each of which must fall through the accessor's
2439    // `_ => None` arm. Any regression that drops `pub const fn` back to
2440    // `pub fn` (a body edit that reaches for a non-const operation on the
2441    // accessor path — e.g. an `.iter().collect::<Vec<_>>().leak()` detour
2442    // that would silently allocate on every call) fails this test at
2443    // compile time rather than at runtime, matching the sibling
2444    // `as_list_projection_is_const_fn` pin on the paired single-arm
2445    // compound projection, the `seq_delims_is_const` pin on the paired
2446    // writer-half of the compound-arm-set, and the caixa-ast source-
2447    // position primitive family's `pub const fn` shape's const-eval
2448    // discipline extended onto the caixa-ast [`NodeKind`] outer-sum-type's
2449    // last remaining `Option<&_>` projection axis.
2450    #[test]
2451    fn as_seq_body_is_const() {
2452        const NIL: NodeKind = NodeKind::Nil;
2453        const NIL_BODY: Option<&[Node]> = NIL.as_seq_body();
2454        const _: () = assert!(NIL_BODY.is_none());
2455
2456        const INT: NodeKind = NodeKind::Int(0);
2457        const INT_BODY: Option<&[Node]> = INT.as_seq_body();
2458        const _: () = assert!(INT_BODY.is_none());
2459
2460        const BOOL: NodeKind = NodeKind::Bool(false);
2461        const BOOL_BODY: Option<&[Node]> = BOOL.as_seq_body();
2462        const _: () = assert!(BOOL_BODY.is_none());
2463
2464        const FLOAT: NodeKind = NodeKind::Float(0.0);
2465        const FLOAT_BODY: Option<&[Node]> = FLOAT.as_seq_body();
2466        const _: () = assert!(FLOAT_BODY.is_none());
2467
2468        // Runtime cross-check against the sibling runtime dispatch — any
2469        // future divergence between the `const fn` path and the runtime
2470        // path (a hand-rolled shadow `impl` overriding one side, a
2471        // `#[cfg(...)]`-gated body that shipped only one lane) trips
2472        // here under `PartialEq` on the `Option<&[Node]>` return shape.
2473        const fn body_via_const_fn(k: &NodeKind) -> Option<&[Node]> {
2474            k.as_seq_body()
2475        }
2476        let empty_list = NodeKind::List(Vec::new());
2477        let empty_map = NodeKind::Map(Vec::new());
2478        let empty_vec = NodeKind::Vector(Vec::new());
2479        let nil = NodeKind::Nil;
2480        assert_eq!(body_via_const_fn(&empty_list), empty_list.as_seq_body());
2481        assert_eq!(body_via_const_fn(&empty_map), empty_map.as_seq_body());
2482        assert_eq!(body_via_const_fn(&empty_vec), empty_vec.as_seq_body());
2483        assert_eq!(body_via_const_fn(&nil), nil.as_seq_body());
2484    }
2485
2486    // Projection contract on the outer-`NodeKind` sum-type's writer-side
2487    // delimiter-pair accessor: exactly the three D4-dialect compound-
2488    // carrying arms return `Some((open, close))` with the per-arm
2489    // reader-side delimiter pair (`List` → `('(', ')')`, `Map` → `('{',
2490    // '}')`, `Vector` → `('[', ']')`); every other arm of the closed
2491    // fourteen-arm partition returns `None`. Pins the "one canonical
2492    // projection dispatch per typed arm-set on the substrate primitive"
2493    // discipline the four per-consumer writer sites in
2494    // caixa-fmt/src/printer.rs route through via `.seq_delims()`. A
2495    // regression that flipped one arm's delimiter pair (a copy-paste
2496    // that routed `List` through `('[', ']')` and `Vector` through
2497    // `('(', ')')`) would silently start rendering every parenthesised
2498    // form as a bracket vector and every vector as a list at every
2499    // writer site.
2500    #[test]
2501    fn seq_delims_projects_only_compound_arms_with_dialect_pair() {
2502        let variants = all_variants();
2503        for (variant, name) in &variants {
2504            let projected = variant.seq_delims();
2505            let expected = match variant {
2506                NodeKind::List(_) => Some(('(', ')')),
2507                NodeKind::Map(_) => Some(('{', '}')),
2508                NodeKind::Vector(_) => Some(('[', ']')),
2509                _ => None,
2510            };
2511            assert_eq!(
2512                projected, expected,
2513                "NodeKind::{name}.seq_delims() must project onto the \
2514                 per-arm reader-side delimiter pair (List → ('(', ')'), \
2515                 Map → ('{{', '}}'), Vector → ('[', ']')) — otherwise \
2516                 the four converged caixa-fmt writer sites would \
2517                 silently start emitting the wrong delimiters"
2518            );
2519        }
2520        // Empty compound — the accessor is a projection over the arm
2521        // discriminator, not a gate on the body length; an empty-`()`
2522        // list, empty-`{}` map, and empty-`[]` vector each round-trip
2523        // as `Some((open, close))` with the arm's own pair, because the
2524        // arm still IS a `List` / `Map` / `Vector` regardless of body
2525        // arity.
2526        assert_eq!(NodeKind::List(Vec::new()).seq_delims(), Some(('(', ')')));
2527        assert_eq!(NodeKind::Map(Vec::new()).seq_delims(), Some(('{', '}')));
2528        assert_eq!(NodeKind::Vector(Vec::new()).seq_delims(), Some(('[', ']')));
2529        // Strict-compound-arm-only boundary vs the sibling reader-macro-
2530        // carrying arms — all four reader-macro arms are wrapper syntax
2531        // (`'x` / `` `x `` / `,x` / `,@x`) with no delimiter PAIR (the
2532        // one-character sigil is not a matched pair the writer emits
2533        // around a body), so a future widening to admit a reader-macro
2534        // arm through `seq_delims` would silently start bracketing
2535        // reader-macro inners as if they were containers. Pinned across
2536        // all four reader-macro arms — a per-arm-specific regression
2537        // would slip past a single-arm pin.
2538        assert_eq!(
2539            NodeKind::Quote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))).seq_delims(),
2540            None,
2541            "Quote arm is reader-macro sigil syntax with no delimiter \
2542             pair — MUST NOT project through seq_delims"
2543        );
2544        assert_eq!(
2545            NodeKind::Quasiquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))).seq_delims(),
2546            None,
2547            "Quasiquote arm is reader-macro sigil syntax with no \
2548             delimiter pair — MUST NOT project through seq_delims"
2549        );
2550        assert_eq!(
2551            NodeKind::Unquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))).seq_delims(),
2552            None,
2553            "Unquote arm is reader-macro sigil syntax with no delimiter \
2554             pair — MUST NOT project through seq_delims"
2555        );
2556        assert_eq!(
2557            NodeKind::UnquoteSplice(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0))))
2558                .seq_delims(),
2559            None,
2560            "UnquoteSplice arm is reader-macro sigil syntax with no \
2561             delimiter pair — MUST NOT project through seq_delims"
2562        );
2563    }
2564
2565    // Contract with sibling [`NodeKind::as_seq_body`] — for every
2566    // variant, either both accessors return `Some` (the three D4-
2567    // dialect compound arms) or both return `None` (every other arm).
2568    // Pinned live rather than by inspection: a future addition of a
2569    // fourth compound arm (a hypothetical `NodeKind::Set(Vec<Node>)`
2570    // once the tatara-lisp reader grows a `#{…}` set literal) that
2571    // extended one accessor but not the other would silently produce
2572    // a walker-half-of-the-reader/writer-duality drift (as_seq_body
2573    // Some but seq_delims None → the emit dispatch's `.expect(…)` on
2574    // the writer half would panic at runtime for a body the walker
2575    // already treats as compound; the reverse would let the writer
2576    // emit delimiters around a body the walker refuses to descend
2577    // into). The `.expect(…)` calls at the caixa-fmt converge sites
2578    // are load-bearing on this partition-equivalence — this test
2579    // is what turns them from a runtime assertion into a build-time
2580    // pin.
2581    #[test]
2582    fn seq_delims_partitions_the_same_arm_set_as_as_seq_body() {
2583        for (variant, name) in all_variants() {
2584            assert_eq!(
2585                variant.seq_delims().is_some(),
2586                variant.as_seq_body().is_some(),
2587                "NodeKind::{name} — seq_delims().is_some() must equal \
2588                 as_seq_body().is_some() (reader/writer-duality partition \
2589                 equivalence); the caixa-fmt writer sites depend on this \
2590                 for their `expect(…)` on the delims half after gating \
2591                 through as_seq_body"
2592            );
2593        }
2594    }
2595
2596    // Byte-parity pin on the pre-lift three-arm disjunctive
2597    // `match &_.kind { NodeKind::List(_) => Some(('(', ')')) | … | _ =>
2598    // None }` shape the four caixa-fmt writer sites route through
2599    // today via `.seq_delims()`. Refuses a future accidental split
2600    // between the accessor's return contract and its pre-lift
2601    // per-arm shape (a hand-rolled shadow `impl` that overrides one
2602    // path, an accidental rebrand of one converged call site back to
2603    // the raw `NodeKind::List(_) => Delims::PAREN | …` form, a per-
2604    // arm delimiter-pair flip) on the load-bearing writer-half-of-the-
2605    // reader/writer-duality axis every downstream compound-emit site
2606    // partitions on.
2607    #[test]
2608    fn seq_delims_byte_equal_pre_lift_pattern_match_shape() {
2609        for (variant, name) in all_variants() {
2610            let via_pattern: Option<(char, char)> = match &variant {
2611                NodeKind::List(_) => Some(('(', ')')),
2612                NodeKind::Map(_) => Some(('{', '}')),
2613                NodeKind::Vector(_) => Some(('[', ']')),
2614                _ => None,
2615            };
2616            let via_accessor = variant.seq_delims();
2617            assert_eq!(
2618                via_accessor, via_pattern,
2619                "NodeKind::{name}.seq_delims() must byte-equal \
2620                 `match &_ {{ NodeKind::List(_) => Some(('(', ')')) | \
2621                 NodeKind::Map(_) => Some(('{{', '}}')) | \
2622                 NodeKind::Vector(_) => Some(('[', ']')) | _ => None }}` \
2623                 — otherwise the four converged caixa-fmt writer sites \
2624                 would silently disagree with their pre-lift shape"
2625            );
2626        }
2627    }
2628
2629    // Pin the const-eval surface: the substrate-primitive writer-half-of-
2630    // the-reader/writer-duality projection accessor reaches into `const`
2631    // context, so a future compile-time caixa-fmt writer-side delimiter-
2632    // pair-lookup truth-table / caixa-lint no-delimiter-in-non-compound-
2633    // context rule / caixa-lsp writer const-registry can key off
2634    // `NodeKind::seq_delims` without being forced onto the runtime code
2635    // path. The `const _: () = assert!(…)` bindings resolve the projection
2636    // at compile time on the non-compound arm-set (the compound arms
2637    // themselves carry a `Vec<Node>` payload with an `impl Drop` whose
2638    // const-drop stability is behind the same rust-lang/rust #143874
2639    // tracking issue the sibling `Span::union` open-codes `Ord::min` /
2640    // `Ord::max` around), pinning the four `Nil` / `Int` / `Bool` /
2641    // `Float` non-compound atom arms — each of which is
2642    // `const`-constructible in-place through its raw arm ctor
2643    // (`NodeKind::Nil` a unit ctor; `NodeKind::Int(i64)` / `Bool(bool)` /
2644    // `Float(f64)` all one-slot tuple-newtype ctors on `Copy` scalar
2645    // payloads) and each of which must fall through the accessor's
2646    // `_ => None` arm. Any regression that drops `pub const fn` back to
2647    // `pub fn` (a body edit that reaches for a non-const operation on
2648    // the accessor path) fails this test at compile time rather than at
2649    // runtime, matching the sibling
2650    // `reader_macro_prefix_is_const` pin on the paired reader-macro-arm-
2651    // set writer-half accessor and the caixa-ast source-position primitive
2652    // family's `Span::new` / `Span::point` / `Span::contains` /
2653    // `Span::union` / `Span::len` / `Span::is_empty` / `Position::new` /
2654    // `Position::origin` / `line_column` `pub const fn` shape's const-
2655    // eval discipline extended onto the caixa-ast [`NodeKind`] outer-
2656    // sum-type's per-arm identity-projection axis.
2657    #[test]
2658    fn seq_delims_is_const() {
2659        const NIL: NodeKind = NodeKind::Nil;
2660        const NIL_DELIMS: Option<(char, char)> = NIL.seq_delims();
2661        const _: () = assert!(NIL_DELIMS.is_none());
2662
2663        const INT: NodeKind = NodeKind::Int(0);
2664        const INT_DELIMS: Option<(char, char)> = INT.seq_delims();
2665        const _: () = assert!(INT_DELIMS.is_none());
2666
2667        const BOOL: NodeKind = NodeKind::Bool(false);
2668        const BOOL_DELIMS: Option<(char, char)> = BOOL.seq_delims();
2669        const _: () = assert!(BOOL_DELIMS.is_none());
2670
2671        const FLOAT: NodeKind = NodeKind::Float(0.0);
2672        const FLOAT_DELIMS: Option<(char, char)> = FLOAT.seq_delims();
2673        const _: () = assert!(FLOAT_DELIMS.is_none());
2674    }
2675
2676    // Projection contract on the outer-`NodeKind` sum-type's writer-side
2677    // reader-macro-sigil accessor: exactly the four reader-macro-carrying
2678    // arms return `Some(&'static str)` with the per-arm sigil bytes
2679    // (`Quote` → `"'"`, `Quasiquote` → `` "`" ``, `Unquote` → `","`,
2680    // `UnquoteSplice` → `",@"`); every other arm of the closed fourteen-
2681    // arm partition returns `None`. Pins the "one canonical projection
2682    // dispatch per typed arm-set on the substrate primitive" discipline
2683    // the two per-consumer writer sites in caixa-fmt/src/printer.rs route
2684    // through via `.reader_macro_prefix()`. A regression that flipped one
2685    // arm's sigil (a copy-paste that routed `Quote` through `` "`" `` and
2686    // `Quasiquote` through `"'"`, or `Unquote` through `",@"` and
2687    // `UnquoteSplice` through `","`) would silently rewrite every quote
2688    // as a quasiquote and every unquote-splice as an unquote at every
2689    // writer site.
2690    #[test]
2691    fn reader_macro_prefix_projects_only_reader_macro_arms_with_sigil_bytes() {
2692        let variants = all_variants();
2693        for (variant, name) in &variants {
2694            let projected = variant.reader_macro_prefix();
2695            let expected = match variant {
2696                NodeKind::Quote(_) => Some("'"),
2697                NodeKind::Quasiquote(_) => Some("`"),
2698                NodeKind::Unquote(_) => Some(","),
2699                NodeKind::UnquoteSplice(_) => Some(",@"),
2700                _ => None,
2701            };
2702            assert_eq!(
2703                projected, expected,
2704                "NodeKind::{name}.reader_macro_prefix() must project onto \
2705                 the per-arm reader-side sigil (Quote → \"'\", Quasiquote \
2706                 → \"`\", Unquote → \",\", UnquoteSplice → \",@\") — \
2707                 otherwise the two converged caixa-fmt writer sites would \
2708                 silently start emitting the wrong sigil"
2709            );
2710        }
2711        // Strict-reader-macro-arm-only boundary vs the sibling D4-dialect
2712        // compound-carrying arms — all three compound arms carry a
2713        // `Vec<Node>` body with matched delimiter pairs (`(…)`, `{…}`,
2714        // `[…]`), not a one- or two-byte sigil prefix, so a future
2715        // widening to admit a compound arm through `reader_macro_prefix`
2716        // (silently collapsing the semantic distinction between a
2717        // reader-macro wrapper `'x` and a container `(x)`) would break
2718        // every writer site that gates on the reader-macro-arm-set.
2719        // Pinned across all three compound arms — a per-arm-specific
2720        // regression would slip past a single-arm pin.
2721        assert_eq!(
2722            NodeKind::List(Vec::new()).reader_macro_prefix(),
2723            None,
2724            "List arm is a matched-delimiter compound container with no \
2725             one-byte reader-macro sigil prefix — MUST NOT project through \
2726             reader_macro_prefix"
2727        );
2728        assert_eq!(
2729            NodeKind::Map(Vec::new()).reader_macro_prefix(),
2730            None,
2731            "Map arm is a matched-delimiter compound container with no \
2732             one-byte reader-macro sigil prefix — MUST NOT project through \
2733             reader_macro_prefix"
2734        );
2735        assert_eq!(
2736            NodeKind::Vector(Vec::new()).reader_macro_prefix(),
2737            None,
2738            "Vector arm is a matched-delimiter compound container with no \
2739             one-byte reader-macro sigil prefix — MUST NOT project through \
2740             reader_macro_prefix"
2741        );
2742    }
2743
2744    // Contract with sibling [`NodeKind::as_reader_macro_inner`] — for
2745    // every variant, either both accessors return `Some` (the four reader-
2746    // macro arms) or both return `None` (every other arm). Pinned live
2747    // rather than by inspection: a future addition of a fifth reader-macro
2748    // arm (a hypothetical `NodeKind::Splice(Box<Node>)` once the tatara-
2749    // lisp reader grows a splicing-quote variant, a `NodeKind::TaggedQuote
2750    // (Box<Node>, QuoteTag)` shape once the sexp→JSON bridge stabilizes
2751    // tagged-quote sugar) that extended one accessor but not the other
2752    // would silently produce a walker-half-of-the-reader/writer-duality
2753    // drift (as_reader_macro_inner Some but reader_macro_prefix None →
2754    // the emit dispatch's `.expect(…)` on the writer half would panic at
2755    // runtime for a wrapper the walker already treats as reader-macro;
2756    // the reverse would let the writer emit a sigil around a payload the
2757    // walker refuses to descend into). The `.expect(…)` calls at the
2758    // caixa-fmt converge sites are load-bearing on this partition-
2759    // equivalence — this test is what turns them from a runtime assertion
2760    // into a build-time pin. Sibling in shape to
2761    // `seq_delims_partitions_the_same_arm_set_as_as_seq_body` on the
2762    // peer D4-dialect compound axis.
2763    #[test]
2764    fn reader_macro_prefix_partitions_the_same_arm_set_as_as_reader_macro_inner() {
2765        for (variant, name) in all_variants() {
2766            assert_eq!(
2767                variant.reader_macro_prefix().is_some(),
2768                variant.as_reader_macro_inner().is_some(),
2769                "NodeKind::{name} — reader_macro_prefix().is_some() must \
2770                 equal as_reader_macro_inner().is_some() (reader/writer-\
2771                 duality partition equivalence on the reader-macro-arm-\
2772                 set); the caixa-fmt writer sites depend on this for their \
2773                 `expect(…)` on the prefix half after gating through \
2774                 as_reader_macro_inner"
2775            );
2776        }
2777    }
2778
2779    // Byte-parity pin on the pre-lift four-arm disjunctive
2780    // `match &_.kind { NodeKind::Quote(_) => Some("'") | Quasiquote(_) =>
2781    // Some("`") | Unquote(_) => Some(",") | UnquoteSplice(_) => Some(",@")
2782    // | _ => None }` shape the two caixa-fmt writer sites route through
2783    // today via `.reader_macro_prefix()`. Refuses a future accidental
2784    // split between the accessor's return contract and its pre-lift
2785    // per-arm shape (a hand-rolled shadow `impl` that overrides one path,
2786    // an accidental rebrand of one converged call site back to the raw
2787    // four-arm `NodeKind::Quote(inner) => self.out.push('\'') | …` form,
2788    // a per-arm sigil flip) on the load-bearing writer-half-of-the-
2789    // reader/writer-duality axis every downstream reader-macro-emit site
2790    // partitions on.
2791    #[test]
2792    fn reader_macro_prefix_byte_equal_pre_lift_pattern_match_shape() {
2793        for (variant, name) in all_variants() {
2794            let via_pattern: Option<&'static str> = match &variant {
2795                NodeKind::Quote(_) => Some("'"),
2796                NodeKind::Quasiquote(_) => Some("`"),
2797                NodeKind::Unquote(_) => Some(","),
2798                NodeKind::UnquoteSplice(_) => Some(",@"),
2799                _ => None,
2800            };
2801            let via_accessor = variant.reader_macro_prefix();
2802            assert_eq!(
2803                via_accessor, via_pattern,
2804                "NodeKind::{name}.reader_macro_prefix() must byte-equal \
2805                 `match &_ {{ NodeKind::Quote(_) => Some(\"'\") | \
2806                 Quasiquote(_) => Some(\"`\") | Unquote(_) => Some(\",\") \
2807                 | UnquoteSplice(_) => Some(\",@\") | _ => None }}` — \
2808                 otherwise the two converged caixa-fmt writer sites would \
2809                 silently disagree with their pre-lift shape"
2810            );
2811        }
2812    }
2813
2814    // Pin the const-eval surface: the substrate-primitive writer-half-of-
2815    // the-reader/writer-duality projection accessor reaches into `const`
2816    // context, so a future compile-time caixa-fmt writer-side sigil-lookup
2817    // truth-table / caixa-lint no-quote-sigil-in-non-reader-macro-context
2818    // rule / caixa-lsp writer const-registry can key off
2819    // `NodeKind::reader_macro_prefix` without being forced onto the runtime
2820    // code path. The `const _: () = assert!(…)` bindings resolve the
2821    // projection at compile time on the non-reader-macro arm-set (the
2822    // reader-macro arms themselves carry a `Box<Node>` payload which
2823    // `Box::new` cannot construct in `const` context yet — that
2824    // half of the const-eval surface unlocks on the same
2825    // rust-lang/rust #143874 tracking issue the sibling `Span::union`
2826    // open-codes `Ord::min` / `Ord::max` around), pinning the four `Nil`
2827    // / `Int` / `Bool` / `Float` non-reader-macro atom arms — each of
2828    // which is `const`-constructible in-place through its raw arm ctor
2829    // (`NodeKind::Nil` a unit ctor; `NodeKind::Int(i64)` / `Bool(bool)` /
2830    // `Float(f64)` all one-slot tuple-newtype ctors on `Copy` scalar
2831    // payloads) and each of which must fall through the accessor's
2832    // `_ => None` arm. Any regression that drops `pub const fn` back to
2833    // `pub fn` (a body edit that reaches for a non-const operation on
2834    // the accessor path) fails this test at compile time rather than at
2835    // runtime, matching the sibling caixa-ast source-position primitive
2836    // family's `Span::new` / `Span::point` / `Span::contains` /
2837    // `Span::union` / `Span::len` / `Span::is_empty` / `Position::new` /
2838    // `Position::origin` / `line_column` `pub const fn` shape's const-
2839    // eval discipline extended onto the caixa-ast [`NodeKind`] outer-
2840    // sum-type's per-arm identity-projection axis.
2841    #[test]
2842    fn reader_macro_prefix_is_const() {
2843        const NIL: NodeKind = NodeKind::Nil;
2844        const NIL_PREFIX: Option<&'static str> = NIL.reader_macro_prefix();
2845        const _: () = assert!(NIL_PREFIX.is_none());
2846
2847        const INT: NodeKind = NodeKind::Int(0);
2848        const INT_PREFIX: Option<&'static str> = INT.reader_macro_prefix();
2849        const _: () = assert!(INT_PREFIX.is_none());
2850
2851        const BOOL: NodeKind = NodeKind::Bool(false);
2852        const BOOL_PREFIX: Option<&'static str> = BOOL.reader_macro_prefix();
2853        const _: () = assert!(BOOL_PREFIX.is_none());
2854
2855        const FLOAT: NodeKind = NodeKind::Float(0.0);
2856        const FLOAT_PREFIX: Option<&'static str> = FLOAT.reader_macro_prefix();
2857        const _: () = assert!(FLOAT_PREFIX.is_none());
2858    }
2859
2860    // Pin the [`Node::to_tatara_sexp`] compound-arm-set converge onto the
2861    // lifted [`NodeKind::as_seq_body`] `Option<&[Node]>` accessor.
2862    //
2863    // Every one of the three D4-dialect compound arms (List, Map, Vector)
2864    // must lower to a `Sexp::List` whose body is the arm's own child
2865    // sequence lowered element-wise — the pre-lift shape had `List` and
2866    // `Map | Vector` as two match arms restating the identical
2867    // `Sexp::List(items.iter().map(Node::to_tatara_sexp).collect())`
2868    // body, and this pin refuses a future accidental split where one arm
2869    // silently changes shape (a per-arm `NodeKind::Map(items) =>
2870    // Sexp::List(items.iter().rev().map(…).collect())` reorder, a
2871    // `NodeKind::Vector(items) => Sexp::Nil` drop, a partial arm-set
2872    // widening back to the raw pattern-match that misses a future D4-
2873    // adjacent compound arm addition). Sibling in shape to the peer
2874    // `as_seq_body_projects_only_compound_arms` partition pin on the
2875    // walker half of the compound axis, extended onto the substrate-side
2876    // lowering-half converge in `Node::to_tatara_sexp`.
2877    #[test]
2878    fn to_tatara_sexp_lowers_every_compound_arm_to_sexp_list_with_identical_body() {
2879        use tatara_lisp::{Atom, Sexp};
2880        let expected_body: Vec<Sexp> = vec![
2881            Sexp::Atom(Atom::Symbol("a".into())),
2882            Sexp::Atom(Atom::Int(1)),
2883            Sexp::Atom(Atom::Keyword("k".into())),
2884        ];
2885        let child_nodes: Vec<Node> = vec![
2886            Node::new(NodeKind::Symbol("a".into()), Span::new(0, 0)),
2887            Node::new(NodeKind::Int(1), Span::new(0, 0)),
2888            Node::new(NodeKind::Keyword("k".into()), Span::new(0, 0)),
2889        ];
2890        for (ctor, name) in [
2891            (NodeKind::List as fn(Vec<Node>) -> NodeKind, "List"),
2892            (NodeKind::Map, "Map"),
2893            (NodeKind::Vector, "Vector"),
2894        ] {
2895            let node = Node::new(ctor(child_nodes.clone()), Span::new(0, 0));
2896            let lowered = node.to_tatara_sexp();
2897            match lowered {
2898                Sexp::List(body) => assert_eq!(
2899                    body, expected_body,
2900                    "NodeKind::{name} — to_tatara_sexp must lower to \
2901                     Sexp::List whose body is the element-wise lowering \
2902                     of the arm's own child sequence, byte-identically \
2903                     across all three D4-dialect compound arms"
2904                ),
2905                other => panic!(
2906                    "NodeKind::{name}.to_tatara_sexp() must lower to \
2907                     Sexp::List (Sexp has no Map/Vector variant yet — see \
2908                     theory/TATARA-LISP-CONSOLIDATION.md D4 Phase 2); \
2909                     got {other:?}"
2910                ),
2911            }
2912        }
2913        // Empty compound — the lowering is a projection, not a gate; an
2914        // empty-`()` list, empty-`{}` map, and empty-`[]` vector each
2915        // lower to `Sexp::List(vec![])`, not `Sexp::Nil`, because the
2916        // arm still carries a (zero-length) child sequence a lowerer is
2917        // entitled to iterate over. Pinned across all three compound
2918        // arms — a per-arm-specific regression would slip past a single-
2919        // arm pin.
2920        for (ctor, name) in [
2921            (NodeKind::List as fn(Vec<Node>) -> NodeKind, "List"),
2922            (NodeKind::Map, "Map"),
2923            (NodeKind::Vector, "Vector"),
2924        ] {
2925            let empty = Node::new(ctor(Vec::new()), Span::new(0, 0));
2926            let lowered = empty.to_tatara_sexp();
2927            match lowered {
2928                Sexp::List(body) => assert!(
2929                    body.is_empty(),
2930                    "empty NodeKind::{name} — to_tatara_sexp must lower \
2931                     to Sexp::List(vec![]), not Sexp::Nil or a \
2932                     stringified delimiter"
2933                ),
2934                other => panic!(
2935                    "empty NodeKind::{name}.to_tatara_sexp() must lower \
2936                     to Sexp::List(vec![]); got {other:?}"
2937                ),
2938            }
2939        }
2940    }
2941
2942    // Byte-parity pin on the pre-lift four-arm compound-body shape
2943    // `match &self.kind { NodeKind::List(items) => Sexp::List(items.iter()
2944    // .map(Node::to_tatara_sexp).collect()) | NodeKind::Map(items) |
2945    // NodeKind::Vector(items) => Sexp::List(items.iter().map(Node::
2946    // to_tatara_sexp).collect()) | _ => (fall through to atoms) }` shape
2947    // the [`Node::to_tatara_sexp`] compound-body dispatch routed through
2948    // before the converge. Refuses a future accidental split between the
2949    // accessor-routed compound-body dispatch and its pre-lift per-arm
2950    // shape (a hand-rolled shadow `impl` that overrides one path, an
2951    // accidental rebrand of the converged dispatch back to the raw two-
2952    // arm pattern-match with a fourth new-D4-arm silently missed, a per-
2953    // arm body-map reorder that would silently corrupt only one of the
2954    // three arms). Sibling in shape to the peer
2955    // `seq_delims_byte_equal_pre_lift_pattern_match_shape` /
2956    // `as_seq_body_byte_equal_pre_lift_pattern_match_shape` pins on the
2957    // sibling writer + walker halves of the compound axis.
2958    #[test]
2959    #[allow(
2960        clippy::match_same_arms,
2961        reason = "the two same-body compound arms ARE the pre-lift shape \
2962                  this pin reproduces; collapsing them would erase what \
2963                  the pin documents"
2964    )]
2965    fn to_tatara_sexp_compound_arm_body_byte_equal_pre_lift_pattern_match_shape() {
2966        use tatara_lisp::Sexp;
2967        let child_nodes: Vec<Node> = vec![
2968            Node::new(NodeKind::Nil, Span::new(0, 0)),
2969            Node::new(NodeKind::Symbol("h".into()), Span::new(0, 0)),
2970            Node::new(NodeKind::Str("s".into()), Span::new(0, 0)),
2971            Node::new(NodeKind::Float(1.5), Span::new(0, 0)),
2972            Node::new(NodeKind::Bool(true), Span::new(0, 0)),
2973        ];
2974        for (ctor, name) in [
2975            (NodeKind::List as fn(Vec<Node>) -> NodeKind, "List"),
2976            (NodeKind::Map, "Map"),
2977            (NodeKind::Vector, "Vector"),
2978        ] {
2979            let node = Node::new(ctor(child_nodes.clone()), Span::new(0, 0));
2980            let via_accessor = node.to_tatara_sexp();
2981            // Reconstruct the pre-lift per-arm shape byte-for-byte from the
2982            // arm's own child sequence, independent of the accessor path
2983            // — a hand-rolled shadow that reproduces the pre-lift dispatch.
2984            let via_pattern = match &node.kind {
2985                NodeKind::List(items) => {
2986                    Sexp::List(items.iter().map(Node::to_tatara_sexp).collect())
2987                }
2988                NodeKind::Map(items) | NodeKind::Vector(items) => {
2989                    Sexp::List(items.iter().map(Node::to_tatara_sexp).collect())
2990                }
2991                _ => unreachable!("guarded by the ctor loop above"),
2992            };
2993            let via_pattern_body = match via_pattern {
2994                Sexp::List(b) => b,
2995                other => panic!("pre-lift shape must produce Sexp::List; got {other:?}"),
2996            };
2997            let via_accessor_body = match via_accessor {
2998                Sexp::List(b) => b,
2999                other => panic!(
3000                    "NodeKind::{name}.to_tatara_sexp() — accessor-routed \
3001                     dispatch must produce Sexp::List; got {other:?}"
3002                ),
3003            };
3004            assert_eq!(
3005                via_accessor_body, via_pattern_body,
3006                "NodeKind::{name}.to_tatara_sexp() body must byte-equal \
3007                 the pre-lift per-arm `Sexp::List(items.iter().map(Node::\
3008                 to_tatara_sexp).collect())` shape — otherwise the \
3009                 converged dispatch would silently disagree with the \
3010                 pre-lift shape at exactly this compound arm"
3011            );
3012            // A grounding sanity check: the body carries five children
3013            // (one per child_nodes element), each of which lowered to a
3014            // Sexp::Atom (the atom arms) — a per-child drop or duplicate
3015            // would slip past a pure body-vs-body equality if BOTH the
3016            // accessor path and the pre-lift shadow shared the same bug.
3017            assert_eq!(
3018                via_accessor_body.len(),
3019                child_nodes.len(),
3020                "NodeKind::{name}.to_tatara_sexp() must preserve the \
3021                 arm's child-count exactly"
3022            );
3023            for (i, s) in via_accessor_body.iter().enumerate() {
3024                assert!(
3025                    matches!(s, Sexp::Atom(_) | Sexp::Nil),
3026                    "NodeKind::{name}.to_tatara_sexp() body[{i}] — every \
3027                     child in this fixture is an atom or nil arm; a \
3028                     compound projection here would signal a lowerer \
3029                     drift"
3030                );
3031            }
3032        }
3033    }
3034}