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}