tatara_lisp/error.rs
1use thiserror::Error;
2
3use crate::span::Span;
4
5pub type Result<T> = std::result::Result<T, LispError>;
6
7// Compile-time pairwise-distinctness witnesses — one `const _: () =
8// crate::ast::assert_str_array_pairwise_distinct(&…)` per family-wide
9// `[&'static str; N]` array on this module's closed-set diagnostic /
10// classification algebras. Each invocation is const-evaluated at
11// `cargo check` time; a regression that silently collides two entries
12// on any of these arrays fails the build rather than the test suite.
13//
14// Sibling to the runtime `_pairwise_distinct` tests (`compiler_spec_
15// io_stage_labels_pairwise_distinct`, `macro_def_head_keywords_
16// pairwise_distinct`, `unquote_form_markers_pairwise_distinct`,
17// `unquote_form_iac_forge_tags_pairwise_distinct`,
18// `unquote_form_labels_pairwise_distinct`, `kwarg_path_kind_labels_
19// pairwise_distinct`, `expected_kwarg_shape_labels_pairwise_distinct`,
20// `sexp_shape_labels_pairwise_distinct`,
21// `structural_kind_labels_pairwise_distinct`) plus the runtime cross-
22// check `assert_str_array_pairwise_distinct_accepts_every_family_
23// wide_error_module_array` on this module's tests submodule — the
24// three surfaces enforce the SAME theorem at THREE stages of the
25// toolchain (compile-time `const _` sweep, per-array runtime pin,
26// runtime cross-check), so a build that skips tests still catches
27// the regression here, and a build that runs tests catches it a
28// second and third time as safety nets if the const-eval sweep is
29// ever silently dropped.
30//
31// Column-dual peer to the FIVE `assert_str_array_pairwise_distinct`
32// witnesses on `ast.rs`'s closed-set outer algebras
33// (`Atom::BOOL_LITERALS`, `AtomKind::LABELS`, `QuoteForm::PREFIXES`,
34// `QuoteForm::IAC_FORGE_TAGS`, `QuoteForm::LABELS`) on the
35// (declaration-file) axis: `ast.rs` covers the reader / atomic-
36// payload / quote-family vocabulary; `error.rs` covers the
37// diagnostic / classification / typed-entry-rejection vocabulary
38// across NINE closed-set algebras (`CompilerSpecIoStage`,
39// `TemplateInvariantKind` (STATIC + DYNAMIC subsets),
40// `MacroDefHead`, `OptionalParamMalformedReason` (STATIC + DYNAMIC
41// subsets), `UnquoteForm` (three per-role vocabularies —
42// MARKERS / IAC_FORGE_TAGS / LABELS), `KwargPathKind`,
43// `ExpectedKwargShape`, `SexpShape`, `StructuralKind`) — the two
44// files together lift ALL EIGHTEEN family-wide `[&'static str; N]`
45// arrays declared on `tatara-lisp`'s closed-set outer algebras into
46// a COMPILE-TIME pairwise-distinctness theorem.
47//
48// `CompilerSpecIoStage::OPERATIONS` (`[&'static str; 4]` at
49// line 1744) is DELIBERATELY excluded from this block: it is a
50// variant-aligned projection whose four entries are
51// `[REALIZE_TO_DISK, REALIZE_TO_DISK, LOAD_FROM_DISK, LOAD_FROM_DISK]`
52// (the first two arms and the last two arms collapse onto the
53// per-operation labels), so the array is INTENTIONALLY non-distinct.
54// Enrolling it in this block would fail const-eval; the intended
55// invariant on `OPERATIONS` is "variant-index alignment with
56// `CompilerSpecIoStage::ALL`", not injectivity — different theorem,
57// different helper. The peer helper the substrate now carries is
58// `crate::ast::assert_str_array_is_concatenation_of_two_scalar_
59// replicas` on the (contract-shape ∈ {MANY-TO-ONE-BLOCK-CONSTANCY})
60// corner of the (`&'static str`) row; its ONE `const _` witness
61// binding `CompilerSpecIoStage::OPERATIONS` lives further down this
62// module in the STR-BLOCK-CONSTANCY witness block (search for
63// `assert_str_array_is_concatenation_of_two_scalar_replicas`).
64//
65// Adding a new family-wide `[&'static str; N]` array to this module:
66// add ONE co-located `const _: () = crate::ast::assert_str_array_
67// pairwise_distinct(&<TYPE>::<ARRAY>);` line in this block AND ONE
68// runtime call in the cross-check test on the tests submodule. A
69// regression that silently collided two entries on the new array
70// would fail `cargo check` at the module-level `const _` line
71// before the test suite runs. Rustc's forced-arity `[&'static str;
72// N]` composes with this const-eval sweep so BOTH cardinality AND
73// injectivity are compile-time theorems on the SAME array
74// declaration.
75const _: () = crate::ast::assert_str_array_pairwise_distinct(&CompilerSpecIoStage::LABELS);
76const _: () =
77 crate::ast::assert_str_array_pairwise_distinct(&TemplateInvariantKind::STATIC_MESSAGES);
78const _: () =
79 crate::ast::assert_str_array_pairwise_distinct(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
80const _: () = crate::ast::assert_str_array_pairwise_distinct(&MacroDefHead::KEYWORDS);
81const _: () =
82 crate::ast::assert_str_array_pairwise_distinct(&OptionalParamMalformedReason::STATIC_LABELS);
83const _: () = crate::ast::assert_str_array_pairwise_distinct(
84 &OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
85);
86const _: () = crate::ast::assert_str_array_pairwise_distinct(&UnquoteForm::MARKERS);
87const _: () = crate::ast::assert_str_array_pairwise_distinct(&UnquoteForm::IAC_FORGE_TAGS);
88const _: () = crate::ast::assert_str_array_pairwise_distinct(&UnquoteForm::LABELS);
89const _: () = crate::ast::assert_str_array_pairwise_distinct(&KwargPathKind::LABELS);
90const _: () = crate::ast::assert_str_array_pairwise_distinct(&ExpectedKwargShape::LABELS);
91const _: () = crate::ast::assert_str_array_pairwise_distinct(&SexpShape::LABELS);
92const _: () = crate::ast::assert_str_array_pairwise_distinct(&StructuralKind::LABELS);
93
94// Compile-time NONEMPTY-CARDINALITY-LOWER-BOUND witnesses — one
95// `const _: () = crate::ast::assert_str_array_all_nonempty(&…)` per
96// family-wide `[&'static str; N]` array declared in this module.
97// Sibling to the `_pairwise_distinct` witnesses above — those pin
98// INJECTIVITY on each array (`∀ i ≠ j : arr[i] ≠ arr[j]`), these pin
99// the strictly-weaker per-entry cardinality gate (`∀ i : arr[i].len()
100// > 0`). The two contracts compose orthogonally: an array carrying
101// `["", ""]` fails INJECTIVITY at the zero-length-pair corner, but an
102// array carrying `["", "a"]` passes INJECTIVITY while failing NONEMPTY
103// — so the NONEMPTY witnesses close the remaining `""`-carrying corner
104// that INJECTIVITY alone cannot pin. Every consumer that spells a
105// closed-set variant through its `&'static str` label
106// (`AtomKind::label` / `QuoteForm::label` / `UnquoteForm::label` /
107// `StructuralKind::label` / `SexpShape::label` label projections;
108// `KwargPathKind::label` / `ExpectedKwargShape::label` for the kwarg-
109// diagnostic vocabulary; `MacroDefHead::KEYWORDS` /
110// `UnquoteForm::MARKERS` / `UnquoteForm::IAC_FORGE_TAGS` for the
111// reader-boundary sub-vocabularies; `CompilerSpecIoStage::LABELS` for
112// the compiler-spec I/O stage diagnostic) treats each entry as a
113// NONEMPTY identifier and would silently mis-behave on a `""` entry.
114// The two `TemplateInvariantKind` message / descriptor arrays and the
115// two `OptionalParamMalformedReason` message / descriptor arrays are
116// nonempty for the same reason — an empty diagnostic message erases
117// the failure signal at the display boundary.
118const _: () = crate::ast::assert_str_array_all_nonempty(&CompilerSpecIoStage::LABELS);
119const _: () = crate::ast::assert_str_array_all_nonempty(&TemplateInvariantKind::STATIC_MESSAGES);
120const _: () =
121 crate::ast::assert_str_array_all_nonempty(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
122const _: () = crate::ast::assert_str_array_all_nonempty(&MacroDefHead::KEYWORDS);
123const _: () =
124 crate::ast::assert_str_array_all_nonempty(&OptionalParamMalformedReason::STATIC_LABELS);
125const _: () =
126 crate::ast::assert_str_array_all_nonempty(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
127const _: () = crate::ast::assert_str_array_all_nonempty(&UnquoteForm::MARKERS);
128const _: () = crate::ast::assert_str_array_all_nonempty(&UnquoteForm::IAC_FORGE_TAGS);
129const _: () = crate::ast::assert_str_array_all_nonempty(&UnquoteForm::LABELS);
130const _: () = crate::ast::assert_str_array_all_nonempty(&KwargPathKind::LABELS);
131const _: () = crate::ast::assert_str_array_all_nonempty(&ExpectedKwargShape::LABELS);
132const _: () = crate::ast::assert_str_array_all_nonempty(&SexpShape::LABELS);
133const _: () = crate::ast::assert_str_array_all_nonempty(&StructuralKind::LABELS);
134
135// Compile-time ASCII-BYTE-RANGE witnesses — one `const _: () =
136// crate::ast::assert_str_array_all_ascii(&…)` per family-wide
137// `[&'static str; N]` array declared in this module. Sibling to the
138// `_all_nonempty` witnesses above on the (per-entry × contract-shape)
139// axis: those pin the length-lower-bound gate (`∀ i : arr[i].len() >
140// 0`), these pin the strictly-orthogonal byte-range gate (`∀ i, ∀ b ∈
141// arr[i].as_bytes() : b <= 0x7F`). The two contracts compose
142// orthogonally — an array carrying `["café"]` passes NONEMPTY while
143// failing ASCII; `["", "a"]` passes ASCII while failing NONEMPTY.
144// Every consumer that ships an entry through a seven-bit-clean
145// downstream surface (K8s annotation keys + label values; YAML
146// flow-scalar map keys; BLAKE3 hash inputs; Rust byte-pattern
147// `matches!` arms) treats each entry as ASCII — a non-ASCII byte
148// silently invites Unicode-normalization drift on the wire and
149// lookalike-label collisions that byte-equality parsing cannot
150// detect. Post-lift a regression that silently re-inlined one label
151// constant to a lookalike non-ASCII spelling fails at `cargo check`
152// BEFORE any test scheduler runs. The thirteen arrays covered here
153// mirror the thirteen arrays already pinned by the `_all_nonempty`
154// witnesses above — the (per-entry × contract-shape) coverage matrix
155// on the (`&'static str`) row of this module now holds at BOTH
156// corners {NONEMPTY, ASCII} for every family-wide array declared
157// here.
158const _: () = crate::ast::assert_str_array_all_ascii(&CompilerSpecIoStage::LABELS);
159const _: () = crate::ast::assert_str_array_all_ascii(&TemplateInvariantKind::STATIC_MESSAGES);
160const _: () = crate::ast::assert_str_array_all_ascii(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
161const _: () = crate::ast::assert_str_array_all_ascii(&MacroDefHead::KEYWORDS);
162const _: () = crate::ast::assert_str_array_all_ascii(&OptionalParamMalformedReason::STATIC_LABELS);
163const _: () =
164 crate::ast::assert_str_array_all_ascii(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
165const _: () = crate::ast::assert_str_array_all_ascii(&UnquoteForm::MARKERS);
166const _: () = crate::ast::assert_str_array_all_ascii(&UnquoteForm::IAC_FORGE_TAGS);
167const _: () = crate::ast::assert_str_array_all_ascii(&UnquoteForm::LABELS);
168const _: () = crate::ast::assert_str_array_all_ascii(&KwargPathKind::LABELS);
169const _: () = crate::ast::assert_str_array_all_ascii(&ExpectedKwargShape::LABELS);
170const _: () = crate::ast::assert_str_array_all_ascii(&SexpShape::LABELS);
171const _: () = crate::ast::assert_str_array_all_ascii(&StructuralKind::LABELS);
172
173// Compile-time SUBSET-embedding witnesses — the THREE family-wide
174// `[&'static str; N]` sub-vocabularies of the substrate's twelve-arm
175// outer-shape label vocabulary at `SexpShape::LABELS` whose distinct-
176// value set is an intentionally-closed PROPER SUBSET of the outer
177// twelve-arm `[&'static str; 12]` superset. Pre-lift the three subset
178// relations lived only as prose in the outer array's twelve-arm
179// partition-rule docstring plus runtime per-role alias-chain checks
180// (each per-role `<TYPE>::<ROLE>_LABEL` on the three sub-vocabulary
181// algebras (`AtomKind`, `QuoteForm`, `StructuralKind`) aliases the
182// namesake `SexpShape::<ROLE>_LABEL` on the outer algebra); post-lift
183// the ARRAY-LEVEL subset embedding of the three pinned pairs binds at
184// rustc time via ONE `const _` line per pair. A regression that
185// silently re-inlined either the SUBSET side (e.g. dropping
186// `AtomKind::SYMBOL_LABEL`'s `SexpShape::SYMBOL_LABEL` alias to a
187// fresh distinct byte spelling `"sym"` — the array would still be
188// pairwise-distinct AND still disjoint from `QuoteForm::LABELS`, but
189// the subset embedding into `SexpShape::LABELS` would break) or the
190// SUPERSET side (e.g. dropping `SexpShape::SYMBOL_LABEL` and leaving
191// `AtomKind::SYMBOL_LABEL` as a stale copy of `"symbol"` outside the
192// outer vocabulary) fails at `cargo check` BEFORE any test scheduler
193// runs. Sibling to the FIVE `assert_str_array_pairwise_distinct`
194// witnesses on `ast.rs` and the THIRTEEN `assert_str_array_pairwise_
195// distinct` witnesses above on the (contract-shape ∈ {pairwise-
196// distinctness}) column: those pin INJECTIVITY on each individual
197// array; these pin SUBSET containment ORIENTED across PAIRS of arrays
198// on the (contract-shape ∈ {subset-embedding}) column.
199//
200// The three pinned pairs are (all under the shared `&'static str`
201// element-type):
202// 1. `AtomKind::LABELS` ⊂ `SexpShape::LABELS` (6-in-12 arms —
203// atomic-payload kind labels; symbol / keyword / string / int /
204// float / bool).
205// 2. `QuoteForm::LABELS` ⊂ `SexpShape::LABELS` (4-in-12 arms —
206// quote-family labels; quote / quasiquote / unquote / unquote-
207// splice).
208// 3. `StructuralKind::LABELS` ⊂ `SexpShape::LABELS` (2-in-12 arms —
209// structural-shape labels; nil / list).
210//
211// Cardinality composition: 6 + 4 + 2 = 12 = `SexpShape::LABELS.len()`
212// closes a NON-CONTIGUOUS PARTITION of the outer twelve-arm
213// vocabulary at compile time when composed with the THREE pairwise-
214// disjointness witnesses on the sub-vocabulary triple: one lives on
215// `ast.rs` at the (`AtomKind::LABELS`, `QuoteForm::LABELS`) pair (the
216// two sub-vocabularies whose HOST TYPES both live in `ast.rs`), the
217// two peers below at the two (`_, StructuralKind::LABELS`) pairs
218// (each with ONE sub-vocabulary hosted here in `error.rs`). Together
219// with the pre-existing pairwise-distinctness witnesses on each of
220// the four arrays, the three subset-embedding witnesses on the three
221// sub-vocabulary → superset pairs, AND the three pairwise-disjointness
222// witnesses on the three sub-vocabulary × sub-vocabulary pairs, the
223// substrate now carries a COMPLETE COMPILE-TIME PROOF that
224// `SexpShape::LABELS` is a DISJOINT UNION of the three sub-vocabulary
225// arrays — every rearrangement of the twelve outer-shape labels
226// across the four algebras is either (a) a legal partition rebalance
227// (which passes every witness) or (b) drift that breaks at least one
228// of INJECTIVITY / SUBSET-EMBEDDING / PAIRWISE-DISJOINTNESS on at
229// least one array or pair, failing `cargo check` before any test
230// scheduler runs.
231const _: () = crate::ast::assert_str_array_within_str_finite_set::<6, 12>(
232 &crate::ast::AtomKind::LABELS,
233 &SexpShape::LABELS,
234);
235const _: () = crate::ast::assert_str_array_within_str_finite_set::<4, 12>(
236 &crate::ast::QuoteForm::LABELS,
237 &SexpShape::LABELS,
238);
239const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 12>(
240 &StructuralKind::LABELS,
241 &SexpShape::LABELS,
242);
243
244// Compile-time DISJOINTNESS witnesses closing the twelve-arm
245// `SexpShape::LABELS` partition proof — the TWO remaining sub-
246// vocabulary × sub-vocabulary pairs on the partition triple
247// (`AtomKind::LABELS`, `QuoteForm::LABELS`, `StructuralKind::LABELS`)
248// whose two hosts split across `ast.rs` and `error.rs` (the third
249// pair `(AtomKind::LABELS, QuoteForm::LABELS)`, whose two hosts both
250// live in `ast.rs`, is pinned there). Pre-lift the two disjointness
251// relations lived only as prose in the outer partition-rule
252// docstring plus the runtime `sexp_shape_labels_are_disjoint_union`
253// cross-check; post-lift the ARRAY-LEVEL disjointness of the two
254// pinned pairs binds at rustc time via ONE `const _` line per pair.
255// A regression that silently renamed `StructuralKind::NIL_LABEL`
256// (`"nil"`) to `"symbol"` (aliasing `AtomKind::SYMBOL_LABEL`),
257// renamed `StructuralKind::LIST_LABEL` (`"list"`) to `"quote"`
258// (aliasing `QuoteForm::QUOTE_LABEL`), or drifted either entry of
259// `StructuralKind::LABELS` to bytes shared with an entry of the
260// atomic-kind or quote-family peer sub-vocabulary fails at
261// `cargo check` BEFORE any test scheduler runs. Sibling to the FOUR
262// `assert_str_arrays_disjoint` witnesses in `ast.rs`.
263//
264// The two pinned pairs are (all under the shared `&'static str`
265// element-type):
266// 1. `AtomKind::LABELS` ∩ `StructuralKind::LABELS` = ∅
267// 2. `QuoteForm::LABELS` ∩ `StructuralKind::LABELS` = ∅
268//
269// Composed with the ast.rs-pinned
270// (`AtomKind::LABELS`, `QuoteForm::LABELS`) disjointness witness,
271// the THREE pairwise-disjointness witnesses cover every pair on the
272// three-element sub-vocabulary triple (C(3, 2) = 3 pairs). Together
273// with the three pairwise-distinctness INTRA-array witnesses AND the
274// three SUBSET-embedding witnesses above, the substrate now carries
275// the full DISJOINT-UNION theorem
276// `SexpShape::LABELS ≡ AtomKind::LABELS ⊕ QuoteForm::LABELS ⊕
277// StructuralKind::LABELS`
278// as a rustc-time proof obligation, closing the twelve-arm outer
279// vocabulary partition at compile time.
280const _: () = crate::ast::assert_str_arrays_disjoint::<6, 2>(
281 &crate::ast::AtomKind::LABELS,
282 &StructuralKind::LABELS,
283);
284const _: () = crate::ast::assert_str_arrays_disjoint::<4, 2>(
285 &crate::ast::QuoteForm::LABELS,
286 &StructuralKind::LABELS,
287);
288
289// Compile-time (⊆) SET-COVERAGE witness closing the twelve-arm
290// `SexpShape::LABELS` disjoint-union theorem — every entry of the
291// parent outer-shape vocabulary is reached by at least one of the
292// three sub-vocabularies (`AtomKind::LABELS`, `QuoteForm::LABELS`,
293// `StructuralKind::LABELS`). Composes with the pre-existing (⊇)
294// SUBSET-embedding witnesses (three `_within_str_finite_set` lines
295// above), the pairwise-DISJOINTNESS witnesses (two above plus one
296// (`AtomKind::LABELS` ∩ `QuoteForm::LABELS`) in `ast.rs`), and the
297// parent INJECTIVITY witness (`assert_str_array_pairwise_distinct(
298// &SexpShape::LABELS)` above) to close the FULL disjoint-union
299// theorem
300// `SexpShape::LABELS ≡ AtomKind::LABELS ⊕ QuoteForm::LABELS ⊕
301// StructuralKind::LABELS`
302// as a rustc-time proof obligation. Pre-lift the (⊆) direction lived
303// only as the runtime cross-check
304// `sexp_shape_labels_is_disjoint_union_of_three_sub_vocabularies` at
305// the tests submodule; post-lift the (⊆) direction binds at `cargo
306// check` time via ONE `const _` line on the partition quadruple. A
307// regression that silently drops a variant from ONE sub-vocabulary
308// (e.g. removing `AtomKind::Bool` and its `AtomKind::BOOL_LABEL`
309// alias while leaving `SexpShape::Bool` and `SexpShape::BOOL_LABEL`
310// in the parent vocabulary) fires the SET-STR-MISSING panic at
311// `cargo check` BEFORE any test scheduler runs.
312//
313// Composition law with the sibling (⊇) SUBSET + pairwise-
314// DISJOINTNESS + parent INJECTIVITY witnesses: given the four
315// corners, the disjoint-union theorem follows CONSTRUCTIVELY —
316// (⊇) every sub-vocabulary label is in the parent (three SUBSET
317// witnesses);
318// (⊆) every parent label is in some sub-vocabulary (this witness);
319// pairwise-disjoint sub-vocabularies (three DISJOINTNESS witnesses);
320// parent has no duplicate entries (INJECTIVITY witness).
321// The cardinality composition `6 + 4 + 2 = 12` is a CONSEQUENCE of
322// the four corners rather than a separate pre-check; the rustc-
323// enforced arities `[&'static str; N]` for N ∈ {6, 4, 2, 12} pin the
324// cardinality quadruple at the declaration site.
325const _: () = crate::ast::assert_str_finite_set_covered_by_three_str_arrays::<6, 4, 2, 12>(
326 &crate::ast::AtomKind::LABELS,
327 &crate::ast::QuoteForm::LABELS,
328 &StructuralKind::LABELS,
329 &SexpShape::LABELS,
330);
331
332// Compile-time SLICE-EQUALS-ARRAY witnesses closing the twelve-arm
333// `SexpShape::LABELS` POSITIONAL decomposition — the FOUR canonical
334// sub-slices of the twelve-slot outer container each byte-equal a
335// sub-carving's canonical `[&'static str; M]` listing at rustc time.
336// Strictly STRONGER on the (contract-strength) axis than the seven
337// sibling SET-level witnesses above (three `_within_str_finite_set`
338// SUBSET-embedding + three `_arrays_disjoint` pairwise-disjointness +
339// one `_finite_set_covered_by_three_str_arrays` coverage): the SET-
340// level disjoint-union theorem `SexpShape::LABELS ≡ AtomKind::LABELS ⊕
341// QuoteForm::LABELS ⊕ StructuralKind::LABELS` those seven witnesses
342// close is SILENT on which SLOTS each sub-vocabulary's arms occupy —
343// a regression that permuted `SexpShape::LABELS` from the CANONICAL
344// declaration order `[NIL, SYMBOL, KEYWORD, STRING, INT, FLOAT, BOOL,
345// LIST, QUOTE, QUASIQUOTE, UNQUOTE, UNQUOTE_SPLICE]` (`StructuralKind`
346// at slots `{0, 7}`, `AtomKind` at slots `[1..7)`, `QuoteForm` at
347// slots `[8..12)`) to `[SYMBOL, NIL, KEYWORD, STRING, INT, FLOAT,
348// BOOL, LIST, QUOTE, QUASIQUOTE, UNQUOTE, UNQUOTE_SPLICE]` (swapping
349// slots `0` and `1`, interleaving `AtomKind` into a slot the
350// structural-residual carving previously owned) preserves the SET-
351// level disjoint-union theorem (both sub-vocabularies still embed
352// into the parent, still cover, still disjoint, parent still
353// injective) but silently misaligns every consumer indexing
354// `SexpShape::LABELS[0]` for the NIL diagnostic literal. Post-lift
355// the ARRAY-LEVEL positional decomposition binds at rustc time via
356// FOUR `const _` lines below, one invocation stage earlier than the
357// runtime pin.
358//
359// Sibling posture to the (u8)-row's FOUR positional witnesses on
360// `SexpShape::HASH_DISCRIMINATORS` at `ast.rs` (two singleton
361// `assert_u8_array_slice_equals_u8_array::<12, 1, {0, 7}>` witnesses
362// on `[0..1)` + `[7..8)`, one six-slot `assert_u8_array_slice_is_
363// scalar_replica::<12, 1, 7>` witness on `[1..7)`, one four-slot
364// `assert_u8_array_slice_equals_u8_array::<12, 4, 8>` witness on
365// `[8..12)`). The (u8) row's `[1..7)` slice collapses to a SCALAR
366// replica (all six atomic-payload shapes share the outer-`Sexp`
367// `Atom` marker byte `1u8`); the (str) row's `[1..7)` slice preserves
368// all six SLOTS individually (each atomic variant carries a DISTINCT
369// diagnostic label byte-string). The (str) row's stronger information
370// content at the `[1..7)` slice is precisely why the (str) row picks
371// up the FULL slice-equals-array shape at ALL FOUR slices (not the
372// SCALAR-REPLICA sibling at the interior slice).
373//
374// The FOUR pinned pairs — all on the shared `&'static str` element-
375// type and on the shared parent `[&'static str; 12]` outer container
376// `SexpShape::LABELS`:
377// 1. `SexpShape::LABELS[0..1) == [StructuralKind::NIL_LABEL]`
378// (singleton left-endpoint slot, structural-residual NIL role)
379// 2. `SexpShape::LABELS[1..7) == AtomKind::LABELS`
380// (six-slot atomic-payload middle slice — the six atomic-kind
381// labels in CANONICAL variant-declaration order)
382// 3. `SexpShape::LABELS[7..8) == [StructuralKind::LIST_LABEL]`
383// (singleton mirror-endpoint slot at the atomic-collapse right
384// endpoint, structural-residual LIST role)
385// 4. `SexpShape::LABELS[8..12) == QuoteForm::LABELS`
386// (four-slot quote-family tail — the four quote-family labels in
387// CANONICAL variant-declaration order)
388//
389// The four disjoint slice ranges `[0..1) ∪ [1..7) ∪ [7..8) ∪ [8..12)`
390// exhaust the twelve-slot outer container's position space —
391// `1 + 6 + 1 + 4 = 12`. Composed with the pre-existing pairwise-
392// distinctness witness on the parent array, the FOUR positional
393// witnesses jointly close the POSITIONAL DECOMPOSITION theorem
394// `SexpShape::LABELS == [NIL] ++ AtomKind::LABELS ++ [LIST] ++
395// QuoteForm::LABELS`
396// as a rustc-time proof obligation, strictly stronger than the SET-
397// level disjoint-union theorem the seven sibling witnesses above
398// close. A regression that reordered ANY of the twelve slots in the
399// outer array's initializer fails HERE at `cargo check` BEFORE any
400// test scheduler runs.
401//
402// The two singleton pairs use INLINE `[&'static str; 1]` singleton
403// arrays rather than `&StructuralKind::LABELS[0..1]` slice syntax
404// because the helper's signature takes `&[&'static str; M]` (a const-
405// generic array reference, arity-known at rustc time) rather than
406// `&[&'static str]` (a slice type with runtime-length). The inline
407// arrays project the two per-role `pub const *_LABEL` bytes directly
408// — a regression that renamed one of the two aliases fails at the
409// alias's declaration site FIRST (a missing symbol referent), which
410// routes to a distinct diagnostic axis rather than to the witness's
411// STR-SLICE-EQUALS-ARRAY-VIOLATION panic.
412const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 1, 0>(
413 &SexpShape::LABELS,
414 &[StructuralKind::NIL_LABEL],
415);
416const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 6, 1>(
417 &SexpShape::LABELS,
418 &crate::ast::AtomKind::LABELS,
419);
420const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 1, 7>(
421 &SexpShape::LABELS,
422 &[StructuralKind::LIST_LABEL],
423);
424const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 4, 8>(
425 &SexpShape::LABELS,
426 &crate::ast::QuoteForm::LABELS,
427);
428
429// Compile-time SUBSET-embedding witnesses closing the `[&'static str;
430// N]` STR row of the (UnquoteForm ⊂ QuoteForm) 2-of-4 subset-carve
431// axis on the substrate's `str`-side vocabulary triple. Pre-lift the
432// three subset relations lived per-role at the SCALAR alias sites on
433// this module (`UnquoteForm::UNQUOTE_MARKER = crate::ast::QuoteForm::
434// UNQUOTE_PREFIX`, `UnquoteForm::SPLICE_MARKER = crate::ast::QuoteForm::
435// UNQUOTE_SPLICE_PREFIX`, `UnquoteForm::UNQUOTE_LABEL = crate::ast::
436// QuoteForm::UNQUOTE_LABEL`, `UnquoteForm::SPLICE_LABEL = crate::ast::
437// QuoteForm::UNQUOTE_SPLICE_LABEL`, `UnquoteForm::UNQUOTE_IAC_FORGE_
438// TAG = crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG`, `UnquoteForm::
439// SPLICE_IAC_FORGE_TAG = crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_
440// FORGE_TAG`) plus a runtime cross-check that iterated each subset
441// array and matched against the parent superset's array. The ARRAY-
442// LEVEL subset embedding of the three sibling vocabularies onto their
443// respective four-arm supersets was NOT bound at rustc const-eval time
444// — a regression that silently re-inlined the subset array with a
445// fresh literal drifted OUT of the alias-composition (e.g.
446// `UnquoteForm::LABELS: [&'static str; 2] = ["unquote", "splice"]`
447// with the second entry spelling `"splice"` instead of the aliased
448// `"unquote-splice"`) would still pass INJECTIVITY (the array remains
449// pairwise-distinct) AND still pass every DISJOINTNESS witness above
450// (the array's shared vocabulary with `AtomKind::LABELS` /
451// `StructuralKind::LABELS` doesn't gain a member) BUT would silently
452// break the `UnquoteForm::to_quote_form()` composition law consumers
453// route through — every `UnquoteForm::LABELS[i]` would fail its
454// composition round-trip through the parent superset's label
455// projection at runtime rather than at rustc time.
456//
457// The three pinned pairs are (all under the shared `&'static str`
458// element-type):
459// 1. `UnquoteForm::LABELS` ⊂ `crate::ast::QuoteForm::LABELS`
460// (2-in-4 arms — the two substitution-subset LABELS embed into
461// the four-arm quote-family LABELS' vocabulary).
462// 2. `UnquoteForm::MARKERS` ⊂ `crate::ast::QuoteForm::PREFIXES`
463// (2-in-4 arms — the two substitution-subset reader-marker
464// prefixes embed into the four-arm quote-family reader-prefix
465// vocabulary).
466// 3. `UnquoteForm::IAC_FORGE_TAGS` ⊂ `crate::ast::QuoteForm::IAC_FORGE_TAGS`
467// (2-in-4 arms — the two substitution-subset iac-forge canonical-
468// form tag bytes embed into the four-arm quote-family iac-forge
469// canonical-form tag vocabulary).
470//
471// Sibling to the pre-existing u8-row `assert_u8_array_within_u8_
472// finite_set::<2, 4>(&UnquoteForm::HASH_DISCRIMINATORS,
473// &QuoteForm::HASH_DISCRIMINATORS)` witness at `ast.rs`: together
474// the u8 + str rows close the (element-type × vocabulary-axis) 2×3 =
475// 6-corner face on the (UnquoteForm ⊂ QuoteForm) 2-of-4 subset-carve
476// column at compile time. A future fourth vocabulary axis (e.g. a
477// hypothetical `UnquoteForm::LEADS ⊂ QuoteForm::LEADS` on the
478// `[char; N]` element-type row) picks up ONE new `const _:` witness
479// through `assert_char_array_within_char_finite_set` with the same
480// posture — the 3-vocabulary str row extends mechanically to a fourth
481// (element-type × vocabulary-axis) column.
482//
483// Composition law with the sibling u8-row witness: for every `i ∈
484// [0, 2)`, all four projections stay in lockstep — `UnquoteForm::
485// LABELS[i] == QuoteForm::UNQUOTE_LABEL` or `QuoteForm::UNQUOTE_
486// SPLICE_LABEL` (at some `j ∈ [0, 4)`), and similarly for MARKERS
487// through PREFIXES, IAC_FORGE_TAGS through IAC_FORGE_TAGS, and (via
488// the pre-existing u8 witness) HASH_DISCRIMINATORS through HASH_
489// DISCRIMINATORS. The FOUR per-axis subset-embedding witnesses
490// jointly enforce the (UnquoteForm ⊂ QuoteForm) 2-of-4 subset-carve
491// on every vocabulary axis simultaneously; any single-axis regression
492// (a drifted marker byte, a drifted label spelling, a drifted iac-
493// forge tag, a drifted cache-key byte) fails cargo check on ONE of
494// the four witnesses BEFORE it reaches the runtime cross-check on
495// `unquote_form_v_marker_aliases_quote_form_v_prefix_bytes` /
496// `unquote_form_v_label_aliases_quote_form_v_label_bytes` /
497// `unquote_form_v_iac_forge_tag_aliases_quote_form_v_iac_forge_tag_
498// bytes`.
499const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 4>(
500 &UnquoteForm::LABELS,
501 &crate::ast::QuoteForm::LABELS,
502);
503const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 4>(
504 &UnquoteForm::MARKERS,
505 &crate::ast::QuoteForm::PREFIXES,
506);
507const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 4>(
508 &UnquoteForm::IAC_FORGE_TAGS,
509 &crate::ast::QuoteForm::IAC_FORGE_TAGS,
510);
511
512// Compile-time SLICE-EQUALS-ARRAY witnesses tightening the (UnquoteForm ⊂
513// QuoteForm) 2-of-4 subset-carve from set-level SUBSET to positionwise
514// SLICE-EQUALS at a specific contiguous START offset on the SAME three
515// `[&'static str; N]` sub-vocabulary → superset pairs the SUBSET witnesses
516// above cover. Pre-lift, the sub-carving's positional relationship to the
517// superset was pinned indirectly: (a) each sub-array's INJECTIVITY via the
518// three `assert_str_array_pairwise_distinct` witnesses at the top of this
519// module, (b) each sub-array's SET-EMBEDDING via the three SUBSET witnesses
520// immediately above, (c) each per-role scalar (`UnquoteForm::UNQUOTE_MARKER =
521// QuoteForm::UNQUOTE_PREFIX`, etc.) via alias equality at rustc time. What
522// NEITHER (a) + (b) NOR (c) alone pinned was the ARRAY-LEVEL POSITIONWISE
523// composition — that `UnquoteForm::MARKERS == QuoteForm::PREFIXES[2..4]`
524// element-for-element. A regression that silently swapped the two per-role
525// slot bindings in the sub-array initializer (e.g. `UnquoteForm::MARKERS:
526// [&'static str; 2] = [Self::SPLICE_MARKER, Self::UNQUOTE_MARKER]`) would
527// still pass INJECTIVITY (both aliases distinct), still pass SET-EMBEDDING
528// (both aliases still appear in `QuoteForm::PREFIXES`), still pass per-role
529// scalar equality (each alias still binds to the same `QuoteForm::*_PREFIX`
530// source constant), BUT would silently mis-align every consumer that reads
531// the sub-array by slot ordinal (`UnquoteForm::MARKERS[0]` semantically
532// naming the two-quote-family-arms-in-declaration-order for e.g. a per-slot
533// error diagnostic that mirrors `QuoteForm`'s slot ordinal 2 vs 3). The
534// SLICE-EQUALS witness pins the sub-carving's slot-ordering to a SPECIFIC
535// contiguous slice of the superset (`[2..4)` — the two UnquoteForm-family
536// arms sit at the tail of QuoteForm's four-arm declaration listing,
537// after the two non-substitution `Quote` + `Quasiquote` arms). Both drifts
538// the SUBSET witness stayed silent on (sub-carving reorder AND SUPERSET
539// reorder that leaves the sub-array's two aliases still present but at
540// different positions) fail HERE at rustc time with the
541// `STR-SLICE-EQUALS-ARRAY-VIOLATION` axis panic naming the drifted position.
542//
543// Sibling posture to the four-witness `SexpShape::LABELS` (12) SLICE-EQUALS
544// cluster at lines 331..=346 above (the outer twelve-arm SexpShape
545// vocabulary bound as the SEGMENTED CONCATENATION `[NIL_LABEL] ++
546// AtomKind::LABELS ++ [LIST_LABEL] ++ QuoteForm::LABELS` at rustc time via
547// four `assert_str_array_slice_equals_str_array` witnesses on the four
548// slot segments). Together the seven witnesses close the SLICE-EQUALS
549// column on BOTH sub-carving directions: those four pin sub-vocabularies
550// AS SEGMENTS OF the twelve-arm SexpShape outer array (the outer superset
551// pinned by-segment against its sub-carvings); these three pin
552// UnquoteForm sub-arrays AS SLICES OF the QuoteForm superset (the outer
553// superset pinned as the position-source for the sub-carvings). The
554// (SexpShape ⊃ {AtomKind, QuoteForm, StructuralKind}) SUPERSET-AS-SEGMENTS
555// posture and the (UnquoteForm ⊂ QuoteForm) SUB-AS-SLICE posture are the
556// two orientations of the same positionwise-composition theorem the
557// SLICE-EQUALS helper carries.
558const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 2, 2>(
559 &crate::ast::QuoteForm::LABELS,
560 &UnquoteForm::LABELS,
561);
562const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 2, 2>(
563 &crate::ast::QuoteForm::PREFIXES,
564 &UnquoteForm::MARKERS,
565);
566const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 2, 2>(
567 &crate::ast::QuoteForm::IAC_FORGE_TAGS,
568 &UnquoteForm::IAC_FORGE_TAGS,
569);
570
571// Compile-time STR-DISJOINTNESS witnesses closing the ARRAY-LEVEL
572// vocabulary-disjointness corner of the STATIC ⊕ DYNAMIC partition
573// idiom on the substrate's TWO peer diagnostic algebras
574// (`TemplateInvariantKind` — the bytecode-runtime invariant surface;
575// `OptionalParamMalformedReason` — the `&optional` parser rejection
576// surface). Each algebra carries the SAME closed-set carving idiom
577// documented on `Self::DYNAMIC_DESCRIPTORS`'s docstring: "every closed-
578// set outer algebra the substrate carries now pins BOTH its STATIC-
579// subset canonical-bytes array AND its DYNAMIC-subset per-role-slot
580// array at ONE `pub const` per axis" — TemplateInvariantKind at
581// 2 ⊕ 2 asymmetric widths, OptionalParamMalformedReason at 3 ⊕ 1
582// asymmetric widths. Pre-lift, EACH array's INTRA-array pairwise-
583// distinctness bound at compile time via the four `assert_str_array_
584// pairwise_distinct` witnesses at the top of this module — but the
585// CROSS-array (STATIC ∩ DYNAMIC = ∅) vocabulary-disjointness clause
586// lived ONLY as an emergent property of the two carvings' semantic
587// contract (STATIC arms bind FULL diagnostic bytes; DYNAMIC arms bind
588// only the noun-slot bytes that a shared `format!("… {descriptor} …")`
589// template interpolates). Post-lift the ARRAY-LEVEL disjointness of
590// the STATIC ⊕ DYNAMIC 2-of-2 face binds at rustc time via ONE `const _`
591// line per algebra. A regression that silently drifts a DYNAMIC
592// descriptor into a STATIC message/label byte (e.g. renames
593// `TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR` from `"param"`
594// into `"compiled template produced no value"`, colliding with
595// `TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE`; renames
596// `OptionalParamMalformedReason::EXTRA_ELEMENTS_DESCRIPTOR` from
597// `"elements"` into `"empty list"`, colliding with
598// `OptionalParamMalformedReason::EMPTY_LIST_LABEL`) fails at
599// `cargo check` BEFORE any test scheduler runs. The invariant is
600// load-bearing at three consumer layers documented on the two
601// algebras' DYNAMIC_DESCRIPTORS docstrings: (i) `tatara-check`
602// coverage assertions grep the STATIC arms' full label bytes and the
603// DYNAMIC arms' descriptor noun bytes as DISJOINT vocabularies, so a
604// collision would silently mis-classify one carving's diagnostic as
605// the other's; (ii) Sekiban audit-trail metrics keyed on the STATIC-
606// subset FULL message OR the DYNAMIC-subset noun-descriptor treat
607// the two byte-spaces as disjoint metric-label sub-vocabularies; (iii)
608// LSP quick-fix vocabularies rendered from the DYNAMIC-arm descriptor
609// slot MUST NOT collide with the STATIC-arm full-label vocabulary
610// (else a quick-fix keyed on the descriptor would fire on a STATIC
611// arm whose full label happens to string-equal the descriptor). The
612// two witnesses close the STR-DISJOINTNESS column on the (algebra ×
613// carving-pair) face at ONE `const _` line per algebra; adding a
614// hypothetical fifth per-algebra rejection reason on either the
615// STATIC or DYNAMIC arm extends only the respective array (with
616// rustc's forced-arity `[&'static str; N]` gate re-firing the
617// disjointness sweep against the extended array in lockstep).
618// Sibling to the FIVE substrate-pinned `assert_str_arrays_disjoint`
619// witnesses on the (SexpShape sub-vocabulary × sub-vocabulary) triple
620// (three at the (`AtomKind`, `QuoteForm`, `StructuralKind`) triple
621// hosted across `ast.rs` and `error.rs`) plus the (UnquoteForm ⊂
622// QuoteForm) subset-embedding face — those witnesses close the DISJOINT
623// UNION theorem on the substrate's twelve-arm outer-shape LABELS
624// vocabulary; these witnesses close the STATIC ⊕ DYNAMIC carving
625// theorem on the substrate's TWO peer diagnostic algebras.
626//
627// The two pinned pairs are (all under the shared `&'static str`
628// element-type):
629// 1. `TemplateInvariantKind::STATIC_MESSAGES` ∩
630// `TemplateInvariantKind::DYNAMIC_DESCRIPTORS` = ∅
631// (2-arm STATIC subset carrying the full `"compiled template:
632// EndList with empty stack"` / `"compiled template produced no
633// value"` messages vs. 2-arm DYNAMIC subset carrying the `"param"`
634// / `"splice"` noun slots — no byte shared).
635// 2. `OptionalParamMalformedReason::STATIC_LABELS` ∩
636// `OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS` = ∅
637// (3-arm STATIC subset carrying the full `"empty list"` /
638// `"missing default"` / `"name not a symbol"` labels vs. 1-arm
639// DYNAMIC subset carrying the `"elements"` noun slot — no byte
640// shared).
641const _: () = crate::ast::assert_str_arrays_disjoint::<2, 2>(
642 &TemplateInvariantKind::STATIC_MESSAGES,
643 &TemplateInvariantKind::DYNAMIC_DESCRIPTORS,
644);
645const _: () = crate::ast::assert_str_arrays_disjoint::<3, 1>(
646 &OptionalParamMalformedReason::STATIC_LABELS,
647 &OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
648);
649
650// Compile-time STR-BLOCK-CONSTANCY witness closing the ARRAY-LEVEL
651// MANY-TO-ONE variant → canonical-projection corner on the substrate's
652// TYPED-VARIANT × CANONICAL-PROJECTION MANY-TO-ONE surface at
653// `CompilerSpecIoStage::OPERATIONS`. Where every OTHER family-wide
654// `[&'static str; N]` array on the substrate carries a BIJECTIVE
655// per-index projection (`AtomKind::LABELS[i]` is bijective with
656// `AtomKind::ALL[i].label()`; `QuoteForm::LABELS[i]` is bijective with
657// `QuoteForm::ALL[i].label()`; `CompilerSpecIoStage::LABELS[i]` is
658// bijective with `CompilerSpecIoStage::ALL[i].label()`) and binds
659// INJECTIVITY through `assert_str_array_pairwise_distinct` at the
660// module top, `CompilerSpecIoStage::OPERATIONS` is the ONE substrate
661// array whose per-index projection is INTENTIONALLY MANY-TO-ONE —
662// the four `CompilerSpecIoStage::ALL[i].operation()` per-variant
663// projections collapse onto EXACTLY TWO canonical operation-label
664// bytes (both `Realize…` variants share `"realize_to_disk"`; both
665// `Load…` variants share `"load_from_disk"`) yielding the
666// `[REALIZE_TO_DISK, REALIZE_TO_DISK, LOAD_FROM_DISK, LOAD_FROM_DISK]`
667// 2-of-2-to-2 block-constant array literal at the declaration site.
668// The pre-existing module-top comment (adjacent to the family-wide
669// `assert_str_array_pairwise_distinct` witnesses) documents the
670// EXCLUSION from injectivity as intentional AND names the intended
671// axis: "variant-index alignment with `CompilerSpecIoStage::ALL`, not
672// injectivity — different theorem, different helper". This witness
673// IS the different helper the comment named — the ARRAY-LEVEL block-
674// constant structure `arr = [head; K] ++ [tail; N - K]` at
675// `(N, K) = (4, 2)`, `(head, tail) = (REALIZE_TO_DISK_OPERATION,
676// LOAD_FROM_DISK_OPERATION)`. Pre-lift the block-constant structure
677// bound only at runtime through `compiler_spec_io_stage_operations_
678// align_with_all_by_index` (positional projection through
679// `stage.operation()`) plus `compiler_spec_io_stage_operations_
680// partition_all_two_ways` (multiplicity counts of the two operation
681// labels); post-lift the ARRAY-LEVEL block-constancy binds at rustc
682// time via ONE `const _` line. A regression that silently flip-
683// flopped the projection (reorder `OPERATIONS` to `[REALIZE, LOAD,
684// REALIZE, LOAD]`; drift a slot to a distinct third operation label;
685// collapse to a single-block `[REALIZE, REALIZE, REALIZE, REALIZE]`)
686// fails at `cargo check` BEFORE any test scheduler runs. The
687// invariant is load-bearing at THREE consumer layers documented on
688// `Self::OPERATIONS`'s docstring: (i) an LSP / REPL completion
689// provider surfacing every legal operation label in an `operation=`
690// metrics query bar (the deduped block-labels IS the ONE typed
691// coverage sweep); (ii) `tatara-check` coverage assertions verifying
692// every workspace `.lisp` file's `CompilerSpec` rejection classifies
693// to some entry of `Self::OPERATIONS`; (iii) Sekiban audit-trail
694// metrics jointly labeled by `Self::operation` × `Self::label` whose
695// `operation=` label set IS the deduped `Self::OPERATIONS` (e.g.
696// `tatara_lisp_compiler_spec_io_total{operation="realize_to_disk",
697// stage="serialize"}`) — all three layers rely on the block-constant
698// partition holding at ONE cargo-check-time site. Adding a hypothetical
699// fifth variant (`LoadFromStrDeserialize` once an in-memory
700// `load_from_str` lands, `RealizeToDiskAtomicReplace` if the realize
701// path grows a crash-safe-rename stage) extends `Self::ALL` ONCE +
702// `Self::operation` ONCE + `Self::OPERATIONS` ONCE — the block-
703// constant witness re-fires against the extended arity through the
704// rustc-forced `[&'static str; N]` gate (with the appropriate new
705// `(N, K)` const-generic reflecting the new 2-of-2-to-3 or 2-of-3-to-2
706// partition) so the extension picks up the block-constancy contract
707// mechanically. Sibling to the FOURTEEN family-wide
708// `assert_str_array_pairwise_distinct` witnesses at the module top on
709// the (contract-shape ∈ {INJECTIVITY}) column of the (`&'static str`)
710// row — this witness closes the (contract-shape ∈ {MANY-TO-ONE-BLOCK-
711// CONSTANCY}) corner peer to the (INJECTIVITY) column at ONE `const
712// _` line, closing the (INJECTIVITY, MANY-TO-ONE-BLOCK-CONSTANCY)
713// 2-corner face on the (`&'static str`) row on the substrate's
714// per-index projection surface.
715const _: () = crate::ast::assert_str_array_is_concatenation_of_two_scalar_replicas::<4, 2>(
716 &CompilerSpecIoStage::OPERATIONS,
717 CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
718 CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
719);
720
721// Compile-time per-position ORDER witnesses closing the (`&'static
722// str`)-row FULL-ARRAY LITERAL column across the FIVE remaining scalar-
723// composed `[&'static str; N]` label vocabularies in this module —
724// `ExpectedKwargShape::LABELS` (7), `KwargPathKind::LABELS` (3),
725// `MacroDefHead::KEYWORDS` (3), `CompilerSpecIoStage::LABELS` (4),
726// `StructuralKind::LABELS` (2). Peer to ast.rs's FIVE-witness
727// (str)-row cluster on `Atom::BOOL_LITERALS` (2), `AtomKind::LABELS`
728// (6), and `QuoteForm::PREFIXES` / `LABELS` / `IAC_FORGE_TAGS`
729// (4 each) at lines 1252..=1269 in that file, and to the pre-existing
730// FOUR-witness `SexpShape::LABELS` (12) per-position cluster at lines
731// 331..=346 above in this file — together the ten witnesses bind the
732// (str)-row FULL-ARRAY per-position ORDER column across TEN family-
733// wide `[&'static str; N]` label vocabularies on the substrate at
734// rustc time.
735//
736// Each per-role scalar `pub const *_LABEL` / `*_KEYWORD` alias on the
737// closed-set outer algebra binds AT rustc TIME to its CANONICAL
738// literal-str value through the outer array's `[_; N]` initializer
739// positional composition. Two failure axes survive the pre-lift
740// `_pairwise_distinct` cluster at the module top that the post-lift
741// LITERAL witnesses reject at rustc time:
742// 1. Outer-array SLOT REORDER: a regression that swaps
743// `KwargPathKind::NAMED_LABEL` (`"named"`) and
744// `KwargPathKind::ITEM_LABEL` (`"item"`) in the outer
745// `KwargPathKind::LABELS` initializer preserves pairwise-
746// distinctness (both strs still distinct) but silently
747// misaligns every consumer indexing `LABELS[0]` for the canonical
748// NAMED-arm diagnostic dispatch. Analogous reorder-drifts on
749// `ExpectedKwargShape::LABELS` / `MacroDefHead::KEYWORDS` /
750// `CompilerSpecIoStage::LABELS` / `StructuralKind::LABELS`
751// survive the pairwise-distinctness witness the SAME way.
752// 2. Per-role scalar VALUE DRIFT: a silent value-drift of a per-
753// role scalar (e.g. an operator-facing polish
754// `ExpectedKwargShape::LIST_OF_STRINGS_LABEL` from `"list of
755// strings"` to `"list<string>"`; a Racket-compat rename
756// `MacroDefHead::DEFMACRO_KEYWORD` from `"defmacro"` to
757// `"define-syntax"`; a shorter idiom migration
758// `CompilerSpecIoStage::REALIZE_TO_DISK_SERIALIZE_LABEL` from
759// `"serialize"` to `"ser"`) that flows through the composed
760// array without updating the outer array's literal image fails
761// HERE with the `STR-SLICE-EQUALS-ARRAY-VIOLATION` panic naming
762// the drifted position, where the pairwise-distinctness witness
763// stays silent because the drifted str remains distinct from
764// its peers.
765//
766// Future extensions that benefit: adding a hypothetical eighth
767// expected-shape variant (a distinct `Float` once `extract_float`
768// stops accepting integers, a `Symbol`, or a parameterized
769// `ListOfInts`) extends `ExpectedKwargShape::ALL` AND
770// `ExpectedKwargShape::LABELS` AND requires widening this witness's
771// `<7, 7, 0>` const-generic turbofish AND appending the new label
772// literal to the RHS listing in lockstep — rustc's forced-arity
773// check on `[&'static str; N]` fails compilation if either side
774// drifts out of lockstep. Analogous single-variant extensions on the
775// four sibling algebras (`KwargPathKind`, `MacroDefHead`,
776// `CompilerSpecIoStage`, `StructuralKind`) propagate through their
777// respective `<N, N, 0>` witnesses under the same forced-arity
778// gate.
779const _: () = crate::ast::assert_str_array_slice_equals_str_array::<7, 7, 0>(
780 &ExpectedKwargShape::LABELS,
781 &[
782 "keyword",
783 "string",
784 "int",
785 "number",
786 "bool",
787 "list",
788 "list of strings",
789 ],
790);
791const _: () = crate::ast::assert_str_array_slice_equals_str_array::<3, 3, 0>(
792 &KwargPathKind::LABELS,
793 &["named", "item", "slot"],
794);
795const _: () = crate::ast::assert_str_array_slice_equals_str_array::<3, 3, 0>(
796 &MacroDefHead::KEYWORDS,
797 &["defmacro", "defpoint-template", "defcheck"],
798);
799const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 4, 0>(
800 &CompilerSpecIoStage::LABELS,
801 &["serialize", "write", "read", "deserialize"],
802);
803const _: () = crate::ast::assert_str_array_slice_equals_str_array::<2, 2, 0>(
804 &StructuralKind::LABELS,
805 &["nil", "list"],
806);
807
808#[derive(Debug, Error)]
809pub enum LispError {
810 #[error("unexpected character {0:?} at position {1}")]
811 UnexpectedChar(char, usize),
812 /// Unterminated string literal. The span runs from the opening `"`
813 /// to end-of-input — the whole run the tokenizer consumed looking
814 /// for a closer — so the rendered diagnostic underlines the
815 /// unterminated literal rather than pointing at its opening quote.
816 #[error("unterminated string literal at position {}", .0.start)]
817 UnterminatedString(Span),
818 /// A `)` with no matching `(`. The span is the offending token — one
819 /// byte — so this renders as a single caret, same as pre-lift.
820 #[error("unmatched closing paren at position {}", .span.start)]
821 UnmatchedParen { span: Span },
822 /// A `(` the reader never found a closer for. The span runs from the
823 /// unclosed `(` to the END OF THE LAST TOKEN read, i.e. the extent of
824 /// the partial form — NOT to `src.len()`, which would drag trailing
825 /// whitespace and comments under the underline. This is the variant
826 /// the `Span` lift buys the most: pre-lift it named the `(` byte and
827 /// rendered one caret, post-lift it underlines the form that was left
828 /// open.
829 #[error("unmatched opening paren at position {}", .span.start)]
830 UnmatchedOpenParen { span: Span },
831 /// Input ran out where a datum was required. A zero-width span AT the
832 /// end offset — there is no text to underline, so `caret_run`'s
833 /// clamp-up-to-one renders the single caret one column past the last
834 /// visible char, exactly as pre-lift.
835 #[error("unexpected end of input at position {}", .span.start)]
836 Eof { span: Span },
837 #[error("invalid number literal {0:?}")]
838 InvalidNumber(String),
839 #[error("unknown symbol: {0}")]
840 UnknownSymbol(String),
841 #[error("type error: expected {expected}, got {got}")]
842 Type { expected: &'static str, got: String },
843 #[error("compile error in {form}: {message}")]
844 Compile { form: String, message: String },
845 /// Structured type mismatch — both sides are first-class fields, not
846 /// embedded substrings of `message`. Rendered as `"compile error in
847 /// {form}: expected {expected}, got {got}"` so the user-facing string
848 /// matches the legacy `Compile`-shaped diagnostic byte-for-byte; the
849 /// gain is structural — authoring tools (REPL, LSP, `tatara-check`)
850 /// pattern-match on the variant and bind directly to `expected` /
851 /// `got` instead of substring-parsing the rendered message.
852 ///
853 /// `form` is the typed closed-set `KwargPath` enum so consumers
854 /// pattern-match on path-shape identity (`KwargPath::Item { .. }`,
855 /// `KwargPath::Slot(_)`, `KwargPath::Named(_)`) directly rather than
856 /// substring-matching the rendered prefix. The Display projection
857 /// flows through `KwargPath::Display`, so the user-facing
858 /// `"compile error in {form}: …"` rendering matches the legacy
859 /// `String`-shaped diagnostic byte-for-byte.
860 ///
861 /// `expected` is the typed closed-set `ExpectedKwargShape` enum —
862 /// the seven reachable expected-shape labels the typed-entry kwarg
863 /// gate emits (`Keyword` ⊎ `String` ⊎ `Int` ⊎ `Number` ⊎ `Bool` ⊎
864 /// `List` ⊎ `ListOfStrings`) encoded as a TYPE so a typo in any
865 /// label literal can never drift into the diagnostic at runtime;
866 /// consumers (REPL, LSP, `tatara-check`) pattern-match on
867 /// `ExpectedKwargShape::Number` etc. directly instead of
868 /// substring-matching `expected == "number"`. Same posture as
869 /// `LispError::Defmacro*.head: MacroDefHead`,
870 /// `LispError::UnboundTemplateVar.prefix: UnquoteForm`,
871 /// `LispError::CompilerSpecIo.stage: CompilerSpecIoStage`,
872 /// `LispError::TemplateInvariant.kind: TemplateInvariantKind`, and
873 /// `LispError::TypeMismatch.form: KwargPath`: the closed set
874 /// becomes a TYPE rather than a `&'static str` projection at the
875 /// helper boundary. The Display projection flows through
876 /// `ExpectedKwargShape::Display`, so the user-facing
877 /// `"... expected {expected}, ..."` rendering matches the legacy
878 /// `&'static str`-shaped diagnostic byte-for-byte.
879 ///
880 /// `got` is the typed closed-set `SexpShape` enum — the twelve
881 /// reachable Sexp outermost shapes (`Nil` ⊎ `Symbol` ⊎ `Keyword` ⊎
882 /// `String` ⊎ `Int` ⊎ `Float` ⊎ `Bool` ⊎ `List` ⊎ `Quote` ⊎
883 /// `Quasiquote` ⊎ `Unquote` ⊎ `UnquoteSplice`) encoded as variant
884 /// identities so the SexpShape that the typed-entry gate observed
885 /// is load-bearing data in the type system. Consumers (REPL, LSP,
886 /// `tatara-check`) pattern-match on `SexpShape::Int` etc. directly
887 /// instead of substring-matching `got == "int"`. Same posture as
888 /// `form: KwargPath` and `expected: ExpectedKwargShape`: after this
889 /// lift the TypeMismatch variant's identity is fully closed-set
890 /// typed in ALL THREE of its slots — no `&'static str` projection
891 /// at any helper boundary, every reachable identity encoded as a
892 /// variant of a typed enum. The Display projection flows through
893 /// `SexpShape::Display` (which delegates to `SexpShape::label()`),
894 /// so the user-facing `"... got {got}"` rendering matches the
895 /// legacy `&'static str`-shaped diagnostic byte-for-byte. When a
896 /// future run gives `Sexp` source spans, `pos: Option<usize>`
897 /// lands here in ONE place and every type-mismatch site picks up
898 /// positional rendering via `crate::diagnostic::format_diagnostic`
899 /// mechanically.
900 #[error("compile error in {form}: expected {expected}, got {got}")]
901 TypeMismatch {
902 form: KwargPath,
903 expected: ExpectedKwargShape,
904 got: SexpShape,
905 },
906 /// Structural head-mismatch — the `(head ...)` of a top-level form
907 /// didn't match `T::KEYWORD`. Both sides are first-class fields, not
908 /// embedded substrings of `message`. Rendered as `"compile error in
909 /// {keyword}: expected ({keyword} ...), got ({got} ...)"` so the
910 /// user-facing string matches the legacy `Compile`-shaped diagnostic
911 /// byte-for-byte; the gain is structural — authoring tools (REPL,
912 /// LSP, `tatara-check`) pattern-match on the variant and bind
913 /// directly to `keyword` / `got` instead of substring-parsing the
914 /// rendered message.
915 ///
916 /// `keyword` is `&'static str` because it always comes from
917 /// `T::KEYWORD`, a compile-time literal; `got` is `String` because
918 /// it is an arbitrary symbol from the source. When a future run
919 /// gives `Sexp` source spans, `pos: Option<usize>` lands here in
920 /// ONE place and every head-mismatch site picks up positional
921 /// rendering via `crate::diagnostic::format_diagnostic`
922 /// mechanically.
923 #[error("compile error in {keyword}: expected ({keyword} ...), got ({got} ...)")]
924 HeadMismatch { keyword: &'static str, got: String },
925 #[error("unknown {category}: {value}")]
926 Unknown {
927 category: &'static str,
928 value: String,
929 },
930 #[error("missing required field: {0}")]
931 Missing(&'static str),
932 /// A kwargs list of odd length: the last element has no partner. The
933 /// `dangling` field holds the offending element's `Sexp::Display`
934 /// projection — `:query` for a keyword whose value got lost, or the
935 /// literal form of a stray non-keyword. Naming both halves of the
936 /// failure (the failure mode AND the offending element) is the
937 /// typed-entry gate's structural-completeness floor (THEORY.md §V.1):
938 /// without it the operator must re-read the source to find what
939 /// actually misfired.
940 #[error("odd keyword arguments: dangling element `{dangling}`")]
941 OddKwargs { dangling: String },
942 /// An unquote (`,name`) or unquote-splice (`,@name`) in a macro template
943 /// body referenced a name that wasn't bound by the macro's params (during
944 /// `compile_template`) or wasn't available in the substitution scope
945 /// (during `substitute`). `prefix` is the syntactic marker — `","` for
946 /// unquote, `",@"` for splice — so the rendered diagnostic preserves the
947 /// form exactly as the author wrote it. `hint` is `Some(name)` when the
948 /// substrate found a near-miss against the available bound names within
949 /// the bounded edit distance (see `crate::domain::suggest`); `None`
950 /// otherwise — a wrong hint is worse than no hint, so the slot stays
951 /// empty unless the substrate is confident.
952 ///
953 /// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
954 /// variants are EXACTLY the two reachable syntactic markers
955 /// (`Unquote` ⊎ `Splice`). Encoding the closed set as a TYPE makes the
956 /// constraint that ONLY 2 marker identities are reachable load-bearing
957 /// in the type system — a third pseudo-marker can never drift into the
958 /// diagnostic at runtime; consumers (REPL, LSP, `tatara-check`)
959 /// pattern-match on `UnquoteForm::Splice` etc. directly instead of
960 /// substring-matching `prefix == ",@"`. Same posture as
961 /// `LispError::Defmacro*.head: MacroDefHead`,
962 /// `LispError::CompilerSpecIo.stage: CompilerSpecIoStage`, and
963 /// `LispError::TemplateInvariant.kind: TemplateInvariantKind`: the
964 /// closed set becomes a TYPE rather than a `&'static str` projection
965 /// at the helper boundary. `name` and `hint` are `String` because
966 /// they come from arbitrary source / the live bindings set. When a
967 /// future run gives `Sexp` source spans, `pos: Option<usize>` lands
968 /// here in ONE place and every unbound-template-var site picks up
969 /// positional rendering via `crate::diagnostic::format_diagnostic`
970 /// mechanically.
971 ///
972 /// Display matches the legacy `Compile`-shaped diagnostic byte-for-byte
973 /// when `hint` is `None` — `"compile error in {prefix}{name}: unbound"`
974 /// — so existing consumer assertions that pattern-match on the message
975 /// substring keep passing. With a hint, the suffix `"; did you mean
976 /// {prefix}{hint}?"` is appended; the prefix is preserved in the hint so
977 /// the operator can copy-paste the suggestion verbatim.
978 #[error(
979 "compile error in {prefix}{name}: unbound{}",
980 unbound_hint_suffix(*prefix, hint.as_deref())
981 )]
982 UnboundTemplateVar {
983 prefix: UnquoteForm,
984 name: String,
985 hint: Option<String>,
986 },
987 /// A kwargs slice contained the same `:key` twice. The offending key is
988 /// carried as a structural field — not embedded in a free-form message —
989 /// so authoring surfaces (REPL, LSP, `tatara-check`) pattern-match on
990 /// the variant and bind to `key` directly instead of substring-parsing
991 /// the rendered diagnostic. Same posture as the `OddKwargs { dangling }`
992 /// sibling: every distinct `parse_kwargs` failure mode (odd length,
993 /// not-a-keyword-at-position, duplicate key) is now a structural variant
994 /// of `LispError`, not a `Compile`-shaped substring.
995 ///
996 /// `key` is `String` because it comes from arbitrary source. Display
997 /// renders `"compile error in :{key}: duplicate keyword"` byte-for-byte
998 /// equivalent to the legacy `Compile { form: kwarg_form(key), message:
999 /// "duplicate keyword" }` shape, so existing consumer assertions
1000 /// (`msg.contains(":name")`, `msg.contains("duplicate keyword")`) pass
1001 /// unchanged. When a future run gives `Sexp` source spans, `pos:
1002 /// Option<usize>` lands here in ONE place and every duplicate-kwarg
1003 /// site picks up positional rendering via
1004 /// `crate::diagnostic::format_diagnostic` mechanically.
1005 #[error("compile error in :{key}: duplicate keyword")]
1006 DuplicateKwarg { key: String },
1007 /// A required kwarg was absent from the kwargs slice. The offending key
1008 /// is carried as a structural field — not embedded in a free-form
1009 /// message — so authoring surfaces (REPL, LSP, `tatara-check`)
1010 /// pattern-match on the variant and bind to `key` directly instead of
1011 /// substring-parsing the rendered diagnostic. Same posture as
1012 /// `DuplicateKwarg { key }` and `OddKwargs { dangling }`: every
1013 /// distinct typed-entry kwarg failure mode is now a structural variant
1014 /// of `LispError`, not a `Compile`-shaped substring. Sibling of the
1015 /// pre-existing `Missing(&'static str)` variant — `MissingKwarg`
1016 /// covers the runtime-key path the kwargs extractors share, while
1017 /// `Missing` stays for compile-time-known names.
1018 ///
1019 /// `key` is `String` because it comes from the runtime kwargs lookup
1020 /// (each derive-generated extractor and every hand-written
1021 /// `TataraDomain` impl can pass an arbitrary key). Display renders
1022 /// `"compile error in :{key}: required but not provided"`
1023 /// byte-for-byte equivalent to the legacy `Compile { form:
1024 /// kwarg_form(key), message: "required but not provided" }` shape, so
1025 /// existing consumer assertions (`msg.contains(":threshold")`,
1026 /// `msg.contains("required")`) pass unchanged. When a future run gives
1027 /// `Sexp` source spans, `pos: Option<usize>` lands here in ONE place
1028 /// and every missing-kwarg site picks up positional rendering via
1029 /// `crate::diagnostic::format_diagnostic` mechanically.
1030 #[error("compile error in :{key}: required but not provided")]
1031 MissingKwarg { key: String },
1032 /// A kwargs slice contained a `:key` that isn't in the allowed-kwarg
1033 /// set for the surrounding `TataraDomain`. The offending key, the
1034 /// near-miss hint (if any), and the full allowed set are all carried
1035 /// as first-class fields — not embedded in a free-form message — so
1036 /// authoring surfaces (REPL, LSP, `tatara-check`) pattern-match on
1037 /// the variant and bind to `key` / `hint` / `allowed` directly
1038 /// instead of substring-parsing the rendered message. Same posture
1039 /// as the `OddKwargs { dangling }`, `DuplicateKwarg { key }`, and
1040 /// `MissingKwarg { key }` siblings: every distinct typed-entry
1041 /// kwarg-gate failure mode is now a structural variant of
1042 /// `LispError`, not a `Compile`-shaped substring.
1043 ///
1044 /// `key` is `String` because it comes from arbitrary source. `hint`
1045 /// is `Some(allowed_keyword)` when `crate::domain::suggest` ranks an
1046 /// allowed kwarg within the bounded edit distance; `None`
1047 /// otherwise — a wrong hint is worse than no hint, so the slot
1048 /// stays empty unless the substrate is confident. `allowed` is
1049 /// `Vec<String>` (sorted lexicographically by `unknown_kwarg`)
1050 /// because the variant owns its data — the derive-emitted
1051 /// `&'static [&'static str]` allowed-set crosses the structural
1052 /// boundary as owned `String`s so the diagnostic crosses thread
1053 /// boundaries cleanly and lives independent of the call frame.
1054 /// Display matches the legacy `Compile { form: kwarg_form(key),
1055 /// message: "unknown keyword (...)" }` rendering byte-for-byte
1056 /// (`"compile error in :{key}: unknown keyword (did you mean
1057 /// :{hint}?; allowed: :a, :b, :c)"` with a hint, `"compile error
1058 /// in :{key}: unknown keyword (allowed: :a, :b, :c)"` without),
1059 /// so existing consumer assertions
1060 /// (`msg.contains("unknown keyword")`,
1061 /// `msg.contains("did you mean :threshold?")`,
1062 /// `msg.contains("allowed: ")`) pass unchanged. When a future
1063 /// run gives `Sexp` source spans, `pos: Option<usize>` lands
1064 /// here in ONE place and every unknown-kwarg site picks up
1065 /// positional rendering via
1066 /// `crate::diagnostic::format_diagnostic` mechanically.
1067 #[error(
1068 "compile error in :{key}: unknown keyword{}",
1069 unknown_kwarg_suffix(hint.as_deref(), allowed)
1070 )]
1071 UnknownKwarg {
1072 key: String,
1073 hint: Option<String>,
1074 allowed: Vec<String>,
1075 },
1076 /// A registry-dispatched form `(<keyword> ...)` whose head symbol isn't in
1077 /// the global `TataraDomain` registry. The offending keyword, the
1078 /// near-miss hint (if any), and the full registered keyword set are all
1079 /// carried as first-class fields — not embedded in a free-form message —
1080 /// so authoring surfaces (REPL, LSP, `tatara-check`) pattern-match on the
1081 /// variant and bind to `keyword` / `hint` / `registered` directly instead
1082 /// of substring-parsing the rendered message. Same posture as the
1083 /// `UnknownKwarg { key, hint, allowed }` sibling: the kwarg-gate's
1084 /// unknown-allowed-set rejection and the registry-gate's
1085 /// unknown-registered-set rejection share ONE structural shape.
1086 ///
1087 /// `keyword` is `String` because it comes from arbitrary source (a
1088 /// top-level form's head symbol). `hint` is
1089 /// `Some(registered_keyword)` when `crate::domain::suggest_keyword`
1090 /// ranks a registered keyword within the bounded edit distance;
1091 /// `None` otherwise — a wrong hint is worse than no hint, so the slot
1092 /// stays empty unless the substrate is confident. `registered` is
1093 /// `Vec<String>` (sorted lexicographically by
1094 /// `unknown_domain_keyword`) because the variant owns its data — the
1095 /// registry's `&'static [&'static str]` keyword-set crosses the
1096 /// structural boundary as owned `String`s so the diagnostic crosses
1097 /// thread boundaries cleanly and lives independent of the call frame.
1098 /// Empty `registered` (no domains seeded) renders `(no domains
1099 /// registered)` so the operator sees the structural reason — the
1100 /// registry has no candidates at all — instead of a misleading empty
1101 /// "registered: " suffix. When a future run gives `Sexp` source
1102 /// spans, `pos: Option<usize>` lands here in ONE place and every
1103 /// unknown-domain-keyword site picks up positional rendering via
1104 /// `crate::diagnostic::format_diagnostic` mechanically.
1105 #[error(
1106 "unknown domain keyword: ({keyword} ...){}",
1107 unknown_domain_keyword_suffix(hint.as_deref(), registered)
1108 )]
1109 UnknownDomainKeyword {
1110 keyword: String,
1111 hint: Option<String>,
1112 registered: Vec<String>,
1113 },
1114 /// The slot inside a `Sexp::Unquote(_)` (`,X`) or
1115 /// `Sexp::UnquoteSplice(_)` (`,@X`) was not a symbol. The `prefix`
1116 /// field is the syntactic marker — `","` for unquote, `",@"` for
1117 /// splice — so the rendered diagnostic preserves the form exactly
1118 /// as the author wrote it. The `got` field is the offending inner's
1119 /// `Sexp::Display` projection so the operator sees both what was
1120 /// expected (a symbol — the only form a no-evaluator template can
1121 /// substitute) and what was actually written (the literal value —
1122 /// `(list 1 2)`, `5`, `:foo`, etc.). Naming both halves of the
1123 /// failure is the typed-entry gate's structural-completeness floor
1124 /// (THEORY.md §V.1).
1125 ///
1126 /// Sibling of `UnboundTemplateVar { prefix, name, hint }` for the
1127 /// same template-side typed-entry surface — that variant fires when
1128 /// the slot IS a symbol but the symbol isn't bound; this variant
1129 /// fires when the slot isn't a symbol at all. After this lift every
1130 /// distinct typed-entry template-gate failure mode binds to ONE
1131 /// structural variant of `LispError`, not a `Compile`-shaped
1132 /// substring.
1133 ///
1134 /// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
1135 /// variants are EXACTLY the two reachable syntactic markers
1136 /// (`Unquote` ⊎ `Splice`). Encoding the closed set as a TYPE makes
1137 /// the constraint load-bearing in the type system — a third
1138 /// pseudo-marker can never drift into the diagnostic at runtime;
1139 /// consumers (REPL, LSP, `tatara-check`) pattern-match on
1140 /// `UnquoteForm::Splice` etc. directly instead of substring-matching
1141 /// `prefix == ",@"`. Same typed-slot posture as `UnboundTemplateVar`'s
1142 /// `prefix` slot, parallel to `LispError::Defmacro*.head:
1143 /// MacroDefHead`. `got` is `SexpWitness` — the closed-set typed
1144 /// joint identity pairing the offending inner's `SexpShape` (the
1145 /// twelve reachable outermost shapes the reader can produce) with
1146 /// its `Sexp::Display` projection (the literal value the author
1147 /// wrote — `(list 1 2)`, `5`, `:foo`, etc.). Same typed-witness
1148 /// posture as `SpliceOutsideList.got: SexpWitness`: authoring
1149 /// tools (REPL, LSP, `tatara-check`) bind to BOTH `got.shape`
1150 /// (structurally pattern-matchable on `SexpShape::List` etc.) AND
1151 /// `got.display` (the literal value, renderable verbatim) without
1152 /// losing either side. The two template-gate `,X/,@X` rejection
1153 /// variants (`NonSymbolUnquoteTarget` AND `SpliceOutsideList`)
1154 /// now share ONE typed witness identity at their `got` slot —
1155 /// every Sexp-display-source `got` slot on the template-gate's
1156 /// distinct rejection variants carries the SAME typed primitive.
1157 /// When a future run gives `Sexp` source spans, `pos:
1158 /// Option<usize>` lands here in ONE place and every
1159 /// non-symbol-unquote-target site picks up positional rendering
1160 /// via `crate::diagnostic::format_diagnostic` mechanically.
1161 #[error("compile error in {prefix}: expected symbol, got {got}")]
1162 NonSymbolUnquoteTarget {
1163 prefix: UnquoteForm,
1164 got: SexpWitness,
1165 },
1166 /// A `,@X` (unquote-splice) appeared at a syntactic position where there
1167 /// is no containing list to splice into — i.e. the splice is the entire
1168 /// macro-template body, not nested inside a `(... ,@xs ...)` list. Splice
1169 /// is always list-flattening: `,@(a b c)` inside `(outer ,@xs)` becomes
1170 /// `(outer a b c)`. At a non-list position there is no list to flatten
1171 /// into; the form is ill-positioned regardless of whether the inner slot
1172 /// is a symbol, a literal, or a bound list.
1173 ///
1174 /// Sibling of `NonSymbolUnquoteTarget { prefix, got }` and
1175 /// `UnboundTemplateVar { prefix, name, hint }` for the template-gate's
1176 /// other distinct failure modes — together the three close every
1177 /// distinct typed-entry template-gate failure mode for the no-evaluator
1178 /// template language: each is a structural variant of `LispError`, not
1179 /// a `Compile`-shaped substring. `prefix` is implicit (always `,@`) and
1180 /// elided from the variant: this failure mode names ONE syntactic
1181 /// marker, parallel to how `OddKwargs` names ONE failure mode (odd-length
1182 /// kwargs slice) without a syntactic-marker slot.
1183 ///
1184 /// `got` is `SexpWitness` — the closed-set typed joint identity
1185 /// pairing the offending inner's `SexpShape` (the twelve reachable
1186 /// outermost shapes the reader can produce) with its
1187 /// `Sexp::Display` projection (the literal value the author wrote
1188 /// — `xs`, `(list 1 2)`, `5`, `:foo`, etc.). Promotes the legacy
1189 /// `got: String` shape to a typed witness so authoring tools (REPL,
1190 /// LSP, `tatara-check`) bind to BOTH `got.shape` (structurally
1191 /// pattern-matchable on `SexpShape::List` etc.) AND `got.display`
1192 /// (the literal value, renderable verbatim) without losing either
1193 /// side. Naming both the failure mode AND the offending element
1194 /// is the typed-entry gate's structural-completeness floor
1195 /// (THEORY.md §V.1) — without it the operator must re-read the
1196 /// source to find what actually misfired. After this lift the
1197 /// structural identity is part of the variant's typed data shape;
1198 /// a regression that re-collapses `got` to a free-form `String`
1199 /// loses the rustc-enforced closed-set guarantee on shape
1200 /// identity.
1201 ///
1202 /// First consumer of the `SexpWitness` primitive. Sibling lifts
1203 /// landed for `NonSymbolUnquoteTarget.got`, `NonSymbolParam.got`,
1204 /// `DefmacroNonSymbolName.got`, and `DefmacroNonListParams.got`;
1205 /// the remaining trajectory —
1206 /// `RestParamMissingName.got: Option<String>` and
1207 /// `MissingHeadSymbol.got: Option<String>` — is the next set of
1208 /// moves: every `got: String` (or `Option<String>`) slot whose
1209 /// source is `Sexp::Display` picks up the typed witness
1210 /// mechanically once the variant's data shape is bumped. The
1211 /// remaining two are both `Option<String>` — the typed witness
1212 /// lands on the `Some` arm directly, the `None` arm encodes the
1213 /// "missing entirely" sub-mode that's structurally distinct from
1214 /// "present but malformed".
1215 ///
1216 /// When a future run gives `Sexp` source spans, `pos: Option<usize>`
1217 /// lands on `SexpWitness` ONCE and every splice-outside-list site
1218 /// picks up positional rendering via
1219 /// `crate::diagnostic::format_diagnostic` mechanically.
1220 ///
1221 /// Display renders `"compile error in ,@: \`,@\` may only appear inside
1222 /// a list (got ,@{got})"` — the legacy substring `"\`,@\` may only
1223 /// appear inside a list"` is preserved verbatim so authoring tools that
1224 /// substring-match on the rendered diagnostic see no drift; the
1225 /// parenthetical `(got ,@{got})` names the offending form so an LSP
1226 /// quick-fix that surfaces "the splice has no containing list; you
1227 /// wrote `,@xs`" gains the literal value as data, no message re-parsing
1228 /// required. The `{got}` slot flows through `SexpWitness::Display`,
1229 /// which writes only the `display` field, so the rendering is
1230 /// byte-for-byte identical to the legacy `got: String` shape.
1231 #[error("compile error in ,@: `,@` may only appear inside a list (got ,@{got})")]
1232 SpliceOutsideList { got: SexpWitness },
1233 /// A macro was called with fewer arguments than its required-param arity:
1234 /// `(defmacro f (a b) `(,a ,b)) (f 1)` — `b` has no arg. Both the failing
1235 /// macro's name AND the un-bound param are first-class structural fields,
1236 /// not embedded substrings of `message`, so authoring surfaces (REPL,
1237 /// LSP, `tatara-check`) pattern-match on the variant and bind to
1238 /// `macro_name` / `param` directly instead of substring-parsing the
1239 /// rendered message. Sibling of `MissingKwarg { key }` for the
1240 /// macro-call-gate's positional-arity surface — that variant fires when
1241 /// a `(<head> :key value …)` kwargs form omits a required keyword;
1242 /// this variant fires when a `(<macroname> a b …)` call omits a required
1243 /// positional param. The two close every distinct typed-entry
1244 /// missing-required surface in the substrate.
1245 ///
1246 /// Same single emission shape across both expansion strategies — the
1247 /// substitute path's `bind_args` and the bytecode path's
1248 /// `apply_compiled` share ONE structural variant, parallel to how the
1249 /// template-gate's `SpliceOutsideList` is shared across both paths
1250 /// (THEORY.md §II.1 invariant 2 — free middle: which strategy you
1251 /// picked must not change which inputs you reject). Before this lift
1252 /// the same failure mode emitted ONE `LispError::Compile { form:
1253 /// format!("call to {macro_name}"), message: format!("missing
1254 /// required arg: {param}") }` triple at TWO call sites — the
1255 /// three-times rule had two sites with byte-identical shape and one
1256 /// failure mode.
1257 ///
1258 /// `macro_name` and `param` are `String` because they come from
1259 /// arbitrary source (the call-site head symbol AND the
1260 /// macro-definition's param symbol). Display matches the legacy
1261 /// `Compile`-shaped diagnostic byte-for-byte — `"compile error in call
1262 /// to {macro_name}: missing required arg: {param}"` — so existing
1263 /// consumer assertions (`msg.contains("missing required arg")`) pass
1264 /// unchanged. When a future run gives `Sexp` source spans, `pos:
1265 /// Option<usize>` lands here in ONE place and every missing-macro-arg
1266 /// site picks up positional rendering via
1267 /// `crate::diagnostic::format_diagnostic` mechanically.
1268 #[error("compile error in call to {macro_name}: missing required arg: {param}")]
1269 MissingMacroArg { macro_name: String, param: String },
1270 /// A macro was called with MORE arguments than its declared
1271 /// required+optional arity, on a param list with NO `&rest` slot to
1272 /// collect the surplus: `(defmacro f (a b) `(,a ,b)) (f 1 2 3)` — `3`
1273 /// has nowhere to bind. The mirror at the call-site of
1274 /// `RestParamTrailingTokens` (the definition-site rejection that
1275 /// surfaces tokens trailing a `&rest <name>` clause, lifted in the
1276 /// prior-run typed-promotion lineage): that variant rejects malformed
1277 /// DEFINITIONS that the typed `MacroParams` shape cannot hold (a
1278 /// `&rest` clause is structurally LAST); this variant rejects
1279 /// malformed CALLS that the typed `bind` cannot honor (a rest-less
1280 /// param list has a FIXED maximum arity equal to
1281 /// `required.len() + optional.len()`). Together with
1282 /// `MissingMacroArg`, the macro-call-gate's positional-arity surface
1283 /// is now structurally complete in both directions — too-few AND
1284 /// too-many — closing the asymmetry where the typed-entry gate
1285 /// rejected too-few-args loudly but silently truncated too-many to
1286 /// the slice `bind` could consume.
1287 ///
1288 /// A rest-PRESENT param list has no maximum arity (the `&rest` slot
1289 /// collects every trailing arg into a `Sexp::List`), so this
1290 /// rejection fires ONLY when `MacroParams.rest` is `None`. The
1291 /// `expected` slot is `required.len() + optional.len()` — the maximum
1292 /// number of args the rest-less binder can consume; `got` is
1293 /// `args.len()` — the actual number supplied. Surfacing both lets
1294 /// authoring tools (REPL, LSP, `tatara-check`) name the
1295 /// "you supplied {got} args but the macro takes at most {expected}"
1296 /// quick-fix without re-deriving either count from the source.
1297 ///
1298 /// The leftmost-priority discipline is preserved: `MissingMacroArg`
1299 /// for a missing REQUIRED arg fires BEFORE this too-many gate
1300 /// (`bind` iterates the required walk first and bails on the first
1301 /// missing slot), so `(defmacro f (a b c) …) (f 1)` is
1302 /// `MissingMacroArg { param: "b" }`, NOT `TooManyMacroArgs`. The two
1303 /// failure modes are structurally disjoint: too-few-required vs.
1304 /// too-many-with-no-rest.
1305 ///
1306 /// `macro_name` is `String` because it comes from arbitrary source
1307 /// (the call-site head symbol); `expected` and `got` are `usize`
1308 /// arities. Display matches the legacy `Compile`-shaped diagnostic
1309 /// style — `"compile error in call to {macro_name}: too many args:
1310 /// expected at most {expected}, got {got}"` — so the same
1311 /// `"compile error in call to {macro_name}:"` substring authoring
1312 /// tools' assertions key on stays unchanged across the new
1313 /// rejection mode. When a future run gives `Sexp` source spans,
1314 /// `pos: Option<usize>` lands here in ONE place and every
1315 /// too-many-macro-args site picks up positional rendering via
1316 /// `crate::diagnostic::format_diagnostic` mechanically — same
1317 /// posture as `MissingMacroArg`.
1318 #[error(
1319 "compile error in call to {macro_name}: too many args: \
1320 expected at most {expected}, got {got}"
1321 )]
1322 TooManyMacroArgs {
1323 macro_name: String,
1324 expected: usize,
1325 got: usize,
1326 },
1327 /// A non-symbol element appeared in a `defmacro` / `defpoint-template`
1328 /// / `defcheck` param list at the named position. The legacy
1329 /// `LispError::Compile { form: "defmacro params", message: "expected
1330 /// symbol" }` shape named only the failure mode — it didn't say WHICH
1331 /// element of the param list misfired NOR what was found in its slot.
1332 /// The structural variant names both: `position` is the 0-based index
1333 /// of the offending element within the param list, `got` is its
1334 /// `Sexp::Display` projection so the operator sees the literal value
1335 /// they wrote (`5`, `"x"`, `:foo`, `(nested)`) instead of the bare
1336 /// "expected symbol" verdict. Naming both the position AND the
1337 /// offending element is the typed-entry gate's
1338 /// structural-completeness floor (THEORY.md §V.1) — without both an
1339 /// LSP that wants to surface "the third element of your param list
1340 /// isn't a symbol; you wrote `5`" must re-parse the source.
1341 ///
1342 /// Sibling of `MissingMacroArg { macro_name, param }` for the
1343 /// macro-call-gate's positional-arity surface — that variant fires
1344 /// when a CALL `(<macroname> a b …)` omits a required positional
1345 /// param; this variant fires when the DEFMACRO `(defmacro <name> (a
1346 /// b …) …)` declaration's param list contains a non-symbol where a
1347 /// param name was expected. The two are the macro-call-gate and the
1348 /// defmacro-syntax-gate's first-named structural failure modes
1349 /// respectively — call-site malformed vs. definition-site malformed.
1350 ///
1351 /// `position` is `usize` because it is always the loop index inside
1352 /// `parse_params`; `got` is `SexpWitness` — the closed-set typed
1353 /// joint identity (structural `SexpShape` + renderable
1354 /// `Sexp::Display` projection) the offending-value side of the
1355 /// typed-entry rejection owes the operator. Third consumer of the
1356 /// `SexpWitness` primitive (after `SpliceOutsideList.got` and
1357 /// `NonSymbolUnquoteTarget.got`); same posture — authoring tools
1358 /// (REPL, LSP, `tatara-check`) bind to BOTH `got.shape`
1359 /// (`SexpShape::Int`, `SexpShape::Keyword`, `SexpShape::List`, etc.)
1360 /// AND `got.display` (the literal value, renderable verbatim)
1361 /// jointly across the variant slot rather than substring-grepping
1362 /// a free-form `String`. Display projects the witness's `display`
1363 /// field verbatim into the `#[error(... got {got})]` annotation's
1364 /// `{got}` slot, so the rendered `"compile error in defmacro params:
1365 /// expected symbol at position {position}, got <display>"` shape is
1366 /// byte-for-byte identical to the pre-lift `got: String` rendering;
1367 /// authoring tools that substring-grep on the rendered diagnostic
1368 /// see no drift. When a future run gives `Sexp` source spans, `pos:
1369 /// Option<usize>` lands inside `SexpWitness` in ONE place and every
1370 /// non-symbol-param site picks up positional rendering via
1371 /// `crate::diagnostic::format_diagnostic` mechanically.
1372 #[error(
1373 "compile error in defmacro params: expected symbol at position \
1374 {position}, got {got}"
1375 )]
1376 NonSymbolParam { position: usize, got: SexpWitness },
1377 /// A `&rest` marker in a `defmacro` / `defpoint-template` / `defcheck`
1378 /// param list was followed by no element at all (`(&rest)`,
1379 /// `(a &rest)`) OR by a non-symbol element (`(&rest 5)`,
1380 /// `(&rest :foo)`). The legacy `LispError::Compile { form: "defmacro
1381 /// params", message: "&rest needs a name" }` shape named only the
1382 /// failure mode — it didn't say WHICH `&rest` (i.e. its position
1383 /// within the param list) misfired NOR what was found in the slot
1384 /// where the rest-name should have been. The structural variant
1385 /// names both: `rest_position` is the 0-based index of the `&rest`
1386 /// marker within the param list, `got` is the offending follower's
1387 /// typed witness (`Some(SexpWitness::new(SexpShape::Int, "5"))`,
1388 /// `Some(SexpWitness::new(SexpShape::Keyword, ":foo"))`,
1389 /// `Some(SexpWitness::new(SexpShape::List, "(nested)"))`) or
1390 /// `None` when the marker was the last element in the list and
1391 /// nothing followed at all. Naming both the marker position AND
1392 /// the offending follower (or its absence) is the typed-entry
1393 /// gate's structural-completeness floor (THEORY.md §V.1) —
1394 /// without both, an LSP that wants to surface "your `&rest` at
1395 /// param-list position 1 has no name; you wrote `5` instead of
1396 /// a symbol" must re-parse the source.
1397 ///
1398 /// Sibling of `NonSymbolParam { position, got }` for the
1399 /// defmacro-syntax-gate's other definition-site failure mode —
1400 /// that variant fires when a NON-`&rest` element at a param
1401 /// position isn't a symbol; this variant fires specifically on the
1402 /// post-`&rest` follower slot, where the failure mode bifurcates
1403 /// into "missing entirely" vs. "present but not a symbol". Both
1404 /// modes share ONE structural variant via `got: Option<SexpWitness>`
1405 /// (parallel to how `UnboundTemplateVar` and `UnknownKwarg` carry
1406 /// `hint: Option<String>` for a present-or-absent secondary slot)
1407 /// rather than splitting into two near-identical variants — the
1408 /// failure mode IS one ("rest name missing"); the bifurcation is
1409 /// in the renderable detail, not in what the gate rejects.
1410 ///
1411 /// Together, `NonSymbolParam` and `RestParamMissingName` close the
1412 /// `parse_params` pair — every distinct failure mode the
1413 /// `parse_params` walker can emit is now a structural variant of
1414 /// `LispError`, not a `Compile`-shaped substring.
1415 ///
1416 /// `rest_position` is `usize` because it is always the loop index
1417 /// inside `parse_params` at which the `&rest` marker was matched;
1418 /// `got` is `Option<SexpWitness>` — the SIXTH consumer of the typed
1419 /// `SexpWitness` primitive (after `SpliceOutsideList.got`,
1420 /// `NonSymbolUnquoteTarget.got`, `NonSymbolParam.got`,
1421 /// `DefmacroNonSymbolName.got`, and `DefmacroNonListParams.got`).
1422 /// The `Option`-shape bifurcates structurally into "missing
1423 /// entirely" (`None`, when the marker was the param list's last
1424 /// element) and "present but malformed" (`Some(SexpWitness)`, when
1425 /// a non-symbol follower came from arbitrary source via
1426 /// `Sexp::Display`); the typed witness lands on the `Some` arm
1427 /// only. Display preserves the legacy `"compile error in defmacro
1428 /// params: &rest needs a name"` prefix byte-for-byte so authoring
1429 /// tools that substring-grep on the rendered diagnostic see no
1430 /// drift; the structural detail (`(rest marker at position
1431 /// {rest_position}, got {got})` when present, `(rest marker at
1432 /// position {rest_position}, none provided)` when absent) is
1433 /// appended. When a future run gives `Sexp` source spans, `pos:
1434 /// Option<usize>` lands inside `SexpWitness` in ONE place and
1435 /// every rest-param-missing-name site picks up positional
1436 /// rendering via `crate::diagnostic::format_diagnostic`
1437 /// mechanically.
1438 #[error(
1439 "compile error in defmacro params: &rest needs a name{}",
1440 rest_param_missing_name_suffix(*rest_position, got.as_ref().map(|w| w.display.as_str()))
1441 )]
1442 RestParamMissingName {
1443 rest_position: usize,
1444 got: Option<SexpWitness>,
1445 },
1446 /// A `&rest <name>` param was followed by one or more further tokens —
1447 /// `(defmacro f (a &rest xs extra) …)`. The `&rest` name binds every
1448 /// remaining call arg into a list, so it is structurally the LAST thing
1449 /// a param list can name: nothing can follow it. Before this variant
1450 /// `parse_params` returned the moment it bound the rest name, SILENTLY
1451 /// DROPPING any trailing tokens — `extra` above vanished with no error,
1452 /// so an author who fat-fingered a stray param (or wrote `&rest xs
1453 /// &optional y` expecting a feature that doesn't exist yet) got no
1454 /// signal that their text was ignored. This variant turns that silent
1455 /// drop into a loud rejection at the typed-entry gate.
1456 ///
1457 /// Sibling of `NonSymbolParam` and `RestParamMissingName` — the third
1458 /// and final `parse_params` definition-site failure mode. The earlier
1459 /// two fire on a param SLOT (a non-symbol where a name was expected) and
1460 /// on the post-`&rest` follower (missing-or-malformed name); this one
1461 /// fires once the rest name is bound and the walker discovers the param
1462 /// list does not end there. Together the three now genuinely close the
1463 /// `parse_params` walker — every shape it accepts is a well-formed
1464 /// `MacroParams`, and every shape it rejects is a structural variant of
1465 /// `LispError`, not a silently-truncated `Vec` nor a `Compile`-shaped
1466 /// substring.
1467 ///
1468 /// `rest_position` is the loop index at which the `&rest` marker was
1469 /// matched (parallel to `RestParamMissingName.rest_position`), so an LSP
1470 /// quick-fix can point at the `&rest` form whose name must be last.
1471 /// `extra` is the count of trailing tokens (always ≥ 1) and `first` is
1472 /// the typed witness of the first of them — the SEVENTH consumer of the
1473 /// `SexpWitness` primitive. `first` is a non-`Option` witness (unlike
1474 /// `RestParamMissingName.got`) because the trailing run is non-empty by
1475 /// construction: this variant is only built when `list[rest_position +
1476 /// 2..]` has a first element. Display appends `(rest marker at position
1477 /// {rest_position}, {extra} trailing after name, first: {first})` via
1478 /// `rest_param_trailing_tokens_suffix`, which delegates the bare
1479 /// parenthetical to the shared `paren_suffix`. When a future run gives
1480 /// `Sexp` source spans, `pos: Option<usize>` lands inside `SexpWitness`
1481 /// in ONE place and this site picks up positional rendering
1482 /// mechanically — exactly as its two siblings do.
1483 #[error(
1484 "compile error in defmacro params: &rest name must be last{}",
1485 rest_param_trailing_tokens_suffix(*rest_position, *extra, &first.display)
1486 )]
1487 RestParamTrailingTokens {
1488 rest_position: usize,
1489 extra: usize,
1490 first: SexpWitness,
1491 },
1492 /// A `defmacro` / `defpoint-template` / `defcheck` param list carried a
1493 /// SECOND `&optional` marker — `(defmacro f (a &optional b &optional c)
1494 /// …)`. The canonical Lisp lambda-list (`(req* &optional opt* &rest r)`,
1495 /// the shape [`MacroParams`](crate::macro_expand::MacroParams) makes a
1496 /// type) has exactly ONE optional section, between the required run and
1497 /// the rest. A second `&optional` is unrepresentable: `MacroParams.optional`
1498 /// is one flat `Vec`, not a sequence of sections. Without this gate the
1499 /// walker would treat the second `&optional` as an optional param literally
1500 /// NAMED `&optional`, binding call args to a marker symbol — the precise
1501 /// silent misalignment the typed param-list shape exists to forbid (a
1502 /// sibling of the index-misalignment `MacroParams` ruled out when it
1503 /// replaced `Vec<Param>`).
1504 ///
1505 /// Sibling of `RestParamTrailingTokens` — both fire INSIDE `parse_params`
1506 /// once a marker is matched and the walker finds the surrounding param
1507 /// list's marker structure is one the canonical lambda-list ordering
1508 /// cannot represent (tokens after `&rest <name>`; a repeated `&optional`).
1509 /// `first_position` is the loop index of the first `&optional` marker,
1510 /// `second_position` the index of the offending second one — naming both
1511 /// lets an LSP quick-fix point at the redundant marker to delete. Neither
1512 /// is a `SexpWitness`: both elements ARE the `&optional` symbol by
1513 /// construction (the variant is only built when `s == "&optional"` twice),
1514 /// so there is nothing to witness — only the two positions carry
1515 /// information. When a future run gives `Sexp` source spans, the marker
1516 /// positions gain editor-ready rendering by threading spans here.
1517 #[error(
1518 "compile error in defmacro params: &optional may appear at most once{}",
1519 optional_marker_repeated_suffix(*first_position, *second_position)
1520 )]
1521 OptionalMarkerRepeated {
1522 first_position: usize,
1523 second_position: usize,
1524 },
1525 /// A `defmacro` / `defpoint-template` / `defcheck` `&optional` section
1526 /// carried a list-form entry that did not match the only admissible
1527 /// shape `(NAME DEFAULT)` — exactly two elements with a symbol head.
1528 /// Per-param default forms are the typed promotion of `optional:
1529 /// Vec<String>` to `optional: Vec<OptionalParam>` that the prior
1530 /// `&optional` run signposted, and this variant is the gate that
1531 /// admits only the canonical `(NAME DEFAULT)` shape into the typed
1532 /// `OptionalParam.default` slot. Four distinct list shapes are
1533 /// rejected, named via the closed-set typed `reason`
1534 /// ([`OptionalParamMalformedReason`]):
1535 ///
1536 /// * `()` — empty list spec.
1537 /// * `(name)` — one element, no default form supplied.
1538 /// * `(name d e …)` — three or more elements (CL's
1539 /// `(name default supplied-p)` shape is not yet supported — no
1540 /// `supplied-p` variable binding without an evaluator).
1541 /// * `(5 default)` — first element isn't a symbol.
1542 ///
1543 /// Sibling of `OptionalMarkerRepeated` (the `&optional`-section
1544 /// marker gate) and `NonSymbolParam` (the bare-symbol gate): together
1545 /// the three close every distinct typed-entry rejection the optional
1546 /// section can emit. The bare-symbol form `&optional x` is still
1547 /// admitted through the bare-symbol path; the list form `&optional
1548 /// (x default)` is admitted iff this gate accepts the spec.
1549 ///
1550 /// `position` is the loop index of the offending list inside
1551 /// `parse_params`, parallel to `OptionalMarkerRepeated.first_position`
1552 /// / `RestParamTrailingTokens.rest_position` — naming the position
1553 /// lets an LSP quick-fix point at the spec to repair. `got` is
1554 /// `SexpWitness` — the closed-set typed joint identity pairing the
1555 /// offending list's `SexpShape::List` with its `Sexp::Display`
1556 /// projection, so consumers (REPL, LSP, `tatara-check`) bind to
1557 /// BOTH the structural shape AND the renderable literal jointly,
1558 /// same posture as `NonSymbolParam.got` / `OptionalMarkerRepeated`'s
1559 /// SexpWitness siblings. `reason` is `OptionalParamMalformedReason`
1560 /// — the closed-set typed enum whose four variants are EXACTLY the
1561 /// four reachable list-spec rejection modes, encoded as a TYPE so a
1562 /// future fifth reason (e.g. supplied-p once an evaluator lands)
1563 /// becomes a type-level extension rather than a substring drift.
1564 /// Mirror at the `parse_params` optional-section boundary of the
1565 /// prior-run `MacroDefHead` / `UnquoteForm` / `TemplateInvariantKind`
1566 /// / `CompilerSpecIoStage` closed-set lifts.
1567 ///
1568 /// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid
1569 /// states unrepresentable"; the four malformed list-spec shapes are
1570 /// nonsense `MacroParams` cannot hold, so the gate must REJECT
1571 /// rather than bind args to a marker symbol or drop the extras
1572 /// silently. THEORY.md §II.1 invariant 1 — typed entry; a malformed
1573 /// default-form spec is exactly the failure mode the typed-entry
1574 /// gate exists to reject — and the gate must reject DEFINITIONS as
1575 /// readily as it rejects CALLS. THEORY.md §II.1 invariant 2 — free
1576 /// middle; the gate fires inside `parse_params` BEFORE either
1577 /// expansion strategy runs, so both `Expander::new()` (bytecode) and
1578 /// `Expander::new_substitute_only()` (substitute) reject the SAME
1579 /// malformed spec at the SAME gate.
1580 #[error(
1581 "compile error in defmacro params: malformed &optional spec, got {got}{}",
1582 optional_param_malformed_suffix(*position, *reason)
1583 )]
1584 OptionalParamMalformed {
1585 position: usize,
1586 got: SexpWitness,
1587 reason: OptionalParamMalformedReason,
1588 },
1589 /// A `defmacro` / `defpoint-template` / `defcheck` form had fewer
1590 /// than 4 list elements: the head keyword must be followed by a
1591 /// name symbol, a param list, and a body — three required slots
1592 /// after the head, total length 4. The legacy `LispError::Compile
1593 /// { form: head.to_string(), message: "(defmacro name (params)
1594 /// body) required" }` shape named only the failure mode — it
1595 /// didn't say HOW MANY elements the operator actually wrote, so
1596 /// an authoring surface that wants to surface "you wrote 2
1597 /// elements; need 4" had to re-parse the source. The structural
1598 /// variant carries both: `head` is the head keyword (one of
1599 /// `"defmacro"` / `"defpoint-template"` / `"defcheck"`); `arity`
1600 /// is the actual length of the form, including the head element.
1601 /// Naming the actual arity is the typed-entry gate's structural-
1602 /// completeness floor (THEORY.md §V.1).
1603 ///
1604 /// Sibling of `NonSymbolParam` and `RestParamMissingName` for
1605 /// the defmacro-syntax-gate's other definition-site failure
1606 /// modes — those variants fire INSIDE `parse_params`, AFTER the
1607 /// arity gate has passed; this variant fires AT the arity gate
1608 /// itself, BEFORE name / params / body validation can run.
1609 /// Together, the three close `macro_def_from`'s outermost
1610 /// rejection chain — every distinct failure mode the gate can
1611 /// emit at the top level becomes a structural variant of
1612 /// `LispError`, not a `Compile`-shaped substring.
1613 ///
1614 /// `head` is `MacroDefHead` — the closed-set typed enum whose
1615 /// three variants are EXACTLY the three reachable head keywords
1616 /// (`Defmacro` ⊎ `DefpointTemplate` ⊎ `Defcheck`). Encoding the
1617 /// closed set as a TYPE makes the constraint that ONLY 3 head
1618 /// identities are reachable load-bearing in the type system — a
1619 /// fourth pseudo-head can never drift into the diagnostic at
1620 /// runtime; consumers (REPL, LSP, `tatara-check`) pattern-match
1621 /// on `MacroDefHead::Defcheck` etc. directly instead of
1622 /// substring-matching `head == "defcheck"`. Same posture as
1623 /// `LispError::CompilerSpecIo.stage: CompilerSpecIoStage` and
1624 /// `LispError::TemplateInvariant.kind: TemplateInvariantKind`:
1625 /// the closed set becomes a TYPE rather than a `&'static str`
1626 /// projection at the helper boundary. `arity` is `usize` because
1627 /// it is always `list.len()` at the call site (the length of
1628 /// the form including the head element). Display renders the
1629 /// head via `MacroDefHead`'s Display impl (which projects
1630 /// through `MacroDefHead::keyword()` to the canonical `&'static
1631 /// str` literal), so the legacy `head: &'static str`-shaped
1632 /// diagnostic rides through byte-for-byte.
1633 ///
1634 /// Display preserves the legacy `"(defmacro name (params) body)
1635 /// required"` substring byte-for-byte: the head is parameterized
1636 /// in the prefix `compile error in {head}:`, but the example
1637 /// template literal stays `(defmacro name (params) body)` —
1638 /// matching the legacy form's small infidelity for non-defmacro
1639 /// heads (the legacy shape rendered `compile error in
1640 /// defpoint-template: (defmacro name (params) body) required`)
1641 /// so authoring tools that substring-grep on the legacy
1642 /// rendering see no drift; the structural detail (`got {arity}
1643 /// elements, need 4`) is appended. When a future run gives
1644 /// `Sexp` source spans, `pos: Option<usize>` lands here in ONE
1645 /// place and every defmacro-arity site picks up positional
1646 /// rendering via `crate::diagnostic::format_diagnostic`
1647 /// mechanically.
1648 #[error(
1649 "compile error in {head}: (defmacro name (params) body) required \
1650 (got {arity} elements, need 4)"
1651 )]
1652 DefmacroArity { head: MacroDefHead, arity: usize },
1653 /// A `defmacro` / `defpoint-template` / `defcheck` form passed the
1654 /// arity gate (≥4 elements) but its name slot — `list[1]`, the
1655 /// element directly after the head — wasn't a symbol. The legacy
1656 /// `LispError::Compile { form: head.to_string(), message: "expected
1657 /// name symbol" }` shape named only the failure mode — it didn't
1658 /// say WHAT was found in the name slot, so an authoring surface
1659 /// that wants to surface "you wrote `5` where a name symbol was
1660 /// expected" had to re-parse the source. The structural variant
1661 /// carries both: `head` is the head keyword (one of `"defmacro"` /
1662 /// `"defpoint-template"` / `"defcheck"`); `got` is the offending
1663 /// `Sexp::Display` projection of the non-symbol element. Naming
1664 /// both the head AND the offending element is the typed-entry
1665 /// gate's structural-completeness floor (THEORY.md §V.1).
1666 ///
1667 /// Sibling of `DefmacroArity` and the `parse_params` pair
1668 /// (`NonSymbolParam`, `RestParamMissingName`) for the
1669 /// defmacro-syntax-gate's other definition-site failure modes.
1670 /// Walking a malformed `(defmacro …)` from the outside in, the
1671 /// gate fires:
1672 /// 1. `DefmacroArity { head, arity }` if the form has fewer
1673 /// than 4 elements (`(defmacro)`, `(defmacro f)`).
1674 /// 2. `DefmacroNonSymbolName { head, got }` if list[1] isn't a
1675 /// symbol (`(defmacro 5 () body)`, `(defmacro :foo () body)`).
1676 /// 3. Inside `parse_params`: `NonSymbolParam { position, got }`
1677 /// and `RestParamMissingName { rest_position, got }`.
1678 ///
1679 /// This run lifts step 2; the only remaining `Compile`-shaped
1680 /// site in `macro_def_from` is the `expected param list` gate
1681 /// (list[2] is not a list), which is the next move in the same
1682 /// rejection chain.
1683 ///
1684 /// `head` is `MacroDefHead` — same typed closed-set posture as
1685 /// `DefmacroArity.head`: the three reachable head identities
1686 /// (`Defmacro` ⊎ `DefpointTemplate` ⊎ `Defcheck`) are encoded as
1687 /// a TYPE so consumers pattern-match on variant identity rather
1688 /// than substring-comparing the rendered `head` literal.
1689 /// `got` is `SexpWitness` — the closed-set typed joint identity
1690 /// pairing the offending name-slot element's `SexpShape` (the
1691 /// twelve reachable outermost shapes the reader can produce) with
1692 /// its `Sexp::Display` projection (`5`, `:foo`, `"name"`,
1693 /// `(nested)`, etc.). Fourth consumer of the `SexpWitness`
1694 /// primitive (after `SpliceOutsideList.got`,
1695 /// `NonSymbolUnquoteTarget.got`, and `NonSymbolParam.got`):
1696 /// authoring tools (REPL, LSP, `tatara-check`) bind to BOTH
1697 /// `got.shape` (structurally pattern-matchable on `SexpShape::Int`,
1698 /// `SexpShape::Keyword`, `SexpShape::List`, etc.) AND `got.display`
1699 /// (the literal value, renderable verbatim) jointly across the
1700 /// variant slot.
1701 ///
1702 /// Display preserves the legacy `"expected name symbol"` substring
1703 /// byte-for-byte: the prefix `compile error in {head}:` matches
1704 /// the legacy `Compile { form: head.to_string(), message:
1705 /// "expected name symbol" }` shape; the structural detail (`,
1706 /// got {got}`) is appended. `{got}` flows through
1707 /// `SexpWitness::Display`, which writes only the `display` field,
1708 /// so the rendering is byte-for-byte identical to the legacy
1709 /// `got: String` shape. When a future run gives `Sexp` source
1710 /// spans, `pos: Option<usize>` lands inside `SexpWitness` in ONE
1711 /// place and every non-symbol-name site picks up positional
1712 /// rendering via `crate::diagnostic::format_diagnostic`
1713 /// mechanically.
1714 #[error("compile error in {head}: expected name symbol, got {got}")]
1715 DefmacroNonSymbolName {
1716 head: MacroDefHead,
1717 got: SexpWitness,
1718 },
1719 /// A `defmacro` / `defpoint-template` / `defcheck` form passed both
1720 /// the arity gate (≥4 elements) AND the name-symbol gate (list[1]
1721 /// is a symbol) but its param-list slot — `list[2]`, the third
1722 /// element after the head — wasn't a list. The legacy
1723 /// `LispError::Compile { form: head.to_string(), message: "expected
1724 /// param list" }` shape named only the failure mode — it didn't
1725 /// say WHAT was found in the param-list slot, so an authoring
1726 /// surface that wants to surface "you wrote `x` where a param list
1727 /// was expected" had to re-parse the source. The structural variant
1728 /// carries both: `head` is the head keyword (one of `"defmacro"` /
1729 /// `"defpoint-template"` / `"defcheck"`); `got` is the offending
1730 /// `Sexp::Display` projection of the non-list element. Naming both
1731 /// the head AND the offending element is the typed-entry gate's
1732 /// structural-completeness floor (THEORY.md §V.1).
1733 ///
1734 /// Sibling of `DefmacroArity`, `DefmacroNonSymbolName`, and the
1735 /// `parse_params` pair (`NonSymbolParam`, `RestParamMissingName`)
1736 /// for the defmacro-syntax-gate's other definition-site failure
1737 /// modes. Walking a malformed `(defmacro …)` from the outside in,
1738 /// the gate fires:
1739 /// 1. `DefmacroArity { head, arity }` if the form has fewer
1740 /// than 4 elements (`(defmacro)`, `(defmacro f)`).
1741 /// 2. `DefmacroNonSymbolName { head, got }` if list[1] isn't a
1742 /// symbol (`(defmacro 5 () body)`).
1743 /// 3. `DefmacroNonListParams { head, got }` if list[2] isn't a
1744 /// list (`(defmacro f x body)`).
1745 /// 4. Inside `parse_params`: `NonSymbolParam { position, got }`
1746 /// and `RestParamMissingName { rest_position, got }`.
1747 ///
1748 /// This run lifts step 3; after it, every inline `LispError::Compile
1749 /// { … }` triple in `macro_def_from` has been lifted to a structural
1750 /// variant — the entire `macro_def_from` rejection chain (arity →
1751 /// name-symbol → param-list → parse_params) is structurally typed
1752 /// for failure modes, with each variant naming WHICH failure mode
1753 /// AND WHAT was offending.
1754 ///
1755 /// `head` is `MacroDefHead` — same typed closed-set posture as
1756 /// `DefmacroArity.head` and `DefmacroNonSymbolName.head`. After
1757 /// this lift all three `Defmacro*` variants share ONE typed
1758 /// head identity, parallel to how `LispError::CompilerSpecIo`
1759 /// carries `stage: CompilerSpecIoStage` for the four
1760 /// disk-persistence (operation, stage) pairs.
1761 /// `got` is `SexpWitness` — the closed-set typed joint identity
1762 /// pairing the offending param-list-slot element's `SexpShape`
1763 /// (the twelve reachable outermost shapes the reader can produce)
1764 /// with its `Sexp::Display` projection (`x`, `5`, `:foo`,
1765 /// `"params"`, etc.). Fifth consumer of the `SexpWitness`
1766 /// primitive (after `SpliceOutsideList.got`,
1767 /// `NonSymbolUnquoteTarget.got`, `NonSymbolParam.got`, and
1768 /// `DefmacroNonSymbolName.got`): authoring tools (REPL, LSP,
1769 /// `tatara-check`) bind to BOTH `got.shape` (structurally
1770 /// pattern-matchable on `SexpShape::Symbol`, `SexpShape::Int`,
1771 /// `SexpShape::Keyword`, `SexpShape::String`, etc.) AND
1772 /// `got.display` (the literal value, renderable verbatim) jointly
1773 /// across the variant slot. After this lift the entire
1774 /// `macro_def_from` rejection chain — arity → name-symbol →
1775 /// param-list — shares ONE typed witness identity at every
1776 /// `Sexp::Display`-source slot; the only remaining unlifted
1777 /// rejection points in `macro_def_from`'s typed-entry chain are
1778 /// `RestParamMissingName.got: Option<String>` (inside
1779 /// `parse_params`) and `MissingHeadSymbol.got: Option<String>`
1780 /// (at the outer typed-entry gate).
1781 ///
1782 /// Display preserves the legacy `"expected param list"` substring
1783 /// byte-for-byte: the prefix `compile error in {head}:` matches
1784 /// the legacy `Compile { form: head.to_string(), message:
1785 /// "expected param list" }` shape; the structural detail (`, got
1786 /// {got}`) is appended. `{got}` flows through
1787 /// `SexpWitness::Display`, which writes only the `display` field,
1788 /// so the rendering is byte-for-byte identical to the legacy
1789 /// `got: String` shape. When a future run gives `Sexp` source
1790 /// spans, `pos: Option<usize>` lands inside `SexpWitness` in ONE
1791 /// place and every non-list-params site picks up positional
1792 /// rendering via `crate::diagnostic::format_diagnostic`
1793 /// mechanically.
1794 #[error("compile error in {head}: expected param list, got {got}")]
1795 DefmacroNonListParams {
1796 head: MacroDefHead,
1797 got: SexpWitness,
1798 },
1799 /// `T::compile_from_sexp` (the `TataraDomain` trait default) was
1800 /// passed something that isn't a list — a bare atom (`5`, `:foo`,
1801 /// `"x"`, `name`) where a top-level `(KEYWORD …)` form was
1802 /// expected. The legacy `LispError::Compile { form:
1803 /// keyword.to_string(), message: "expected list form" }` shape
1804 /// named only the failure mode and the keyword, and required
1805 /// authoring tools (REPL, LSP, `tatara-check`) to substring-grep
1806 /// the rendered message to recognize this specific gate. The
1807 /// structural variant carries `keyword` as a first-class field so
1808 /// consumers pattern-match on the variant and bind directly to
1809 /// the keyword instead of substring-parsing.
1810 ///
1811 /// Sibling of `HeadMismatch` — both are typed-entry rejection
1812 /// gates inside the trait default `compile_from_sexp` walking a
1813 /// malformed form from the outside in:
1814 /// 1. `NotAListForm { keyword }` if the form isn't a list at
1815 /// all (`5`, `:foo`, `"x"`, `name` — bare atoms).
1816 /// 2. `LispError::Compile { form, message: "missing head
1817 /// symbol" }` (NOT YET LIFTED) if the list is empty or
1818 /// list[0] isn't a symbol (`()`, `(5 …)`, `(:foo …)`).
1819 /// 3. `HeadMismatch { keyword, got }` if list[0] is a symbol
1820 /// but doesn't match `T::KEYWORD` (`(other-name …)`).
1821 ///
1822 /// After this lift step 1 is structural; the `missing head
1823 /// symbol` gate is the next move in the same rejection chain
1824 /// (its own structural-variant lift, parallel to how the
1825 /// `defmacro_*` family was lifted gate-by-gate).
1826 ///
1827 /// `keyword` is `&'static str` because every call site passes
1828 /// `Self::KEYWORD` from the trait default — a compile-time
1829 /// literal sourced from the `#[tatara(keyword = "...")]` derive
1830 /// attribute (or hand-written const). Using a static slot makes
1831 /// that compile-time guarantee load-bearing in the type system
1832 /// (a typo in the keyword can never drift into the diagnostic at
1833 /// runtime — the type system is the floor, same posture as
1834 /// `HeadMismatch.keyword`, `TypeMismatch.expected`, and the
1835 /// `Defmacro*.head` family).
1836 ///
1837 /// Display preserves the legacy `"expected list form"` substring
1838 /// AND the `"compile error in {keyword}:"` prefix byte-for-byte
1839 /// — `"compile error in {keyword}: expected list form"` — so
1840 /// existing consumer assertions (e.g., the
1841 /// `compile_from_sexp_emits_compile_for_non_list_form` test
1842 /// against `MonitorSpec`, `tatara-check`'s diagnostic capture)
1843 /// pass unchanged. The variant carries no `got` slot because the
1844 /// offending value's type is itself the diagnostic — `5` /
1845 /// `:foo` / `"x"` / `name` all reduce to the same failure mode
1846 /// (not a list); naming the type would be redundant with what
1847 /// the source already shows. When a future run gives `Sexp`
1848 /// source spans, `pos: Option<usize>` lands here in ONE place
1849 /// and every not-a-list-form site picks up positional rendering
1850 /// via `crate::diagnostic::format_diagnostic` mechanically.
1851 #[error("compile error in {keyword}: expected list form")]
1852 NotAListForm { keyword: &'static str },
1853 /// `T::compile_from_sexp` was passed a list whose head can't be
1854 /// projected to a symbol — either the list is empty (`()` — there
1855 /// is no first element) or its first element exists but isn't a
1856 /// symbol (`(5 …)`, `(:foo …)`, `("x" …)`, `((nested) …)`). The
1857 /// legacy `LispError::Compile { form: keyword.to_string(),
1858 /// message: "missing head symbol" }` shape collapsed both
1859 /// sub-modes into one diagnostic — a `()` form and a `(5 …)` form
1860 /// produced byte-identical messages, so an authoring surface that
1861 /// wants to surface "your form is empty" vs. "your form's head is
1862 /// `5`, not a symbol" had to re-parse the source. The structural
1863 /// variant carries `got: Option<String>` so the two sub-modes are
1864 /// distinguishable structurally — `None` for the empty-list case,
1865 /// `Some(g)` for the present-but-not-symbol case where `g` is
1866 /// the offending head's `Sexp::Display` projection. Naming both
1867 /// the failure mode AND the structural detail (empty vs. offending
1868 /// head) is the typed-entry gate's structural-completeness floor
1869 /// (THEORY.md §V.1).
1870 ///
1871 /// Sibling of `NotAListForm { keyword }` and `HeadMismatch
1872 /// { keyword, got }` — together the three close every distinct
1873 /// failure mode the trait-default `compile_from_sexp` rejection
1874 /// chain can emit. Walking a malformed `(KEYWORD …)` form from
1875 /// the outside in:
1876 /// 1. `NotAListForm { keyword }` — the form isn't a list at all
1877 /// (`5`, `:foo`, `"x"`, `name` — bare atoms).
1878 /// 2. `MissingHeadSymbol { keyword, got }` — the form is a
1879 /// list but list[0] doesn't exist (`()`) or isn't a symbol
1880 /// (`(5 …)`, `(:foo …)`).
1881 /// 3. `HeadMismatch { keyword, got }` — list[0] is a symbol
1882 /// but doesn't match `T::KEYWORD` (`(other-name …)`).
1883 ///
1884 /// After this lift the entire `compile_from_sexp` rejection chain
1885 /// is structurally typed for failure modes — every distinct
1886 /// rejection binds to ONE structural variant of `LispError`, not
1887 /// a `Compile`-shaped substring. The `got: Option<String>`
1888 /// posture parallels `RestParamMissingName.got: Option<String>`:
1889 /// the failure mode IS one ("head can't be projected to a
1890 /// symbol"); the bifurcation is in the renderable detail (empty
1891 /// vs. present-but-wrong-type), not in what the gate rejects, so
1892 /// the two sub-modes share ONE variant rather than splitting into
1893 /// near-identical siblings.
1894 ///
1895 /// `keyword` is `&'static str` because every call site passes
1896 /// `Self::KEYWORD` from the trait default — a compile-time literal
1897 /// sourced from the `#[tatara(keyword = "...")]` derive attribute
1898 /// (or hand-written const). Using a static slot makes that
1899 /// compile-time guarantee load-bearing in the type system (a typo
1900 /// in the keyword can never drift into the diagnostic at runtime —
1901 /// the type system is the floor, same posture as
1902 /// `NotAListForm.keyword`, `HeadMismatch.keyword`, and the
1903 /// `Defmacro*.head` family). `got` is `Option<SexpWitness>` — the
1904 /// SEVENTH consumer of the typed `SexpWitness` primitive (after
1905 /// `SpliceOutsideList.got`, `NonSymbolUnquoteTarget.got`,
1906 /// `NonSymbolParam.got`, `DefmacroNonSymbolName.got`,
1907 /// `DefmacroNonListParams.got`, and `RestParamMissingName.got`).
1908 /// The `Option`-wrap bifurcates structurally between "missing
1909 /// entirely" (`None`, when the list is empty) and "present but
1910 /// malformed" (`Some(SexpWitness)`, when the head exists but
1911 /// isn't a symbol); the typed witness lands on the `Some` arm
1912 /// only — same posture as `RestParamMissingName.got:
1913 /// Option<SexpWitness>`. With this lift EVERY Sexp-display-source
1914 /// `got` slot in the substrate carries ONE typed identity:
1915 /// the typed-entry / template-gate / defmacro-syntax-gate
1916 /// rejection surface is structurally unified end-to-end across
1917 /// ALL `got: <T>` slots where `<T>` projects from `Sexp::Display`.
1918 ///
1919 /// Display preserves the legacy `"missing head symbol"` substring
1920 /// AND the `"compile error in {keyword}:"` prefix byte-for-byte —
1921 /// `"compile error in {keyword}: missing head symbol"` is the
1922 /// stable prefix; the structural detail (`(empty list)` for
1923 /// `None`, `(got {g})` for `Some(g)`) is appended in a
1924 /// parenthetical, parallel to how `RestParamMissingName` appends
1925 /// `(rest marker at position {n}, got {g})` /
1926 /// `(rest marker at position {n}, none provided)` and how
1927 /// `SpliceOutsideList` appends `(got ,@{got})`. The `{g}` slot
1928 /// flows through `SexpWitness::Display`, which writes only the
1929 /// `display` field, so the rendering is byte-for-byte identical
1930 /// to the pre-lift `Option<String>` shape. When a future run
1931 /// gives `Sexp` source spans, `pos: Option<usize>` lands inside
1932 /// `SexpWitness` in ONE place and every missing-head-symbol site
1933 /// picks up positional rendering via
1934 /// `crate::diagnostic::format_diagnostic` mechanically.
1935 #[error(
1936 "compile error in {keyword}: missing head symbol{}",
1937 missing_head_symbol_suffix(got.as_ref().map(|w| w.display.as_str()))
1938 )]
1939 MissingHeadSymbol {
1940 keyword: &'static str,
1941 got: Option<SexpWitness>,
1942 },
1943 /// `compile_named_from_forms::<T>` — driving every `(KEYWORD NAME …)`
1944 /// positional-name surface (`(defpoint NAME …)`, `(defalertpolicy
1945 /// NAME …)`) — was passed a list whose head matched `T::KEYWORD` but
1946 /// whose tail had no NAME slot at all. `(defpoint)` — list.len() == 1
1947 /// (just the keyword); the gate fires before NAME extraction. The
1948 /// legacy `LispError::Compile { form: T::KEYWORD.to_string(),
1949 /// message: format!("expected ({} NAME …)", T::KEYWORD) }` shape
1950 /// named the failure mode AND the keyword by embedding both into a
1951 /// formatted message — required authoring tools (REPL, LSP,
1952 /// `tatara-check`) to substring-grep the rendered diagnostic to
1953 /// recognize this specific gate. The structural variant carries
1954 /// `keyword` as a first-class field so consumers pattern-match on
1955 /// the variant and bind to the keyword directly instead of
1956 /// substring-parsing.
1957 ///
1958 /// Sibling of `NotAListForm { keyword }`, `MissingHeadSymbol
1959 /// { keyword, got }`, and `HeadMismatch { keyword, got }` — those
1960 /// close the trait-default `compile_from_sexp` rejection chain
1961 /// (the keyword-only entry point, `(KEYWORD :k v …)`); this
1962 /// variant opens the parallel `compile_named_from_forms`
1963 /// rejection chain (the positional-name entry point, `(KEYWORD
1964 /// NAME :k v …)`). Walking a malformed `(KEYWORD NAME …)` form
1965 /// from the outside in:
1966 /// 1. `NamedFormMissingName { keyword }` — the form passes the
1967 /// keyword-head match but has no NAME slot (`(defpoint)`).
1968 /// 2. `LispError::Compile { form, message: "positional NAME
1969 /// must be a symbol or string" }` (NOT YET LIFTED) — the
1970 /// form has a NAME slot but it's not a symbol or string
1971 /// (`(defpoint 5 …)`, `(defpoint :foo …)`, `(defpoint
1972 /// (nested) …)`).
1973 /// 3. Inside `T::compile_from_args(&list[2..])` — derive-
1974 /// generated kwargs handling with its own structural
1975 /// variants (`UnknownKwarg`, `MissingKwarg`, etc.).
1976 ///
1977 /// This run lifts step 1; step 2 is the next move in the same
1978 /// rejection chain (its own structural-variant lift, parallel to
1979 /// how the `compile_from_sexp` chain was lifted gate-by-gate
1980 /// across `092a2b2` (`NotAListForm`) and `b3e941e`
1981 /// (`MissingHeadSymbol`)).
1982 ///
1983 /// `keyword` is `&'static str` because every call site passes
1984 /// `T::KEYWORD` from `compile_named_from_forms` — a compile-time
1985 /// literal sourced from the `#[tatara(keyword = "...")]` derive
1986 /// attribute (or hand-written const). Using a static slot makes
1987 /// that compile-time guarantee load-bearing in the type system —
1988 /// a typo in the keyword can never drift into the diagnostic at
1989 /// runtime, the type system is the floor, same posture as
1990 /// `NotAListForm.keyword`, `MissingHeadSymbol.keyword`,
1991 /// `HeadMismatch.keyword`, `TypeMismatch.expected`, and the
1992 /// `Defmacro*.head` family. The variant carries no `arity` slot
1993 /// because the offending form's structure is invariant — every
1994 /// trigger has list.len() == 1 exactly (list[0] is the keyword,
1995 /// no list[1] for NAME); naming a fixed value would be
1996 /// misleading data, parallel to how `NotAListForm` carries no
1997 /// `got` slot (the form's not-a-list type is itself the
1998 /// diagnostic).
1999 ///
2000 /// Display matches the legacy `Compile`-shaped diagnostic
2001 /// byte-for-byte — `"compile error in {keyword}: expected
2002 /// ({keyword} NAME …)"` (note: `…` is the Unicode horizontal
2003 /// ellipsis U+2026, preserved verbatim from the legacy
2004 /// `format!("expected ({} NAME …)", T::KEYWORD)` shape) — so
2005 /// existing consumer assertions (`tatara-check`'s diagnostic
2006 /// capture, REPL substring-greps) pass unchanged. When a future
2007 /// run gives `Sexp` source spans, `pos: Option<usize>` lands
2008 /// here in ONE place and every named-form-missing-name site
2009 /// picks up positional rendering via
2010 /// `crate::diagnostic::format_diagnostic` mechanically.
2011 #[error("compile error in {keyword}: expected ({keyword} NAME …)")]
2012 NamedFormMissingName { keyword: &'static str },
2013 /// `compile_named_from_forms::<T>` was passed a `(KEYWORD NAME …)` form
2014 /// whose NAME slot exists but isn't projectable to a symbol or string —
2015 /// `(defpoint 5 …)`, `(defpoint :foo …)`, `(defpoint (nested) …)`. Gate
2016 /// 2 of the same rejection chain `NamedFormMissingName` opens: that
2017 /// variant fires when there is no NAME slot at all (`(defpoint)` —
2018 /// list.len() == 1); this variant fires when the NAME slot exists but
2019 /// is wrong-typed. Together the two close `compile_named_from_forms`'s
2020 /// outer rejection chain — every typed-entry rejection mode in the
2021 /// positional-name authoring surface is now a structural variant of
2022 /// `LispError`, not a `Compile`-shaped substring.
2023 ///
2024 /// `keyword` is `&'static str` because every call site passes
2025 /// `T::KEYWORD` — a compile-time literal sourced from the
2026 /// `#[tatara(keyword = "...")]` derive attribute (or hand-written
2027 /// const); a typo in the keyword can never drift into the diagnostic
2028 /// at runtime. `got` is the typed closed-set `SexpShape` enum —
2029 /// the twelve reachable Sexp outermost shapes encoded as variant
2030 /// identities so the SexpShape that the typed-entry gate observed
2031 /// is load-bearing data in the type system. Same posture as
2032 /// `TypeMismatch.got: SexpShape`: consumers pattern-match on
2033 /// `SexpShape::Int` etc. directly instead of substring-matching
2034 /// `got == "int"`. Encoding the closed set as a TYPE makes the
2035 /// compile-time guarantee load-bearing, parallel to
2036 /// `NotAListForm.keyword`, `MissingHeadSymbol.keyword`,
2037 /// `HeadMismatch.keyword`, and the `Defmacro*.head` family.
2038 ///
2039 /// Display preserves the legacy `"positional NAME must be a symbol
2040 /// or string"` substring AND the `"compile error in {keyword}:"`
2041 /// prefix byte-for-byte; the structural detail (`(got {got})`) is
2042 /// appended in a parenthetical, parallel to how `MissingHeadSymbol`
2043 /// appends `(got {g})` / `(empty list)` and how `RestParamMissingName`
2044 /// appends `(rest marker at position {n}, got {g})`. When a future
2045 /// run gives `Sexp` source spans, `pos: Option<usize>` lands here in
2046 /// ONE place and every named-form-non-symbol-name site picks up
2047 /// positional rendering via `crate::diagnostic::format_diagnostic`
2048 /// mechanically.
2049 #[error("compile error in {keyword}: positional NAME must be a symbol or string (got {got})")]
2050 NamedFormNonSymbolName {
2051 keyword: &'static str,
2052 got: SexpShape,
2053 },
2054 /// `rewrite_typed::<T>` — the typed-exit gate of the self-optimization
2055 /// primitive (THEORY.md §II.1 invariant 3) — was handed a rewriter
2056 /// closure whose output, after typed round-trip through canonical JSON,
2057 /// did not project to `Sexp::List`. The round-trip contract is:
2058 /// serialize `T` → `Sexp::List` (alternating kwargs), hand the list
2059 /// to the rewriter `F`, re-enter `T::compile_from_args` over the
2060 /// returned list's items. A non-list result violates that contract —
2061 /// the gate fires before `compile_from_args` runs, so a wrong-shaped
2062 /// rewriter output is rejected at the typed-exit boundary rather than
2063 /// confusingly later inside the kwargs decoder.
2064 ///
2065 /// Mirror at the typed-exit boundary of the typed-entry-side
2066 /// `NamedFormNonSymbolName` lift: the latter rejects a wrong-typed
2067 /// NAME slot at `compile_named_from_forms::<T>`'s entry; this variant
2068 /// rejects a wrong-typed rewriter output at `rewrite_typed::<T>`'s
2069 /// exit. Both round-trip the same compile-time `T::KEYWORD` projection
2070 /// into the variant's `keyword` slot, so authoring tools (REPL, LSP,
2071 /// `tatara-check`) bind on variant identity at both boundaries of the
2072 /// self-optimization primitive rather than substring-grepping the
2073 /// rendered diagnostic.
2074 ///
2075 /// `keyword` is `&'static str` because every call site passes
2076 /// `T::KEYWORD` from `rewrite_typed::<T>` — a compile-time literal
2077 /// sourced from the `#[tatara(keyword = "...")]` derive attribute (or
2078 /// hand-written const). Using a static slot makes that compile-time
2079 /// guarantee load-bearing in the type system — a typo can never drift
2080 /// into the diagnostic at runtime, the type system is the floor, same
2081 /// posture as `NotAListForm.keyword`, `MissingHeadSymbol.keyword`,
2082 /// `HeadMismatch.keyword`, `NamedFormMissingName.keyword`, and
2083 /// `NamedFormNonSymbolName.keyword`.
2084 ///
2085 /// `got` is `SexpWitness` — the closed-set typed joint identity
2086 /// pairing the offending rewriter output's `SexpShape` (the twelve
2087 /// reachable Sexp outermost shapes the rewriter closure can produce)
2088 /// with its `Sexp::Display` projection (the literal value the rewriter
2089 /// actually returned — `42`, `:foo`, `"bad"`, `notify-ref`, `()`,
2090 /// etc.). EIGHTH consumer of the typed `SexpWitness` primitive
2091 /// introduced in `error.rs`'s `SpliceOutsideList.got` lift, and the
2092 /// FIRST consumer on the typed-EXIT boundary — sibling lifts of
2093 /// `SpliceOutsideList.got: SexpWitness`, `NonSymbolUnquoteTarget.got:
2094 /// SexpWitness`, `NonSymbolParam.got: SexpWitness`,
2095 /// `DefmacroNonSymbolName.got: SexpWitness`,
2096 /// `DefmacroNonListParams.got: SexpWitness`,
2097 /// `RestParamMissingName.got: Option<SexpWitness>`, and
2098 /// `MissingHeadSymbol.got: Option<SexpWitness>` close the typed-ENTRY
2099 /// rejection surface across the substrate's seven entry-side gates.
2100 /// This eighth lift extends the typed-identity unification contract
2101 /// across BOTH boundaries of the typed-IR algebra
2102 /// (THEORY.md §II.1 invariant 1 + invariant 3) — every
2103 /// `Sexp::Display`-source `got` slot in the substrate, regardless of
2104 /// whether the rejection fires at typed-ENTRY (compile_from_sexp
2105 /// chain, template-gate, defmacro-syntax-gate, parse_params walker)
2106 /// or typed-EXIT (rewrite_typed's `Sexp::List`-contract gate), now
2107 /// shares ONE typed witness identity at the variant slot. Authoring
2108 /// tools (REPL, LSP, `tatara-check`) bind to BOTH `got.shape`
2109 /// (structurally pattern-matchable on `SexpShape::Int` etc.) AND
2110 /// `got.display` (the literal value, renderable verbatim) jointly
2111 /// for a typed-exit-side rejection too — no projection-to-`String`
2112 /// at the helper boundary loses the structural identity. Promotes
2113 /// the legacy `got: String` shape parallel to how the seven entry-
2114 /// side lifts promoted theirs.
2115 ///
2116 /// Display matches the legacy `Compile`-shaped diagnostic byte-for-
2117 /// byte — `"compile error in {keyword}: rewriter must return a list;
2118 /// got {got}"` — so existing consumer assertions (`tatara-check`'s
2119 /// diagnostic capture, REPL substring-greps that match on `"rewriter
2120 /// must return a list; got "`) pass unchanged across the lift. The
2121 /// `{got}` slot flows through `SexpWitness::Display`, which writes
2122 /// only the `display` field, so the rendering is byte-for-byte
2123 /// identical to the pre-lift `got: String` shape. When a future run
2124 /// gives `Sexp` source spans, `pos: Option<usize>` lands inside
2125 /// `SexpWitness` in ONE place and every rewriter-non-list site
2126 /// picks up positional rendering via
2127 /// `crate::diagnostic::format_diagnostic` mechanically.
2128 #[error("compile error in {keyword}: rewriter must return a list; got {got}")]
2129 RewriterNonList {
2130 keyword: &'static str,
2131 got: SexpWitness,
2132 },
2133 /// `serde_json::to_value` of a typed `T` value (any registered
2134 /// `TataraDomain`) errored. Two sites share this failure mode:
2135 /// `register::<T>`'s registry-dispatch closure (the registered
2136 /// handler serializes the just-typed value to JSON for the
2137 /// dispatcher) and `rewrite_typed::<T>`'s round-trip prelude (the
2138 /// self-optimization primitive serializes its input to JSON before
2139 /// projecting it to a `Sexp::List` for the rewriter closure). Both
2140 /// funnel through `serialize_to_json_err::<T>` so the type-level
2141 /// `T::KEYWORD` projection is mechanically threaded into the
2142 /// `keyword` slot, parallel to how `rewriter_non_list_err::<T>`
2143 /// threads `T::KEYWORD` into `RewriterNonList.keyword`.
2144 ///
2145 /// Mirror at the typed-exit boundary of the typed-entry-side
2146 /// `from_value` failure path: `extract_via_serde` /
2147 /// `extract_optional_via_serde` / `extract_vec_via_serde` route
2148 /// through `deserialize_err` / `deserialize_item_err`, which now
2149 /// produce the structural `LispError::KwargDeserialize { key, idx,
2150 /// message }` variant — the typed-entry-side sibling of this lift.
2151 /// After both lifts BOTH directions of the JSON-projection round-
2152 /// trip — `to_value` (typed-exit, keyword-keyed) AND `from_value`
2153 /// (typed-entry, key-keyed) — are structurally typed; there are
2154 /// zero `LispError::Compile { ... }` construction sites left in
2155 /// `tatara-lisp/src/domain.rs`.
2156 ///
2157 /// Sibling of `RewriterNonList { keyword, got }` for the
2158 /// `rewrite_typed::<T>` rejection chain — that variant fires when
2159 /// the rewriter's OUTPUT is not a list; this variant fires when
2160 /// the round-trip's INPUT (the typed value) fails to project to
2161 /// JSON at all. Together with `RewriterNonList`, every distinct
2162 /// `to_value`-side rejection mode in the self-optimization
2163 /// primitive and the registry-dispatch closure binds to ONE
2164 /// structural variant of `LispError`, not a `Compile`-shaped
2165 /// substring.
2166 ///
2167 /// `keyword` is `&'static str` because every call site projects
2168 /// `T::KEYWORD` via `serialize_to_json_err::<T>` — a compile-time
2169 /// literal sourced from the `#[tatara(keyword = "...")]` derive
2170 /// attribute (or hand-written const). Using a static slot makes
2171 /// that compile-time guarantee load-bearing in the type system —
2172 /// a typo can never drift into the diagnostic at runtime, the
2173 /// type system is the floor, same posture as
2174 /// `RewriterNonList.keyword`, `NamedFormMissingName.keyword`,
2175 /// `NamedFormNonSymbolName.keyword`, `NotAListForm.keyword`,
2176 /// `MissingHeadSymbol.keyword`, `HeadMismatch.keyword`, and the
2177 /// `Defmacro*.head` family. `message` is `String` because it
2178 /// carries the `serde_json::Error::Display` projection (errors
2179 /// render `expected … at line L column C` shapes — arbitrary text
2180 /// from the underlying `serde_json::Error`). Carrying the rendered
2181 /// message rather than a `#[source] serde_json::Error` keeps the
2182 /// variant's structural shape parallel to every other String-
2183 /// carrying variant in this enum — every consumer renders via
2184 /// Display, none consumes the underlying error chain.
2185 ///
2186 /// Display matches the legacy `Compile`-shaped diagnostic byte-
2187 /// for-byte — `"compile error in {keyword}: serialize: {message}"`
2188 /// — so existing consumer assertions (`tatara-check`'s diagnostic
2189 /// capture, REPL substring-greps that match on `"serialize: "`)
2190 /// pass unchanged across the lift. When a future run gives `Sexp`
2191 /// source spans, `pos: Option<usize>` lands here in ONE place and
2192 /// every domain-serialize site picks up positional rendering via
2193 /// `crate::diagnostic::format_diagnostic` mechanically.
2194 #[error("compile error in {keyword}: serialize: {message}")]
2195 DomainSerialize {
2196 keyword: &'static str,
2197 message: String,
2198 },
2199 /// `serde_json::from_value::<T>` of a kwarg's canonical-JSON projection
2200 /// errored. Two distinct sites share this failure mode through ONE
2201 /// structural variant whose data carries the typed closed-set
2202 /// `KwargPath` enum directly — `KwargPath::Named(key)` for the scalar
2203 /// / `Option<T>` path, `KwargPath::Item { key, idx }` for the per-item
2204 /// path inside a `Vec<T>` kwarg. The bifurcation lives inside the
2205 /// typed enum's variant identity, not in a sibling `idx: Option<usize>`
2206 /// slot:
2207 ///
2208 /// 1. `extract_via_serde` (required) and `extract_optional_via_serde`
2209 /// (optional) — kwarg-keyed `from_value` failures at the scalar /
2210 /// `Option<T>` path. `path: KwargPath::Named(key)`; the failure
2211 /// binds to the kwarg slot identity ONLY (`:{key}`).
2212 /// 2. `extract_vec_via_serde` (per-item) — kwarg-AND-index-keyed
2213 /// `from_value` failures inside a `Vec<T>` kwarg's items.
2214 /// `path: KwargPath::Item { key, idx }`; the failure binds to the
2215 /// kwarg slot AND the failing item index (`:{key}[{i}]`).
2216 ///
2217 /// Mirror at the typed-entry JSON boundary of the typed-exit-side
2218 /// `DomainSerialize { keyword, message }` lift: the latter rejects a
2219 /// `to_value::<T>` failure (typed-exit, keyword-keyed, sourced from
2220 /// `T::KEYWORD` and so `&'static str`); this variant rejects a
2221 /// `from_value::<T>` failure (typed-entry, kwargs-path-keyed, sourced
2222 /// from the runtime kwarg lookup and carried as a typed `KwargPath`).
2223 /// Together the two close the JSON-projection boundary of the
2224 /// typed-domain surface — every distinct `serde_json` failure mode at
2225 /// the typed-domain boundary binds to ONE structural variant of
2226 /// `LispError`, not a `Compile`-shaped substring.
2227 ///
2228 /// Sibling of `TypeMismatch.form: KwargPath`: both kwargs-path-keyed
2229 /// typed-entry rejection modes now carry the SAME typed kwargs-path
2230 /// identity inside their variant's data shape. The `(key, idx:
2231 /// Option<usize>)` bifurcation collapses into `KwargPath`'s variant
2232 /// identity — `Named` vs. `Item` — so the invalid combination
2233 /// `(key: "", idx: Some(0))` for a scalar path (or any combination that
2234 /// invented a fourth sub-mode) becomes structurally unrepresentable
2235 /// rather than re-asserted at the helper boundary via runtime
2236 /// `Option::is_some` comparison. Same closed-set posture as
2237 /// `LispError::TypeMismatch.form: KwargPath`,
2238 /// `LispError::Defmacro*.head: MacroDefHead`,
2239 /// `LispError::UnboundTemplateVar.prefix: UnquoteForm`,
2240 /// `LispError::CompilerSpecIo.stage: CompilerSpecIoStage`, and
2241 /// `LispError::TemplateInvariant.kind: TemplateInvariantKind`.
2242 ///
2243 /// `path` is `KwargPath` — the closed-set typed enum whose variants
2244 /// are EXACTLY the reachable kwargs-path shapes (`Named(String)` /
2245 /// `Item { key: String, idx: usize }` / `Slot(usize)`). The runtime
2246 /// `kwarg lookup` source-of-key is carried inside the typed enum's
2247 /// `String` payload; the per-item-index bifurcation is the enum's
2248 /// `Named` vs. `Item` variant identity, not a sibling Option slot.
2249 /// `message` is `String` because it carries the
2250 /// `serde_json::Error::Display` projection (errors render `expected …
2251 /// at line L column C` shapes — arbitrary text from the underlying
2252 /// `serde_json::Error`); carrying the rendered message rather than a
2253 /// `#[source] serde_json::Error` keeps the variant's structural shape
2254 /// parallel to every other String-carrying variant in this enum
2255 /// (`DomainSerialize.message`, `Compile.message`).
2256 ///
2257 /// `message` carries the raw `serde_json::Error::Display` projection
2258 /// — NO `"deserialize: "` prefix in the field, the prefix is in the
2259 /// `Display` rendering — so consumers that pattern-match on
2260 /// `message` get the underlying diagnostic unchanged, parallel to how
2261 /// `DomainSerialize.message` carries the raw `serde_json` projection
2262 /// (the `"serialize: "` prefix lives in Display, not in the slot).
2263 ///
2264 /// Display matches the legacy `Compile`-shaped diagnostic byte-for-
2265 /// byte across both sub-modes via `KwargPath`'s Display projection:
2266 /// `"compile error in :{key}: deserialize: {message}"` for
2267 /// `KwargPath::Named`, `"compile error in :{key}[{idx}]: deserialize:
2268 /// {message}"` for `KwargPath::Item` — so existing substring-grep
2269 /// consumers (`tatara-check`'s diagnostic capture, REPL substring-greps
2270 /// that match on `"deserialize: "`, `":steps[1]"`, `":level"`) pass
2271 /// unchanged across the lift. When a future run gives `Sexp` source
2272 /// spans, `pos: Option<usize>` lands here in ONE place and every
2273 /// kwarg-deserialize site picks up positional rendering via
2274 /// `crate::diagnostic::format_diagnostic` mechanically.
2275 #[error("compile error in {path}: deserialize: {message}")]
2276 KwargDeserialize { path: KwargPath, message: String },
2277 /// `compiler_spec.rs`'s disk-persistence surface emitted an
2278 /// I/O or serde failure. Four call sites in `compiler_spec.rs`
2279 /// share this failure mode through ONE structural variant keyed
2280 /// on the closed-set `CompilerSpecIoStage` enum (`realize_to_disk`
2281 /// × {serialize, write} ⊎ `load_from_disk` × {read, deserialize}).
2282 ///
2283 /// Encoding the (operation, stage) pair as ONE typed enum (rather
2284 /// than two `&'static str` slots `operation` × `stage`) makes the
2285 /// constraint that ONLY 4 of the 2×4 = 8 hypothetical pairs are
2286 /// reachable load-bearing in the type system — a typo like
2287 /// `(operation: "load_from_disk", stage: "write")` becomes
2288 /// structurally unrepresentable rather than re-asserted at the
2289 /// helper boundary via runtime string comparison. Same posture as
2290 /// `MacroDefHead` in `macro_expand.rs`: the closed set becomes a
2291 /// TYPE, and rustc's exhaustiveness check is the future invariant-
2292 /// keeper. Adding a new disk-persistence operation (e.g.,
2293 /// `load_from_str`) requires extending `CompilerSpecIoStage`,
2294 /// which rustc-enforces matching at every projection site
2295 /// (`operation()` / `label()`).
2296 ///
2297 /// Mirror at the disk boundary of the typed-domain JSON-projection
2298 /// round-trip's `DomainSerialize` / `KwargDeserialize` sibling pair
2299 /// at the in-memory kwarg boundary: those variants reject
2300 /// `to_value::<T>` / `from_value::<T>` failures at the typed-domain
2301 /// boundary; this variant rejects file I/O + top-level JSON
2302 /// failures at the disk boundary. After this lift, every distinct
2303 /// failure mode in `tatara-lisp/src/compiler_spec.rs`'s persistence
2304 /// surface is structurally typed; there are zero
2305 /// `LispError::Compile { ... }` construction sites left in
2306 /// `tatara-lisp/src/compiler_spec.rs`.
2307 ///
2308 /// `stage` is `CompilerSpecIoStage` — a closed-set typed enum
2309 /// whose `operation()` and `label()` projections feed the Display
2310 /// rendering — so the compile-time guarantee on BOTH slots is
2311 /// load-bearing in the type system. `message` is `String` because
2312 /// it carries the underlying error's `Display` projection
2313 /// (`serde_json::Error` for serialize / deserialize, `std::io::Error`
2314 /// for read / write — arbitrary text); carrying the rendered
2315 /// message rather than a `#[source]` chain keeps the variant's
2316 /// structural shape parallel to every other String-carrying variant
2317 /// in this enum (`DomainSerialize.message`, `KwargDeserialize.message`,
2318 /// `Compile.message`).
2319 ///
2320 /// `message` carries the raw underlying-error `Display` projection
2321 /// — NO `"{stage}: "` prefix in the field, the prefix is in the
2322 /// `Display` rendering — so consumers that pattern-match on
2323 /// `message` get the underlying diagnostic unchanged, parallel to
2324 /// how `DomainSerialize.message` carries the raw `serde_json`
2325 /// projection (the `"serialize: "` prefix lives in Display, not in
2326 /// the slot) and `KwargDeserialize.message` carries the raw
2327 /// `serde_json` projection (the `"deserialize: "` prefix lives in
2328 /// Display, not in the slot).
2329 ///
2330 /// Display matches the legacy `Compile`-shaped diagnostic byte-for-
2331 /// byte across all four stages — `"compile error in {operation}:
2332 /// {stage}: {message}"` where `{operation}` is `stage.operation()`
2333 /// and `{stage}` is `stage.label()` — so existing consumer
2334 /// assertions (`tatara-check`'s diagnostic capture, REPL substring-
2335 /// greps that match on `"realize_to_disk"`, `"load_from_disk"`,
2336 /// `"serialize: "`, `"write: "`, `"read: "`, `"deserialize: "`)
2337 /// pass unchanged across the lift. When a future run gives `Sexp`
2338 /// source spans, `pos: Option<usize>` lands here in ONE place
2339 /// (though the disk surface is non-positional — failures originate
2340 /// from file I/O / serde, not from a Sexp slot — so the field
2341 /// would stay `None` at every call site, the variant joining the
2342 /// `position_is_none_for_non_positional_variants` cohort).
2343 #[error("compile error in {}: {}: {message}", stage.operation(), stage.label())]
2344 CompilerSpecIo {
2345 stage: CompilerSpecIoStage,
2346 message: String,
2347 },
2348 /// `apply_compiled`'s bytecode-runtime invariant violation. Four call
2349 /// sites in `macro_expand.rs::apply_compiled` share this failure mode
2350 /// through ONE structural variant keyed on the closed-set
2351 /// `TemplateInvariantKind` enum. Every violation here is a
2352 /// COMPILER-INTERNAL bug — the bytecode that drives `apply_compiled`
2353 /// is produced by `compile_template` / `compile_node` in this same
2354 /// module, and a well-formed bytecode never references an
2355 /// out-of-bounds param index (Subst / Splice gates) nor leaves the
2356 /// runtime stack unbalanced at the final pop (EndList / no-value
2357 /// gates).
2358 ///
2359 /// Encoding the four failure modes as ONE typed enum (rather than a
2360 /// free-form `message: String` slot) makes the constraint that ONLY
2361 /// 4 distinct violations are reachable load-bearing in the type
2362 /// system — a regression that drifts the failure mode (e.g. a fifth
2363 /// "wrong opcode" gate added without a `TemplateInvariantKind`
2364 /// extension) becomes a `match` compile error at the projection site,
2365 /// not a substring-grep regression that ships. Same posture as
2366 /// `CompilerSpecIoStage` for `CompilerSpecIo`: the closed set becomes
2367 /// a TYPE, not a `matches!` literal in the helper. The index slot of
2368 /// the Subst / Splice gates lives INSIDE the variant
2369 /// (`SubstBadIndex(usize)` / `SpliceBadIndex(usize)`) rather than on
2370 /// the outer variant as `op_index: Option<usize>`, so the invalid
2371 /// combination `EndListEmptyStack { op_index: Some(_) }` is
2372 /// structurally unrepresentable — the type system encodes "this gate
2373 /// has an index, that gate does not."
2374 ///
2375 /// Display matches the legacy `Compile`-shaped diagnostic
2376 /// byte-for-byte across all four kinds — `"compile error in
2377 /// {macro_name}: {kind.message()}"` — so existing consumer assertions
2378 /// (`tatara-check`'s diagnostic capture, REPL substring-greps that
2379 /// match on `"compiled template referenced bad param index"`,
2380 /// `"compiled template referenced bad splice index"`, `"compiled
2381 /// template: EndList with empty stack"`, `"compiled template produced
2382 /// no value"`) pass unchanged across the lift.
2383 ///
2384 /// `macro_name` is `String` because it comes from arbitrary source
2385 /// (the call-site head symbol). `kind` is `TemplateInvariantKind` —
2386 /// a closed-set typed enum whose `message()` projection feeds the
2387 /// Display rendering.
2388 ///
2389 /// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
2390 /// of bytecode-invariant failure modes becomes a TYPE rather than a
2391 /// runtime string-comparison-and-format dance. THEORY.md §VI.1 —
2392 /// generation over composition; the typed enum lands the structural-
2393 /// completeness floor for the bytecode-runtime surface, parallel to
2394 /// how `CompilerSpecIoStage` lands the structural-completeness floor
2395 /// for the disk-persistence surface and `MacroDefHead` lands it for
2396 /// the macro-definition-head closed set. THEORY.md §II.1 invariant 5
2397 /// (composition preserves proofs): a well-formed bytecode invariant
2398 /// is the proof that drives the interpreter; the structural variant
2399 /// makes the proof's REJECTION shape first-class.
2400 #[error("compile error in {macro_name}: {}", kind.message())]
2401 TemplateInvariant {
2402 macro_name: String,
2403 kind: TemplateInvariantKind,
2404 },
2405 /// `Expander::expand` recursed past its configured ceiling
2406 /// (`Expander::max_expansion_depth`, default
2407 /// `crate::macro_expand::DEFAULT_MAX_EXPANSION_DEPTH = 256`). Fires
2408 /// on the runaway `(defmacro loop (x) `(loop ,x))` — a macro
2409 /// whose expansion contains another call to itself — which pre-
2410 /// lift stack-overflowed the process (a below-floor failure with
2411 /// no `Result` witness). The typed-entry gate turns "we abort" into
2412 /// "we reject at the expansion boundary" so authoring surfaces
2413 /// (REPL, LSP, `tatara-check`) see a `Result::Err` at the same
2414 /// call boundary as every other macro-expansion failure and can
2415 /// pattern-match on the variant identity to route diagnostics
2416 /// through the same channel as `TooManyMacroArgs` /
2417 /// `MissingMacroArg` / `UnboundTemplateVar`.
2418 ///
2419 /// `macro_name` is `String` because it comes from arbitrary
2420 /// source — the head symbol of the offending call at the moment
2421 /// the ceiling is hit; `limit` is `usize` because it is the
2422 /// configured ceiling the expander enforced at that call. The
2423 /// pair names BOTH the offending macro AND the ceiling it
2424 /// breached, so an LSP that wants to surface "your macro `loop`
2425 /// re-expanded past 256 levels; is it recursive without a base
2426 /// case?" has both halves at the variant boundary without
2427 /// substring-parsing the rendered diagnostic.
2428 ///
2429 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
2430 /// runaway-macro-expansion failure mode becomes a first-class
2431 /// typed rejection at the expander boundary instead of an
2432 /// unhandled stack overflow that aborts the process without
2433 /// producing a `Result` at all. THEORY.md §II.1 invariant 1 —
2434 /// typed entry; every macro-expansion failure mode the expander
2435 /// admits is a variant of `LispError`, so the below-floor abort
2436 /// is the last un-typed rejection in the expander's dispatch
2437 /// chain — closing it moves the expansion surface fully into the
2438 /// `Result` algebra.
2439 #[error(
2440 "compile error in call to {macro_name}: macro expansion depth exceeded (limit: {limit})"
2441 )]
2442 ExpansionDepthExceeded { macro_name: String, limit: usize },
2443 /// `Expander::expand`'s freshly-applied macro output crossed the
2444 /// configured node-count ceiling (`Expander::max_expansion_size`,
2445 /// default `crate::macro_expand::DEFAULT_MAX_EXPANSION_SIZE`).
2446 /// Peer to [`Self::ExpansionDepthExceeded`] one RESOURCE axis over:
2447 /// where the depth ceiling bounds RECURSION LENGTH (how many times
2448 /// a macro re-expands into itself before the process aborts) and
2449 /// [`crate::macro_expand::DEFAULT_MAX_CACHE_ENTRIES`] bounds
2450 /// MEMOIZATION WIDTH (how many distinct `(name, args)` pairs the
2451 /// cache retains), this variant bounds OUTPUT SIZE (how many
2452 /// [`crate::ast::Sexp`] nodes a single macro `apply` may produce
2453 /// before the expander rejects rather than descending into a
2454 /// runaway tree). Fires on the canonical "expansion bomb"
2455 /// `(defmacro bomb (x) `(list ,x ,x ,x ,x ,x ,x))` — a well-defined
2456 /// but exponentially-productive macro whose depth stays small
2457 /// while the produced tree grows past any reasonable working-set
2458 /// budget. Pre-lift the runaway ran until the process exhausted
2459 /// its heap; post-lift the expander returns this typed rejection
2460 /// at the first `apply` output whose `node_count` exceeds the
2461 /// ceiling, with `macro_name` populated from the offending call,
2462 /// `size` populated from the observed node count, and `limit`
2463 /// populated from the enforced ceiling — so authoring surfaces
2464 /// (REPL, LSP, `tatara-check`) see BOTH halves at the variant
2465 /// boundary rather than substring-parsing free-form text.
2466 ///
2467 /// The three-field shape names BOTH the offending macro AND the
2468 /// observed size AND the configured ceiling. An LSP that wants to
2469 /// surface "your macro `bomb` produced a 512-node result past the
2470 /// 256-node ceiling; is it exponentially productive?" reads all
2471 /// three from the typed variant without substring parsing. Joins
2472 /// the [`Self::ExpansionDepthExceeded`] cohort so the
2473 /// [`Self::position`] projection routes both resource-ceiling
2474 /// rejections through the SAME `None` arm — the offending macro's
2475 /// byte offset is not what a runaway diagnostic anchors to; the
2476 /// (macro name, observed resource, ceiling) triple is.
2477 ///
2478 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
2479 /// expansion-bomb failure mode becomes a first-class typed
2480 /// rejection at the expander boundary rather than a heap-exhausted
2481 /// abort that never reaches a `Result`. THEORY.md §II.1 invariant
2482 /// 1 — typed entry; every resource-ceiling failure mode the
2483 /// expander admits (recursion, memory, output size) is now a
2484 /// variant of `LispError`, closing the "runaway macro" surface at
2485 /// three RESOURCE-DIMENSION axes.
2486 #[error(
2487 "compile error in call to {macro_name}: macro expansion output size exceeded (size: {size}, limit: {limit})"
2488 )]
2489 ExpansionSizeExceeded {
2490 macro_name: String,
2491 size: usize,
2492 limit: usize,
2493 },
2494 /// `Expander::register_macro_def` rejected a macro definition whose
2495 /// body's [`crate::ast::Sexp::node_count`] exceeded the configured
2496 /// ceiling (`Expander::max_macro_body_size`, default
2497 /// `crate::macro_expand::DEFAULT_MAX_MACRO_BODY_SIZE`). Peer to
2498 /// [`Self::ExpansionSizeExceeded`] one PIPELINE-STAGE axis over:
2499 /// where `ExpansionSizeExceeded` bounds a macro `apply`'s OUTPUT
2500 /// size at EXPAND time (the runtime bomb — `(defmacro bomb (x) `(list ,x ,x ,x ,x))`
2501 /// producing a large tree from a small input), this variant bounds
2502 /// a macro's BODY size at REGISTER time (the authoring bomb —
2503 /// `(defmacro huge (x) `<template-of-N-million-nodes>)` whose
2504 /// storage in the `Expander::macros` table alone would exhaust
2505 /// memory before a single `apply` ran). Fires at
2506 /// [`crate::macro_expand::Expander::register_macro_def`] BEFORE
2507 /// `compile_template` walks the body or the entry lands in the
2508 /// macros table — a failed registration leaves both tables exactly
2509 /// as they were, matching the ordering `TemplateInvariant` /
2510 /// `UnboundTemplateVar` already enjoy on the compile-time reject
2511 /// path.
2512 ///
2513 /// The three-field shape names BOTH the offending macro AND the
2514 /// observed body size AND the configured ceiling — same shape as
2515 /// `ExpansionSizeExceeded` (its EXPAND-time peer) so authoring
2516 /// surfaces that route resource-ceiling rejections through one
2517 /// diagnostic channel bind to ONE structural pattern. `macro_name`
2518 /// is `String` because it comes from arbitrary source — the
2519 /// `(defmacro NAME …)` head at the moment the ceiling is hit;
2520 /// `size` is the observed `body.node_count()`; `limit` is the
2521 /// configured ceiling.
2522 ///
2523 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
2524 /// authoring-bomb failure mode (a macro body large enough to hurt
2525 /// storage or template-compile time) becomes a first-class typed
2526 /// rejection at the REGISTRATION boundary. THEORY.md §II.1
2527 /// invariant 1 — typed entry; every resource-ceiling failure mode
2528 /// the expander admits (expand-time recursion, expand-time
2529 /// memoization width, expand-time output size, register-time body
2530 /// size) is now a variant of `LispError`, closing the "runaway
2531 /// macro" surface at FOUR RESOURCE-DIMENSION axes across TWO
2532 /// pipeline stages (register + expand).
2533 #[error(
2534 "compile error in defmacro {macro_name}: macro body size exceeded (size: {size}, limit: {limit})"
2535 )]
2536 MacroBodySizeExceeded {
2537 macro_name: String,
2538 size: usize,
2539 limit: usize,
2540 },
2541 /// `Expander::register_macro_def` rejected a fresh (non-overwrite)
2542 /// registration because the [`Expander::macros`] table already held
2543 /// `Expander::max_registered_macros` entries (default
2544 /// `crate::macro_expand::DEFAULT_MAX_REGISTERED_MACROS`). Peer to
2545 /// [`Self::MacroBodySizeExceeded`] one RESOURCE-DIMENSION axis over
2546 /// on the REGISTER-time surface — where `MacroBodySizeExceeded`
2547 /// bounds a single macro's BODY SIZE (the per-registration
2548 /// authoring bomb — one huge template landing in
2549 /// `Expander::macros`), this variant bounds the TABLE ENTRY COUNT
2550 /// (the cumulative registration bomb — a code-generator emitting
2551 /// unbounded `(defmacro fresh-N (x) `(list ,x))` heads at REGISTER
2552 /// time whose bodies each sit comfortably under
2553 /// `Expander::max_macro_body_size` yet collectively saturate
2554 /// process memory). Also peer to
2555 /// [`crate::macro_expand::DEFAULT_MAX_CACHE_ENTRIES`] one
2556 /// PIPELINE-STAGE axis over: where the cache-entries ceiling
2557 /// bounds EXPAND-time memoization width, this ceiling bounds
2558 /// REGISTER-time macro-table width — the two entry-count guards
2559 /// close the two pipeline stages symmetrically.
2560 ///
2561 /// A re-registration of an already-registered key (`self.macros`
2562 /// contains `macro_name` before the call) is an OVERWRITE, which
2563 /// does not grow the table and therefore never fires this variant
2564 /// — operators can redefine a macro at capacity without hitting
2565 /// the ceiling, and only FRESH keys are gated. The gate fires
2566 /// BEFORE [`compile_template`] walks the body or the entry lands
2567 /// in the [`crate::macro_expand::Expander::macros`] /
2568 /// `templates` tables — a failed registration leaves both tables
2569 /// exactly as they were, matching the pristine-tables invariant
2570 /// [`Self::MacroBodySizeExceeded`] holds on the body-size axis.
2571 ///
2572 /// The three-field shape names BOTH the offending macro AND the
2573 /// observed table count AND the configured ceiling — same shape
2574 /// as `MacroBodySizeExceeded` / `ExpansionSizeExceeded` (its
2575 /// pipeline-stage peers) so authoring surfaces that route
2576 /// resource-ceiling rejections through one diagnostic channel
2577 /// bind to ONE structural pattern. `macro_name` is `String`
2578 /// because it comes from arbitrary source — the `(defmacro NAME
2579 /// …)` head at the moment the ceiling is hit; `count` is the
2580 /// observed `self.macros.len()` (which equals `limit` at
2581 /// rejection in the common case, or exceeds it if `limit` was
2582 /// lowered mid-run past the current table size); `limit` is the
2583 /// configured ceiling.
2584 ///
2585 /// Joins the [`Self::ExpansionDepthExceeded`] cohort in
2586 /// [`Self::position`] so all four resource-ceiling rejections
2587 /// route through the SAME `None` arm — the offending macro's
2588 /// byte offset is not what a registration-bomb diagnostic
2589 /// anchors to; the (macro name, observed table count, ceiling)
2590 /// triple is.
2591 ///
2592 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
2593 /// registration-bomb failure mode (unbounded macro-table growth)
2594 /// becomes a first-class typed rejection at the REGISTRATION
2595 /// boundary rather than an unhandled resource-drift that no
2596 /// `Result` witnesses. THEORY.md §II.1 invariant 1 — typed entry;
2597 /// every resource-ceiling failure mode the expander admits
2598 /// (expand-time recursion, expand-time memoization width,
2599 /// expand-time output size, register-time body size, register-
2600 /// time table width) is now a variant of `LispError`, closing the
2601 /// "runaway macro" surface at FIVE RESOURCE-DIMENSION axes across
2602 /// TWO pipeline stages — the register-time surface is now a
2603 /// two-dimensional (size × count) closure symmetric with the
2604 /// expand-time (depth + count + size) triple.
2605 #[error(
2606 "compile error in defmacro {macro_name}: registered macros count exceeded (count: {count}, limit: {limit})"
2607 )]
2608 RegisteredMacrosExceeded {
2609 macro_name: String,
2610 count: usize,
2611 limit: usize,
2612 },
2613 /// `Expander::register_macro_def` rejected a macro definition whose
2614 /// lambda-list arity — the [`crate::macro_expand::MacroParams::total_arity`]
2615 /// projection: `required.len() + optional.len() + rest.is_some() as
2616 /// usize` — exceeded the configured ceiling
2617 /// (`Expander::max_macro_arity`, default
2618 /// `crate::macro_expand::DEFAULT_MAX_MACRO_ARITY`). Peer to
2619 /// [`Self::MacroBodySizeExceeded`] one RESOURCE-DIMENSION axis over
2620 /// on the REGISTER-time surface — where `MacroBodySizeExceeded`
2621 /// bounds a per-registration BODY node count (the AST-nodes of the
2622 /// template a macro rewrites INTO), this variant bounds a
2623 /// per-registration PARAM slot count (the fresh symbols
2624 /// `compile_template`'s `,name`-index resolution AND
2625 /// `MacroParams::bind`'s per-index binder walk have to thread
2626 /// through on every call). Also peer to
2627 /// [`Self::RegisteredMacrosExceeded`] one RESOURCE-DIMENSION axis
2628 /// over on the REGISTER-time surface — where `RegisteredMacrosExceeded`
2629 /// bounds cumulative registration COUNT (the table-scoped
2630 /// resource), this variant bounds a per-registration PARAM-LIST
2631 /// WIDTH (the per-entry resource). Together the three
2632 /// REGISTER-time variants (this one +
2633 /// [`Self::MacroBodySizeExceeded`] + [`Self::RegisteredMacrosExceeded`])
2634 /// close the (per-body SIZE, per-body ARITY, per-table COUNT)
2635 /// three-corner surface. Fires at
2636 /// [`crate::macro_expand::Expander::register_macro_def`] BETWEEN the
2637 /// table-count gate (cheapest — O(1) `HashMap::len`) and the
2638 /// body-size gate (O(N) on `def.body.node_count()`) — the arity
2639 /// projection is a three-arm addition on `def.params` fields, no
2640 /// AST walk, so it sits BEFORE the body-size gate on the
2641 /// O(1)-first ordering of the REGISTER-time chain. A failed
2642 /// registration leaves BOTH `self.macros` AND `self.templates`
2643 /// tables exactly as they were, matching the pristine-tables
2644 /// invariant [`Self::MacroBodySizeExceeded`] and
2645 /// [`Self::RegisteredMacrosExceeded`] hold on their axes.
2646 ///
2647 /// Fires on the canonical arity-bomb `(defmacro huge (a-1 a-2 …
2648 /// a-N-million) `,a-1)` — a code-generator whose macro body sits
2649 /// at ONE node (`Sexp::Quasiquote` wrapping a single `,a-1`
2650 /// unquote is 3 nodes total, well under
2651 /// `Expander::max_macro_body_size`) yet whose param list carries
2652 /// millions of fresh names each subsequent call would have to
2653 /// walk in the per-index binder — a below-floor resource-drift
2654 /// the two prior REGISTER-time guards admit through because
2655 /// neither of them fires against the PER-REGISTRATION PARAM-LIST
2656 /// WIDTH.
2657 ///
2658 /// The three-field shape names BOTH the offending macro AND the
2659 /// observed arity AND the configured ceiling — same shape as
2660 /// `MacroBodySizeExceeded` / `ExpansionSizeExceeded` (its
2661 /// register-time peer and expand-time peer). `macro_name` is
2662 /// `String` because it comes from arbitrary source — the
2663 /// `(defmacro NAME …)` head at the moment the ceiling is hit;
2664 /// `arity` is the observed `def.params.total_arity()`; `limit` is
2665 /// the configured ceiling. An LSP that wants to surface "your
2666 /// macro `huge` declares 512 params past the 128-param ceiling;
2667 /// is it a `Vec<TypedEntity>`-unroller?" reads all three from the
2668 /// typed variant without substring parsing.
2669 ///
2670 /// Joins the [`Self::ExpansionDepthExceeded`] cohort in
2671 /// [`Self::position`] so all six resource-ceiling rejections
2672 /// route through the SAME `None` arm — the offending macro's byte
2673 /// offset is not what an arity-bomb diagnostic anchors to; the
2674 /// (macro name, observed arity, ceiling) triple is.
2675 ///
2676 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
2677 /// arity-bomb failure mode (an unbounded per-registration param
2678 /// list) becomes a first-class typed rejection at the REGISTRATION
2679 /// boundary rather than an unhandled per-call binder-walk cost
2680 /// that no `Result` witnesses. THEORY.md §II.1 invariant 1 —
2681 /// typed entry; every resource-ceiling failure mode the expander
2682 /// admits (expand-time recursion, expand-time memoization width,
2683 /// expand-time output size, register-time body size, register-
2684 /// time table width, register-time param-list width) is now a
2685 /// variant of `LispError`, closing the "runaway macro" surface at
2686 /// SIX RESOURCE-DIMENSION axes across TWO pipeline stages — the
2687 /// register-time surface is now a three-dimensional (size × arity
2688 /// × count) closure symmetric with the expand-time (depth + count
2689 /// + size) triple.
2690 ///
2691 /// Frontier inspiration: Common Lisp's `LAMBDA-PARAMETERS-LIMIT`
2692 /// standard variable (CLHS §3.4.1) — the runtime-reflectable
2693 /// "upper exclusive bound on the number of parameters that may
2694 /// appear in a lambda list" every conforming implementation
2695 /// carries; this variant is the substrate's typed-Rust peer at
2696 /// compile time, translated through pleme-io primitives: a typed
2697 /// `LispError` variant rather than an implementation-defined
2698 /// condition, wired into the substrate's `Result` algebra at
2699 /// [`crate::macro_expand::Expander::register_macro_def`] rather
2700 /// than raised at eval time.
2701 #[error(
2702 "compile error in defmacro {macro_name}: macro arity exceeded (arity: {arity}, limit: {limit})"
2703 )]
2704 MacroArityExceeded {
2705 macro_name: String,
2706 arity: usize,
2707 limit: usize,
2708 },
2709}
2710
2711/// Closed-set identifier for the (operation, stage) pair of a
2712/// `LispError::CompilerSpecIo` failure. Encodes the four reachable
2713/// pairs in `tatara-lisp/src/compiler_spec.rs`'s disk-persistence
2714/// surface — `realize_to_disk` × {serialize, write} ⊎ `load_from_disk`
2715/// × {read, deserialize} — as a typed enum, so invalid combinations
2716/// like `(load_from_disk, write)` or `(realize_to_disk, deserialize)`
2717/// are structurally unrepresentable rather than re-asserted at the
2718/// helper boundary via runtime string comparison.
2719///
2720/// Same posture as `MacroDefHead` in `macro_expand.rs`: the closed set
2721/// becomes a TYPE, not a `matches!` literal AND a triplicate
2722/// `match operation { ... }` projection inside each error helper. The
2723/// `operation()` / `label()` projections feed the
2724/// `LispError::CompilerSpecIo` Display rendering directly via the
2725/// `#[error(...)]` annotation; adding a new disk-persistence operation
2726/// (e.g., `load_from_str` for in-memory loads) requires extending this
2727/// enum, which rustc-enforces matching at every projection site.
2728///
2729/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
2730/// of (operation, stage) pairs becomes a TYPE rather than a runtime
2731/// string-comparison-and-format dance. THEORY.md §VI.1 — generation
2732/// over composition; the typed enum lands the structural-completeness
2733/// floor for the disk-persistence surface, parallel to how
2734/// `MacroDefHead` lands the structural-completeness floor for the
2735/// macro-definition-head closed set.
2736///
2737/// `#[derive(tatara_closed_set::DeriveClosedSet)]` emits the
2738/// substrate-wide `impl tatara_closed_set::ClosedSet for CompilerSpecIoStage` +
2739/// the `pub struct UnknownCompilerSpecIoStage(pub String)`
2740/// parse-rejection carrier alongside the enum declaration. The
2741/// `#[closed_set(no_from_str)]` axis suppresses the auto-emitted
2742/// `FromStr` delegation — this enum's parse surface is the
2743/// compound `"{operation}: {label}"` key (a projection PAIR
2744/// rather than a single label), so the FromStr body below stays
2745/// hand-rolled. The `#[closed_set(generate_unknown)]` axis emits
2746/// the `UnknownCompilerSpecIoStage` carrier with the
2747/// auto-projected `"unknown compiler spec io stage: {0}"`
2748/// `#[error(...)]` annotation (matching the pre-lift wording
2749/// byte-for-byte via `pascal_to_spaced_lowercase`). `via` defaults
2750/// to `"label"` so the trait's `ClosedSet::label` projection
2751/// delegates to the inherent `label()` method — generic
2752/// consumers walking `ALL` and stringifying through the trait see
2753/// the singular labels (`"serialize"` / `"write"` / `"read"` /
2754/// `"deserialize"`) while the operator-facing `Display` rendering
2755/// stays at the compound `"{operation}: {label}"` shape via the
2756/// hand-rolled block.
2757#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
2758#[closed_set(no_from_str, generate_unknown)]
2759pub enum CompilerSpecIoStage {
2760 /// `serde_json::to_string_pretty` of a `CompilerSpec` errored
2761 /// inside `realize_to_disk`.
2762 RealizeToDiskSerialize,
2763 /// `std::fs::write` of the serialized `CompilerSpec` JSON errored
2764 /// inside `realize_to_disk`.
2765 RealizeToDiskWrite,
2766 /// `std::fs::read_to_string` of the on-disk `CompilerSpec` JSON
2767 /// errored inside `load_from_disk`.
2768 LoadFromDiskRead,
2769 /// `serde_json::from_str` of the on-disk `CompilerSpec` JSON
2770 /// errored inside `load_from_disk`.
2771 LoadFromDiskDeserialize,
2772}
2773
2774impl CompilerSpecIoStage {
2775 /// The canonical `&'static str` bytes returned by
2776 /// [`Self::operation`] for the [`Self::RealizeToDiskSerialize`]
2777 /// and [`Self::RealizeToDiskWrite`] variants — the
2778 /// `"realize_to_disk"` `{operation}` slot of the legacy
2779 /// `Compile`-shaped triple emitted by the public
2780 /// `CompilerSpec::realize_to_disk` entry point.
2781 ///
2782 /// Sibling posture to [`Self::LOAD_FROM_DISK_OPERATION`]
2783 /// (`"load_from_disk"`) on the same disk-persistence
2784 /// operation-label algebra, and to
2785 /// [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`] /
2786 /// [`Self::REALIZE_TO_DISK_WRITE_LABEL`] /
2787 /// [`Self::LOAD_FROM_DISK_READ_LABEL`] /
2788 /// [`Self::LOAD_FROM_DISK_DESERIALIZE_LABEL`] on the paired
2789 /// stage-label algebra — together the two axes span the
2790 /// (operation × stage) compound-key surface every
2791 /// [`CompilerSpecIoStage`] variant projects through.
2792 ///
2793 /// Pre-lift the same `"realize_to_disk"` bytes lived inline at
2794 /// the [`Self::operation`] match arm plus at the paired
2795 /// `compiler_spec_io_stage_operation_projects_realize_for_serialize_and_write`
2796 /// truth-table test plus at every `operation="realize_to_disk"`
2797 /// metric-label pin. Post-lift the (`RealizeToDiskSerialize |
2798 /// RealizeToDiskWrite`, canonical `&'static str`) pairing binds
2799 /// at ONE `pub const` on the typed [`CompilerSpecIoStage`]
2800 /// algebra — the pre-lift ≥2 PRIME-DIRECTIVE trigger becomes ONE
2801 /// typed source of truth. Consumers that need the
2802 /// `"realize_to_disk"` bytes at compile time (`disallowed_names`
2803 /// clippy config; static-dispatch metric-label tables in
2804 /// Prometheus recorders; `tatara-check` grep budgets) bind
2805 /// against `CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION`
2806 /// rather than re-typing the literal.
2807 pub const REALIZE_TO_DISK_OPERATION: &'static str = "realize_to_disk";
2808
2809 /// The canonical `&'static str` bytes returned by
2810 /// [`Self::operation`] for the [`Self::LoadFromDiskRead`] and
2811 /// [`Self::LoadFromDiskDeserialize`] variants — the
2812 /// `"load_from_disk"` `{operation}` slot of the legacy
2813 /// `Compile`-shaped triple emitted by the public
2814 /// `CompilerSpec::load_from_disk` entry point.
2815 ///
2816 /// Sibling posture to [`Self::REALIZE_TO_DISK_OPERATION`]
2817 /// (`"realize_to_disk"`) on the same disk-persistence
2818 /// operation-label algebra.
2819 pub const LOAD_FROM_DISK_OPERATION: &'static str = "load_from_disk";
2820
2821 /// The closed set of four canonical `&'static str` operation
2822 /// labels — one per variant of [`Self::ALL`] in the SAME
2823 /// declaration order, so `Self::OPERATIONS[i] ==
2824 /// Self::ALL[i].operation()` element-wise (pinned by
2825 /// `compiler_spec_io_stage_operations_align_with_all_by_index`).
2826 ///
2827 /// Unlike [`Self::LABELS`] — whose four elements are pairwise
2828 /// distinct because each `label()` projection is bijective with
2829 /// its variant — [`Self::OPERATIONS`] contains DUPLICATES by
2830 /// construction: the operation projection is 2-of-2-to-2 (both
2831 /// `Realize…` variants share `"realize_to_disk"`, both `Load…`
2832 /// variants share `"load_from_disk"`), pinned by
2833 /// `compiler_spec_io_stage_operations_partition_all_two_ways`.
2834 /// This asymmetry between the two projection axes IS the load-
2835 /// bearing structural property of the (operation × label)
2836 /// compound-key surface — a naive lift that omits it would
2837 /// silently collapse the two-way partition on the operation
2838 /// axis by pinning distinctness where it doesn't hold.
2839 ///
2840 /// Sibling posture to [`Self::LABELS`] (`[&'static str; 4]`) on
2841 /// the same disk-persistence stage-label algebra —
2842 /// [`Self::OPERATIONS`] and [`Self::LABELS`] together span the
2843 /// (operation, label) compound-key rendering surface every
2844 /// [`Display`] / [`FromStr`] round-trip walks, with rustc's
2845 /// forced-arity `[&'static str; 4]` check on BOTH arrays
2846 /// enforcing that any future fifth pair (`LoadFromStrDeserialize`
2847 /// once an in-memory `load_from_str` lands, `RealizeToDiskAtomicReplace`
2848 /// if the realize path grows a crash-safe-rename stage) extends
2849 /// [`Self::ALL`] ONCE + [`Self::operation`] ONCE +
2850 /// [`Self::OPERATIONS`] ONCE + [`Self::label`] ONCE +
2851 /// [`Self::LABELS`] ONCE + adds ONE per-role label `pub const`
2852 /// — the (variant → (operation, label)) product surface picks
2853 /// up the extension mechanically at every downstream consumer.
2854 ///
2855 /// Future consumers that compose against [`Self::OPERATIONS`]:
2856 /// an LSP / REPL completion provider surfacing every legal
2857 /// operation label in an `operation=` metrics query bar
2858 /// ([`Self::OPERATIONS`] deduped is the ONE typed sweep over
2859 /// every legal disk-persistence entry-point name); a
2860 /// `tatara-check` coverage assertion (every workspace `.lisp`
2861 /// file's `CompilerSpec` rejection must classify to some
2862 /// entry of [`Self::OPERATIONS`]); a Sekiban audit-trail metric
2863 /// jointly labeled by [`Self::operation`] × [`Self::label`]
2864 /// whose `operation=` label set IS the deduped
2865 /// [`Self::OPERATIONS`] (e.g.
2866 /// `tatara_lisp_compiler_spec_io_total{operation="realize_to_disk",
2867 /// stage="serialize"}`).
2868 pub const OPERATIONS: [&'static str; 4] = [
2869 Self::REALIZE_TO_DISK_OPERATION,
2870 Self::REALIZE_TO_DISK_OPERATION,
2871 Self::LOAD_FROM_DISK_OPERATION,
2872 Self::LOAD_FROM_DISK_OPERATION,
2873 ];
2874
2875 /// The public entry point's name — the `{form}` slot of the legacy
2876 /// `Compile`-shaped diagnostic. `realize_to_disk` for the
2877 /// serialize / write variants; `load_from_disk` for the read /
2878 /// deserialize variants.
2879 ///
2880 /// Body routes each arm through the per-role
2881 /// [`Self::REALIZE_TO_DISK_OPERATION`] /
2882 /// [`Self::LOAD_FROM_DISK_OPERATION`] `pub const` so the two
2883 /// canonical `&'static str` operation labels live at ONE
2884 /// `pub const` per role rather than at TWO sites (the per-role
2885 /// `pub const` AND an inline arm literal). Sibling posture to
2886 /// [`Self::label`]'s arms routing through the per-role
2887 /// [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`] /
2888 /// [`Self::REALIZE_TO_DISK_WRITE_LABEL`] /
2889 /// [`Self::LOAD_FROM_DISK_READ_LABEL`] /
2890 /// [`Self::LOAD_FROM_DISK_DESERIALIZE_LABEL`] constants on the
2891 /// paired stage-label algebra.
2892 #[must_use]
2893 pub fn operation(self) -> &'static str {
2894 match self {
2895 Self::RealizeToDiskSerialize | Self::RealizeToDiskWrite => {
2896 Self::REALIZE_TO_DISK_OPERATION
2897 }
2898 Self::LoadFromDiskRead | Self::LoadFromDiskDeserialize => {
2899 Self::LOAD_FROM_DISK_OPERATION
2900 }
2901 }
2902 }
2903
2904 /// The canonical `&'static str` bytes returned by
2905 /// [`Self::label`] for the [`Self::RealizeToDiskSerialize`]
2906 /// variant — the `"serialize"` `{stage}` slot of the legacy
2907 /// `"{stage}: {error}"` message shape emitted when
2908 /// `serde_json::to_string_pretty` errors inside `realize_to_disk`.
2909 /// Sibling posture to [`Self::REALIZE_TO_DISK_WRITE_LABEL`]
2910 /// (`"write"`), [`Self::LOAD_FROM_DISK_READ_LABEL`] (`"read"`),
2911 /// and [`Self::LOAD_FROM_DISK_DESERIALIZE_LABEL`]
2912 /// (`"deserialize"`) on the same disk-persistence stage-label
2913 /// algebra.
2914 ///
2915 /// Pre-lift the same `"serialize"` bytes lived inline at the
2916 /// [`Self::label`] match arm plus at the paired
2917 /// `compiler_spec_io_stage_label_projects_canonical_stage_strings`
2918 /// truth-table test plus at the `stage="serialize"` metric-label
2919 /// pin. Post-lift the (`RealizeToDiskSerialize` variant, canonical
2920 /// `&'static str`) pairing binds at ONE `pub const` on the typed
2921 /// [`CompilerSpecIoStage`] algebra — the pre-lift ≥2 PRIME-
2922 /// DIRECTIVE trigger becomes ONE typed source of truth. Consumers
2923 /// that need the `"serialize"` bytes at compile time
2924 /// (`disallowed_names` clippy config; static-dispatch metric-label
2925 /// tables in Prometheus recorders; `tatara-check` grep budgets)
2926 /// bind against `CompilerSpecIoStage::REALIZE_TO_DISK_SERIALIZE_LABEL`
2927 /// rather than re-typing the literal.
2928 pub const REALIZE_TO_DISK_SERIALIZE_LABEL: &'static str = "serialize";
2929
2930 /// The canonical `&'static str` bytes returned by
2931 /// [`Self::label`] for the [`Self::RealizeToDiskWrite`] variant —
2932 /// the `"write"` `{stage}` slot of the legacy `"{stage}: {error}"`
2933 /// message shape emitted when `std::fs::write` of the serialized
2934 /// `CompilerSpec` JSON errors inside `realize_to_disk`. Sibling
2935 /// posture to [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`]
2936 /// (`"serialize"`), [`Self::LOAD_FROM_DISK_READ_LABEL`]
2937 /// (`"read"`), and [`Self::LOAD_FROM_DISK_DESERIALIZE_LABEL`]
2938 /// (`"deserialize"`) on the same disk-persistence stage-label
2939 /// algebra.
2940 pub const REALIZE_TO_DISK_WRITE_LABEL: &'static str = "write";
2941
2942 /// The canonical `&'static str` bytes returned by
2943 /// [`Self::label`] for the [`Self::LoadFromDiskRead`] variant —
2944 /// the `"read"` `{stage}` slot of the legacy `"{stage}: {error}"`
2945 /// message shape emitted when `std::fs::read_to_string` of the
2946 /// on-disk `CompilerSpec` JSON errors inside `load_from_disk`.
2947 /// Sibling posture to [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`]
2948 /// (`"serialize"`), [`Self::REALIZE_TO_DISK_WRITE_LABEL`]
2949 /// (`"write"`), and [`Self::LOAD_FROM_DISK_DESERIALIZE_LABEL`]
2950 /// (`"deserialize"`) on the same disk-persistence stage-label
2951 /// algebra.
2952 pub const LOAD_FROM_DISK_READ_LABEL: &'static str = "read";
2953
2954 /// The canonical `&'static str` bytes returned by
2955 /// [`Self::label`] for the [`Self::LoadFromDiskDeserialize`]
2956 /// variant — the `"deserialize"` `{stage}` slot of the legacy
2957 /// `"{stage}: {error}"` message shape emitted when
2958 /// `serde_json::from_str` of the on-disk `CompilerSpec` JSON
2959 /// errors inside `load_from_disk`. Sibling posture to
2960 /// [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`] (`"serialize"`),
2961 /// [`Self::REALIZE_TO_DISK_WRITE_LABEL`] (`"write"`), and
2962 /// [`Self::LOAD_FROM_DISK_READ_LABEL`] (`"read"`) on the same
2963 /// disk-persistence stage-label algebra.
2964 pub const LOAD_FROM_DISK_DESERIALIZE_LABEL: &'static str = "deserialize";
2965
2966 /// The closed set of four canonical `&'static str` stage labels —
2967 /// the [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`] (`"serialize"`)
2968 /// `serde_json::to_string_pretty` failure followed by the
2969 /// [`Self::REALIZE_TO_DISK_WRITE_LABEL`] (`"write"`)
2970 /// `std::fs::write` failure followed by the
2971 /// [`Self::LOAD_FROM_DISK_READ_LABEL`] (`"read"`)
2972 /// `std::fs::read_to_string` failure followed by the
2973 /// [`Self::LOAD_FROM_DISK_DESERIALIZE_LABEL`] (`"deserialize"`)
2974 /// `serde_json::from_str` failure. Canonical declaration order
2975 /// matches [`Self::ALL`] so `Self::LABELS[i] == Self::ALL[i].label()`
2976 /// element-wise — pinned by
2977 /// `compiler_spec_io_stage_labels_align_with_all_by_index`.
2978 ///
2979 /// Sibling posture to [`KwargPathKind::LABELS`]
2980 /// (`[&'static str; 3]`), [`ExpectedKwargShape::LABELS`]
2981 /// (`[&'static str; 7]`), and [`MacroDefHead::KEYWORDS`]
2982 /// (`[&'static str; 3]`) — every closed-set algebra now pins its
2983 /// label-projection ALL array at the declaration site via a
2984 /// forced-arity `[&'static str; N]` array whose length fails
2985 /// compilation if a new variant lands without being added to the
2986 /// set. Adding a hypothetical fifth pair (a
2987 /// `LoadFromStrDeserialize` once an in-memory `load_from_str`
2988 /// lands, a `RealizeToDiskAtomicReplace` if the realize path grows
2989 /// a crash-safe-rename stage) extends [`Self::ALL`] ONCE +
2990 /// [`Self::label`] ONCE + [`Self::LABELS`] ONCE + adds ONE
2991 /// per-role label `pub const` — rustc's forced-arity check on the
2992 /// `[Self; N]` array + `[&'static str; N]` array pair enforces
2993 /// every downstream consumer picks up the extension mechanically.
2994 ///
2995 /// Future consumers that compose against [`Self::LABELS`]: an
2996 /// LSP / REPL completion provider surfacing every legal stage
2997 /// label in a `stage=` metrics query bar (`Self::LABELS.iter()` is
2998 /// the ONE typed sweep over every legal disk-persistence stage
2999 /// label), a `tatara-check` coverage assertion (every workspace
3000 /// `.lisp` file's `CompilerSpec` rejection must classify to some
3001 /// entry of `Self::LABELS`), a Sekiban audit-trail metric jointly
3002 /// labeled by [`Self::label`] whose metric-label set IS
3003 /// `Self::LABELS` (e.g.
3004 /// `tatara_lisp_compiler_spec_io_total{operation="realize_to_disk",
3005 /// stage="serialize"}`).
3006 pub const LABELS: [&'static str; 4] = [
3007 Self::REALIZE_TO_DISK_SERIALIZE_LABEL,
3008 Self::REALIZE_TO_DISK_WRITE_LABEL,
3009 Self::LOAD_FROM_DISK_READ_LABEL,
3010 Self::LOAD_FROM_DISK_DESERIALIZE_LABEL,
3011 ];
3012
3013 /// The step within the operation that failed — the `{stage}` slot
3014 /// of the legacy `"{stage}: {error}"` message shape. One of
3015 /// `"serialize"`, `"write"`, `"read"`, `"deserialize"`.
3016 ///
3017 /// Body routes each arm through the per-role
3018 /// [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`] /
3019 /// [`Self::REALIZE_TO_DISK_WRITE_LABEL`] /
3020 /// [`Self::LOAD_FROM_DISK_READ_LABEL`] /
3021 /// [`Self::LOAD_FROM_DISK_DESERIALIZE_LABEL`] `pub const` so the
3022 /// four canonical `&'static str` labels live at ONE `pub const`
3023 /// per variant rather than at TWO sites (the per-role `pub const`
3024 /// AND an inline arm literal). Sibling posture to
3025 /// [`KwargPathKind::label`]'s arms routing through the per-role
3026 /// [`KwargPathKind::NAMED_LABEL`] / [`KwargPathKind::ITEM_LABEL`]
3027 /// / [`KwargPathKind::SLOT_LABEL`] constants, and to
3028 /// [`ExpectedKwargShape::label`]'s arms routing through
3029 /// [`ExpectedKwargShape::KEYWORD_LABEL`] etc.
3030 #[must_use]
3031 pub fn label(self) -> &'static str {
3032 match self {
3033 Self::RealizeToDiskSerialize => Self::REALIZE_TO_DISK_SERIALIZE_LABEL,
3034 Self::RealizeToDiskWrite => Self::REALIZE_TO_DISK_WRITE_LABEL,
3035 Self::LoadFromDiskRead => Self::LOAD_FROM_DISK_READ_LABEL,
3036 Self::LoadFromDiskDeserialize => Self::LOAD_FROM_DISK_DESERIALIZE_LABEL,
3037 }
3038 }
3039
3040 /// Closed-set enumeration of every reachable [`CompilerSpecIoStage`]
3041 /// variant — the four (operation, label) pairs the disk-persistence
3042 /// surface emits (`realize_to_disk` × {`serialize`, `write`} ⊎
3043 /// `load_from_disk` × {`read`, `deserialize`}). The `[Self; 4]`
3044 /// array literal forces the arity so a fifth pair — a hypothetical
3045 /// `LoadFromStrDeserialize` once an in-memory `load_from_str` lands,
3046 /// or a `RealizeToDiskAtomicReplace` if the realize path grows a
3047 /// crash-safe-rename stage — cannot be added at the type without
3048 /// extending this constant.
3049 ///
3050 /// Sibling closed-set lift to every other typed-shape enum the
3051 /// substrate carries: this crate's own [`ExpectedKwargShape::ALL`]
3052 /// (the seven reachable expected-kwarg shapes — single-projection
3053 /// closed set), [`SexpShape::ALL`] (the twelve reachable observed-
3054 /// Sexp shapes — single-projection closed set), [`MacroDefHead::ALL`]
3055 /// (the three reachable macro-definition heads), [`UnquoteForm::ALL`]
3056 /// (the two reachable template-marker forms),
3057 /// [`crate::ast::AtomKind::ALL`], [`crate::ast::QuoteForm::ALL`], and
3058 /// across the workspace `ConditionKind::ALL`, `ProcessPhase::ALL`,
3059 /// `RequestorKind::ALL`, `ReceiptKind::ALL`, … . What's distinct here:
3060 /// this is the substrate's first *compound-key* closed set — the
3061 /// reachable identity is the PAIR `(operation, label)`, not either
3062 /// projection alone (`operation` partitions ALL into 2-of-2 halves,
3063 /// `label` is bijective with ALL by accident of the current four
3064 /// variants). [`FromStr`] keys on the compound rendering so the
3065 /// cross-product reachability constraint — only 4 of the 8
3066 /// conceivable `(operation, label)` pairs are reachable, e.g.
3067 /// `(load_from_disk, write)` is structurally absurd — becomes a
3068 /// load-bearing property of the parse boundary, not a runtime
3069 /// re-assertion at the helper.
3070 ///
3071 /// Future consumers that compose against [`Self::ALL`]: LSP / REPL
3072 /// completion for the operator-facing rendered (operation, label)
3073 /// pairs (every `compile error in X: Y: ...` substring in
3074 /// `LispError::CompilerSpecIo` keys on this set's projection through
3075 /// [`Self::operation`] and [`Self::label`]); `tatara-check` coverage
3076 /// assertions over which disk-persistence stages reach a
3077 /// [`LispError::CompilerSpecIo`] site at all — the typed sweep
3078 /// replaces a hand-rolled `match`-over-string vocabulary at consumer
3079 /// boundaries; any future audit-trail metric jointly labeled by
3080 /// [`Self::operation`] × [`Self::label`] (e.g.
3081 /// `tatara_lisp_compiler_spec_io_total{operation="realize_to_disk",
3082 /// stage="serialize"}`) — the metric label set IS [`Self::ALL`]
3083 /// mapped through the projection pair.
3084 pub const ALL: [Self; 4] = [
3085 Self::RealizeToDiskSerialize,
3086 Self::RealizeToDiskWrite,
3087 Self::LoadFromDiskRead,
3088 Self::LoadFromDiskDeserialize,
3089 ];
3090
3091 /// The canonical `&'static str` bytes that separate the
3092 /// [`Self::operation`] slot from the [`Self::label`] slot in the
3093 /// compound-key rendering `"{operation}{SEP}{label}"` — literally
3094 /// `": "` (a colon followed by ONE ASCII space, no more, no less).
3095 /// This is the sole load-bearing separator on the compound-key
3096 /// projection axis: [`Display`] composes through it, [`FromStr`]
3097 /// splits on it, and every doc-comment reference to the compound
3098 /// shape (`"realize_to_disk: read"`, `"load_from_disk: write"`, …)
3099 /// silently threads the same three bytes.
3100 ///
3101 /// Pre-lift the same `": "` bytes lived inline at three sites — the
3102 /// [`Display`] `write!` format string, the [`FromStr`] `split_once`
3103 /// argument, and two doc-comment references — with no typed source
3104 /// of truth binding them together. Post-lift the (`Self`, compound-
3105 /// key separator bytes) pairing binds at ONE `pub const` on the
3106 /// typed [`CompilerSpecIoStage`] algebra — the pre-lift ≥2 PRIME-
3107 /// DIRECTIVE trigger becomes ONE typed source of truth. A future
3108 /// refactor that changes the separator (e.g. to `" — "` on the way
3109 /// to a richer diagnostic surface) touches ONE `pub const` rather
3110 /// than THREE unrelated inline literals; a future refactor that
3111 /// silently drifts one call site (e.g. leaves `Display` at `": "`
3112 /// but drops the space from `split_once` after a `clippy::single_char_pattern`
3113 /// suggestion) fails the round-trip test loudly at the parse
3114 /// boundary rather than silently bifurcating the compound key.
3115 ///
3116 /// Sibling posture to [`Self::REALIZE_TO_DISK_OPERATION`] +
3117 /// [`Self::LOAD_FROM_DISK_OPERATION`] (operation-slot bytes) and
3118 /// [`Self::REALIZE_TO_DISK_SERIALIZE_LABEL`] etc. (label-slot bytes)
3119 /// on the same disk-persistence compound-key algebra — the three
3120 /// canonical slot-byte families together (operation × separator ×
3121 /// label) span the every-byte-of-the-compound-key surface; no
3122 /// character of a rendered `CompilerSpecIoStage` compound key lives
3123 /// at more than ONE typed constant on the algebra.
3124 ///
3125 /// Distinct from the substrate-wide `unknown {SET_LABEL}: {input}`
3126 /// separator in [`crate::closed_set`]'s well-formed-carrier
3127 /// rendering: those bytes happen to be identical (`": "`) but live
3128 /// at a distinct algebraic level (the ClosedSet trait's error-
3129 /// carrier composition, not the per-type compound-key surface).
3130 /// Two distinct separators that happen to be equal bytes; a future
3131 /// diagnostic-surface change that touches one must not touch the
3132 /// other by accident, so keeping them at separate `pub const` /
3133 /// hand-composed sites is load-bearing structure, not duplication.
3134 ///
3135 /// Consumers that need the compound-key separator bytes at compile
3136 /// time bind against `CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR`
3137 /// rather than re-typing the literal — this includes the
3138 /// hand-rolled [`Display`] / [`FromStr`] pair below and any future
3139 /// LSP / REPL diagnostic-substring extractor that needs to
3140 /// `rsplit_once(CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR)` a
3141 /// captured operator-facing `"compile error in {operation}: {label}:
3142 /// {message}"` prefix.
3143 pub const COMPOUND_KEY_SEPARATOR: &'static str = ": ";
3144}
3145
3146/// Standalone rendering of a [`CompilerSpecIoStage`] in the canonical
3147/// compound `"{operation}{SEP}{label}"` form — byte-for-byte the same
3148/// substring that lands inside the
3149/// [`LispError::CompilerSpecIo`] diagnostic between `"compile error in
3150/// "` and `": {message}"`, where `{SEP}` is
3151/// [`CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR`] (`": "`). Pinning
3152/// Display to the compound form means a consumer that extracts the
3153/// (operation, label) prefix from a rendered diagnostic — for example
3154/// by `s.strip_prefix("compile error in ")
3155/// .and_then(|t| t.rsplit_once(CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR))`
3156/// — round-trips the captured substring through [`FromStr`] back into
3157/// the typed variant exactly.
3158///
3159/// The compound form is load-bearing: `label` alone would be bijective
3160/// with the current four variants (`"serialize"` / `"write"` / `"read"`
3161/// / `"deserialize"`) but a future fifth variant like
3162/// `LoadFromStrDeserialize` would collide with `LoadFromDiskDeserialize`
3163/// on `label` alone. Display-ing the compound key means the bijection
3164/// survives the closed-set extension without a label-disambiguation
3165/// dance.
3166///
3167/// The body composes through [`CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR`]
3168/// so the exact separator bytes bind at ONE `pub const` on the typed
3169/// algebra rather than at TWO sites (the [`Display`] `write!` format
3170/// string AND the [`FromStr`] `split_once` argument) — a lift symmetric
3171/// to how [`Self::operation`] and [`Self::label`] route through their
3172/// per-role slot-byte constants.
3173impl std::fmt::Display for CompilerSpecIoStage {
3174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3175 f.write_str(self.operation())?;
3176 f.write_str(Self::COMPOUND_KEY_SEPARATOR)?;
3177 f.write_str(self.label())
3178 }
3179}
3180
3181/// Decode a canonical [`CompilerSpecIoStage`] compound key
3182/// `"{operation}: {label}"` back into the typed variant — `Ok(stage)`
3183/// when the input matches the [`Display`] rendering of one of the four
3184/// variants in [`CompilerSpecIoStage::ALL`] byte-for-byte (case-sensitive
3185/// because the labels are the rendered diagnostic surface and any case
3186/// drift would silently bifurcate the round-trip), and
3187/// [`Err(UnknownCompilerSpecIoStage)`] for every other string.
3188///
3189/// Crucially the decode REJECTS the four conceivable-but-unreachable
3190/// cross-product pairs — `"realize_to_disk: read"`,
3191/// `"realize_to_disk: deserialize"`, `"load_from_disk: serialize"`,
3192/// `"load_from_disk: write"` — because none of those pairs appears in
3193/// [`CompilerSpecIoStage::ALL`]. The cross-product reachability
3194/// constraint, previously a Code-level invariant (only the four call
3195/// sites in `compiler_spec.rs` construct stages, each construction
3196/// site pairs the correct operation with the correct stage), becomes
3197/// a type-level invariant: the parse boundary refuses to deserialize
3198/// an unreachable pair.
3199///
3200/// Partial keys — `"serialize"` alone, `"realize_to_disk"` alone — also
3201/// reject. The [`CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR`] (`": "`)
3202/// bytes must appear at least once for the decode to consider any
3203/// variant; otherwise the diagnostic substring shape isn't a compound
3204/// key at all.
3205///
3206/// Round-trip invariant pinned by
3207/// `compiler_spec_io_stage_compound_key_round_trips_through_from_str`:
3208/// for every variant `s` in [`CompilerSpecIoStage::ALL`],
3209/// `s.to_string().parse() == Ok(s)`. The compound-rendering site is
3210/// singular (the [`Display`] impl projects through [`Self::operation`]
3211/// and [`Self::label`]) so the round-trip is the only way the typed
3212/// surface and the rendered diagnostic literal can drift apart —
3213/// pinning it here means they cannot. Mirror of every sibling
3214/// closed-set round-trip in the workspace ([`SexpShape::from_str`],
3215/// [`ExpectedKwargShape::from_str`], [`MacroDefHead::from_str`],
3216/// [`UnquoteForm::from_str`], `RequestorKind::from_str`,
3217/// `ReceiptKind::from_str`, `ConditionKind::from_str`,
3218/// `ProcessPhase::from_str`, …) — the difference is the compound-key
3219/// shape rather than a single label, which sharpens the closed-set
3220/// constraint from "cardinality matches the variant count" to
3221/// "cardinality matches the variant count AND the projection product
3222/// is partial, not total."
3223impl std::str::FromStr for CompilerSpecIoStage {
3224 type Err = UnknownCompilerSpecIoStage;
3225 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
3226 let Some((op, lbl)) = s.split_once(Self::COMPOUND_KEY_SEPARATOR) else {
3227 return Err(UnknownCompilerSpecIoStage(s.to_owned()));
3228 };
3229 for stage in Self::ALL {
3230 if op == stage.operation() && lbl == stage.label() {
3231 return Ok(stage);
3232 }
3233 }
3234 Err(UnknownCompilerSpecIoStage(s.to_owned()))
3235 }
3236}
3237
3238// `pub struct UnknownCompilerSpecIoStage(pub String)` is generated by
3239// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the `CompilerSpecIoStage`
3240// declaration above through `#[closed_set(generate_unknown)]`. The
3241// auto-projected `#[error("unknown compiler spec io stage: {0}")]`
3242// annotation (via `pascal_to_spaced_lowercase("CompilerSpecIoStage")` →
3243// `"compiler spec io stage"` — pinned by
3244// `pascal_to_spaced_lowercase_tests::contiguous_uppercase_runs_collapse_to_lowercase_without_inner_spaces`)
3245// matches the pre-lift hand-rolled wording byte-for-byte; LSP / REPL
3246// substring-matching `"unknown compiler spec io stage: "` continues to
3247// filter this rejection class without binding to the specific input.
3248// The substrate-wide carrier shape (`Debug + Clone + PartialEq + Eq +
3249// thiserror::Error` derives, `pub struct UnknownX(pub String)` with the
3250// `#[error("unknown <thing>: {0}")]` annotation) — symmetric to every
3251// sibling `Unknown*` error in the workspace
3252// (`UnknownExpectedKwargShape`, `UnknownSexpShape`,
3253// `UnknownMacroDefHead`, `UnknownUnquoteForm`,
3254// `crate::ast::UnknownAtomKind`, `crate::ast::UnknownQuoteForm`,
3255// `tatara_process::allocation::UnknownRequestorKind`,
3256// `tatara_process::receipt::UnknownReceiptKind`,
3257// `tatara_process::phase::UnknownPhase`,
3258// `tatara_process::boundary::UnknownConditionKind`,
3259// `tatara_process::lifetime::UnknownTeardownPolicy`, …) — emits from
3260// the derive rather than from a per-declaration hand-roll.
3261
3262/// Closed-set identifier for a bytecode-runtime invariant violation
3263/// surfaced by `macro_expand.rs::apply_compiled`. Encodes the four
3264/// reachable failure modes — Subst with an out-of-bounds param index,
3265/// Splice with an out-of-bounds param index, EndList against an empty
3266/// stack, and a final pop yielding no value — as a typed enum, so the
3267/// invalid combination of "stack-gate kind with an op-index payload"
3268/// (e.g. `EndListEmptyStack` carrying a `usize`) is structurally
3269/// unrepresentable: the index payload lives INSIDE the variants that
3270/// actually carry one (`SubstBadIndex(usize)` / `SpliceBadIndex(usize)`).
3271///
3272/// Same posture as `CompilerSpecIoStage`: the closed set becomes a
3273/// TYPE, not a free-form `message: String` slot inside the helper. The
3274/// `message()` projection feeds the `LispError::TemplateInvariant`
3275/// Display rendering directly via the `#[error(...)]` annotation;
3276/// adding a new bytecode-runtime invariant (e.g. a future `WrongOpcode`
3277/// gate that names a malformed bytecode header at the type level)
3278/// requires extending this enum, which rustc-enforces matching at the
3279/// projection site.
3280///
3281/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
3282/// of bytecode-invariant failure modes becomes a TYPE rather than a
3283/// runtime string-format dance. THEORY.md §VI.1 — generation over
3284/// composition; the typed enum lands the structural-completeness floor
3285/// for the bytecode-runtime surface, parallel to how `CompilerSpecIoStage`
3286/// lands the structural-completeness floor for the disk-persistence
3287/// surface.
3288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3289pub enum TemplateInvariantKind {
3290 /// `TemplateOp::Subst(idx)` referenced a param index that
3291 /// `args_by_index.get(idx)` returned `None` for — the compiled
3292 /// bytecode referenced an out-of-bounds required-param slot.
3293 SubstBadIndex(usize),
3294 /// `TemplateOp::Splice(idx)` referenced a param index that
3295 /// `args_by_index.get(idx)` returned `None` for — the compiled
3296 /// bytecode referenced an out-of-bounds splice-target param slot.
3297 SpliceBadIndex(usize),
3298 /// `TemplateOp::EndList` ran against an empty runtime stack —
3299 /// `stack.pop()` returned `None`, meaning the compiled bytecode
3300 /// emitted an `EndList` without a matching `BeginList`. The stack
3301 /// is the proof artifact; an unbalanced stack is the bytecode
3302 /// compiler's proof obligation having been silently dropped.
3303 EndListEmptyStack,
3304 /// The final `stack.pop()` after the bytecode loop yielded `None`
3305 /// — the compiled bytecode produced no value at all (an empty op
3306 /// list, or a body that consumes its own output). Distinct from
3307 /// `EndListEmptyStack`: that fires mid-loop on an explicit
3308 /// `EndList`; this fires after the loop on the implicit final
3309 /// pop.
3310 FinalNoValue,
3311}
3312
3313impl TemplateInvariantKind {
3314 /// Canonical `&'static str` bytes for [`Self::EndListEmptyStack`] —
3315 /// the mid-loop stack-gate invariant fired inside the bytecode
3316 /// runtime when `TemplateOp::EndList` runs against an empty stack.
3317 /// Per-role peer of `Self::EndListEmptyStack` on the STATIC 2-of-4
3318 /// subset carving of the four-arm closed set.
3319 ///
3320 /// Pre-lift the same `"compiled template: EndList with empty stack"`
3321 /// bytes lived inline at [`Self::message`]'s match arm plus at the
3322 /// truth-table test `template_invariant_kind_message_for_endlist_empty_stack`
3323 /// — the ≥2 PRIME-DIRECTIVE trigger. Post-lift the
3324 /// (`EndListEmptyStack` variant, canonical `&'static str`) pairing
3325 /// binds at ONE `pub const` on the typed [`TemplateInvariantKind`]
3326 /// algebra: the [`Self::message`] arm routes through this constant
3327 /// via [`Self::message_static`], the peer [`Self::STATIC_MESSAGES`]
3328 /// array composes it into a forced-arity `[&'static str; 2]` at ONE
3329 /// site, and every downstream consumer that pins the exact bytes
3330 /// (an LSP quick-fix that surfaces the invariant-name inline, a
3331 /// Sekiban audit-trail metric labeled by the invariant identity)
3332 /// binds through here.
3333 ///
3334 /// Sibling posture to the closed set of per-role canonical
3335 /// `pub const` bytes on the substrate's other closed-set outer
3336 /// algebras: [`MacroDefHead::DEFMACRO_KEYWORD`] (`"defmacro"`),
3337 /// [`CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION`] (`"realize
3338 /// to disk"`), [`UnquoteForm::UNQUOTE_LABEL`] (`","`),
3339 /// [`SexpShape::NIL_LABEL`] (`"nil"`). Each closed-set enum in the
3340 /// substrate now names its canonical per-variant bytes at ONE
3341 /// `pub const` per role rather than at TWO sites (the projection-
3342 /// method arm AND at least one truth-table test).
3343 pub const END_LIST_EMPTY_STACK_MESSAGE: &'static str =
3344 "compiled template: EndList with empty stack";
3345
3346 /// Canonical `&'static str` bytes for [`Self::FinalNoValue`] — the
3347 /// post-loop final-pop invariant fired after the bytecode runtime
3348 /// exhausts its op stream and the final `stack.pop()` yields no
3349 /// value. Per-role peer of `Self::FinalNoValue` on the STATIC
3350 /// 2-of-4 subset carving of the four-arm closed set.
3351 ///
3352 /// Pre-lift the same `"compiled template produced no value"` bytes
3353 /// lived inline at [`Self::message`]'s match arm plus at the truth-
3354 /// table test `template_invariant_kind_message_for_final_no_value`.
3355 /// Post-lift the (`FinalNoValue` variant, canonical `&'static str`)
3356 /// pairing binds at ONE `pub const` on the typed
3357 /// [`TemplateInvariantKind`] algebra: the [`Self::message`] arm
3358 /// routes through this constant via [`Self::message_static`], the
3359 /// peer [`Self::STATIC_MESSAGES`] array composes it into a forced-
3360 /// arity `[&'static str; 2]` at ONE site.
3361 ///
3362 /// Sibling posture to [`Self::END_LIST_EMPTY_STACK_MESSAGE`] on
3363 /// the same STATIC 2-of-4 subset carving — the dynamic arms
3364 /// (`SubstBadIndex(usize)` / `SpliceBadIndex(usize)`) format their
3365 /// `usize` payload into the message and cannot bind to a
3366 /// `&'static str` on this axis. The 2-of-4 STATIC subset is where
3367 /// the `Option<&'static str>` typed subset projection
3368 /// [`Self::message_static`] lives.
3369 pub const FINAL_NO_VALUE_MESSAGE: &'static str = "compiled template produced no value";
3370
3371 /// The closed set of two canonical `&'static str` bytes on the
3372 /// STATIC 2-of-4 subset carving of [`TemplateInvariantKind`]'s
3373 /// four-arm closed set — the [`Self::END_LIST_EMPTY_STACK_MESSAGE`]
3374 /// (`"compiled template: EndList with empty stack"`) mid-loop
3375 /// stack-gate invariant followed by the [`Self::FINAL_NO_VALUE_MESSAGE`]
3376 /// (`"compiled template produced no value"`) post-loop final-pop
3377 /// invariant. Canonical declaration order matches
3378 /// [`Self::message_static`]'s arm order so
3379 /// `Self::STATIC_MESSAGES[0] == Self::message_static(Self::EndListEmptyStack).unwrap()`
3380 /// and `Self::STATIC_MESSAGES[1] == Self::message_static(Self::FinalNoValue).unwrap()`
3381 /// element-wise — pinned by
3382 /// `template_invariant_kind_static_messages_align_with_message_static_by_index`.
3383 ///
3384 /// Sibling posture to every other closed-set STATIC-subset ALL
3385 /// array on the substrate: peer 2-of-4 carving to
3386 /// [`OptionalParamMalformedReason`]'s hypothetical STATIC subset
3387 /// (`EmptyList` / `MissingDefault` / `NonSymbolName` — a 3-of-4
3388 /// static carving on the sibling optional-section-malformed
3389 /// surface), 3-of-3 total to [`KwargPathKind::LABELS`],
3390 /// [`MacroDefHead::KEYWORDS`], and 7-of-7 to
3391 /// [`ExpectedKwargShape::LABELS`]. Adding a hypothetical fifth
3392 /// static invariant (e.g. `WrongOpcode` — a bytecode-header gate
3393 /// naming a malformed bytecode header at the type level with a
3394 /// `&'static str` message payload) extends [`Self::STATIC_MESSAGES`]
3395 /// ONCE + [`Self::message_static`]'s arms ONCE + adds ONE per-role
3396 /// message `pub const` — rustc's forced-arity check on the
3397 /// `[&'static str; N]` array fails compilation if the array grows
3398 /// without the projection method being extended.
3399 ///
3400 /// Future consumers that compose against [`Self::STATIC_MESSAGES`]:
3401 /// an LSP quick-fix that surfaces the static-message vocabulary
3402 /// inline at every `LispError::TemplateInvariant` diagnostic
3403 /// (`Self::STATIC_MESSAGES.iter()` is the ONE typed sweep over
3404 /// every legal static bytecode-invariant message); a
3405 /// `tatara-check` coverage assertion (every workspace `.lisp`
3406 /// file's macro-body must expand without producing a
3407 /// `TemplateInvariantKind` rejection whose message lies OUTSIDE
3408 /// [`Self::STATIC_MESSAGES`] ∪ formatted-dynamic-arm messages —
3409 /// the typed sweep replaces the per-call-site vocabulary of two
3410 /// `&'static str` literals); a Sekiban audit-trail metric jointly
3411 /// labeled by the STATIC-subset invariant message (e.g.
3412 /// `tatara_lisp_template_invariant_total{message="compiled
3413 /// template produced no value"}` — the metric label set on the
3414 /// STATIC subset IS [`Self::STATIC_MESSAGES`]).
3415 pub const STATIC_MESSAGES: [&'static str; 2] = [
3416 Self::END_LIST_EMPTY_STACK_MESSAGE,
3417 Self::FINAL_NO_VALUE_MESSAGE,
3418 ];
3419
3420 /// Canonical `&'static str` descriptor bytes for the
3421 /// [`Self::SubstBadIndex`] rejection — the noun-slot bytes `"param"`
3422 /// naming the required-param slot the compiled bytecode indexed
3423 /// out-of-bounds. Per-role peer of [`Self::SubstBadIndex`] on the
3424 /// DYNAMIC 2-of-4 subset carving of the four-arm closed set — the
3425 /// two arms whose diagnostic bytes format their `usize` payload
3426 /// into the shared message template rather than binding to a fixed
3427 /// `&'static str` on the STATIC axis.
3428 ///
3429 /// Pre-lift the same `"param"` bytes lived inline at
3430 /// [`Self::message`]'s `SubstBadIndex(idx)` match arm as the
3431 /// literal noun slot of `format!("compiled template referenced bad
3432 /// param index {idx}")` plus at the truth-table tests
3433 /// `template_invariant_kind_message_for_subst_bad_idx` and
3434 /// `template_invariant_display_renders_legacy_compile_shape_for_subst_bad_idx`
3435 /// — the ≥2 PRIME-DIRECTIVE trigger. Post-lift the (`SubstBadIndex`
3436 /// variant, canonical `&'static str` descriptor) pairing binds at
3437 /// ONE `pub const` on the typed [`TemplateInvariantKind`] algebra:
3438 /// [`Self::message_dynamic`] routes through this constant plus the
3439 /// [`Self::SPLICE_BAD_INDEX_DESCRIPTOR`] peer via
3440 /// [`Self::descriptor_dynamic`], the peer [`Self::DYNAMIC_DESCRIPTORS`]
3441 /// array composes it into a forced-arity `[&'static str; 2]` at ONE
3442 /// site, and the shared `format!("compiled template referenced bad
3443 /// {descriptor} index {idx}")` template binds at ONE site inside
3444 /// [`Self::message_dynamic`] rather than at TWO byte-for-byte
3445 /// duplicated `format!(...)` arms (the pre-lift `SubstBadIndex(idx)`
3446 /// arm's `"…bad param index {idx}"` AND the `SpliceBadIndex(idx)`
3447 /// arm's `"…bad splice index {idx}"`).
3448 ///
3449 /// Sibling posture to the closed-set per-role canonical `pub const`
3450 /// bytes on peer STATIC-subset carvings: this axis is the DYNAMIC
3451 /// 2-of-4 peer to [`Self::END_LIST_EMPTY_STACK_MESSAGE`] +
3452 /// [`Self::FINAL_NO_VALUE_MESSAGE`]'s STATIC 2-of-4 carving —
3453 /// together the four per-role `pub const`s partition the four-arm
3454 /// closed set into its STATIC ⊕ DYNAMIC halves, each with its own
3455 /// typed subset projection ([`Self::message_static`],
3456 /// [`Self::descriptor_dynamic`]). Cross-axis peer to
3457 /// [`OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY`] on
3458 /// the sibling algebra's `ExtraElements` dynamic arm — both name
3459 /// the load-bearing typed slot that the pre-lift `format!(...)`
3460 /// arm embedded as a bare literal.
3461 ///
3462 /// Future consumers keyed on the descriptor: LSP quick-fix for the
3463 /// out-of-bounds Subst rejection (surface `"the compiled template
3464 /// referenced a bad {descriptor} index"` where `{descriptor}`
3465 /// binds to this const); a Sekiban audit-trail metric jointly
3466 /// labeled by the DYNAMIC-subset descriptor
3467 /// (`tatara_lisp_template_invariant_total{descriptor="param"}` —
3468 /// the metric label set on the DYNAMIC subset IS
3469 /// [`Self::DYNAMIC_DESCRIPTORS`]).
3470 pub const SUBST_BAD_INDEX_DESCRIPTOR: &'static str = "param";
3471
3472 /// Canonical `&'static str` descriptor bytes for the
3473 /// [`Self::SpliceBadIndex`] rejection — the noun-slot bytes
3474 /// `"splice"` naming the splice-target param slot the compiled
3475 /// bytecode indexed out-of-bounds. Per-role peer of
3476 /// [`Self::SpliceBadIndex`] on the DYNAMIC 2-of-4 subset carving of
3477 /// the four-arm closed set.
3478 ///
3479 /// Sibling of [`Self::SUBST_BAD_INDEX_DESCRIPTOR`] on the same
3480 /// DYNAMIC 2-of-4 carving — see that constant's doc for the
3481 /// algebra-level pre-lift/post-lift contract every DYNAMIC-subset
3482 /// per-role descriptor shares. Post-lift the shared
3483 /// `format!("compiled template referenced bad {descriptor} index
3484 /// {idx}")` template inside [`Self::message_dynamic`] binds
3485 /// `{descriptor}` to whichever per-role const the DYNAMIC arm
3486 /// projects through [`Self::descriptor_dynamic`].
3487 pub const SPLICE_BAD_INDEX_DESCRIPTOR: &'static str = "splice";
3488
3489 /// The closed set of two canonical `&'static str` descriptor bytes
3490 /// on the DYNAMIC 2-of-4 subset carving of
3491 /// [`TemplateInvariantKind`]'s four-arm closed set — the
3492 /// [`Self::SUBST_BAD_INDEX_DESCRIPTOR`] (`"param"`) noun for the
3493 /// required-param slot followed by the
3494 /// [`Self::SPLICE_BAD_INDEX_DESCRIPTOR`] (`"splice"`) noun for the
3495 /// splice-target param slot. Canonical declaration order matches
3496 /// [`Self::descriptor_dynamic`]'s arm order so
3497 /// `Self::DYNAMIC_DESCRIPTORS[0] == Self::descriptor_dynamic(Self::SubstBadIndex(_)).unwrap()`
3498 /// and `Self::DYNAMIC_DESCRIPTORS[1] == Self::descriptor_dynamic(Self::SpliceBadIndex(_)).unwrap()`
3499 /// element-wise — pinned by
3500 /// `template_invariant_kind_dynamic_descriptors_align_with_descriptor_dynamic_by_index`.
3501 ///
3502 /// Peer 2-of-4 carving to [`Self::STATIC_MESSAGES`] — together the
3503 /// STATIC + DYNAMIC 2-of-4 arrays partition the four-arm closed
3504 /// set into its `&'static str`-projectable halves: the STATIC array
3505 /// carries the two arms whose FULL message is `&'static str`, the
3506 /// DYNAMIC array carries the two arms whose NOUN slot is
3507 /// `&'static str` and whose full message is a
3508 /// `format!("compiled template referenced bad {descriptor} index
3509 /// {idx}")` composition. Adding a hypothetical fifth static
3510 /// invariant lands only on the STATIC array; adding a hypothetical
3511 /// fifth dynamic invariant (e.g. a `KwargBadIndex(usize)` gate
3512 /// naming a keyword-param slot the compiled bytecode indexed
3513 /// out-of-bounds) lands only on this array — rustc's forced-arity
3514 /// check on the `[&'static str; N]` shape fails compilation if the
3515 /// array grows without the corresponding per-role descriptor +
3516 /// [`Self::descriptor_dynamic`] arm being extended in lockstep.
3517 ///
3518 /// Sibling posture to [`OptionalParamMalformedReason::STATIC_LABELS`]
3519 /// (peer algebra's STATIC 3-of-4 carving) — every closed-set outer
3520 /// algebra the substrate carries now pins BOTH its STATIC-subset
3521 /// canonical-bytes array AND its DYNAMIC-subset per-role-slot array
3522 /// at ONE `pub const` per axis rather than at N inline literals
3523 /// scattered across projections and tests. The DYNAMIC axis is
3524 /// what this array lands for `TemplateInvariantKind`.
3525 ///
3526 /// Future consumers that compose against
3527 /// [`Self::DYNAMIC_DESCRIPTORS`]: a `tatara-check` coverage
3528 /// assertion over the workspace corpus (every rendered
3529 /// `TemplateInvariant` diagnostic with a DYNAMIC-arm shape must
3530 /// substring-match one of the two descriptor bytes —
3531 /// `Self::DYNAMIC_DESCRIPTORS.iter()` is the ONE typed sweep over
3532 /// every legal dynamic-arm noun); a Sekiban audit-trail metric
3533 /// keyed on the descriptor slot; an LSP quick-fix that pluralizes
3534 /// the descriptor into a suggestion.
3535 #[allow(dead_code)]
3536 pub const DYNAMIC_DESCRIPTORS: [&'static str; 2] = [
3537 Self::SUBST_BAD_INDEX_DESCRIPTOR,
3538 Self::SPLICE_BAD_INDEX_DESCRIPTOR,
3539 ];
3540
3541 /// Typed `Option<&'static str>` zero-allocation projection over the
3542 /// STATIC 2-of-4 subset carving of [`TemplateInvariantKind`]'s
3543 /// four-arm closed set. Returns `Some(bytes)` for the two arms
3544 /// whose diagnostic bytes bind at a per-role `pub const &'static
3545 /// str` ([`Self::EndListEmptyStack`] →
3546 /// [`Self::END_LIST_EMPTY_STACK_MESSAGE`], [`Self::FinalNoValue`] →
3547 /// [`Self::FINAL_NO_VALUE_MESSAGE`]) and `None` for the two arms
3548 /// whose bytes format their `usize` payload dynamically
3549 /// ([`Self::SubstBadIndex`] / [`Self::SpliceBadIndex`]) and
3550 /// cannot bind to a `&'static str` on this axis.
3551 ///
3552 /// The `const fn` posture matches [`MacroDefHead::keyword`],
3553 /// [`UnquoteForm::label`], [`SexpShape::label`], and every peer
3554 /// closed-set projection method on the substrate — the projection
3555 /// evaluates at rustc const-eval time when the variant is a
3556 /// const, so a future const-context consumer (an
3557 /// `#[allow(dead_code)] const _: Option<&'static str> = ...`
3558 /// static assertion, a `tatara-check` predicate binding at
3559 /// module-init time) picks up the STATIC-subset selector without
3560 /// runtime dispatch.
3561 ///
3562 /// Sibling posture to a hypothetical
3563 /// `OptionalParamMalformedReason::label_static` on the peer
3564 /// 3-of-4 static carving of the sibling optional-section-malformed
3565 /// four-arm closed set: both project the STATIC subset of a
3566 /// mixed-payload closed set (payload-carrying dynamic arms
3567 /// project to `None`) into a typed `Option<&'static str>` — the
3568 /// same idiom applied at two peer boundaries of the substrate's
3569 /// bytecode-runtime and macro-authoring surfaces.
3570 ///
3571 /// Post-lift the substrate's STATIC-subset selector over the
3572 /// four-arm closed set binds at ONE typed function on the algebra
3573 /// rather than at zero-primitive-plus-runtime-dispatch through
3574 /// [`Self::message`]'s match body (which pre-lift owned the
3575 /// selector by matching two `&'static str` literals inline). A
3576 /// hypothetical fifth arm on the STATIC subset (e.g.
3577 /// `WrongOpcode` — a bytecode-header gate) extends
3578 /// [`Self::STATIC_MESSAGES`] AND [`Self::message_static`]'s arms
3579 /// AND adds ONE per-role message `pub const` in lockstep —
3580 /// rustc's exhaustive-match check on the four-arm closed set
3581 /// fails compilation without the new arm's inclusion.
3582 #[must_use]
3583 pub const fn message_static(self) -> Option<&'static str> {
3584 match self {
3585 Self::EndListEmptyStack => Some(Self::END_LIST_EMPTY_STACK_MESSAGE),
3586 Self::FinalNoValue => Some(Self::FINAL_NO_VALUE_MESSAGE),
3587 Self::SubstBadIndex(_) | Self::SpliceBadIndex(_) => None,
3588 }
3589 }
3590
3591 /// Typed `Option<&'static str>` zero-allocation projection over the
3592 /// DYNAMIC 2-of-4 subset carving of [`TemplateInvariantKind`]'s
3593 /// four-arm closed set — returns `Some(descriptor)` for the two
3594 /// arms whose diagnostic bytes carry a `usize` payload and format
3595 /// through the shared `"compiled template referenced bad
3596 /// {descriptor} index {idx}"` template ([`Self::SubstBadIndex`] →
3597 /// [`Self::SUBST_BAD_INDEX_DESCRIPTOR`] (`"param"`),
3598 /// [`Self::SpliceBadIndex`] → [`Self::SPLICE_BAD_INDEX_DESCRIPTOR`]
3599 /// (`"splice"`)), and `None` for the two arms whose full diagnostic
3600 /// bytes bind at a `&'static str` on the STATIC axis
3601 /// ([`Self::EndListEmptyStack`], [`Self::FinalNoValue`]) and carry
3602 /// no per-role descriptor slot.
3603 ///
3604 /// Peer projection to [`Self::message_static`] on the sibling
3605 /// STATIC 2-of-4 subset carving — the two projections together
3606 /// partition the four-arm closed set into its STATIC ⊕ DYNAMIC
3607 /// halves at zero allocation cost. Callers holding a variant of
3608 /// unknown identity thread it through BOTH projections; exactly ONE
3609 /// of `message_static(self)` and `descriptor_dynamic(self)` returns
3610 /// `Some` for every legal variant (`message_static` on the
3611 /// two-arm STATIC subset, `descriptor_dynamic` on the two-arm
3612 /// DYNAMIC subset). The partition is pinned by the truth-table
3613 /// test `template_invariant_kind_static_dynamic_projections_partition_closed_set`.
3614 ///
3615 /// The `const fn` posture matches [`Self::message_static`],
3616 /// [`OptionalParamMalformedReason::label_static`], and every peer
3617 /// closed-set projection method on the substrate — evaluates at
3618 /// rustc const-eval time when the variant is a const.
3619 ///
3620 /// Sibling posture to a hypothetical
3621 /// `OptionalParamMalformedReason::descriptor_dynamic` projecting
3622 /// `Self::ExtraElements` into its "elements" descriptor bytes on
3623 /// the peer algebra's 1-of-4 dynamic carving — the same idiom
3624 /// applied at two peer boundaries of the substrate's bytecode-
3625 /// runtime and macro-authoring surfaces.
3626 ///
3627 /// Future consumers that compose against this projection: an LSP
3628 /// quick-fix keyed on the DYNAMIC-subset descriptor slot (surfaces
3629 /// `"the compiled template referenced a bad {descriptor} index"`
3630 /// with `{descriptor}` bound to whatever this projection returns);
3631 /// a `tatara-check` predicate that filters a corpus's
3632 /// `TemplateInvariant` rejections into static-arm vs dynamic-arm
3633 /// buckets in ONE typed subset selector rather than three inline
3634 /// `matches!(kind, Self::SubstBadIndex(_) | Self::SpliceBadIndex(_))`
3635 /// gates threaded through per-consumer allocation.
3636 #[must_use]
3637 pub const fn descriptor_dynamic(self) -> Option<&'static str> {
3638 match self {
3639 Self::SubstBadIndex(_) => Some(Self::SUBST_BAD_INDEX_DESCRIPTOR),
3640 Self::SpliceBadIndex(_) => Some(Self::SPLICE_BAD_INDEX_DESCRIPTOR),
3641 Self::EndListEmptyStack | Self::FinalNoValue => None,
3642 }
3643 }
3644
3645 /// Typed `Option<String>` projection over the DYNAMIC 2-of-4 subset
3646 /// carving of [`TemplateInvariantKind`]'s four-arm closed set —
3647 /// returns `Some(message)` for the two arms whose diagnostic bytes
3648 /// format their `usize` payload through the SHARED
3649 /// `"compiled template referenced bad {descriptor} index {idx}"`
3650 /// template, and `None` for the two STATIC arms whose full bytes
3651 /// bind on the [`Self::message_static`] axis.
3652 ///
3653 /// Composes [`Self::descriptor_dynamic`] into the shared format
3654 /// template — the `{descriptor}` slot binds to whichever per-role
3655 /// [`Self::SUBST_BAD_INDEX_DESCRIPTOR`] /
3656 /// [`Self::SPLICE_BAD_INDEX_DESCRIPTOR`] constant the DYNAMIC arm
3657 /// projects through, and the `{idx}` slot binds the `usize`
3658 /// payload the enum's dynamic variant carries. Pre-lift
3659 /// [`Self::message`]'s body carried TWO byte-for-byte duplicated
3660 /// `format!(...)` arms whose only variance was the noun slot
3661 /// (`"param"` vs `"splice"`) — post-lift the shared template binds
3662 /// at ONE `format!(...)` site inside this method, the noun slot
3663 /// binds at the per-role descriptor const, and
3664 /// [`Self::descriptor_dynamic`] carries the arm→descriptor
3665 /// mapping.
3666 ///
3667 /// Peer projection to [`Self::message_static`] on the sibling
3668 /// STATIC 2-of-4 subset carving — the two projections together
3669 /// partition the four-arm closed set into its STATIC ⊕ DYNAMIC
3670 /// message-projection halves. [`Self::message`]'s body reduces to
3671 /// `message_static().map(str::to_string).or_else(||
3672 /// message_dynamic()).expect(...)` — ONE compositional flow over
3673 /// TWO typed subset projections rather than FOUR duplicated match
3674 /// arms (two `.into()` sites + two `format!(...)` sites in the
3675 /// pre-lift body).
3676 ///
3677 /// Sibling posture to a hypothetical
3678 /// `OptionalParamMalformedReason::label_dynamic` composing
3679 /// `Self::ExtraElements`'s `{length}` payload through
3680 /// [`OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY`] on
3681 /// the peer algebra's 1-of-4 dynamic carving.
3682 ///
3683 /// Future consumers that compose against this projection: a
3684 /// `tatara-check` predicate that filters a corpus's
3685 /// `TemplateInvariant` rejections into static-arm vs dynamic-arm
3686 /// message buckets; a Sekiban audit-trail metric that samples
3687 /// dynamic-arm messages without paying the allocation cost on the
3688 /// STATIC subset (the `None` branch short-circuits).
3689 #[must_use]
3690 pub fn message_dynamic(self) -> Option<String> {
3691 let descriptor = self.descriptor_dynamic()?;
3692 let idx = match self {
3693 Self::SubstBadIndex(idx) | Self::SpliceBadIndex(idx) => idx,
3694 Self::EndListEmptyStack | Self::FinalNoValue => {
3695 // Unreachable: descriptor_dynamic() returned Some only
3696 // for SubstBadIndex / SpliceBadIndex on the `?` above.
3697 // Kept exhaustive rather than `_ =>` so a future refactor
3698 // that adds a fifth dynamic arm without extending
3699 // descriptor_dynamic fails compilation at THIS callsite
3700 // rather than at runtime.
3701 unreachable!(
3702 "descriptor_dynamic returned Some only for dynamic-arm variants; \
3703 the STATIC-arm match here is unreachable via the ? short-circuit"
3704 )
3705 }
3706 };
3707 Some(format!(
3708 "compiled template referenced bad {descriptor} index {idx}"
3709 ))
3710 }
3711
3712 /// The `{message}` slot of the legacy `LispError::Compile { form:
3713 /// macro_name, message: <invariant> }` shape. Each variant projects
3714 /// to the canonical message string the pre-lift inline triples
3715 /// emitted — byte-for-byte equivalent so authoring-tool substring
3716 /// greps (`tatara-check`, REPL) see no drift across the lift.
3717 ///
3718 /// Body composes through BOTH typed subset projections — the
3719 /// STATIC 2-of-4 [`Self::message_static`] and the DYNAMIC 2-of-4
3720 /// [`Self::message_dynamic`]. The partition contract (exactly ONE
3721 /// projection returns `Some` for every legal variant) collapses
3722 /// the pre-lift's FOUR duplicated match arms (two `.into()` sites
3723 /// on the STATIC subset AND two byte-for-byte `format!(...)` sites
3724 /// on the DYNAMIC subset) into ONE `.or_else(...)` composition:
3725 /// `message_static().map(str::to_string).or_else(||
3726 /// message_dynamic()).expect(...)`. The `.expect(...)` is
3727 /// structurally unreachable — the partition test
3728 /// `template_invariant_kind_static_dynamic_projections_partition_closed_set`
3729 /// pins that at least ONE of the two projections returns `Some` for
3730 /// every variant. Sibling posture to
3731 /// `OptionalParamMalformedReason::label` composing through
3732 /// `label_static` on the peer 3-of-4 static carving.
3733 #[must_use]
3734 pub fn message(self) -> String {
3735 self.message_static()
3736 .map(str::to_string)
3737 .or_else(|| self.message_dynamic())
3738 .expect(
3739 "message_static/message_dynamic partition covers the four-arm closed set — \
3740 exactly one projection returns Some for every legal variant, so the None \
3741 branch here is unreachable",
3742 )
3743 }
3744}
3745
3746/// Closed-set identifier for the head keyword of a `defmacro`-shape
3747/// rejection — the three canonical macro-definition heads
3748/// `defmacro` / `defpoint-template` / `defcheck`. Carried as a typed
3749/// slot on `LispError::DefmacroArity`, `LispError::DefmacroNonSymbolName`,
3750/// and `LispError::DefmacroNonListParams` so authoring tools (REPL, LSP,
3751/// `tatara-check`) bind to variant identity rather than substring-matching
3752/// the rendered `head` string.
3753///
3754/// Mirror at the macro-definition-head boundary of the prior-run
3755/// `CompilerSpecIoStage` (disk-persistence surface) and
3756/// `TemplateInvariantKind` (bytecode-runtime surface) closed-set lifts:
3757/// those variants key on a typed enum for the (operation, stage) pair
3758/// and the invariant kind respectively; this enum keys the three
3759/// `Defmacro*` variants on a typed head identity. Adding a new
3760/// macro-definition head requires extending this enum, which rustc-
3761/// enforces matching at every projection site (`keyword()`) — the
3762/// closed set becomes a TYPE rather than a `matches!` literal at the
3763/// `macro_def_from` gate plus three `match head` projections inside
3764/// each variant's helper.
3765///
3766/// `from_keyword(&str) -> Option<Self>` projects an arbitrary source
3767/// symbol into the typed enum; `keyword(self) -> &'static str` projects
3768/// back to the canonical literal for `LispError::Display` rendering.
3769/// The bidirection is the identity on the closed set —
3770/// `from_keyword(k).unwrap().keyword() == k` for every canonical `k`.
3771///
3772/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set of
3773/// macro-definition heads becomes a TYPE rather than a runtime
3774/// string-comparison-and-format dance. THEORY.md §VI.1 — generation
3775/// over composition; the typed enum lands the structural-completeness
3776/// floor for the macro-definition-head surface, parallel to how
3777/// `CompilerSpecIoStage` lands it for the disk-persistence surface and
3778/// `TemplateInvariantKind` for the bytecode-runtime surface.
3779#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
3780#[closed_set(via = "keyword", display, generate_unknown = "macro definition head")]
3781pub enum MacroDefHead {
3782 /// `(defmacro NAME (PARAMS) BODY)` — the canonical Lisp-style macro
3783 /// definition.
3784 Defmacro,
3785 /// `(defpoint-template NAME (PARAMS) BODY)` — the K8s-as-processes
3786 /// authoring surface's macro form (see `tatara-process`).
3787 DefpointTemplate,
3788 /// `(defcheck NAME (PARAMS) BODY)` — the workspace-coherence
3789 /// authoring surface's macro form (see
3790 /// `tatara-reconciler/checks.lisp`).
3791 Defcheck,
3792}
3793
3794impl MacroDefHead {
3795 /// The closed set of three macro-definition heads — single source
3796 /// of truth that drives the [`Self::keyword`] / [`fmt::Display`]
3797 /// projection AND the [`Self::from_keyword`] / [`FromStr`] decode
3798 /// sweeps keyed on [`Self::keyword`]. Adding a hypothetical fourth
3799 /// head (e.g. a `defpoint-fragment` partial-template surface, a
3800 /// `defrewrite` typed-rewriter authoring keyword) lands at one
3801 /// [`Self::ALL`] entry + one [`Self::keyword`] arm — exhaustively
3802 /// checked by the compiler (the `[Self; 3]` array literal forces
3803 /// the arity) AND by the per-variant truth-table tests below.
3804 ///
3805 /// Sibling closed-set lift to every other typed-shape enum in the
3806 /// crate ([`crate::ast::AtomKind::ALL`],
3807 /// [`crate::ast::QuoteForm::ALL`], [`SexpShape::ALL`],
3808 /// [`UnquoteForm::ALL`]) and across the workspace
3809 /// (`ConditionKind::ALL`, `ProcessPhase::ALL`,
3810 /// `ProcessSignal::ALL`, `ChannelKind::ALL`, `IntentKind::ALL`,
3811 /// `LifetimeKind::ALL`, `RequestorKind::ALL`, `ReceiptKind::ALL`,
3812 /// …) every one of which paired its typed projection with `ALL`
3813 /// before this lift.
3814 ///
3815 /// Future consumers that compose against `ALL`: LSP / REPL
3816 /// completion for the macro-definition head at point (every
3817 /// `(defma…` partial input expands through `Self::ALL.iter().
3818 /// map(MacroDefHead::keyword)`), `tatara-check` coverage assertions
3819 /// over which macro-definition heads reach a `DefmacroArity` /
3820 /// `DefmacroNonSymbolName` / `DefmacroNonListParams` arm at all
3821 /// (the typed sweep replaces the per-call-site vocabulary of three
3822 /// `&'static str` literals), any future audit-trail metric jointly
3823 /// labeled by [`Self::keyword`] (e.g.
3824 /// `tatara_lisp_defmacro_arity_total{head="defmacro"}` — the
3825 /// metric label set IS [`Self::ALL`] mapped through
3826 /// [`Self::keyword`]).
3827 pub const ALL: [Self; 3] = [Self::Defmacro, Self::DefpointTemplate, Self::Defcheck];
3828
3829 /// Canonical `&'static str` head keyword for [`Self::Defmacro`] —
3830 /// the `(defmacro NAME (PARAMS) BODY)` Lisp-style macro-definition
3831 /// form. Sibling posture to [`Self::DEFPOINT_TEMPLATE_KEYWORD`]
3832 /// (`"defpoint-template"`) and [`Self::DEFCHECK_KEYWORD`]
3833 /// (`"defcheck"`) on the same head-keyword algebra layer.
3834 ///
3835 /// Pre-lift the same `"defmacro"` bytes lived inline at the
3836 /// [`Self::keyword`] match arm plus at the Display truth-table
3837 /// test (`macro_def_head_display_renders_canonical_keyword_for_each_variant`)
3838 /// — the ≥2 PRIME-DIRECTIVE trigger. Post-lift the (`Defmacro`
3839 /// variant, canonical `&'static str`) pairing binds at ONE `pub
3840 /// const` on the typed [`MacroDefHead`] algebra: the [`Self::keyword`]
3841 /// arm and every consumer that pins the exact bytes route through
3842 /// this constant, so a rename to `defmacro-typed` (say, if a
3843 /// future extension lands a peer `defmacro-hygienic` variant and
3844 /// wants to disambiguate the legacy form) is ONE edit HERE with
3845 /// the derived Display / FromStr surfaces mechanically picking it
3846 /// up. Sibling posture to the closed set of per-role canonical
3847 /// `pub const` bytes on the substrate's other closed-set outer
3848 /// algebras: [`crate::ast::Atom::STR_DELIMITER`] (`'"'`),
3849 /// [`crate::ast::Atom::KEYWORD_MARKER`] (`":"`),
3850 /// [`crate::ast::Sexp::LIST_OPEN`] (`'('`),
3851 /// [`crate::macro_expand::MacroParams::REST_MARKER`] (`"&rest"`),
3852 /// [`crate::macro_expand::MacroParams::OPTIONAL_MARKER`] (`"&optional"`).
3853 pub const DEFMACRO_KEYWORD: &'static str = "defmacro";
3854
3855 /// Canonical `&'static str` head keyword for [`Self::DefpointTemplate`]
3856 /// — the `(defpoint-template NAME (PARAMS) BODY)` K8s-as-processes
3857 /// authoring-surface macro form (see the `tatara-process` crate).
3858 /// Sibling posture to [`Self::DEFMACRO_KEYWORD`] (`"defmacro"`)
3859 /// and [`Self::DEFCHECK_KEYWORD`] (`"defcheck"`) on the same
3860 /// head-keyword algebra layer.
3861 ///
3862 /// Pre-lift the same `"defpoint-template"` bytes lived inline at
3863 /// the [`Self::keyword`] match arm plus at the Display truth-table
3864 /// test. Post-lift the (`DefpointTemplate` variant, canonical
3865 /// `&'static str`) pairing binds at ONE `pub const` on the typed
3866 /// [`MacroDefHead`] algebra.
3867 pub const DEFPOINT_TEMPLATE_KEYWORD: &'static str = "defpoint-template";
3868
3869 /// Canonical `&'static str` head keyword for [`Self::Defcheck`] —
3870 /// the `(defcheck NAME (PARAMS) BODY)` workspace-coherence
3871 /// authoring-surface macro form (see `tatara-reconciler/checks.lisp`).
3872 /// Sibling posture to [`Self::DEFMACRO_KEYWORD`] (`"defmacro"`)
3873 /// and [`Self::DEFPOINT_TEMPLATE_KEYWORD`] (`"defpoint-template"`)
3874 /// on the same head-keyword algebra layer.
3875 ///
3876 /// Pre-lift the same `"defcheck"` bytes lived inline at the
3877 /// [`Self::keyword`] match arm plus at the Display truth-table
3878 /// test AND at seven `iter_named_calls_to(&forms, "defcheck")` /
3879 /// `["defmonitor", "defalertpolicy", "defcheck"]` test-site
3880 /// literals across [`crate::ast`] and [`crate::compile`]. Post-lift
3881 /// the (`Defcheck` variant, canonical `&'static str`) pairing
3882 /// binds at ONE `pub const` on the typed [`MacroDefHead`] algebra.
3883 pub const DEFCHECK_KEYWORD: &'static str = "defcheck";
3884
3885 /// The closed set of three canonical `&'static str` head keywords
3886 /// — the [`Self::DEFMACRO_KEYWORD`] (`"defmacro"`) canonical
3887 /// Lisp-style macro-definition keyword followed by the
3888 /// [`Self::DEFPOINT_TEMPLATE_KEYWORD`] (`"defpoint-template"`)
3889 /// K8s-as-processes authoring-surface keyword followed by the
3890 /// [`Self::DEFCHECK_KEYWORD`] (`"defcheck"`) workspace-coherence
3891 /// authoring-surface keyword. Canonical declaration order matches
3892 /// [`Self::ALL`] so `Self::KEYWORDS[i] == Self::ALL[i].keyword()`
3893 /// element-wise — pinned by
3894 /// `macro_def_head_keywords_align_with_all_by_index`.
3895 ///
3896 /// Sibling posture to
3897 /// [`crate::macro_expand::MacroParams::LAMBDA_LIST_KEYWORDS`]
3898 /// (`[Self; 2]`) — every closed-set algebra now pins its
3899 /// keyword-projection ALL array at the declaration site via a
3900 /// forced-arity `[&'static str; N]` array whose length fails
3901 /// compilation if a new variant lands without being added to the
3902 /// set. Adding a hypothetical fourth head (a `defpoint-fragment`
3903 /// partial-template surface, a `defrewrite` typed-rewriter
3904 /// authoring keyword) extends [`Self::ALL`] ONCE +
3905 /// [`Self::keyword`] ONCE + [`Self::KEYWORDS`] ONCE + adds ONE
3906 /// per-role keyword `pub const` — rustc's forced-arity check on
3907 /// the `[Self; N]` array + `[&'static str; N]` array pair
3908 /// enforces every downstream consumer picks up the extension
3909 /// mechanically.
3910 ///
3911 /// Future consumers that compose against [`Self::KEYWORDS`]: an
3912 /// LSP / REPL completion provider surfacing every `(defma…`
3913 /// partial input against the closed set (`Self::KEYWORDS.iter()`
3914 /// is the ONE typed sweep over every legal macro-definition
3915 /// head), a `tatara-check` coverage assertion (every workspace
3916 /// `.lisp` file's `(def…)` form-head must classify to some entry
3917 /// of `Self::KEYWORDS` OR be a registered `TataraDomain` keyword),
3918 /// a Sekiban audit-trail metric jointly labeled by
3919 /// [`Self::keyword`] whose metric-label set IS `Self::KEYWORDS`.
3920 pub const KEYWORDS: [&'static str; 3] = [
3921 Self::DEFMACRO_KEYWORD,
3922 Self::DEFPOINT_TEMPLATE_KEYWORD,
3923 Self::DEFCHECK_KEYWORD,
3924 ];
3925
3926 /// Project a `head: &str` borrow (a `Sexp` symbol slice) into the
3927 /// typed `MacroDefHead`. Returns `None` if `head` is not one of the
3928 /// three canonical macro-definition head keywords; the caller
3929 /// (`macro_def_from`) then returns `Ok(None)` to mean "this form is
3930 /// not a defmacro form."
3931 ///
3932 /// Implemented as a linear sweep over [`Self::ALL`] keyed on
3933 /// [`Self::keyword`] so the three canonical keyword literals
3934 /// (`"defmacro"` / `"defpoint-template"` / `"defcheck"`) live at
3935 /// ONE site (the `keyword` arms) rather than at TWO sites
3936 /// (`keyword` + a per-variant `from_keyword` match arm). Adding a
3937 /// fourth variant extends only [`Self::ALL`] + [`Self::keyword`],
3938 /// NOT a third per-variant literal site. The `Option<Self>` face
3939 /// is the open-by-design projection [`crate::ast::Sexp::as_call_to_any`]
3940 /// composes against; [`FromStr`] is the typed-error face callers
3941 /// reaching for a parse-rejection diagnostic compose against.
3942 /// Cross-face contract pinned by
3943 /// `macro_def_head_from_keyword_matches_from_str_for_every_input`.
3944 #[must_use]
3945 pub fn from_keyword(head: &str) -> Option<Self> {
3946 head.parse().ok()
3947 }
3948
3949 /// Project the typed `MacroDefHead` back to the canonical
3950 /// `&'static str` literal — feeds the `LispError::Defmacro*` Display
3951 /// rendering via the `#[error(...)]` annotation. The `&'static str`
3952 /// lifetime is load-bearing: it's what lets the variants project
3953 /// through this method into their `compile error in {head}:` prefix
3954 /// without an allocation, parallel to how
3955 /// `CompilerSpecIoStage::operation()` / `label()` feed
3956 /// `LispError::CompilerSpecIo`'s Display.
3957 #[must_use]
3958 pub fn keyword(self) -> &'static str {
3959 match self {
3960 Self::Defmacro => Self::DEFMACRO_KEYWORD,
3961 Self::DefpointTemplate => Self::DEFPOINT_TEMPLATE_KEYWORD,
3962 Self::Defcheck => Self::DEFCHECK_KEYWORD,
3963 }
3964 }
3965}
3966
3967// `impl std::fmt::Display for MacroDefHead` + `impl std::str::FromStr
3968// for MacroDefHead` + `impl tatara_closed_set::ClosedSet for MacroDefHead` +
3969// `pub struct UnknownMacroDefHead(pub String)` are generated by
3970// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
3971// above. `label` delegates to the inherent `MacroDefHead::keyword` via
3972// `#[closed_set(via = "keyword")]` so the domain-canonical
3973// reserved-word projection (`"defmacro"` / `"defpoint-template"` /
3974// `"defcheck"`) stays load-bearing at the inherent surface while the
3975// trait surface unifies every closed-set implementor's projection name
3976// onto `label`. The `display` flag emits the substrate-wide
3977// `f.write_str(Self::keyword(*self))` block.
3978// `#[closed_set(generate_unknown = "macro definition head")]` emits the
3979// typed parse-rejection carrier with the substrate-wide `Debug + Clone
3980// + PartialEq + Eq + thiserror::Error` derives and the `#[error("unknown
3981// macro definition head: {0}")]` annotation byte-for-byte; the explicit
3982// label overrides the auto-derived
3983// `pascal_to_spaced_lowercase("MacroDefHead")` (`"macro def head"`)
3984// which abbreviates `Def` rather than expanding it to `definition`,
3985// pinning the pre-lift operator-facing wording. The FromStr decode is
3986// a linear sweep over `MacroDefHead::ALL` keyed on `keyword`; round-trip
3987// + cross-axis rejection (`"defpoint"` / `"symbol"`) pinned by
3988// `macro_def_head_keyword_round_trips_through_from_str` +
3989// `macro_def_head_from_str_rejects_cross_axis_vocabularies`.
3990
3991/// Closed-set identifier for the way a `Sexp::List` entry in a macro's
3992/// `&optional` section failed to match the canonical `(NAME DEFAULT)`
3993/// shape. Carried as a typed slot on `LispError::OptionalParamMalformed`
3994/// so authoring tools (REPL, LSP, `tatara-check`) bind to variant identity
3995/// rather than substring-matching the rendered suffix.
3996///
3997/// Mirror at the `parse_params` optional-section boundary of the prior-run
3998/// `MacroDefHead` (macro-definition-head closed set), `UnquoteForm`
3999/// (template-marker closed set), `CompilerSpecIoStage` (disk-persistence
4000/// surface), and `TemplateInvariantKind` (bytecode-runtime surface)
4001/// closed-set lifts: those enums key their respective rejection variants
4002/// on a typed identity; this enum keys the four reachable list-spec
4003/// rejection modes the optional-section gate can emit on a typed identity.
4004/// Adding a new mode (e.g., `SuppliedPNotYetSupported` once an evaluator
4005/// lands and the three-element `(name default supplied-p)` shape is
4006/// admitted) requires extending this enum, which rustc-enforces matching
4007/// at every projection site (`label()`).
4008///
4009/// `label(self) -> String` projects the typed reason to a short
4010/// human-readable clause (`"empty list"` / `"missing default"` / `"3
4011/// elements (need 2)"` / `"name not a symbol"`) that the
4012/// `LispError::OptionalParamMalformed` Display rendering threads through
4013/// `optional_param_malformed_suffix` into the parenthetical suffix —
4014/// parallel to how `TemplateInvariantKind::message()` feeds
4015/// `LispError::TemplateInvariant`'s Display.
4016///
4017/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set of
4018/// optional-spec malformed shapes becomes a TYPE rather than a runtime
4019/// string-comparison-and-format dance. A typo like `reason: "empty list
4020/// "` (trailing space) is structurally unrepresentable; the four shapes
4021/// are an exhaustive `match`. THEORY.md §VI.1 — generation over
4022/// composition; the typed enum lands the structural-completeness floor
4023/// for the optional-section-malformed surface, parallel to how
4024/// `TemplateInvariantKind` lands it for the bytecode-runtime surface and
4025/// `MacroDefHead` for the macro-definition-head surface.
4026#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4027pub enum OptionalParamMalformedReason {
4028 /// `&optional ()` — the spec is a list of length zero, with no name
4029 /// and no default form.
4030 EmptyList,
4031 /// `&optional (x)` — the spec is a list of length one, naming an
4032 /// optional but supplying no default form. This is REJECTED rather
4033 /// than reinterpreted as `&optional x` because a bare-symbol spec
4034 /// IS the canonical "no default" shape; a parenthesized
4035 /// single-element spec is ambiguous and would silently DROP the
4036 /// extra parens at the surface.
4037 MissingDefault,
4038 /// `&optional (x default extra …)` — the spec is a list of length
4039 /// three or more. CL's `(name default supplied-p)` shape is NOT
4040 /// supported in v0 (no evaluator → no `supplied-p` variable
4041 /// binding), so any third element is structurally surplus. `length`
4042 /// is the actual element count (≥3).
4043 ExtraElements { length: usize },
4044 /// `&optional (5 default)` — the spec is a list of length two but
4045 /// the first element isn't a symbol. The name slot must be a symbol
4046 /// (the same gate the bare-symbol path enforces); a non-symbol head
4047 /// is rejected here so the `OptionalParam.name: String` slot cannot
4048 /// be populated from a `5` / `:foo` / `(nested)` value.
4049 NonSymbolName,
4050}
4051
4052impl OptionalParamMalformedReason {
4053 /// Canonical `&'static str` diagnostic-label bytes for the
4054 /// [`Self::EmptyList`] rejection — `"empty list"`. Per-role peer of
4055 /// [`Self::EmptyList`] on the closed-set optional-spec-rejection
4056 /// algebra; consumers reach for
4057 /// `OptionalParamMalformedReason::EMPTY_LIST_LABEL` when the caller
4058 /// has a variant in hand at compile time and wants the canonical
4059 /// diagnostic bytes without an intermediate `.to_string()`
4060 /// allocation through [`Self::label`].
4061 ///
4062 /// Sibling posture to [`crate::error::MacroDefHead::DEFMACRO_KEYWORD`]
4063 /// et al (per-role reserved-word algebra on the macro-definition-head
4064 /// closed set) and [`crate::error::StructuralKind::NIL_LABEL`] et al
4065 /// (per-role structural-residual algebra on the SexpShape 2-of-12
4066 /// carving): every closed-set outer algebra the substrate carries
4067 /// now pins its per-variant canonical bytes at ONE `pub const` per
4068 /// role.
4069 ///
4070 /// The three per-role static labels here (this constant,
4071 /// [`Self::MISSING_DEFAULT_LABEL`], [`Self::NON_SYMBOL_NAME_LABEL`])
4072 /// close the STATIC subset of the four-arm closed set — the fourth
4073 /// arm [`Self::ExtraElements`] carries a `usize` payload and formats
4074 /// its label dynamically, so it stays inline at [`Self::label`] and
4075 /// does NOT admit a per-role `&'static str` constant on this axis.
4076 /// The 3-of-4 carving is pinned by
4077 /// [`Self::STATIC_LABELS`]'s `[&'static str; 3]` forced-arity array.
4078 ///
4079 /// Cross-axis overlap: this label's `"empty list"` bytes are
4080 /// byte-for-byte identical to the fallback body emitted by
4081 /// [`missing_head_symbol_suffix`]'s `None` arm (the "no head symbol,
4082 /// list is empty" rejection at [`LispError::MissingHeadSymbol`]).
4083 /// The two sites SHARE the surface bytes but carry DISTINCT
4084 /// semantic scopes (this arm scopes to the `&optional` section
4085 /// gate; the sibling site scopes to a bare call-head list-empty
4086 /// gate), so the alias is intentionally NOT lifted to a
4087 /// substrate-wide constant — a future disambiguation drift on
4088 /// EITHER side must land at the semantic-scope site rather than at
4089 /// a shared canonical byte.
4090 ///
4091 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
4092 /// preserves proofs; the (variant, canonical-bytes) pairing binds
4093 /// at ONE `pub const` on the typed algebra rather than at TWO
4094 /// sites (a match-arm literal AND a Display-test literal).
4095 /// THEORY.md §III — the typescape; the canonical diagnostic bytes
4096 /// bind at ONE `pub const` on the closed-set algebra rather than
4097 /// at inline `.to_string()` literals in the [`Self::label`] match
4098 /// arms.
4099 pub const EMPTY_LIST_LABEL: &'static str = "empty list";
4100
4101 /// Canonical `&'static str` diagnostic-label bytes for the
4102 /// [`Self::MissingDefault`] rejection — `"missing default"`.
4103 /// Sibling of [`Self::EMPTY_LIST_LABEL`] on the closed-set per-role
4104 /// static-label axis; see [`Self::EMPTY_LIST_LABEL`] for the
4105 /// algebra-level round-trip contract every sibling shares.
4106 pub const MISSING_DEFAULT_LABEL: &'static str = "missing default";
4107
4108 /// Canonical `&'static str` diagnostic-label bytes for the
4109 /// [`Self::NonSymbolName`] rejection — `"name not a symbol"`.
4110 /// Sibling of [`Self::EMPTY_LIST_LABEL`] on the closed-set per-role
4111 /// static-label axis; see [`Self::EMPTY_LIST_LABEL`] for the
4112 /// algebra-level round-trip contract every sibling shares.
4113 pub const NON_SYMBOL_NAME_LABEL: &'static str = "name not a symbol";
4114
4115 /// Closed-set forced-arity ALL array over the canonical STATIC
4116 /// diagnostic-label `&'static str` bytes — the three arms that
4117 /// project to a `&'static str` label under [`Self::label`], in
4118 /// canonical declaration order matching the enum's static-label
4119 /// subset ([`Self::EmptyList`], [`Self::MissingDefault`],
4120 /// [`Self::NonSymbolName`] — the fourth variant
4121 /// [`Self::ExtraElements`] formats a `usize` payload dynamically
4122 /// and is excluded from this array).
4123 ///
4124 /// Sibling posture to [`crate::error::StructuralKind::LABELS`]
4125 /// (`[&'static str; 2]` — the 2-of-12 structural-residual carving),
4126 /// [`crate::error::MacroDefHead::KEYWORDS`] (`[&'static str; 3]` —
4127 /// the 3-arm macro-definition-head algebra), and
4128 /// [`crate::ast::AtomKind::LABELS`] (`[&'static str; 6]` — the
4129 /// 6-of-12 atomic-payload carving) — every closed-set outer
4130 /// algebra that carries an `&'static str`-per-variant label now
4131 /// pins its per-role canonical bytes at ONE `pub const` per role
4132 /// PLUS an ALL array for family-wide consumers.
4133 ///
4134 /// Pre-lift the three static rejection-label bytes had NO per-role
4135 /// primitive on this closed-set algebra — a consumer with a
4136 /// [`OptionalParamMalformedReason`] variant in hand at compile time
4137 /// reaching for the canonical bytes had to spell
4138 /// `OptionalParamMalformedReason::EmptyList.label()` (runtime
4139 /// dispatch through the match arm PLUS a `.to_string()` heap
4140 /// allocation) OR inline the `"empty list"` literal at the call
4141 /// site AND at every Display-round-trip test's assertion body.
4142 /// Post-lift the THREE canonical bytes bind at ONE `pub const` per
4143 /// role on the typed algebra AND at [`Self::STATIC_LABELS`] as a
4144 /// family-wide forced-arity array — a future LSP / REPL completion
4145 /// bar keyed on this rejection surface, a `tatara-check` coverage
4146 /// sweep over the `&optional`-spec-malformed arm-set of a
4147 /// `LispError` corpus, or a Sekiban audit-trail metric jointly
4148 /// labeled by the rejection reason
4149 /// (`tatara_lisp_optional_param_malformed_total{reason="empty
4150 /// list"}`) reads through the typed constants without re-deriving
4151 /// the 3-of-4 static-subset carving inline.
4152 ///
4153 /// Adding a hypothetical fifth STATIC rejection reason (e.g. a
4154 /// `KeywordName` rejection for `&optional (:foo default)` if
4155 /// keywords ever became legal in the `&optional` name slot) extends
4156 /// this array AND adds ONE per-role `pub const` in lockstep —
4157 /// rustc's forced-arity check on `[&'static str; N]` fails
4158 /// compilation if the array grows without the const.
4159 ///
4160 /// The `#[allow(dead_code)]` posture matches
4161 /// [`crate::error::StructuralKind::HASH_DISCRIMINATORS`] and other
4162 /// prior-run family-wide ALL arrays: the substrate's current
4163 /// [`Self::label`] body composes through the per-variant match arm
4164 /// rather than sweeping the family-wide array, so no non-test
4165 /// caller currently reaches this array directly. The lift lands
4166 /// the substrate primitive so future consumers keyed on the whole
4167 /// static-subset (a Sekiban metric, a `tatara-check` predicate,
4168 /// a `TypedRewriter<OptionalParamMalformedReasonOp>` sweep) bind
4169 /// to ONE `[&'static str; 3]` primitive rather than re-deriving
4170 /// the array inline per callsite.
4171 ///
4172 /// Theory anchor: THEORY.md §III — the typescape; the three
4173 /// canonical rejection-label bytes bind at ONE typed
4174 /// `[&'static str; 3]` array on the closed-set algebra rather
4175 /// than at zero-primitive-plus-three-inline-`.to_string()`-literals
4176 /// scattered across [`Self::label`]. THEORY.md §V.1 — knowable
4177 /// platform; the static-subset's cardinality becomes a TYPE-level
4178 /// constant on the substrate algebra rather than a per-consumer
4179 /// runtime dispatch through the label match. THEORY.md §VI.1 —
4180 /// generation over composition; the family-wide contract
4181 /// (alignment with the enum's static-label subset, membership
4182 /// through [`Self::label`]) emerges from the composition of FOUR
4183 /// substrate primitives (this `pub const` array + the three
4184 /// per-role `pub const *_LABEL` aliases) rather than as per-
4185 /// variant inline assertions duplicated at each call site.
4186 #[allow(dead_code)]
4187 pub const STATIC_LABELS: [&'static str; 3] = [
4188 Self::EMPTY_LIST_LABEL,
4189 Self::MISSING_DEFAULT_LABEL,
4190 Self::NON_SYMBOL_NAME_LABEL,
4191 ];
4192
4193 /// Canonical target arity of the `&optional (NAME DEFAULT)` list-spec
4194 /// — exactly TWO elements (a symbol head naming the optional and a
4195 /// default form). The accept-arity for `parse_optional_list_spec`'s
4196 /// dispatch; every other arity is a rejection reason on this closed
4197 /// set ([`Self::EmptyList`] at arity 0, [`Self::MissingDefault`] at
4198 /// arity 1, [`Self::ExtraElements`] at arity ≥3).
4199 ///
4200 /// Pre-lift the target arity appeared as the bare literal `2` at
4201 /// FOUR sites: the `match items.len()` accept arm in
4202 /// `parse_optional_list_spec` (macro_expand.rs), the `"need 2"`
4203 /// suffix bytes embedded in [`Self::label`]'s `ExtraElements` arm,
4204 /// the doc-comment enumeration on this enum's Display projection,
4205 /// and every truth-table test that asserts against the rendered
4206 /// diagnostic bytes. A drift on any ONE site (e.g. an evaluator lands
4207 /// and the accept-arity becomes 3 to admit CL's `(name default
4208 /// supplied-p)` shape) had to ripple across all four in lockstep or
4209 /// silently fracture the parser gate from its operator-facing
4210 /// vocabulary. Post-lift the arity binds at ONE `pub const usize` on
4211 /// the typed algebra: the parser reaches through
4212 /// [`Self::classify_arity`] (which threads this constant into the
4213 /// match arm via a local rebind), [`Self::label`] renders `"need
4214 /// {}"` interpolated on this constant, and the doc + tests reach for
4215 /// the const symbol rather than the literal `2`.
4216 ///
4217 /// Sibling posture to the prior-run per-role `&'static str` / `u8` /
4218 /// `char` per-variant algebras ([`Self::EMPTY_LIST_LABEL`],
4219 /// [`crate::error::StructuralKind::HASH_DISCRIMINATORS`],
4220 /// [`crate::ast::QuoteForm::UNQUOTE_PREFIX`], et al) — every prior
4221 /// lift pinned a per-role marker at ONE `pub const` on the typed
4222 /// algebra rather than at N inline literals across parsers,
4223 /// projections, and tests. This lift extends the axis-set with the
4224 /// first per-family-canonical `usize` on the closed-set optional-
4225 /// spec-rejection algebra.
4226 ///
4227 /// Theory anchor: THEORY.md §III — the typescape; the canonical
4228 /// accept-arity of the `&optional (NAME DEFAULT)` spec binds at ONE
4229 /// typed `pub const usize` on the closed-set algebra rather than at
4230 /// four literal-`2` sites scattered across parsers, projections, and
4231 /// tests. THEORY.md §II.1 invariant 5 — composition preserves proofs;
4232 /// a hypothetical bump to arity-3 (once an evaluator admits `(name
4233 /// default supplied-p)`) lands at ONE constant AND rustc-enforces
4234 /// that every downstream projection ([`Self::label`],
4235 /// [`Self::classify_arity`], every truth-table test) recomputes
4236 /// against the new arity. THEORY.md §V.1 — knowable platform; the
4237 /// parser's accept-arity becomes a TYPE-level constant on the
4238 /// substrate algebra rather than a per-callsite magic number.
4239 pub const OPTIONAL_PARAM_SPEC_ARITY: usize = 2;
4240
4241 /// Classify a list-spec's element count against the canonical
4242 /// accept-arity [`Self::OPTIONAL_PARAM_SPEC_ARITY`] — returns `None`
4243 /// iff the arity is exactly the accept target (the parser continues
4244 /// to the head-symbol gate); returns `Some(reason)` otherwise with
4245 /// the arity-derivable rejection reason on the closed set.
4246 ///
4247 /// The three arity-derivable rejection arms are:
4248 ///
4249 /// * arity 0 → [`Self::EmptyList`]
4250 /// * arity 1 → [`Self::MissingDefault`]
4251 /// * arity ≥3 → [`Self::ExtraElements { length }`]
4252 ///
4253 /// The fourth variant [`Self::NonSymbolName`] is NOT arity-derivable
4254 /// — accept-arity with a non-symbol head still rejects — and stays
4255 /// at the caller's head-symbol gate. The caller composes this
4256 /// classifier with the head-symbol check to close the full four-way
4257 /// dispatch. Pre-lift `parse_optional_list_spec` embedded the arity
4258 /// match inline (`match items.len() { 0 => ..., 1 => ..., 2 => ...,
4259 /// length => ... }`) with the accept-arity as a bare literal `2` in
4260 /// the accept arm — a duplicate of the target arity that
4261 /// [`Self::label`]'s `"need 2"` suffix ALSO embedded literally. Post-
4262 /// lift the arity-to-reason projection binds at ONE typed function on
4263 /// the algebra threading [`Self::OPTIONAL_PARAM_SPEC_ARITY`] through
4264 /// a local `const ARITY` rebind (rustc's match-pattern rules require
4265 /// a name binding rather than a `Self::` path pattern).
4266 ///
4267 /// Sibling posture to [`crate::ast::QuoteForm::as_unquote_form`]
4268 /// (typed 2-of-4 subset projection between quote-family enums) and
4269 /// [`crate::ast::AtomKind::from_atom`] (typed constructor from a
4270 /// parent's payload) — every prior-run typed classifier factored an
4271 /// inline dispatch into a `#[must_use]` typed function on the closed
4272 /// set. This method extends the pattern to the arity-to-reason
4273 /// projection.
4274 ///
4275 /// Future consumers that compose against this classifier: a
4276 /// `tatara-check` static analyzer that predicts optional-spec
4277 /// rejections without threading through the whole parser; an LSP
4278 /// quick-fix that suggests wrapping a bare symbol as `(symbol
4279 /// default)` iff `classify_arity(1) == Some(MissingDefault)`; a
4280 /// `TypedRewriter<OptionalParamMalformedReasonOp>` sweep that keys
4281 /// on this reason projection.
4282 ///
4283 /// Theory anchor: THEORY.md §III — the typescape; the arity→reason
4284 /// projection binds at ONE typed function on the algebra rather than
4285 /// at inline `match` bodies duplicated across parsers. THEORY.md
4286 /// §II.1 invariant 1 (typed entry): every parser that accepts an
4287 /// `&optional`-spec list threads its arity through this classifier
4288 /// before touching the head-symbol gate, closing the arity
4289 /// half of the rejection surface at a single typed site.
4290 #[must_use]
4291 pub fn classify_arity(length: usize) -> Option<Self> {
4292 const ARITY: usize = OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY;
4293 match length {
4294 0 => Some(Self::EmptyList),
4295 1 => Some(Self::MissingDefault),
4296 ARITY => None,
4297 length => Some(Self::ExtraElements { length }),
4298 }
4299 }
4300
4301 /// Zero-allocation projection over the STATIC subset of the closed
4302 /// set — `Some(&'static str)` for the three arms whose diagnostic
4303 /// bytes bind at a per-role [`Self::EMPTY_LIST_LABEL`] /
4304 /// [`Self::MISSING_DEFAULT_LABEL`] / [`Self::NON_SYMBOL_NAME_LABEL`]
4305 /// constant, and `None` for the fourth arm [`Self::ExtraElements`]
4306 /// whose diagnostic bytes format the `length` payload dynamically
4307 /// through `format!("{length} elements (need {})",
4308 /// [`Self::OPTIONAL_PARAM_SPEC_ARITY`])` and cannot bind to a
4309 /// `&'static str` constant on this axis.
4310 ///
4311 /// This is the STATIC-subset typed projection over the 3-of-4
4312 /// carving [`Self::STATIC_LABELS`] enumerates — the same 3-of-4
4313 /// shape pinned as a `[&'static str; 3]` forced-arity array on
4314 /// that constant. Callers that KNOW they've matched a static arm
4315 /// at compile time reach for the per-role `pub const *_LABEL`
4316 /// directly; callers that hold a variant of unknown identity
4317 /// reach for this projection to filter the STATIC subset without
4318 /// paying an allocation for the failing branch. The composition
4319 /// gives every downstream site — Sekiban audit-trail metrics
4320 /// (`tatara_lisp_optional_param_malformed_total{reason="empty
4321 /// list"}` where the label is a `&'static str` label slot with
4322 /// no heap indirection), an LSP quick-fix keyed on the STATIC
4323 /// subset, a `tatara-check` predicate that filters a corpus's
4324 /// `OptionalParamMalformed` rejections into static-arm vs
4325 /// dynamic-arm buckets — one typed subset selector rather than
4326 /// three inline `matches!(reason, Self::EmptyList |
4327 /// Self::MissingDefault | Self::NonSymbolName)` gates threaded
4328 /// through per-consumer allocation.
4329 ///
4330 /// Sibling posture to [`Self::classify_arity`] (typed `usize` →
4331 /// `Option<Self>` projection over the arity-derivable 3-of-4
4332 /// carving) — both methods carve the same closed set along the
4333 /// same 3-of-4 axis (the fourth variant [`Self::NonSymbolName`]
4334 /// on the classify side, [`Self::ExtraElements`] on this side)
4335 /// and pin the carving at ONE typed function on the algebra.
4336 /// The two carvings compose: a caller with an arity `n` in hand
4337 /// runs `classify_arity(n)` to get `Option<Self>`, then runs
4338 /// `label_static(reason)` on the `Some` result to get
4339 /// `Option<&'static str>` — the double-`Option` collapses the
4340 /// arity → reason → static-label pipeline into TWO typed
4341 /// projections composable at zero allocation cost for the STATIC
4342 /// subset.
4343 ///
4344 /// [`Self::label`] composes this projection with the dynamic
4345 /// [`Self::ExtraElements`] format arm — an `Option::unwrap_or_else`
4346 /// over `label_static()` that lands on the `format!` branch iff
4347 /// the variant is [`Self::ExtraElements`]. Pre-lift `label`'s
4348 /// body carried FOUR match arms — three inline `.to_string()`
4349 /// literals routing through the per-role `pub const`s AND one
4350 /// `format!` arm formatting the `length` payload. Post-lift
4351 /// `label`'s body reduces to `label_static().map(str::to_string)
4352 /// .unwrap_or_else(|| format!(...))`, and the three per-role
4353 /// static arms bind at ONE typed subset projection rather than
4354 /// at three duplicated `.to_string()` sites.
4355 ///
4356 /// Const-fn on `Self` because the four arms match on unit
4357 /// / payload-carrying variants alike without allocation — the
4358 /// `Self::ExtraElements { .. }` arm ignores the payload via
4359 /// `{ .. }` because the projection routes to `None` regardless
4360 /// of the `length` value.
4361 ///
4362 /// Theory anchor: THEORY.md §III — the typescape; the STATIC-
4363 /// subset projection binds at ONE typed function on the
4364 /// algebra rather than at three inline `matches!` gates
4365 /// scattered across consumers. THEORY.md §II.1 invariant 5 —
4366 /// composition preserves proofs; a hypothetical fifth static
4367 /// rejection reason (e.g. `KeywordName` for `&optional (:foo
4368 /// default)`) extends this projection AND [`Self::STATIC_LABELS`]
4369 /// in lockstep — rustc's exhaustive-match check on this method
4370 /// fails compilation without the new arm. THEORY.md §VI.1 —
4371 /// generation over composition; [`Self::label`]'s dynamic body
4372 /// emerges from the composition of TWO substrate primitives (this
4373 /// static-subset projection + the [`Self::ExtraElements`] format
4374 /// arm) rather than as four duplicated match arms.
4375 #[must_use]
4376 pub const fn label_static(self) -> Option<&'static str> {
4377 match self {
4378 Self::EmptyList => Some(Self::EMPTY_LIST_LABEL),
4379 Self::MissingDefault => Some(Self::MISSING_DEFAULT_LABEL),
4380 Self::NonSymbolName => Some(Self::NON_SYMBOL_NAME_LABEL),
4381 Self::ExtraElements { .. } => None,
4382 }
4383 }
4384
4385 /// Canonical `&'static str` noun-descriptor for the
4386 /// [`Self::ExtraElements`] rejection's dynamic format template —
4387 /// `"elements"`. Per-role peer of `Self::ExtraElements` on the
4388 /// DYNAMIC 1-of-4 subset carving of the four-arm closed set.
4389 ///
4390 /// Pre-lift the `"elements"` noun lived inline inside
4391 /// [`Self::label`]'s `format!("{length} elements (need {})", …)`
4392 /// arm — a single site, but one that any downstream sweep over
4393 /// dynamic-arm nouns had to substring-match rather than bind to a
4394 /// typed constant. Post-lift the (`ExtraElements` variant, canonical
4395 /// noun bytes) pairing binds at ONE `pub const` on the typed
4396 /// [`OptionalParamMalformedReason`] algebra: [`Self::label_dynamic`]
4397 /// composes this constant into the shared `"{length} {descriptor}
4398 /// (need {})"` format template, the peer [`Self::DYNAMIC_DESCRIPTORS`]
4399 /// array closes the DYNAMIC carving as a forced-arity
4400 /// `[&'static str; 1]` at ONE site, and [`Self::descriptor_dynamic`]
4401 /// carries the variant→noun projection.
4402 ///
4403 /// Sibling posture to
4404 /// [`TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR`] /
4405 /// [`TemplateInvariantKind::SPLICE_BAD_INDEX_DESCRIPTOR`] on the peer
4406 /// bytecode-runtime algebra's DYNAMIC 2-of-4 subset carving — both
4407 /// pin per-role noun-descriptor bytes at ONE `pub const` on their
4408 /// closed-set algebra rather than at inline literals inside a
4409 /// `format!(...)` arm. The peer's DYNAMIC subset is TWO arms wide
4410 /// (`SubstBadIndex`, `SpliceBadIndex`); this algebra's DYNAMIC subset
4411 /// is ONE arm wide (`ExtraElements`) — the same carving idiom applied
4412 /// at asymmetric widths.
4413 ///
4414 /// Theory anchor: THEORY.md §III — the typescape; the canonical
4415 /// noun-descriptor of the ExtraElements arm's dynamic format
4416 /// template binds at ONE typed `pub const &'static str` on the
4417 /// closed-set algebra rather than at an inline literal inside
4418 /// [`Self::label`]'s `format!` body. THEORY.md §II.1 invariant 5 —
4419 /// composition preserves proofs; a rename of the noun (e.g. from
4420 /// `"elements"` to `"items"` if an evaluator surface ever admits a
4421 /// more general list-shape) lands at ONE `pub const` edit — every
4422 /// downstream projection ([`Self::label_dynamic`],
4423 /// [`Self::DYNAMIC_DESCRIPTORS`], the [`Self::label`] composition,
4424 /// every truth-table test) recomputes against the new bytes.
4425 /// THEORY.md §VI.1 — generation over composition; the shared format
4426 /// template that emitted the `"elements"` literal inline at ONE site
4427 /// pre-lift now composes ONE typed const AND ONE typed projection
4428 /// AND ONE typed arity, so future dynamic-arm siblings extend at
4429 /// exactly these three axes rather than at inline duplication.
4430 pub const EXTRA_ELEMENTS_DESCRIPTOR: &'static str = "elements";
4431
4432 /// Closed-set forced-arity ALL array over the canonical DYNAMIC
4433 /// noun-descriptor `&'static str` bytes — the ONE arm that projects
4434 /// to a `&'static str` descriptor under [`Self::descriptor_dynamic`],
4435 /// in canonical declaration order matching the enum's dynamic-label
4436 /// subset ([`Self::ExtraElements`] — the other three variants
4437 /// [`Self::EmptyList`], [`Self::MissingDefault`],
4438 /// [`Self::NonSymbolName`] carry their FULL bytes on the STATIC axis
4439 /// via [`Self::STATIC_LABELS`] and are excluded from this array).
4440 ///
4441 /// Peer 1-of-4 carving to [`Self::STATIC_LABELS`] — together the
4442 /// two arrays partition the four-arm closed set into its STATIC ⊕
4443 /// DYNAMIC halves. The STATIC array carries the three arms whose
4444 /// FULL diagnostic is `&'static str`; this DYNAMIC array carries the
4445 /// one arm whose NOUN slot is `&'static str` and whose full label is
4446 /// a `format!("{length} {descriptor} (need {})")` composition over
4447 /// [`Self::OPTIONAL_PARAM_SPEC_ARITY`]. Adding a hypothetical fifth
4448 /// static rejection reason (e.g. `KeywordName` for `&optional (:foo
4449 /// default)`) lands only on the STATIC array; adding a hypothetical
4450 /// fifth dynamic rejection reason (e.g. `NestedList { depth: usize
4451 /// }` naming a spec whose `NAME` slot is itself a list rather than a
4452 /// symbol) lands only on this array — rustc's forced-arity check on
4453 /// the `[&'static str; N]` shape fails compilation if the array
4454 /// grows without the corresponding per-role descriptor +
4455 /// [`Self::descriptor_dynamic`] arm being extended in lockstep.
4456 ///
4457 /// Sibling posture to
4458 /// [`TemplateInvariantKind::DYNAMIC_DESCRIPTORS`] (`[&'static str;
4459 /// 2]` — the peer bytecode-runtime algebra's DYNAMIC 2-of-4 carving)
4460 /// — every closed-set outer algebra the substrate carries now pins
4461 /// BOTH its STATIC-subset canonical-bytes array AND its DYNAMIC-
4462 /// subset per-role-slot array at ONE `pub const` per axis rather
4463 /// than at N inline literals scattered across projections and tests.
4464 /// The two peer algebras carry the same STATIC ⊕ DYNAMIC partition
4465 /// idiom at asymmetric widths (`TemplateInvariantKind` is 2 ⊕ 2;
4466 /// `OptionalParamMalformedReason` is 3 ⊕ 1).
4467 ///
4468 /// The `#[allow(dead_code)]` posture matches
4469 /// [`TemplateInvariantKind::DYNAMIC_DESCRIPTORS`] and
4470 /// [`Self::STATIC_LABELS`]: the substrate's current [`Self::label`]
4471 /// body composes through [`Self::descriptor_dynamic`] rather than
4472 /// sweeping the family-wide array, so no non-test caller currently
4473 /// reaches this array directly. The lift lands the substrate
4474 /// primitive so future consumers keyed on the whole dynamic subset
4475 /// (a Sekiban metric, a `tatara-check` predicate, a
4476 /// `TypedRewriter<OptionalParamMalformedReasonOp>` sweep) bind to
4477 /// ONE `[&'static str; 1]` primitive rather than re-deriving the
4478 /// array inline per callsite.
4479 ///
4480 /// Theory anchor: THEORY.md §III — the typescape; the ONE canonical
4481 /// dynamic-arm noun-descriptor byte binds at ONE typed
4482 /// `[&'static str; 1]` array on the closed-set algebra rather than
4483 /// at zero-primitive-plus-one-inline-literal inside [`Self::label`]'s
4484 /// `format!` body. THEORY.md §V.1 — knowable platform; the DYNAMIC-
4485 /// subset's cardinality becomes a TYPE-level constant on the
4486 /// substrate algebra rather than a per-consumer runtime dispatch
4487 /// through the label match. THEORY.md §VI.1 — generation over
4488 /// composition; the family-wide contract (alignment with the enum's
4489 /// dynamic-label subset, membership through
4490 /// [`Self::descriptor_dynamic`]) emerges from the composition of TWO
4491 /// substrate primitives (this `pub const` array + the one per-role
4492 /// `pub const EXTRA_ELEMENTS_DESCRIPTOR` alias) rather than as
4493 /// per-variant inline assertions duplicated at each call site.
4494 #[allow(dead_code)]
4495 pub const DYNAMIC_DESCRIPTORS: [&'static str; 1] = [Self::EXTRA_ELEMENTS_DESCRIPTOR];
4496
4497 /// Zero-allocation projection over the DYNAMIC subset of the closed
4498 /// set — `Some(&'static str)` for the one arm whose diagnostic bytes
4499 /// carry a `usize` payload and format through the shared
4500 /// `"{length} {descriptor} (need {})"` template
4501 /// ([`Self::ExtraElements`] → [`Self::EXTRA_ELEMENTS_DESCRIPTOR`]
4502 /// (`"elements"`)), and `None` for the three arms whose full
4503 /// diagnostic bytes bind at a `&'static str` on the STATIC axis
4504 /// ([`Self::EmptyList`], [`Self::MissingDefault`],
4505 /// [`Self::NonSymbolName`]) and carry no per-role descriptor slot.
4506 ///
4507 /// Peer projection to [`Self::label_static`] on the sibling STATIC
4508 /// 3-of-4 subset carving — the two projections together partition
4509 /// the four-arm closed set into its STATIC ⊕ DYNAMIC halves at zero
4510 /// allocation cost. Callers holding a variant of unknown identity
4511 /// thread it through BOTH projections; exactly ONE of
4512 /// `label_static(self)` and `descriptor_dynamic(self)` returns
4513 /// `Some` for every legal variant (`label_static` on the three-arm
4514 /// STATIC subset, `descriptor_dynamic` on the one-arm DYNAMIC
4515 /// subset). The partition is pinned by the truth-table test
4516 /// `optional_param_malformed_reason_static_dynamic_projections_partition_closed_set`.
4517 ///
4518 /// The `const fn` posture matches [`Self::label_static`],
4519 /// [`TemplateInvariantKind::descriptor_dynamic`], and every peer
4520 /// closed-set projection method on the substrate — the projection
4521 /// evaluates at rustc const-eval time when the variant is a const,
4522 /// so a future const-context consumer (an `#[allow(dead_code)] const
4523 /// _: Option<&'static str> = ...` static assertion, a `tatara-check`
4524 /// predicate binding at module-init time) picks up the DYNAMIC-
4525 /// subset selector without runtime dispatch.
4526 ///
4527 /// Sibling posture to
4528 /// [`TemplateInvariantKind::descriptor_dynamic`] on the peer
4529 /// bytecode-runtime algebra's DYNAMIC 2-of-4 subset carving — both
4530 /// project the DYNAMIC subset of a mixed-payload closed set (STATIC-
4531 /// only arms project to `None`) into a typed `Option<&'static str>`
4532 /// — the same idiom applied at two peer boundaries of the
4533 /// substrate's bytecode-runtime and macro-authoring surfaces at
4534 /// asymmetric widths (peer is 2-arm DYNAMIC; this is 1-arm DYNAMIC).
4535 ///
4536 /// Future consumers that compose against this projection: an LSP
4537 /// quick-fix keyed on the DYNAMIC-subset descriptor slot (surfaces
4538 /// `"drop the {descriptor} after the default form"` with
4539 /// `{descriptor}` bound to whatever this projection returns); a
4540 /// `tatara-check` predicate that filters a corpus's
4541 /// `OptionalParamMalformed` rejections into static-arm vs dynamic-
4542 /// arm buckets in ONE typed subset selector rather than
4543 /// `matches!(reason, Self::ExtraElements { .. })` gates threaded
4544 /// through per-consumer allocation.
4545 #[must_use]
4546 pub const fn descriptor_dynamic(self) -> Option<&'static str> {
4547 match self {
4548 Self::ExtraElements { .. } => Some(Self::EXTRA_ELEMENTS_DESCRIPTOR),
4549 Self::EmptyList | Self::MissingDefault | Self::NonSymbolName => None,
4550 }
4551 }
4552
4553 /// Typed `Option<String>` projection over the DYNAMIC 1-of-4 subset
4554 /// carving of [`OptionalParamMalformedReason`]'s four-arm closed set
4555 /// — returns `Some(label)` for the one arm whose diagnostic bytes
4556 /// format their `usize` payload through the SHARED `"{length}
4557 /// {descriptor} (need {})"` template, and `None` for the three
4558 /// STATIC arms whose full bytes bind on the [`Self::label_static`]
4559 /// axis.
4560 ///
4561 /// Composes [`Self::descriptor_dynamic`] into the shared format
4562 /// template — the `{descriptor}` slot binds to the per-role
4563 /// [`Self::EXTRA_ELEMENTS_DESCRIPTOR`] constant the DYNAMIC arm
4564 /// projects through, the `{length}` slot binds the `usize` payload
4565 /// the enum's dynamic variant carries, and the `(need {})` suffix
4566 /// binds [`Self::OPTIONAL_PARAM_SPEC_ARITY`]. Pre-lift
4567 /// [`Self::label`]'s body carried the inline `format!("{length}
4568 /// elements (need {})", …)` arm hardcoding the `"elements"` noun —
4569 /// post-lift the shared template binds at ONE `format!(...)` site
4570 /// inside this method, the noun slot binds at the per-role
4571 /// descriptor const, and [`Self::descriptor_dynamic`] carries the
4572 /// arm→descriptor mapping.
4573 ///
4574 /// Peer projection to [`Self::label_static`] on the sibling STATIC
4575 /// 3-of-4 subset carving — the two projections together partition
4576 /// the four-arm closed set into its STATIC ⊕ DYNAMIC label-
4577 /// projection halves. [`Self::label`]'s body reduces to
4578 /// `label_static().map(str::to_string).or_else(||
4579 /// label_dynamic()).expect(...)` — ONE compositional flow over TWO
4580 /// typed subset projections rather than FOUR duplicated match arms
4581 /// (three `.to_string()` sites on the STATIC subset + one
4582 /// `format!(...)` site on the DYNAMIC subset in the pre-lift body).
4583 ///
4584 /// Sibling posture to [`TemplateInvariantKind::message_dynamic`] on
4585 /// the peer bytecode-runtime algebra's DYNAMIC 2-of-4 subset carving
4586 /// — both compose a per-role descriptor projection with a shared
4587 /// `format!(...)` template + a `usize` payload into a typed
4588 /// `Option<String>`. The peer's DYNAMIC subset is 2-arm wide (two
4589 /// noun-slot values, one shared template); this algebra's DYNAMIC
4590 /// subset is 1-arm wide (one noun-slot value, one shared template)
4591 /// — the same STATIC ⊕ DYNAMIC composition idiom at asymmetric
4592 /// widths.
4593 ///
4594 /// Future consumers that compose against this projection: a
4595 /// `tatara-check` predicate that filters a corpus's
4596 /// `OptionalParamMalformed` rejections into static-arm vs dynamic-
4597 /// arm label buckets; a Sekiban audit-trail metric that samples
4598 /// dynamic-arm labels without paying the allocation cost on the
4599 /// STATIC subset (the `None` branch short-circuits); an LSP quick-
4600 /// fix that suggests trimming the surplus elements based on the
4601 /// `{length}` payload the caller extracts from the enum before
4602 /// invoking this projection.
4603 #[must_use]
4604 pub fn label_dynamic(self) -> Option<String> {
4605 let descriptor = self.descriptor_dynamic()?;
4606 let length = match self {
4607 Self::ExtraElements { length } => length,
4608 Self::EmptyList | Self::MissingDefault | Self::NonSymbolName => {
4609 // Unreachable: descriptor_dynamic() returned Some only
4610 // for ExtraElements on the `?` above. Kept exhaustive
4611 // rather than `_ =>` so a future refactor that adds a
4612 // fifth dynamic arm without extending descriptor_dynamic
4613 // fails compilation at THIS callsite rather than at
4614 // runtime.
4615 unreachable!(
4616 "descriptor_dynamic returned Some only for dynamic-arm variants; \
4617 the STATIC-arm match here is unreachable via the ? short-circuit"
4618 )
4619 }
4620 };
4621 Some(format!(
4622 "{length} {descriptor} (need {})",
4623 Self::OPTIONAL_PARAM_SPEC_ARITY
4624 ))
4625 }
4626
4627 /// Short human-readable clause for the parenthetical suffix of
4628 /// `LispError::OptionalParamMalformed`'s Display. The variants
4629 /// project to:
4630 ///
4631 /// * `EmptyList` → [`Self::EMPTY_LIST_LABEL`] (`"empty list"`)
4632 /// * `MissingDefault` → [`Self::MISSING_DEFAULT_LABEL`] (`"missing default"`)
4633 /// * `ExtraElements{N}` → `"N elements (need 2)"` (dynamic; the `2`
4634 /// is [`Self::OPTIONAL_PARAM_SPEC_ARITY`])
4635 /// * `NonSymbolName` → [`Self::NON_SYMBOL_NAME_LABEL`] (`"name not a symbol"`)
4636 ///
4637 /// `label` returns `String` (rather than `&'static str`) because the
4638 /// `ExtraElements` arm formats its `length` payload — the other three
4639 /// arms route through per-role [`Self::EMPTY_LIST_LABEL`] /
4640 /// [`Self::MISSING_DEFAULT_LABEL`] / [`Self::NON_SYMBOL_NAME_LABEL`]
4641 /// constants which `.to_string()` projects through. Mirror of
4642 /// `TemplateInvariantKind::message()`: both return `String`, both
4643 /// project the closed-set typed reason into the `LispError::Display`
4644 /// rendering via the variant's `#[error(...)]` annotation.
4645 ///
4646 /// Composes [`Self::label_static`] (the STATIC-subset zero-allocation
4647 /// projection over the 3-of-4 carving) with [`Self::label_dynamic`]
4648 /// (the DYNAMIC-subset composition over the 1-of-4 carving). Pre-
4649 /// lift the three static arms carried inline `.to_string()` literals
4650 /// routing through the per-role `pub const`s AND the dynamic arm
4651 /// carried an inline `format!(...)` — post-lift the STATIC subset
4652 /// binds at ONE typed projection ([`Self::label_static`]) and the
4653 /// DYNAMIC subset binds at ONE typed projection
4654 /// ([`Self::label_dynamic`]), and this body reduces to
4655 /// `label_static().map(str::to_string).or_else(|| label_dynamic())`
4656 /// composing the two — exactly the shape
4657 /// [`TemplateInvariantKind::message`] carries on the peer bytecode-
4658 /// runtime algebra.
4659 ///
4660 /// The STATIC ⊕ DYNAMIC partition guarantees exactly ONE of the two
4661 /// projections returns `Some` for every legal variant; the `expect`
4662 /// arm is structurally unreachable by construction.
4663 #[must_use]
4664 pub fn label(self) -> String {
4665 self.label_static()
4666 .map(str::to_string)
4667 .or_else(|| self.label_dynamic())
4668 .expect(
4669 "label_static ⊕ label_dynamic partition the four-arm closed set; \
4670 exactly one projection returns Some for every legal variant",
4671 )
4672 }
4673}
4674
4675/// Closed-set identifier for the syntactic marker of a macro-template
4676/// unquote (`,`) or unquote-splice (`,@`). Carried as a typed slot on
4677/// `LispError::UnboundTemplateVar` and `LispError::NonSymbolUnquoteTarget`
4678/// so authoring tools (REPL, LSP, `tatara-check`) bind to variant identity
4679/// via `UnquoteForm::Splice` rather than substring-matching the rendered
4680/// `prefix` literal.
4681///
4682/// Mirror at the template-marker boundary of the prior-run `MacroDefHead`
4683/// (macro-definition-head closed set), `CompilerSpecIoStage`
4684/// (disk-persistence surface), and `TemplateInvariantKind` (bytecode-runtime
4685/// surface) closed-set lifts: those enums key their respective rejection
4686/// variants on a typed identity; this enum keys the two unquote-template
4687/// rejection variants (`UnboundTemplateVar`, `NonSymbolUnquoteTarget`) on a
4688/// typed marker identity. Adding a new unquote variant (e.g., a hypothetical
4689/// `,~` form) requires extending this enum, which rustc-enforces matching at
4690/// every projection site (`marker()`) — the closed set becomes a TYPE rather
4691/// than two `&'static str`-keyed slots that could drift independently.
4692///
4693/// `marker(self) -> &'static str` projects the typed `UnquoteForm` back to
4694/// the canonical literal for `LispError::Display` rendering. The `&'static
4695/// str` lifetime is load-bearing: it lets both variants project through this
4696/// method into their `compile error in {prefix}…:` prefix without an
4697/// allocation, parallel to how `MacroDefHead::keyword()` and
4698/// `CompilerSpecIoStage::operation()` / `label()` feed their respective
4699/// `LispError::*` Display impls.
4700///
4701/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set of
4702/// unquote markers becomes a TYPE rather than a runtime
4703/// string-comparison-and-format dance. A typo like `prefix: ",,"` is
4704/// structurally unrepresentable rather than re-asserted at the helper
4705/// boundary. THEORY.md §VI.1 — generation over composition; the typed enum
4706/// lands the structural-completeness floor for the template-marker surface,
4707/// parallel to how `MacroDefHead` lands it for the macro-definition-head
4708/// surface and `CompilerSpecIoStage` for the disk-persistence surface.
4709/// THEORY.md §II.1 invariant 1 (typed entry): a non-symbol unquote target /
4710/// an unbound template var is exactly the failure mode the typed-entry gate
4711/// exists to reject, and the marker identity is part of the proof.
4712#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
4713#[closed_set(via = "marker", display, generate_unknown = "unquote form")]
4714pub enum UnquoteForm {
4715 /// `,x` — single-value substitution. The `,` marker; the inner symbol
4716 /// is substituted with its bound value at template expansion.
4717 Unquote,
4718 /// `,@x` — list-splice substitution. The `,@` marker; the inner symbol
4719 /// must be bound to a list, whose elements are flattened into the
4720 /// containing list at template expansion.
4721 Splice,
4722}
4723
4724impl UnquoteForm {
4725 /// The closed set of two template-marker syntactic forms — single
4726 /// source of truth that drives the [`Self::marker`] / [`fmt::Display`]
4727 /// projection AND the [`FromStr`] decode sweep keyed on
4728 /// [`Self::marker`]. Adding a hypothetical third variant (e.g. a
4729 /// `,~` reverse-unquote, a `,?` conditional-unquote) lands at one
4730 /// [`Self::ALL`] entry + one [`Self::marker`] arm — exhaustively
4731 /// checked by the compiler (the `[Self; 2]` array literal forces
4732 /// the arity) AND by the per-variant truth-table tests below.
4733 ///
4734 /// Sibling closed-set lift to every other typed-shape enum the
4735 /// substrate carries: this crate's own [`SexpShape::ALL`] (the
4736 /// twelve reachable outer shapes — superset on the structural axis
4737 /// of the `Sexp` algebra), [`crate::ast::AtomKind::ALL`] (the six
4738 /// atomic-payload kinds — peer axis on the same algebra),
4739 /// [`crate::ast::QuoteForm`] (the four homoiconic prefix-wrappers
4740 /// — superset of THIS enum's two template markers via the 2-of-4
4741 /// projection [`crate::ast::QuoteForm::as_unquote_form`]), and the
4742 /// cross-crate `tatara-process` family (`ConditionKind::ALL`,
4743 /// `ProcessPhase::ALL`, `ProcessSignal::ALL`, `ChannelKind::ALL`,
4744 /// `IntentKind::ALL`, `LifetimeKind::ALL`, `RequestorKind::ALL`,
4745 /// `ReceiptKind::ALL`, …) every one of which paired its typed
4746 /// projection with `ALL` before this lift.
4747 ///
4748 /// Future consumers that compose against `ALL`: LSP / REPL
4749 /// completion for the operator-facing rendered template-marker
4750 /// (every `compile error in {prefix}…:` substring in `LispError`'s
4751 /// rendered diagnostics for a template-substitution rejection keys
4752 /// on this set's projection through [`Self::marker`]);
4753 /// `tatara-check` coverage assertions over which template markers
4754 /// reach a `NonSymbolUnquoteTarget` / `UnboundTemplateVar` arm at
4755 /// all — the typed sweep replaces a per-callsite vocabulary of two
4756 /// `&'static str` literals; any future audit-trail metric jointly
4757 /// labeled by [`Self::marker`] (e.g.
4758 /// `tatara_lisp_unbound_template_var_total{prefix=","}`) — the
4759 /// metric label set IS [`Self::ALL`] mapped through
4760 /// [`Self::marker`]; any future structural rewriter (typed
4761 /// analogue of MLIR's `op.walk<UnquoteFormOp>()`) that wants to
4762 /// sweep over every template marker in a typed sequence.
4763 pub const ALL: [Self; 2] = [Self::Unquote, Self::Splice];
4764
4765 /// Canonical `&'static str` template-marker bytes for the
4766 /// [`Self::Unquote`] substitution — aliases
4767 /// [`crate::ast::QuoteForm::UNQUOTE_PREFIX`] on the UnquoteForm ⊂
4768 /// QuoteForm 2-of-4 subset carving so the marker-level per-role
4769 /// bytes bind at ONE `pub const` on the parent superset's Unquote
4770 /// arm rather than at TWO sites (the per-role `pub const` AND a
4771 /// parallel inline literal). Per-role peer of `Self::Unquote` on
4772 /// the closed-set template-substitution algebra; consumers reach
4773 /// for `UnquoteForm::UNQUOTE_MARKER` when the caller has a variant
4774 /// in hand at compile time and wants the canonical diagnostic
4775 /// bytes without runtime dispatch through [`Self::marker`].
4776 ///
4777 /// Sibling posture to [`crate::ast::AtomKind::SYMBOL_LABEL`] et al
4778 /// (commit fc126b8) — both pin the invariant that a typed-subset
4779 /// enum's per-role bytes are structurally derived from the parent
4780 /// superset's per-role `pub const` via a `pub const = Parent::CONST`
4781 /// alias rather than through parallel literal-discipline sites.
4782 pub const UNQUOTE_MARKER: &'static str = crate::ast::QuoteForm::UNQUOTE_PREFIX;
4783
4784 /// Canonical `&'static str` template-marker bytes for the
4785 /// [`Self::Splice`] list-substitution — aliases
4786 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_PREFIX`] on the
4787 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset carving. Per-role peer of
4788 /// `Self::Splice`; the `,@` two-byte prefix is the ONLY multi-char
4789 /// bytes payload on the UnquoteForm algebra — every other marker
4790 /// is a single-byte comma rendered as `&'static str`.
4791 pub const SPLICE_MARKER: &'static str = crate::ast::QuoteForm::UNQUOTE_SPLICE_PREFIX;
4792
4793 /// Closed-set forced-arity ALL array over the canonical template-
4794 /// marker `&'static str` bytes, in declaration order matching
4795 /// [`Self::ALL`] element-wise (pinned by
4796 /// `unquote_form_markers_align_with_all_by_index`). Sibling posture
4797 /// to [`crate::ast::QuoteForm::PREFIXES`] (`[&'static str; 4]` —
4798 /// the superset carving this UnquoteForm subset embeds into),
4799 /// [`crate::ast::AtomKind::LABELS`] (`[&'static str; 6]` — sibling
4800 /// subset-through-parent alias on the AtomKind ⊂ SexpShape
4801 /// carving), [`SexpShape::LABELS`] (`[&'static str; 12]`),
4802 /// [`ExpectedKwargShape::LABELS`] (`[&'static str; 7]`),
4803 /// [`KwargPathKind::LABELS`] (`[&'static str; 3]`),
4804 /// [`MacroDefHead::KEYWORDS`] (`[&'static str; 3]`),
4805 /// [`crate::ast::Atom::BOOL_LITERALS`] (`[&'static str; 2]`), and
4806 /// [`crate::macro_expand::MacroParams::LAMBDA_LIST_KEYWORDS`]
4807 /// (`[&'static str; 2]`) — every closed-set outer projection on
4808 /// the substrate that carries an `&'static str`-per-variant marker
4809 /// now pins its per-role canonical bytes at ONE `pub const` per
4810 /// role PLUS an ALL array for family-wide consumers.
4811 ///
4812 /// Pre-lift the two template-marker bytes had NO per-role primitive
4813 /// on this closed-set subset algebra — a consumer with an
4814 /// `UnquoteForm` variant in hand at compile time reaching for the
4815 /// canonical marker bytes had to spell
4816 /// `UnquoteForm::Splice.marker()` (runtime dispatch through the
4817 /// composition [`Self::to_quote_form`] +
4818 /// [`crate::ast::QuoteForm::prefix`]) OR reach across the algebra
4819 /// boundary into [`crate::ast::QuoteForm::UNQUOTE_SPLICE_PREFIX`]
4820 /// and re-derive the UnquoteForm ⊂ QuoteForm variant pairing at
4821 /// the call site. Post-lift the TWO canonical bytes bind at ONE
4822 /// `pub const` per role on the typed [`UnquoteForm`] algebra AND
4823 /// at [`Self::MARKERS`] as a family-wide forced-arity array — a
4824 /// future LSP / REPL completion bar keyed on
4825 /// `UnquoteForm::MARKERS`, a `tatara-check` coverage sweep over
4826 /// the template-marker arms of a `NonSymbolUnquoteTarget` /
4827 /// `UnboundTemplateVar` corpus, or a Sekiban audit-trail metric
4828 /// jointly labeled by the template marker
4829 /// (`tatara_lisp_unbound_template_var_total{prefix=","}`) reads
4830 /// through the typed constants on this subset algebra without
4831 /// re-deriving the 2-of-4 carving inline.
4832 ///
4833 /// Each entry is byte-for-byte identical to the corresponding
4834 /// [`crate::ast::QuoteForm`] Unquote / UnquoteSplice arm — an
4835 /// intentional cross-axis overlap pinned by
4836 /// `unquote_form_per_role_markers_alias_quote_form_per_role_prefixes_byte_for_byte`
4837 /// so a future marker rename on EITHER side (a
4838 /// [`crate::ast::QuoteForm`] `","` → `",."` drift, or an
4839 /// [`UnquoteForm`] rename that skips the alias) fails-loudly at
4840 /// the alias test rather than as a silent operator-facing
4841 /// vocabulary fracture. Adding a hypothetical third template-
4842 /// marker (e.g. a `,~` reverse-unquote, a `,?` conditional-unquote)
4843 /// extends [`Self::ALL`] AND [`Self::MARKERS`] AND adds ONE per-
4844 /// role `pub const` alias in lockstep — rustc's forced-arity check
4845 /// on the two `[_; N]` arrays fails compilation if EITHER ALL
4846 /// array grows without the other.
4847 ///
4848 /// Theory anchor: THEORY.md §III — the typescape; the two
4849 /// canonical template-marker bytes bind at ONE typed
4850 /// `[&'static str; 2]` array on the closed-set UnquoteForm algebra
4851 /// rather than at zero-primitive-on-this-subset-plus-two-inline-
4852 /// lookups scattered across the substrate. THEORY.md §V.1 —
4853 /// knowable platform; the family's cardinality becomes a TYPE-
4854 /// level constant on the substrate algebra rather than a per-
4855 /// consumer runtime dispatch through the composition. THEORY.md
4856 /// §VI.1 — generation over composition; the family-wide contract
4857 /// sweeps (alignment with `ALL`, pairwise disjointness, membership
4858 /// through [`Self::marker`]) emerge from the composition of TWO
4859 /// substrate primitives (this `pub const` array + the two per-role
4860 /// `pub const *_MARKER` aliases) rather than as per-variant inline
4861 /// assertions duplicated at each call site.
4862 pub const MARKERS: [&'static str; 2] = [Self::UNQUOTE_MARKER, Self::SPLICE_MARKER];
4863
4864 /// Canonical reader-lead `char` byte for BOTH [`Self::Unquote`] AND
4865 /// [`Self::Splice`] — aliases [`crate::ast::QuoteForm::UNQUOTE_LEAD`]
4866 /// (which is `','`) on the UnquoteForm ⊂ QuoteForm 2-of-4 subset
4867 /// carving so the reader-lead-char per-role bytes bind at ONE
4868 /// `pub const` on the parent superset's Unquote arm rather than at
4869 /// TWO sites (the per-role `pub const` AND a parallel inline `','`
4870 /// literal). Per-role peer of [`Self::Unquote`] on the closed-set
4871 /// template-substitution algebra; consumers reach for
4872 /// `UnquoteForm::UNQUOTE_LEAD` when the caller has a variant in
4873 /// hand at compile time and wants the canonical reader-entry lead
4874 /// char without runtime dispatch through
4875 /// `self.to_quote_form().lead_char()`.
4876 ///
4877 /// The (2-of-2 collapse) shape-asymmetry is load-bearing: BOTH
4878 /// UnquoteForm arms share the SAME lead char at
4879 /// [`crate::ast::QuoteForm::lead_char`]'s `Self::Unquote |
4880 /// Self::UnquoteSplice => Self::UNQUOTE_LEAD` merged arm — the
4881 /// reader's outer tokenizer dispatch selects between quote-family
4882 /// entry and every non-quote-family arm on lead char alone, with
4883 /// the `,`-vs-`,@` disambiguation falling out of the reader's
4884 /// second-char peek (`@` promotes to Splice) rather than at
4885 /// [`crate::ast::QuoteForm::from_lead_char`]'s decode. This
4886 /// UnquoteForm ⊂ QuoteForm subset's LEAD alias is
4887 /// SHAPE-ASYMMETRIC-COLLAPSE to `[Self::UNQUOTE_LEAD]` (a
4888 /// single-entry ALL — see [`Self::LEADS`]) because both subset
4889 /// arms project to the same superset lead byte, mirroring the
4890 /// parent's `[QUOTE_LEAD, QUASIQUOTE_LEAD, UNQUOTE_LEAD]` shape-
4891 /// asymmetric collapse (3 distinct leads for 4 arms).
4892 ///
4893 /// Sibling posture to [`Self::UNQUOTE_MARKER`] (commit 3640f76 —
4894 /// aliases the reader-punctuation prefix vocabulary `","`),
4895 /// [`Self::UNQUOTE_LABEL`] (commit da68af5 — aliases the
4896 /// substrate diagnostic-label vocabulary `"unquote"`),
4897 /// [`Self::UNQUOTE_IAC_FORGE_TAG`] (commit 6acab84 — aliases the
4898 /// iac-forge canonical-form vocabulary `"unquote"`), and
4899 /// [`Self::UNQUOTE_HASH_DISCRIMINATOR`] (commit eca4730 —
4900 /// aliases the outer-`Sexp` cache-key `u8` byte `5u8`) — this
4901 /// FIFTH per-role `pub const` axis closes the (reader-lead char,
4902 /// reader-prefix bytes, diagnostic label, iac-forge canonical-form
4903 /// tag, outer-`Sexp` cache-key byte) QUINTUPLE on the UnquoteForm
4904 /// subset algebra in lockstep with the same quintuple on the
4905 /// parent [`crate::ast::QuoteForm`] superset.
4906 pub const UNQUOTE_LEAD: char = crate::ast::QuoteForm::UNQUOTE_LEAD;
4907
4908 /// Closed-set forced-arity ALL array over the canonical
4909 /// reader-lead `char` bytes on the UnquoteForm ⊂ QuoteForm 2-of-4
4910 /// subset carving — SHAPE-ASYMMETRIC-COLLAPSE `[char; 1]` on this
4911 /// subset carving (both arms share the same `','` lead byte), peer
4912 /// to [`crate::ast::QuoteForm::LEADS`] (`[char; 3]` on the 4-arm
4913 /// superset carving, itself shape-asymmetric-collapse from 4 arms
4914 /// to 3 distinct leads because [`crate::ast::QuoteForm::Unquote`]
4915 /// and [`crate::ast::QuoteForm::UnquoteSplice`] share the same
4916 /// lead byte).
4917 ///
4918 /// Peer axis to [`Self::MARKERS`] on the SAME reader-vocabulary
4919 /// production surface: [`Self::MARKERS`] holds the arm-per-arm
4920 /// prefix bytes (`","`, `",@"` — cardinality matches [`Self::ALL`]
4921 /// at `[_; 2]`), and [`Self::LEADS`] holds the DISTINCT lead-byte
4922 /// sub-vocabulary the reader's outer tokenizer dispatch selects on
4923 /// (`","` alone — cardinality collapses from 2 arms to 1 distinct
4924 /// lead byte). The shape-asymmetric arities `[_; 2]` for MARKERS
4925 /// vs `[_; 1]` for LEADS ARE the shared-lead-char structural
4926 /// collapse invariant carried at the type-system level — a future
4927 /// third UnquoteForm arm (e.g. a hypothetical `,~` reverse-unquote
4928 /// binding a NEW lead char `~`) would extend BOTH [`Self::MARKERS`]
4929 /// to `[_; 3]` AND [`Self::LEADS`] to `[_; 2]` in lockstep, adding
4930 /// ONE per-role LEAD `pub const` alias.
4931 ///
4932 /// Sibling posture to [`crate::ast::QuoteForm::LEADS`] (the
4933 /// superset carving's own DISTINCT-lead-byte ALL array on the
4934 /// SAME reader-lead-vocabulary axis), [`Self::MARKERS`],
4935 /// [`Self::LABELS`], [`Self::IAC_FORGE_TAGS`], and
4936 /// [`Self::HASH_DISCRIMINATORS`] — every one a forced-arity ALL
4937 /// array on the SAME UnquoteForm closed-set algebra spanning ONE
4938 /// of the production byte-vocabularies the substrate carries per
4939 /// role.
4940 ///
4941 /// Pre-lift the sole distinct lead byte had NO per-role primitive
4942 /// on this closed-set subset algebra — a consumer wanting the
4943 /// UnquoteForm-only reader-lead-byte set had to spell the two-
4944 /// step composition `UnquoteForm::ALL.iter().map(|uf|
4945 /// uf.to_quote_form().lead_char()).collect()` (with a manual dedup
4946 /// downstream to project the 2-arm output onto its 1 distinct
4947 /// byte) OR reach across into
4948 /// [`crate::ast::QuoteForm::UNQUOTE_LEAD`] and re-derive the
4949 /// UnquoteForm ⊂ QuoteForm variant-share pairing at the call site.
4950 /// Post-lift the sole canonical byte binds at ONE `pub const` on
4951 /// the typed [`UnquoteForm`] algebra AND at [`Self::LEADS`] as a
4952 /// family-wide `[char; 1]` forced-arity array — a future
4953 /// substrate-facing reader-classifier keyed on the substitution-
4954 /// subset's lead-vocabulary specifically (a future LSP completion
4955 /// bar filtering to template-substitution-only entries, a
4956 /// syntax-highlighter's per-char classifier for the reader-entry
4957 /// template-marker subset, a `tatara-check` predicate
4958 /// `(check-substitution-subset-lead-partition …)` that verifies
4959 /// the subset's sole lead byte sits inside the parent
4960 /// [`crate::ast::QuoteForm::LEADS`] set) reads through the typed
4961 /// constants without re-deriving the 2-of-4 → 1-of-3 collapse
4962 /// inline.
4963 ///
4964 /// Theory anchor: THEORY.md §III — the typescape; the sole
4965 /// canonical distinct-lead byte on the substitution-subset
4966 /// carving binds at ONE typed `[char; 1]` array on the closed-set
4967 /// UnquoteForm algebra rather than as a zero-primitive-on-this-
4968 /// subset-plus-inline-lookup at each callsite. Closes the FIFTH
4969 /// per-role `pub const` axis on the UnquoteForm subset carving in
4970 /// lockstep with the same quintuple on [`crate::ast::QuoteForm`].
4971 /// THEORY.md §V.1 — knowable platform; the SHAPE-ASYMMETRIC-
4972 /// COLLAPSE distinct-lead-byte sub-vocabulary becomes load-bearing
4973 /// typed data on the substrate's substitution-subset closed set —
4974 /// the type-system carries the "both subset arms share ONE lead"
4975 /// invariant at the `[char; 1]` arity rather than as a comment on
4976 /// a lookup site. THEORY.md §VI.1 — generation over composition;
4977 /// the family-wide contract sweeps (alignment with the parent
4978 /// [`crate::ast::QuoteForm::LEADS`]'s subset image, cardinality
4979 /// containment `Self::LEADS.len() <= Self::ALL.len()` — the
4980 /// collapse witness) emerge from ONE substrate primitive plus the
4981 /// per-role LEAD alias rather than from per-consumer runtime
4982 /// dispatch through the two-step composition.
4983 pub const LEADS: [char; 1] = [Self::UNQUOTE_LEAD];
4984
4985 /// Canonical `&'static str` iac-forge canonical-form tag bytes for
4986 /// the [`Self::Unquote`] substitution — aliases
4987 /// [`crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG`] on the
4988 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset carving so the
4989 /// iac-forge-tag-level per-role bytes bind at ONE `pub const` on
4990 /// the parent superset's Unquote arm rather than at TWO sites (the
4991 /// per-role `pub const` AND a parallel inline literal). Per-role
4992 /// peer of [`Self::Unquote`] on the closed-set template-
4993 /// substitution algebra; consumers reach for
4994 /// `UnquoteForm::UNQUOTE_IAC_FORGE_TAG` when the caller has a
4995 /// variant in hand at compile time and wants the canonical iac-
4996 /// forge interop-tag bytes without runtime dispatch through
4997 /// [`Self::iac_forge_tag`].
4998 ///
4999 /// Sibling posture to [`Self::UNQUOTE_MARKER`] (commit 3640f76) —
5000 /// both pin the invariant that a typed-subset enum's per-role
5001 /// bytes are structurally derived from the parent superset's
5002 /// per-role `pub const` via a `pub const = Parent::CONST` alias
5003 /// rather than through parallel literal-discipline sites. Where
5004 /// [`Self::UNQUOTE_MARKER`] aliases the reader-punctuation
5005 /// vocabulary (`","`) shared with the Lisp source-code surface,
5006 /// this constant aliases the iac-forge canonical-form vocabulary
5007 /// (`"unquote"`) shared with the cross-crate attestation surface
5008 /// — the two peer axes span the two production byte-vocabularies
5009 /// the parent [`crate::ast::QuoteForm`] closed set carries, and
5010 /// both now bind through the same aliased typed source of truth on
5011 /// this UnquoteForm subset.
5012 pub const UNQUOTE_IAC_FORGE_TAG: &'static str = crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG;
5013
5014 /// Canonical `&'static str` iac-forge canonical-form tag bytes for
5015 /// the [`Self::Splice`] list-substitution — aliases
5016 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG`] on the
5017 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset carving. Per-role peer of
5018 /// [`Self::Splice`]; the `"unquote-splicing"` bytes are the ONLY
5019 /// hyphenated multi-syllable canonical-form tag on the UnquoteForm
5020 /// algebra — the other iac-forge tag is a single word.
5021 ///
5022 /// INTENTIONALLY diverges from [`crate::error::SexpShape::label`]'s
5023 /// splice arm (which renders `"unquote-splice"` for the diagnostic
5024 /// surface) — the iac-forge canonical-form REQUIRES the
5025 /// `unquote-splicing` spelling, while the substrate's operator-
5026 /// facing diagnostic surface renders the shorter idiom. The typed
5027 /// per-role `pub const` documents the divergence structurally: a
5028 /// future "consolidation" PR that homogenizes the two axes would
5029 /// have to touch this constant explicitly, surfacing the boundary-
5030 /// distinct invariant at code-review time rather than silently.
5031 pub const SPLICE_IAC_FORGE_TAG: &'static str =
5032 crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG;
5033
5034 /// Closed-set forced-arity ALL array over the canonical iac-forge
5035 /// canonical-form tag `&'static str` bytes, in declaration order
5036 /// matching [`Self::ALL`] element-wise (pinned by
5037 /// `unquote_form_iac_forge_tags_align_with_all_by_index`). Sibling
5038 /// posture to [`Self::MARKERS`] (`[&'static str; 2]` on the reader-
5039 /// punctuation axis of the SAME [`UnquoteForm`] closed set — commit
5040 /// 3640f76) and to [`crate::ast::QuoteForm::IAC_FORGE_TAGS`]
5041 /// (`[&'static str; 4]` — the superset carving this UnquoteForm
5042 /// subset embeds into on the SAME iac-forge canonical-form axis).
5043 /// The (canonical iac-forge tag) axis + the (canonical reader
5044 /// marker) axis together span the two production byte-vocabularies
5045 /// the [`UnquoteForm`] closed set carries — [`Self::MARKERS`] holds
5046 /// the Lisp source-code prefixes (`","`, `",@"`) the reader
5047 /// tokenizes on, [`Self::IAC_FORGE_TAGS`] holds the cross-crate
5048 /// canonical-form tag strings (`"unquote"`, `"unquote-splicing"`)
5049 /// the iac-forge interop layer round-trips through.
5050 ///
5051 /// Pre-lift the two iac-forge tag bytes had NO per-role primitive
5052 /// on this closed-set subset algebra — a consumer with an
5053 /// [`UnquoteForm`] variant in hand at compile time reaching for
5054 /// the canonical tag bytes had to spell
5055 /// `UnquoteForm::Splice.iac_forge_tag()` (runtime dispatch through
5056 /// the composition [`Self::to_quote_form`] +
5057 /// [`crate::ast::QuoteForm::iac_forge_tag`]) OR reach across the
5058 /// algebra boundary into
5059 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG`] and
5060 /// re-derive the UnquoteForm ⊂ QuoteForm variant pairing at the
5061 /// call site. Post-lift the TWO canonical bytes bind at ONE
5062 /// `pub const` per role on the typed [`UnquoteForm`] algebra AND
5063 /// at [`Self::IAC_FORGE_TAGS`] as a family-wide forced-arity array
5064 /// — a future LSP / REPL completion bar keyed on
5065 /// `UnquoteForm::IAC_FORGE_TAGS`, a `tatara-check` coverage sweep
5066 /// over the template-marker arms of an attestation-payload corpus,
5067 /// or a Sekiban audit-trail metric jointly labeled by the iac-
5068 /// forge tag (`tatara_lisp_iac_forge_tag_total{tag="unquote"}`)
5069 /// reads through the typed constants on this subset algebra
5070 /// without re-deriving the 2-of-4 carving inline.
5071 ///
5072 /// Each entry is byte-for-byte identical to the corresponding
5073 /// [`crate::ast::QuoteForm`] Unquote / UnquoteSplice arm — an
5074 /// intentional cross-axis overlap pinned by
5075 /// `unquote_form_per_role_iac_forge_tags_alias_quote_form_per_role_iac_forge_tags_byte_for_byte`
5076 /// so a future canonical-form tag rename on EITHER side (a
5077 /// [`crate::ast::QuoteForm`] `"unquote"` → `"unquote-value"` drift,
5078 /// or an [`UnquoteForm`] rename that skips the alias) fails-loudly
5079 /// at the alias test rather than as a silent cross-crate
5080 /// canonical-form vocabulary fracture that mis-hashes BLAKE3
5081 /// attestation keys. Adding a hypothetical third template-marker
5082 /// (e.g. a `,~` reverse-unquote with its own canonical `"reverse-
5083 /// unquote"` tag, a `,?` conditional-unquote with its own tag)
5084 /// extends [`Self::ALL`] AND [`Self::IAC_FORGE_TAGS`] AND adds ONE
5085 /// per-role `pub const` alias in lockstep — rustc's forced-arity
5086 /// check on the two `[_; N]` arrays fails compilation if EITHER
5087 /// ALL array grows without the other.
5088 ///
5089 /// Theory anchor: THEORY.md §III — the typescape; the two
5090 /// canonical iac-forge tag bytes bind at ONE typed
5091 /// `[&'static str; 2]` array on the closed-set UnquoteForm algebra
5092 /// rather than at zero-primitive-on-this-subset-plus-two-inline-
5093 /// lookups scattered across the substrate. THEORY.md §V.1 —
5094 /// knowable platform; the family's cardinality becomes a TYPE-
5095 /// level constant on the substrate algebra rather than a per-
5096 /// consumer runtime dispatch through the composition. THEORY.md
5097 /// §V.3 — three-pillar attestation; the canonical iac-forge tag
5098 /// bytes are the surface the substrate's cross-crate `intent_hash`
5099 /// pillar composes over for every substitution-subset arm of a
5100 /// homoiconic template AST — binding the two bytes on the typed
5101 /// algebra makes attestation-key drift a compile error rather than
5102 /// a silent BLAKE3 mis-hash. THEORY.md §VI.1 — generation over
5103 /// composition; the family-wide contract sweeps (alignment with
5104 /// `ALL`, pairwise disjointness, membership through
5105 /// [`Self::iac_forge_tag`]) emerge from the composition of TWO
5106 /// substrate primitives (this `pub const` array + the two per-role
5107 /// `pub const *_IAC_FORGE_TAG` aliases) rather than as per-variant
5108 /// inline assertions duplicated at each call site.
5109 pub const IAC_FORGE_TAGS: [&'static str; 2] =
5110 [Self::UNQUOTE_IAC_FORGE_TAG, Self::SPLICE_IAC_FORGE_TAG];
5111
5112 /// Canonical `&'static str` diagnostic label bytes for the
5113 /// [`Self::Unquote`] substitution — aliases
5114 /// [`crate::ast::QuoteForm::UNQUOTE_LABEL`] on the UnquoteForm ⊂
5115 /// QuoteForm 2-of-4 subset carving so the label-level per-role
5116 /// bytes bind at ONE `pub const` on the parent superset's Unquote
5117 /// arm (which itself aliases [`SexpShape::UNQUOTE_LABEL`] on the
5118 /// QuoteForm ⊂ SexpShape 4-of-12 subset carving) rather than at
5119 /// multiple sites (the per-role `pub const` PLUS a parallel inline
5120 /// literal). Per-role peer of [`Self::Unquote`] on the closed-set
5121 /// template-substitution algebra; consumers reach for
5122 /// `UnquoteForm::UNQUOTE_LABEL` when the caller has a variant in
5123 /// hand at compile time and wants the canonical diagnostic bytes
5124 /// without runtime dispatch through [`Self::label`].
5125 ///
5126 /// Sibling posture to [`Self::UNQUOTE_MARKER`] (commit 3640f76 —
5127 /// aliases the reader-punctuation vocabulary `","`) and to
5128 /// [`Self::UNQUOTE_IAC_FORGE_TAG`] (commit 6acab84 — aliases the
5129 /// iac-forge canonical-form vocabulary `"unquote"`) — this THIRD
5130 /// per-role `pub const` axis closes the (reader punctuation,
5131 /// diagnostic label, iac-forge canonical-form) triple on the
5132 /// UnquoteForm subset algebra in lockstep with the same triple
5133 /// on the parent [`crate::ast::QuoteForm`] superset. At the
5134 /// Unquote arm the diagnostic label AND the iac-forge tag AND the
5135 /// substrate-facing `SexpShape::UNQUOTE_LABEL` all agree
5136 /// byte-for-byte on `"unquote"`; the divergence axis lives at
5137 /// [`Self::SPLICE_LABEL`] (`"unquote-splice"`) vs
5138 /// [`Self::SPLICE_IAC_FORGE_TAG`] (`"unquote-splicing"`).
5139 pub const UNQUOTE_LABEL: &'static str = crate::ast::QuoteForm::UNQUOTE_LABEL;
5140
5141 /// Canonical `&'static str` diagnostic label bytes for the
5142 /// [`Self::Splice`] list-substitution — aliases
5143 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_LABEL`] on the
5144 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset carving. Per-role peer of
5145 /// [`Self::Splice`]. The `"unquote-splice"` short label matches
5146 /// [`SexpShape::UNQUOTE_SPLICE_LABEL`] byte-for-byte through the
5147 /// alias chain and diverges INTENTIONALLY from
5148 /// [`Self::SPLICE_IAC_FORGE_TAG`] (`"unquote-splicing"`) — the two
5149 /// projections key the SAME closed set on TWO distinct boundaries
5150 /// (substrate diagnostic surface vs cross-crate Common-Lisp
5151 /// canonical form). See [`Self::UNQUOTE_LABEL`] for the alias-
5152 /// chain shape both siblings share.
5153 pub const SPLICE_LABEL: &'static str = crate::ast::QuoteForm::UNQUOTE_SPLICE_LABEL;
5154
5155 /// Closed-set forced-arity ALL array over the canonical diagnostic
5156 /// label `&'static str` bytes, in declaration order matching
5157 /// [`Self::ALL`] element-wise (pinned by
5158 /// `unquote_form_labels_align_with_all_by_index`). Sibling posture
5159 /// to [`Self::MARKERS`] (`[&'static str; 2]` — reader-punctuation
5160 /// axis) and [`Self::IAC_FORGE_TAGS`] (`[&'static str; 2]` — iac-
5161 /// forge canonical-form axis) on the SAME UnquoteForm closed set,
5162 /// and to [`crate::ast::QuoteForm::LABELS`] (`[&'static str; 4]` —
5163 /// the superset carving this UnquoteForm subset embeds into on the
5164 /// SAME diagnostic-label axis). The (diagnostic label) axis + the
5165 /// (reader punctuation) axis + the (iac-forge canonical-form)
5166 /// axis together span the THREE production byte-vocabularies the
5167 /// [`UnquoteForm`] closed set carries — [`Self::MARKERS`] holds
5168 /// the Lisp source-code prefixes (`","`, `",@"`), [`Self::LABELS`]
5169 /// holds the substrate diagnostic labels (`"unquote"`,
5170 /// `"unquote-splice"`), [`Self::IAC_FORGE_TAGS`] holds the cross-
5171 /// crate canonical-form tag strings (`"unquote"`, `"unquote-
5172 /// splicing"`).
5173 ///
5174 /// Pre-lift the two diagnostic-label bytes had NO per-role
5175 /// primitive on this closed-set subset algebra — a consumer with
5176 /// an [`UnquoteForm`] variant in hand at compile time reaching for
5177 /// the canonical diagnostic bytes had to spell the two-step
5178 /// composition `uf.to_quote_form().label()` OR reach across the
5179 /// algebra boundary into
5180 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_LABEL`] and re-derive
5181 /// the UnquoteForm ⊂ QuoteForm variant pairing at the call site.
5182 /// Post-lift the TWO canonical bytes bind at ONE `pub const` per
5183 /// role on the typed [`UnquoteForm`] algebra AND at
5184 /// [`Self::LABELS`] as a family-wide forced-arity array — a
5185 /// future LSP / REPL completion bar keyed on
5186 /// `UnquoteForm::LABELS`, a `tatara-check` coverage sweep over
5187 /// the template-marker arms of a diagnostic corpus, or a Sekiban
5188 /// audit-trail metric jointly labeled by the diagnostic label
5189 /// (`tatara_lisp_unbound_template_var_total{label="unquote"}`)
5190 /// reads through the typed constants on this subset algebra
5191 /// without re-deriving the 2-of-4 carving inline.
5192 ///
5193 /// Each entry is byte-for-byte identical to the corresponding
5194 /// [`crate::ast::QuoteForm`] Unquote / UnquoteSplice arm — an
5195 /// intentional cross-axis overlap pinned by
5196 /// `unquote_form_per_role_labels_alias_quote_form_per_role_labels_byte_for_byte`
5197 /// so a future label rename on EITHER side (a
5198 /// [`crate::ast::QuoteForm`] `"unquote"` → `"substitute"` drift,
5199 /// or an [`UnquoteForm`] rename that skips the alias) fails-
5200 /// loudly at the alias test rather than as a silent operator-
5201 /// facing vocabulary fracture. Adding a hypothetical third
5202 /// template-marker (e.g. a `,~` reverse-unquote) extends
5203 /// [`Self::ALL`] AND [`Self::LABELS`] AND adds ONE per-role
5204 /// `pub const` alias in lockstep — rustc's forced-arity check on
5205 /// the two `[_; N]` arrays fails compilation if EITHER array
5206 /// grows without the other.
5207 ///
5208 /// Theory anchor: THEORY.md §III — the typescape; the two
5209 /// canonical diagnostic label bytes bind at ONE typed
5210 /// `[&'static str; 2]` array on the closed-set UnquoteForm algebra
5211 /// rather than at zero-primitive-on-this-subset-plus-two-inline-
5212 /// lookups scattered across the substrate. Closes the THIRD
5213 /// per-role `pub const` axis on the UnquoteForm subset carving in
5214 /// lockstep with the same triple on [`crate::ast::QuoteForm`].
5215 /// THEORY.md §V.1 — knowable platform; the family's cardinality
5216 /// becomes a TYPE-level constant on the substrate algebra rather
5217 /// than a per-consumer runtime dispatch through the composition.
5218 /// THEORY.md §VI.1 — generation over composition; the family-wide
5219 /// contract sweeps (alignment with `ALL`, pairwise disjointness,
5220 /// membership through [`Self::label`]) emerge from the composition
5221 /// of TWO substrate primitives (this `pub const` array + the two
5222 /// per-role `pub const *_LABEL` aliases) rather than as per-
5223 /// variant inline assertions duplicated at each call site.
5224 /// THEORY.md §II.1 invariant 5 — composition preserves proofs;
5225 /// the alias-chain composition law `UnquoteForm::LABELS[i] ==
5226 /// UnquoteForm::ALL[i].to_quote_form().label()` binds the family-
5227 /// wide array to the composition through [`Self::to_quote_form`] +
5228 /// [`crate::ast::QuoteForm::label`] at rustc time.
5229 pub const LABELS: [&'static str; 2] = [Self::UNQUOTE_LABEL, Self::SPLICE_LABEL];
5230
5231 /// Canonical `u8` outer-`Sexp` cache-key byte for the
5232 /// [`Self::Unquote`] substitution — `5`. Aliases
5233 /// [`crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR`] on the
5234 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset carving so the byte-
5235 /// discriminator-level per-role bytes bind at ONE
5236 /// `pub(crate) const` on the parent superset's Unquote arm rather
5237 /// than at TWO sites (the per-role `pub(crate) const` AND a
5238 /// parallel inline literal). Per-role peer of [`Self::Unquote`] on
5239 /// the closed-set template-substitution algebra; consumers reach
5240 /// for `UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR` when the caller
5241 /// has a variant in hand at compile time and wants the canonical
5242 /// cache-key byte without runtime dispatch through
5243 /// [`Self::hash_discriminator`] (which itself composes through
5244 /// [`Self::to_quote_form`] +
5245 /// [`crate::ast::QuoteForm::hash_discriminator`]).
5246 ///
5247 /// Sibling posture to [`Self::UNQUOTE_MARKER`] (commit 3640f76 —
5248 /// aliases the reader-punctuation vocabulary `","`),
5249 /// [`Self::UNQUOTE_IAC_FORGE_TAG`] (commit 6acab84 — aliases the
5250 /// iac-forge canonical-form vocabulary `"unquote"`), and
5251 /// [`Self::UNQUOTE_LABEL`] (commit da68af5 — aliases the substrate
5252 /// diagnostic-label vocabulary `"unquote"`) — this FOURTH per-role
5253 /// `pub const` axis closes the (reader punctuation, iac-forge
5254 /// canonical-form, diagnostic label, outer-`Sexp` cache-key byte)
5255 /// quadruple on the UnquoteForm subset algebra in lockstep with
5256 /// the same quadruple on the parent [`crate::ast::QuoteForm`]
5257 /// superset. The three prior axes span the three `&'static str`
5258 /// production vocabularies the substrate carries at every quote-
5259 /// family arm; this fourth axis spans the ONE `u8` outer-`Sexp`
5260 /// cache-key vocabulary the substrate's
5261 /// [`Hash for Sexp`](crate::ast::Sexp) body composes over — so
5262 /// every substrate-surface byte the parent [`crate::ast::QuoteForm`]
5263 /// superset carries per role now has a matching per-role
5264 /// `pub const` on this substitution subset.
5265 ///
5266 /// `pub(crate)` because the byte-discriminator surface is an
5267 /// implementation detail of the substrate's
5268 /// [`Hash for Sexp`](crate::ast::Sexp) cache-key contract; exposing
5269 /// it publicly would leak the cache-key shape through the API
5270 /// without enabling any external consumer the three public
5271 /// `pub const` axes ([`Self::UNQUOTE_MARKER`],
5272 /// [`Self::UNQUOTE_IAC_FORGE_TAG`], [`Self::UNQUOTE_LABEL`]) don't
5273 /// already serve. Same posture as
5274 /// [`crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR`],
5275 /// [`crate::ast::AtomKind::SYMBOL_HASH_DISCRIMINATOR`],
5276 /// [`StructuralKind::NIL_HASH_DISCRIMINATOR`], and every other
5277 /// per-role `*_HASH_DISCRIMINATOR` `pub const` on the substrate's
5278 /// typed-shape family — every one is crate-private.
5279 pub(crate) const UNQUOTE_HASH_DISCRIMINATOR: u8 =
5280 crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR;
5281
5282 /// Canonical `u8` outer-`Sexp` cache-key byte for the
5283 /// [`Self::Splice`] list-substitution — `6`. Aliases
5284 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR`] on
5285 /// the UnquoteForm ⊂ QuoteForm 2-of-4 subset carving. Per-role
5286 /// peer of [`Self::Splice`]. The gap-free `{5, 6}` partition on
5287 /// the substitution-subset carving is load-bearing: the substrate's
5288 /// [`Hash for Sexp`](crate::ast::Sexp) body reserves `{0, 2}` for
5289 /// [`StructuralKind`], `{1}` for the outer [`crate::ast::Sexp::Atom`]
5290 /// carve, `{3, 4}` for the non-substitution quote-family arms
5291 /// ([`crate::ast::QuoteForm::Quote`],
5292 /// [`crate::ast::QuoteForm::Quasiquote`]), and `{5, 6}` for this
5293 /// substitution subset — a cache-key collision would leak macro-
5294 /// expansion cache hits across carvings. See
5295 /// [`Self::UNQUOTE_HASH_DISCRIMINATOR`] for the alias-chain shape
5296 /// both siblings share.
5297 pub(crate) const SPLICE_HASH_DISCRIMINATOR: u8 =
5298 crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR;
5299
5300 /// Closed-set forced-arity ALL array over the canonical outer-
5301 /// `Sexp` cache-key `u8` bytes, in declaration order matching
5302 /// [`Self::ALL`] element-wise (pinned by
5303 /// `unquote_form_hash_discriminators_align_with_all_by_index`).
5304 /// Sibling posture to [`Self::MARKERS`] (`[&'static str; 2]` —
5305 /// reader-punctuation axis), [`Self::IAC_FORGE_TAGS`]
5306 /// (`[&'static str; 2]` — iac-forge canonical-form axis), and
5307 /// [`Self::LABELS`] (`[&'static str; 2]` — diagnostic-label axis)
5308 /// on the SAME UnquoteForm closed set, and to
5309 /// [`crate::ast::QuoteForm::HASH_DISCRIMINATORS`] (`[u8; 4]` —
5310 /// the superset carving this UnquoteForm subset embeds into on
5311 /// the SAME outer-`Sexp` cache-key axis). Every closed-set
5312 /// carving of the substrate's outer-`Sexp` cache-key partition
5313 /// now pins its per-role canonical cache-key bytes at ONE
5314 /// `pub(crate) const` per role PLUS an ALL array for family-wide
5315 /// consumers: [`Self::HASH_DISCRIMINATORS`] (`[u8; 2]` —
5316 /// substitution-subset), [`StructuralKind::HASH_DISCRIMINATORS`]
5317 /// (`[u8; 2]` — structural-residual),
5318 /// [`crate::ast::AtomKind::HASH_DISCRIMINATORS`] (`[u8; 6]` —
5319 /// atomic-payload nested inner carving),
5320 /// [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`] (scalar
5321 /// `1u8` — atomic-payload outer marker),
5322 /// [`crate::ast::QuoteForm::HASH_DISCRIMINATORS`] (`[u8; 4]` —
5323 /// quote-family), and
5324 /// [`SexpShape::HASH_DISCRIMINATORS`] (`[u8; 12]` — full outer
5325 /// twelve-arm shape).
5326 ///
5327 /// Pre-lift the two cache-key bytes had NO per-role primitive on
5328 /// this closed-set subset algebra — a consumer with an
5329 /// [`UnquoteForm`] variant in hand at compile time reaching for
5330 /// the canonical cache-key byte had to spell the two-step
5331 /// composition `uf.to_quote_form().hash_discriminator()` OR reach
5332 /// across the algebra boundary into
5333 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR`]
5334 /// and re-derive the UnquoteForm ⊂ QuoteForm variant pairing at
5335 /// the call site. Post-lift the TWO canonical bytes bind at ONE
5336 /// `pub(crate) const` per role on the typed [`UnquoteForm`]
5337 /// algebra AND at [`Self::HASH_DISCRIMINATORS`] as a family-wide
5338 /// forced-arity array — a future
5339 /// [`crate::macro_expand::Expander`] cache-invalidation predicate
5340 /// that decides on the 2-of-4 substitution-subset only, a future
5341 /// `tatara-check` predicate `(check-substitution-subset-cache-
5342 /// key-partition-injective …)` that verifies the substitution-
5343 /// subset carving's cache-key partition sits inside `{5, 6}`, a
5344 /// future `TypedRewriter<UnquoteFormOp>` sweep zipping ALL /
5345 /// LABELS / MARKERS / IAC_FORGE_TAGS / HASH_DISCRIMINATORS in
5346 /// lockstep for a family-wide (variant, label, marker, tag, byte)
5347 /// quintuple render, or a Sekiban audit-trail metric jointly
5348 /// labeled by the substitution-subset cache-key partition read
5349 /// through the typed constants on this subset algebra without
5350 /// re-deriving the 2-of-4 carving inline.
5351 ///
5352 /// Each entry is byte-for-byte identical to the corresponding
5353 /// [`crate::ast::QuoteForm`] Unquote / UnquoteSplice arm — an
5354 /// intentional cross-axis overlap pinned by
5355 /// `unquote_form_per_role_hash_discriminators_alias_quote_form_per_role_hash_discriminators_byte_for_byte`
5356 /// so a future cache-key byte reshuffling on EITHER side (a
5357 /// [`crate::ast::QuoteForm`] `5u8` → `7u8` drift, or an
5358 /// [`UnquoteForm`] rename that skips the alias) fails-loudly at
5359 /// the alias test rather than as a silent macro-expansion cache
5360 /// mis-hash. Adding a hypothetical third template-marker (e.g. a
5361 /// `,~` reverse-unquote) extends [`Self::ALL`] AND
5362 /// [`Self::HASH_DISCRIMINATORS`] AND adds ONE per-role
5363 /// `pub(crate) const` alias in lockstep — rustc's forced-arity
5364 /// check on the two `[_; N]` arrays fails compilation if EITHER
5365 /// array grows without the other.
5366 ///
5367 /// Theory anchor: THEORY.md §III — the typescape; the two
5368 /// canonical cache-key bytes bind at ONE typed `[u8; 2]` array
5369 /// on the closed-set UnquoteForm algebra rather than at zero-
5370 /// primitive-on-this-subset-plus-two-inline-lookups scattered
5371 /// across the substrate. Closes the FOURTH per-role `pub const`
5372 /// axis on the UnquoteForm subset carving in lockstep with the
5373 /// same quadruple on [`crate::ast::QuoteForm`]. THEORY.md §V.1 —
5374 /// knowable platform; the family's cardinality becomes a TYPE-
5375 /// level constant on the substrate algebra rather than a per-
5376 /// consumer runtime dispatch through the composition. THEORY.md
5377 /// §V.3 — three-pillar attestation; the cache-key partition is
5378 /// the substrate's outer [`Sexp`](crate::ast::Sexp) `intent_hash`
5379 /// composition axis for every substitution-subset arm — binding
5380 /// the two bytes on the typed algebra makes attestation-key
5381 /// drift a compile error rather than a silent BLAKE3 mis-hash.
5382 /// THEORY.md §VI.1 — generation over composition; the family-
5383 /// wide contract sweeps (alignment with `ALL`, pairwise
5384 /// disjointness, membership through
5385 /// [`Self::hash_discriminator`]) emerge from the composition of
5386 /// TWO substrate primitives (this `pub(crate) const` array + the
5387 /// two per-role `pub(crate) const *_HASH_DISCRIMINATOR` aliases)
5388 /// rather than as per-variant inline assertions duplicated at
5389 /// each call site. THEORY.md §II.1 invariant 5 — composition
5390 /// preserves proofs; the alias-chain composition law
5391 /// `UnquoteForm::HASH_DISCRIMINATORS[i] ==
5392 /// UnquoteForm::ALL[i].to_quote_form().hash_discriminator()`
5393 /// binds the family-wide array to the composition through
5394 /// [`Self::to_quote_form`] +
5395 /// [`crate::ast::QuoteForm::hash_discriminator`] at rustc time.
5396 ///
5397 /// The `#[allow(dead_code)]` posture matches every sibling
5398 /// `HASH_DISCRIMINATORS` array on the substrate's typed-shape
5399 /// family: the substrate's current
5400 /// [`Hash for Sexp`](crate::ast::Sexp) body composes through the
5401 /// four-arm superset's [`crate::ast::QuoteForm::hash_discriminator`]
5402 /// projection rather than splitting the dispatch into
5403 /// (2 non-substitution + 2 substitution) arms — so no non-test
5404 /// caller currently reaches THIS subset-algebra array. The lift
5405 /// lands the substrate primitive so future consumers keyed on
5406 /// the substitution-subset carving specifically bind to ONE
5407 /// `[u8; 2]` primitive rather than re-deriving the array inline
5408 /// per callsite.
5409 #[allow(dead_code)]
5410 pub(crate) const HASH_DISCRIMINATORS: [u8; 2] = [
5411 Self::UNQUOTE_HASH_DISCRIMINATOR,
5412 Self::SPLICE_HASH_DISCRIMINATOR,
5413 ];
5414
5415 /// Closed-set forced-arity ALL array over the reader's promotion
5416 /// triples on the two-variant substitution-subset lattice —
5417 /// `(Self::Unquote, '@', Self::Splice)` for the ONE `,` → `,@`
5418 /// promotion the substrate's reader `promote_via_next_char` arm
5419 /// depends on at the parent superset level. Its forced-arity `1`
5420 /// is INTENTIONAL and load-bearing on the subset carving: only
5421 /// [`Self::Splice`] carries a two-char rendered marker on the
5422 /// substitution subset (its `,@` prefix decomposes into `,` +
5423 /// [`crate::ast::QuoteForm::SPLICE_DISCRIMINATOR`]), so the
5424 /// promotion table's Some-arm is the singleton `{(Unquote, '@') →
5425 /// Splice}` on the `Self × char → Option<Self>` product — the
5426 /// two-of-four substitution-subset peer of the closed set
5427 /// [`crate::ast::QuoteForm::PROMOTIONS`] `[(head, char, promoted);
5428 /// 1]` carries on the four-arm quote-family superset.
5429 ///
5430 /// Cross-algebra composition law (pinned by
5431 /// `unquote_form_promotions_align_with_quote_form_promotions_by_projection`):
5432 /// for the ONE entry `(head, disc, promoted)` in
5433 /// [`Self::PROMOTIONS`],
5434 /// `(head.to_quote_form(), disc, promoted.to_quote_form())` equals
5435 /// [`crate::ast::QuoteForm::PROMOTIONS`]`[0]` byte-for-byte. The
5436 /// subset's promotion triple embeds into the parent superset's
5437 /// promotion triple through the pre-existing 2-of-4 subset →
5438 /// superset projection [`Self::to_quote_form`] composed with the
5439 /// identity on the middle discriminator `char` (which is a
5440 /// [`crate::ast::QuoteForm::SPLICE_DISCRIMINATOR`] alias direct at
5441 /// the constant site). A regression that drifts the subset's
5442 /// triple silently promotes a phantom pairing (e.g. re-inlines
5443 /// `Self::Splice` at the head column) AND fails the composition
5444 /// pin at rustc / test time rather than at silent tokenizer drift
5445 /// where the reader's `,@xs` sequence tokenizes to a phantom
5446 /// substitution-subset marker.
5447 ///
5448 /// Sibling posture to the closed-set forced-arity ALL arrays
5449 /// across the substrate's [`UnquoteForm`] subset algebra:
5450 /// [`Self::ALL`] (`[Self; 2]` — the closed set of variants),
5451 /// [`Self::MARKERS`] (`[&'static str; 2]` — the reader-marker
5452 /// axis, aliased through
5453 /// [`crate::ast::QuoteForm::UNQUOTE_PREFIX`] /
5454 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_PREFIX`]),
5455 /// [`Self::IAC_FORGE_TAGS`] (`[&'static str; 2]` — the iac-forge
5456 /// canonical-form tag axis, aliased through
5457 /// [`crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG`] /
5458 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG`]),
5459 /// [`Self::LABELS`] (`[&'static str; 2]` — the diagnostic-label
5460 /// axis, aliased through
5461 /// [`crate::ast::QuoteForm::UNQUOTE_LABEL`] /
5462 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_LABEL`]), and
5463 /// [`Self::HASH_DISCRIMINATORS`] (`[u8; 2]` — the outer-`Sexp`
5464 /// cache-key byte axis, aliased through
5465 /// [`crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR`] /
5466 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR`]).
5467 /// This lift adds the FIFTH per-family axis on the substitution-
5468 /// subset algebra — the (head, disc, promoted) triple axis on
5469 /// the closed-set promotion product. Each of the five axes now
5470 /// pins its per-role canonical data at ONE `pub const` per role
5471 /// (or ONE `pub const` triple on the promotion-asymmetric single-
5472 /// arm collapse) PLUS an ALL array for family-wide consumers.
5473 ///
5474 /// The middle discriminator column aliases
5475 /// [`crate::ast::QuoteForm::SPLICE_DISCRIMINATOR`] direct at the
5476 /// constant site — the substitution subset does NOT introduce a
5477 /// separate `SPLICE_DISCRIMINATOR` const because the discriminator
5478 /// byte is shape-uniform across the parent superset (it is the
5479 /// SAME `'@'` byte that promotes at every quote-family arm on the
5480 /// parent) and lives at ONE canonical site in `ast.rs`. Alias
5481 /// direction runs from the [`UnquoteForm`] subset INTO the parent
5482 /// [`crate::ast::QuoteForm`] superset — the SAME direction the
5483 /// four prior per-family axes take on this subset (MARKERS,
5484 /// IAC_FORGE_TAGS, LABELS, HASH_DISCRIMINATORS).
5485 ///
5486 /// `pub(crate)` because the promotion algebra is an implementation
5487 /// detail of the substrate's reader — exposing it publicly would
5488 /// leak the promotion-table shape through the API without
5489 /// enabling any external consumer the parent superset's public
5490 /// [`crate::ast::QuoteForm::promote_via_next_char`] projection
5491 /// doesn't already serve on the four-arm surface. Same visibility
5492 /// rationale as [`Self::HASH_DISCRIMINATORS`] on the sibling
5493 /// cache-key axis.
5494 ///
5495 /// The `#[allow(dead_code)]` posture matches
5496 /// [`Self::HASH_DISCRIMINATORS`] AND
5497 /// [`crate::ast::QuoteForm::PROMOTIONS`]: the substrate's current
5498 /// reader dispatches through the parent superset's
5499 /// [`crate::ast::QuoteForm::promote_via_next_char`] arm rather
5500 /// than a UnquoteForm-subset promotion body (which does not
5501 /// exist on this subset — the two-of-four carving does not carry
5502 /// a `promote_via_next_char` method because the reader boundary
5503 /// is the four-arm parent), so no non-test caller currently
5504 /// reaches this substitution-subset promotion array directly.
5505 /// The lift lands the substrate primitive so future consumers
5506 /// keyed on the substitution-subset carving specifically (a
5507 /// future `TypedRewriter<UnquoteFormOp>` sweep zipping ALL /
5508 /// MARKERS / IAC_FORGE_TAGS / LABELS / HASH_DISCRIMINATORS /
5509 /// PROMOTIONS in lockstep for a family-wide render, a future
5510 /// `tatara-check` predicate
5511 /// `(check-substitution-subset-promotion-algebra-injective …)`
5512 /// that verifies the substitution subset's promotion triples
5513 /// embed injectively into the parent superset's, a Sekiban
5514 /// audit-trail metric jointly labeled by the substitution-subset
5515 /// promotion partition) bind to ONE `[(Self, char, Self); N]`
5516 /// primitive rather than re-deriving the promotion triples
5517 /// inline per callsite. Matches the preemptive-primitive posture
5518 /// the prior-run [`Self::HASH_DISCRIMINATORS`] +
5519 /// [`crate::ast::QuoteForm::PROMOTIONS`] +
5520 /// [`crate::ast::QuoteForm::HASH_DISCRIMINATORS`] lifts carried
5521 /// before their downstream consumers materialized.
5522 ///
5523 /// Theory anchor: THEORY.md §II.1 invariant 1 — typed entry; the
5524 /// reader's two-char quote-family classification IS the typed-
5525 /// entry gate on the `,@` boundary, and lifting the substitution-
5526 /// subset's promotion triple to ONE typed `[(Self, char, Self);
5527 /// 1]` primitive on the closed-set subset algebra closes the
5528 /// gate's two-char entry surface onto the subset algebra rather
5529 /// than at zero-primitive-on-this-subset dispatch through the
5530 /// parent superset. THEORY.md §II.1 invariant 5 — composition
5531 /// preserves proofs; the (subset promotion triple, superset
5532 /// promotion triple) pairing binds at rustc time by identity
5533 /// composition through [`Self::to_quote_form`] on the head +
5534 /// promoted columns AND by direct alias on the middle
5535 /// discriminator column. THEORY.md §III — the typescape; the
5536 /// singleton promotion triple binds at ONE typed `pub const` on
5537 /// the closed-set [`UnquoteForm`] algebra rather than at zero-
5538 /// primitive-on-this-subset. Closes the FIFTH per-role
5539 /// `pub const` axis on the UnquoteForm subset carving in lockstep
5540 /// with the same-axis lift on [`crate::ast::QuoteForm`].
5541 /// THEORY.md §V.1 — knowable platform; the family's cardinality
5542 /// becomes a TYPE-level constant on the substrate algebra rather
5543 /// than a per-consumer runtime dispatch through the parent
5544 /// superset's method surface. THEORY.md §VI.1 — generation over
5545 /// composition; the family-wide contract sweeps (alignment with
5546 /// [`Self::ALL`], cardinality, cross-algebra composition against
5547 /// [`crate::ast::QuoteForm::PROMOTIONS`]) emerge from the
5548 /// composition of ONE substrate primitive (this `pub(crate) const`
5549 /// array) rather than as per-arm inline assertions duplicated at
5550 /// each call site.
5551 ///
5552 /// Frontier inspiration: MLIR's typed rewriter registry
5553 /// (`mlir::PatternApplicator`) carries per-op-family static
5554 /// rewrite tables at the closed-set boundary; a sub-op-family
5555 /// (a distinguished carving of the parent op-family) inherits
5556 /// the rewrite table through direct alias from the parent's
5557 /// canonical site, so the subset's rewrite table binds at ONE
5558 /// typed constant on the subset algebra with the alignment
5559 /// composition through the subset → superset projection at
5560 /// const-fn evaluation time. [`Self::PROMOTIONS`] is the Rust-
5561 /// typed peer of MLIR's per-subset rewrite-table projection at
5562 /// the two-variant substitution-subset level — its single entry
5563 /// composes from [`crate::ast::QuoteForm::PROMOTIONS`]`[0]`
5564 /// through subset → superset projection on the endpoints and
5565 /// direct alias on the middle discriminator.
5566 #[allow(dead_code)]
5567 pub(crate) const PROMOTIONS: [(Self, char, Self); 1] = [(
5568 Self::Unquote,
5569 crate::ast::QuoteForm::SPLICE_DISCRIMINATOR,
5570 Self::Splice,
5571 )];
5572
5573 /// Project the typed `UnquoteForm` to the canonical `&'static str`
5574 /// literal — feeds the `LispError::UnboundTemplateVar` /
5575 /// `LispError::NonSymbolUnquoteTarget` Display rendering via the
5576 /// `#[error(...)]` annotation. The `&'static str` lifetime is
5577 /// load-bearing: it lets the variants project through this method into
5578 /// their `compile error in {prefix}…:` prefix without an allocation,
5579 /// parallel to how `MacroDefHead::keyword()` and
5580 /// `CompilerSpecIoStage::operation()` / `label()` feed their respective
5581 /// `LispError::*` Display impls.
5582 ///
5583 /// Composition law: `self.marker() == self.to_quote_form().prefix()`
5584 /// for every `self: UnquoteForm`. Pre-lift the body inlined the two
5585 /// literals (`","` / `",@"`) as a parallel match-table; the same two
5586 /// literals also lived at the matching Unquote/UnquoteSplice arms of
5587 /// [`crate::ast::QuoteForm::prefix`]. Post-lift the body composes
5588 /// `Self::to_quote_form()` (the typed 2-of-4 subset → superset
5589 /// projection) with `crate::ast::QuoteForm::prefix()` (the canonical
5590 /// 4-of-4 prefix-string projection), so the two `&'static str`
5591 /// literals live at ONE canonical site (`QuoteForm::prefix`'s
5592 /// Unquote/UnquoteSplice arms in `ast.rs`) and every consumer of
5593 /// `UnquoteForm::marker` (the `ClosedSet`-trait Display/FromStr
5594 /// surface via `#[closed_set(via = "marker")]`, the unbound-template
5595 /// `did you mean ,@args?` hint suffix in this module's
5596 /// `unbound_hint_suffix` helper, the `#[error("... {prefix}")]`
5597 /// annotation on `LispError::UnboundTemplateVar` /
5598 /// `LispError::NonSymbolUnquoteTarget`, the future LSP / REPL /
5599 /// `tatara-check` completion-bar projections that key on the typed
5600 /// marker) inherits the canonical vocabulary through the composition
5601 /// rather than through a parallel inline table that drifts at the
5602 /// next vocabulary rename.
5603 ///
5604 /// Sibling-shape lift to the prior-run `AtomKind::label` ⊂
5605 /// `SexpShape::label` composition (commit 1db697f): both pin the
5606 /// invariant that a typed-subset enum's projection on a closed-set
5607 /// vocabulary is structurally derived from its parent superset's
5608 /// projection, not a parallel literal table the type system happens
5609 /// to not catch when the vocabularies drift.
5610 ///
5611 /// The bidirectional contract is anchored by tests:
5612 /// `unquote_form_marker_projects_canonical_literal_for_each_variant`
5613 /// pins each variant's canonical literal so a typo in any arm
5614 /// fails-loudly,
5615 /// `unquote_form_display_renders_canonical_marker_for_each_variant`
5616 /// pins Display-equals-marker so any `#[error("... {prefix}")]`
5617 /// annotation that threads through this projection projects
5618 /// byte-for-byte,
5619 /// `unquote_form_marker_round_trips_through_from_str` pins the
5620 /// `marker` ↔ [`Self::FromStr`] round-trip for every variant in
5621 /// [`Self::ALL`] so the typed surface and the rendered diagnostic
5622 /// literal cannot drift, and the post-lift composition-routing
5623 /// pin
5624 /// `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`
5625 /// asserts pointer-equality between `self.marker()` and
5626 /// `self.to_quote_form().prefix()` for every variant — so a
5627 /// regression that re-inlines the literals as a parallel match-table
5628 /// fails the pointer pin even when the rendered bytes still agree
5629 /// (the inline-literal-table copy lives at a different `&'static str`
5630 /// address).
5631 #[must_use]
5632 pub fn marker(self) -> &'static str {
5633 self.to_quote_form().prefix()
5634 }
5635
5636 /// Project the 2-of-4 template-substitution subset back to its
5637 /// parent [`crate::ast::QuoteForm`] superset — the structural
5638 /// inverse of [`crate::ast::QuoteForm::as_unquote_form`]. Returns
5639 /// the [`crate::ast::QuoteForm`] variant whose canonical prefix
5640 /// string matches this `UnquoteForm`'s marker:
5641 /// [`Self::Unquote`] → [`crate::ast::QuoteForm::Unquote`],
5642 /// [`Self::Splice`] → [`crate::ast::QuoteForm::UnquoteSplice`].
5643 ///
5644 /// Total (not `Option`) because every `UnquoteForm` IS a
5645 /// `QuoteForm` — the 2-of-4 subset is a subset by inclusion. The
5646 /// dual projection [`crate::ast::QuoteForm::as_unquote_form`]
5647 /// returns `Option<UnquoteForm>` because the two non-substitution
5648 /// variants ([`crate::ast::QuoteForm::Quote`] and
5649 /// [`crate::ast::QuoteForm::Quasiquote`]) lie outside the
5650 /// substitution subset.
5651 ///
5652 /// Round-trip identity: `self.to_quote_form().as_unquote_form() ==
5653 /// Some(self)` for every `self: UnquoteForm`, pinned by
5654 /// `unquote_form_to_quote_form_round_trips_through_as_unquote_form`.
5655 /// The dual identity `qf.as_unquote_form().map(|uf|
5656 /// uf.to_quote_form()) == qf.as_unquote_form().map(|_| qf)` holds
5657 /// by construction (the projection is a section of
5658 /// [`crate::ast::QuoteForm::as_unquote_form`] restricted to its
5659 /// image — the Unquote/UnquoteSplice variants).
5660 ///
5661 /// Lifts the (UnquoteForm variant, QuoteForm variant) pairing onto
5662 /// the typed algebra so consumers that need the parent superset
5663 /// from a substitution-subset value (the `marker` lift's
5664 /// composition root, the future hint-suffix pretty-printer's
5665 /// `prefix.to_quote_form().prefix()` routing, the future
5666 /// canonical-form interop tag that wants to route an `UnquoteForm`
5667 /// through `iac_forge_tag` via `to_quote_form().iac_forge_tag()`
5668 /// without re-deriving the pairing — now closed at
5669 /// [`Self::iac_forge_tag`]) bind at ONE typed projection
5670 /// rather than at parallel match arms each site re-derives.
5671 ///
5672 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
5673 /// subset-to-superset projection becomes a TYPE projection on the
5674 /// substrate algebra. THEORY.md §VI.1 — generation over
5675 /// composition; the (UnquoteForm variant, QuoteForm variant)
5676 /// pairing emerges from this ONE primitive rather than from
5677 /// per-callsite per-variant literals. THEORY.md §II.1 invariant 1 —
5678 /// typed entry; the typed subset-to-superset projection IS a
5679 /// substrate-owned theorem rather than a hand-inlined match-table
5680 /// duplication discipline N sites had to keep in lockstep.
5681 #[must_use]
5682 pub fn to_quote_form(self) -> crate::ast::QuoteForm {
5683 match self {
5684 Self::Unquote => crate::ast::QuoteForm::Unquote,
5685 Self::Splice => crate::ast::QuoteForm::UnquoteSplice,
5686 }
5687 }
5688
5689 /// Project the 2-of-4 template-substitution subset marker back into
5690 /// its matching [`crate::ast::Sexp`] wrapper variant applied to
5691 /// `inner` — the typed-CONSTRUCT face on the closed-set `UnquoteForm`
5692 /// algebra, sibling section-for-retraction of the existing typed-
5693 /// PROJECT face [`crate::ast::Sexp::as_unquote`] on the outer
5694 /// [`crate::ast::Sexp`] algebra. [`Self::Unquote`] yields
5695 /// [`crate::ast::Sexp::Unquote`], [`Self::Splice`] yields
5696 /// [`crate::ast::Sexp::UnquoteSplice`], each boxing `inner` into the
5697 /// corresponding tuple-variant constructor
5698 /// (`fn(Box<Sexp>) -> Sexp`).
5699 ///
5700 /// Closes the (construct, project) algebra dual on the closed-set
5701 /// `UnquoteForm` algebra — the substitution-subset peer of the
5702 /// closed-set superset algebra's already-closed dual pair
5703 /// ([`crate::ast::QuoteForm::wrap`] +
5704 /// [`crate::ast::Sexp::as_quote_form`]). Where the superset's
5705 /// `wrap` reaches all four homoiconic prefix-wrappers and its
5706 /// projection returns `Option<(QuoteForm, &Sexp)>` on the outer
5707 /// [`crate::ast::Sexp`] algebra, this subset's `wrap` reaches only
5708 /// the two template-substitution wrappers
5709 /// ([`crate::ast::Sexp::Unquote`] and
5710 /// [`crate::ast::Sexp::UnquoteSplice`]) and its projection sibling
5711 /// [`crate::ast::Sexp::as_unquote`] returns
5712 /// `Option<(UnquoteForm, &Sexp)>` — the (construct, project) pair on
5713 /// the subset algebra is symmetric with the (construct, project)
5714 /// pair on the superset algebra, each closed at one method per
5715 /// direction on the respective closed set.
5716 ///
5717 /// Composition law (forward): `self.wrap(inner) ==
5718 /// self.to_quote_form().wrap(inner)` for every `self: UnquoteForm`
5719 /// and every `inner: Sexp` — routes through the superset's
5720 /// [`crate::ast::QuoteForm::wrap`] at the single tuple-variant
5721 /// emission site so the (marker, `Sexp::*` tuple-variant
5722 /// constructor) pairing binds at ONE closed-set match on the
5723 /// superset algebra rather than at a parallel two-arm match table
5724 /// on this subset. Round-trip law (section-for-retraction with the
5725 /// soft-projection sibling): `self.wrap(inner).as_unquote() ==
5726 /// Some((self, &inner))` for every `self: UnquoteForm` and every
5727 /// `inner: Sexp` — the subset's typed constructor pairs
5728 /// section-for-retraction with the outer algebra's soft projection,
5729 /// and the marker + inner body cross-projection preserves identity
5730 /// on the substitution-subset closed set. Sibling-shape lift to the
5731 /// prior-run `UnquoteForm::marker` ⊂ `QuoteForm::prefix` composition
5732 /// (commit 250c001): both pin the invariant that a typed-subset
5733 /// enum's projection on a closed-set operation is structurally
5734 /// derived from its parent superset's projection through the
5735 /// canonical `to_quote_form` composition, not a parallel inline
5736 /// match-table the type system happens to not catch when the
5737 /// vocabularies drift.
5738 ///
5739 /// Pre-lift consumers that had an `UnquoteForm` marker in hand and
5740 /// wanted to build a fresh [`crate::ast::Sexp`] wrapper had to
5741 /// spell the two-step composition
5742 /// `uf.to_quote_form().wrap(inner)` — a future template rewriter
5743 /// consuming [`crate::ast::Sexp::as_unquote`]'s
5744 /// `Option<(UnquoteForm, &Sexp)>` projection and threading a
5745 /// rewritten inner body back into the matching wrapper on the
5746 /// substitution-subset closed set (a future
5747 /// `TypedRewriter<UnquoteFormOp>` sweep, a future macro-template
5748 /// canonicalizer that normalizes `,x` / `,@x` shapes, a future REPL
5749 /// pretty-printer that emits a fresh substitution wrapper from a
5750 /// borrowed marker + inner pair) would have re-derived the
5751 /// `.to_quote_form().wrap(_)` composition at every callsite. Post-
5752 /// lift the composition binds at ONE typed-algebra method on the
5753 /// closed-set `UnquoteForm` algebra, matching the section-for-
5754 /// retraction posture the superset's [`crate::ast::QuoteForm::wrap`]
5755 /// already carries on the outer [`crate::ast::Sexp`] algebra. The
5756 /// (marker, `Sexp::*` tuple-variant constructor) pairing continues
5757 /// to live at ONE site on the superset's `wrap` closed-set match
5758 /// (the single tuple-variant emission point the substrate owns),
5759 /// which is what this subset method routes through — no parallel
5760 /// match table, no per-arm drift risk on the two-variant closed set.
5761 ///
5762 /// The [`crate::ast::Sexp`] (owned) return type complements
5763 /// [`crate::ast::Sexp::as_unquote`]'s `&Sexp` (borrowed) —
5764 /// symmetric with the ([`crate::ast::QuoteForm::wrap`],
5765 /// [`crate::ast::Sexp::as_quote_form`]) asymmetry on the superset
5766 /// algebra: `wrap` consumes the inner body to build the new
5767 /// wrapper, the projection borrows the inner body from the existing
5768 /// wrapper. The typed `Box::new(inner)` allocation lives at ONE
5769 /// site on the superset's [`crate::ast::QuoteForm::wrap`] (the
5770 /// closed-set tuple-variant emission point), so a future
5771 /// allocation-policy change (e.g. arena-allocated wrappers for
5772 /// span-aware [`crate::ast::Sexp`]) lands as ONE edit at the
5773 /// superset's `wrap` and propagates through this subset method
5774 /// byte-for-byte.
5775 ///
5776 /// Theory anchor: THEORY.md §II.1 invariant 1 — typed entry; the
5777 /// (UnquoteForm variant, `Sexp::*` tuple-variant constructor)
5778 /// pairing binds at ONE typed-algebra method on the subset algebra,
5779 /// routed through the ONE closed-set match on the superset algebra
5780 /// the substrate already owns. THEORY.md §II.1 invariant 2 — free
5781 /// middle; every consumer that has an `UnquoteForm` marker and
5782 /// wants to build a wrapper `Sexp` routes through the SAME typed
5783 /// method, so a regression that drifts one consumer's pairing from
5784 /// the others cannot reach the substrate's runtime. THEORY.md §V.1
5785 /// — knowable platform; the typed-construct face becomes a TYPE
5786 /// projection on the subset algebra sitting next to the typed-
5787 /// project face [`crate::ast::Sexp::as_unquote`] on the outer
5788 /// [`crate::ast::Sexp`] algebra rather than a bare
5789 /// `to_quote_form().wrap(_)` two-step composition consumers had to
5790 /// re-derive per site. THEORY.md §VI.1 — generation over
5791 /// composition; the (subset marker, wrapper `Sexp`) pairing emerges
5792 /// from ONE typed-algebra composition on the subset algebra rather
5793 /// than from parallel per-consumer per-variant literals.
5794 ///
5795 /// Frontier inspiration: Racket's `syntax/parse` `~or* (~unquote
5796 /// stx) (~unquote-splice stx)` pattern paired one-for-one with a
5797 /// typed constructor face on the same subset shape — the (project,
5798 /// construct) algebra dual is closed at one method per direction
5799 /// on Racket's substitution-subset surface, and `UnquoteForm::wrap`
5800 /// / [`crate::ast::Sexp::as_unquote`] is the Rust-typed peer on
5801 /// the closed-set `UnquoteForm` algebra with
5802 /// [`crate::ast::QuoteForm::wrap`] standing in for Racket's typed
5803 /// dispatch through the superset. MLIR's typed factory
5804 /// `mlir::OpBuilder::create<UnquoteFamilyOp>(loc, inner)` paired
5805 /// with the projection sibling `mlir::dyn_cast<UnquoteFamilyOp>(op)`
5806 /// — the typed factory + typed downcast pair the IR algebra closes
5807 /// over on every wrapper op subset; `UnquoteForm::wrap` /
5808 /// [`crate::ast::Sexp::as_unquote`] is the unstructured-Rust peer
5809 /// on the outer [`crate::ast::Sexp`] algebra with the closed-set
5810 /// `UnquoteForm` standing in for MLIR's subset-of-`OperationName`
5811 /// taxonomy over the substitution-subset op family.
5812 #[must_use]
5813 pub fn wrap(self, inner: crate::ast::Sexp) -> crate::ast::Sexp {
5814 self.to_quote_form().wrap(inner)
5815 }
5816
5817 /// Canonical [`SexpShape`] embed target for the [`Self::Unquote`]
5818 /// template-substitution arm on the UnquoteForm ⊂ QuoteForm ⊂
5819 /// SexpShape composed carving — aliases
5820 /// [`crate::ast::QuoteForm::UNQUOTE_SHAPE`] on the parent's 4-of-12
5821 /// quote-family carving byte-for-byte (which in turn aliases
5822 /// [`SexpShape::Unquote`]). Per-role peer of `Self::Unquote` on the
5823 /// closed-set UnquoteForm ⊂ SexpShape 2-of-12 composed embed axis;
5824 /// consumers with an `UnquoteForm` variant in hand at compile time
5825 /// bind the canonical embed target through ONE typed `pub const` on
5826 /// the subset algebra rather than through runtime dispatch via
5827 /// `Self::sexp_shape` OR by reaching across the two-hop
5828 /// `Self::to_quote_form().sexp_shape()` composition OR by re-deriving
5829 /// the UnquoteForm ⊂ QuoteForm ⊂ SexpShape variant pairing inline.
5830 /// Sibling posture to [`Self::UNQUOTE_MARKER`] on the reader-prefix
5831 /// axis, [`Self::UNQUOTE_LEAD`] on the reader-lead axis,
5832 /// [`Self::UNQUOTE_IAC_FORGE_TAG`] on the iac-forge canonical-form tag
5833 /// axis, [`Self::UNQUOTE_LABEL`] on the diagnostic-label axis, and
5834 /// [`Self::UNQUOTE_HASH_DISCRIMINATOR`] on the outer-Sexp cache-key
5835 /// byte axis — every one of the pre-existing per-role sub-vocabulary
5836 /// aliases on this subset algebra now has a peer per-role
5837 /// `SexpShape` embed-target alias on the SAME closed-set
5838 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset carving.
5839 pub const UNQUOTE_SHAPE: SexpShape = crate::ast::QuoteForm::UNQUOTE_SHAPE;
5840
5841 /// Canonical [`SexpShape`] embed target for the [`Self::Splice`]
5842 /// template-substitution arm on the UnquoteForm ⊂ QuoteForm ⊂
5843 /// SexpShape composed carving — aliases
5844 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_SHAPE`] on the parent's
5845 /// 4-of-12 quote-family carving byte-for-byte (which in turn aliases
5846 /// [`SexpShape::UnquoteSplice`]). Per-role peer of `Self::Splice` on
5847 /// the closed-set UnquoteForm ⊂ SexpShape 2-of-12 composed embed
5848 /// axis. See [`Self::UNQUOTE_SHAPE`] for the alias-chain shape every
5849 /// sibling shares.
5850 pub const SPLICE_SHAPE: SexpShape = crate::ast::QuoteForm::UNQUOTE_SPLICE_SHAPE;
5851
5852 /// Closed-set forced-arity ALL array over the canonical
5853 /// [`SexpShape`] embed targets on the UnquoteForm ⊂ SexpShape
5854 /// 2-of-12 composed carving, in declaration order matching
5855 /// [`Self::ALL`] element-wise (pinned by
5856 /// `unquote_form_shapes_align_with_all_by_index`). Sibling posture
5857 /// to [`Self::MARKERS`] (`[&'static str; 2]` — reader-prefix bytes),
5858 /// [`Self::LEADS`] (`[char; 1]` — SHAPE-ASYMMETRIC-COLLAPSE distinct-
5859 /// lead byte), [`Self::IAC_FORGE_TAGS`] (`[&'static str; 2]` — iac-
5860 /// forge canonical-form tags), [`Self::LABELS`] (`[&'static str; 2]`
5861 /// — diagnostic labels), [`Self::HASH_DISCRIMINATORS`] (`[u8; 2]` —
5862 /// outer-Sexp cache-key bytes), and [`Self::PROMOTIONS`]
5863 /// (`[(Self, char, Self); 1]` — SHAPE-ASYMMETRIC-COLLAPSE promotion
5864 /// triples) on the SAME closed-set UnquoteForm algebra; where those
5865 /// six arrays lift per-role sub-vocabularies on the substitution
5866 /// subset algebra, this array lifts the per-role [`SexpShape`]
5867 /// embed-target sub-vocabulary at the same `[_; 2]` forced arity.
5868 ///
5869 /// Aliased through [`crate::ast::QuoteForm::UNQUOTE_SHAPE`] +
5870 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_SHAPE`] via the per-role
5871 /// [`Self::UNQUOTE_SHAPE`] + [`Self::SPLICE_SHAPE`] constants — the
5872 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset carving of the parent's
5873 /// 4-of-12 quote-family SHAPES. Sibling posture to
5874 /// [`crate::ast::QuoteForm::SHAPES`] (`[SexpShape; 4]`) —
5875 /// the parent superset's peer array on the QuoteForm ⊂ SexpShape
5876 /// 4-of-12 carving; together with the three closed-set carvings'
5877 /// arrays ([`crate::ast::AtomKind::SHAPES`] 6-of-12,
5878 /// [`crate::ast::QuoteForm::SHAPES`] 4-of-12,
5879 /// [`StructuralKind::SHAPES`] 2-of-12) the four `SHAPES` arrays now
5880 /// close the FULL twelve-arm outer-shape algebra uniformly PLUS the
5881 /// UnquoteForm 2-of-4 subset carving of the quote-family sub-
5882 /// algebra.
5883 ///
5884 /// Pre-lift the two [`SexpShape`] embed targets had NO per-role
5885 /// primitive on this subset algebra — a consumer with an
5886 /// `UnquoteForm` variant in hand at compile time reaching for the
5887 /// canonical embed target had to spell
5888 /// `UnquoteForm::Unquote.sexp_shape()` (runtime dispatch through
5889 /// [`Self::sexp_shape`]'s two-hop `.to_quote_form().sexp_shape()`
5890 /// composition) OR reach across into
5891 /// [`crate::ast::QuoteForm::UNQUOTE_SHAPE`] and re-derive the
5892 /// UnquoteForm ⊂ QuoteForm 2-of-4 subset pairing at the call site.
5893 /// Post-lift the TWO canonical embed targets bind at ONE `pub const`
5894 /// per role on the typed [`UnquoteForm`] subset algebra AND at
5895 /// [`Self::SHAPES`] as a family-wide forced-arity array — a future
5896 /// LSP / REPL completion bar keyed on `UnquoteForm::SHAPES` for the
5897 /// "which SexpShape does this UnquoteForm embed into?" outer-shape
5898 /// column, a `tatara-check` coverage sweep zipping
5899 /// `UnquoteForm::ALL` / `LABELS` / `MARKERS` / `LEADS` /
5900 /// `IAC_FORGE_TAGS` / `HASH_DISCRIMINATORS` / `PROMOTIONS` /
5901 /// `SHAPES` in lockstep for a family-wide (variant, label, marker,
5902 /// lead, iac-forge tag, cache-key byte, promotion triple, embed-
5903 /// target) octuple render, or a Sekiban audit-trail metric jointly
5904 /// labeled by the embed target's SexpShape identity reads through
5905 /// the typed constants on this subset algebra without re-deriving
5906 /// the 2-of-12 composed carving inline.
5907 ///
5908 /// Round-trip identity with the inverse projection
5909 /// [`SexpShape::as_unquote_form`]: for every index `i`,
5910 /// `Self::SHAPES[i].as_unquote_form() == Some(Self::ALL[i])` (pinned
5911 /// by `unquote_form_shapes_align_with_all_by_index_through_as_unquote_form`)
5912 /// — the embed / project section closes as a family-wide array-
5913 /// indexed law on the composed 2-of-12 carving rather than as a
5914 /// per-variant assertion sweep. Adding a hypothetical third
5915 /// template-substitution marker (e.g. `,~` reverse-unquote for
5916 /// destructuring-bind hygiene, `,?` conditional-unquote) extends
5917 /// [`Self::ALL`] AND [`Self::SHAPES`] AND
5918 /// [`crate::ast::QuoteForm::ALL`] AND
5919 /// [`crate::ast::QuoteForm::SHAPES`] AND [`SexpShape::ALL`] AND adds
5920 /// ONE per-role `pub const *_SHAPE` in lockstep — rustc's forced-
5921 /// arity check on the two `[_; N]` arrays fails compilation if
5922 /// EITHER ALL array grows without the other, AND the peer
5923 /// [`SexpShape::as_unquote_form`] arm must grow in lockstep to
5924 /// preserve the round-trip identity.
5925 ///
5926 /// Theory anchor: THEORY.md §III — the typescape; the two canonical
5927 /// [`SexpShape`] embed targets bind at ONE typed `[SexpShape; 2]`
5928 /// array on the closed-set UnquoteForm subset algebra rather than at
5929 /// zero-primitive-on-this-subset-plus-two-inline-lookups-through-
5930 /// two-hop-composition scattered across the substrate. THEORY.md
5931 /// §II.1 invariant 2 — free middle; the (embed, project) pair binds
5932 /// at THREE typed sites now — the projection method
5933 /// [`Self::sexp_shape`], this family-wide array, AND the peer inverse
5934 /// [`SexpShape::as_unquote_form`] — with rustc-enforced consistency
5935 /// across all three via the array-indexed round-trip law. THEORY.md
5936 /// §V.1 — knowable platform; the family's cardinality becomes a
5937 /// TYPE-level constant on the substrate algebra rather than a per-
5938 /// consumer runtime dispatch through the two-hop composition.
5939 /// THEORY.md §VI.1 — generation over composition; the family-wide
5940 /// contract sweeps (alignment with `ALL`, round-trip through
5941 /// `as_unquote_form`, membership through `sexp_shape`, pairwise
5942 /// injectivity across the two embed targets, alias byte-equality
5943 /// with QuoteForm's per-role SHAPE constants) emerge from the
5944 /// composition of TWO substrate primitives (this `pub const` array +
5945 /// the two per-role `pub const *_SHAPE` aliases) rather than as
5946 /// per-variant inline assertions duplicated at each call site.
5947 pub const SHAPES: [SexpShape; 2] = [Self::UNQUOTE_SHAPE, Self::SPLICE_SHAPE];
5948
5949 /// Project the 2-of-4 template-substitution subset marker into its
5950 /// matching [`SexpShape`] variant on the outer [`crate::ast::Sexp`]
5951 /// algebra's twelve-variant shape lattice — [`Self::Unquote`] →
5952 /// [`SexpShape::Unquote`], [`Self::Splice`] → [`SexpShape::UnquoteSplice`].
5953 /// Section peer of [`SexpShape::as_unquote_form`] on the 2-of-12
5954 /// carving that names the template-substitution wrappers; closes the
5955 /// (embed, project) algebra dual on the `UnquoteForm ⊂ SexpShape`
5956 /// typed-shape lattice, sibling of the already-closed
5957 /// ([`crate::ast::AtomKind::sexp_shape`], [`SexpShape::as_atom_kind`])
5958 /// dual on the 6-of-12 atomic-payload carving AND
5959 /// ([`crate::ast::QuoteForm::sexp_shape`], [`SexpShape::as_quote_form`])
5960 /// dual on the 4-of-12 quote-family carving.
5961 ///
5962 /// Composition law: `self.sexp_shape() ==
5963 /// self.to_quote_form().sexp_shape()` for every `self: UnquoteForm`.
5964 /// The (subset marker, outer [`SexpShape`]) pairing binds at ONE
5965 /// closed-set match on the superset's
5966 /// [`crate::ast::QuoteForm::sexp_shape`] rather than at a parallel
5967 /// two-arm inline match table on this subset — matching the posture
5968 /// [`Self::marker`] takes through [`crate::ast::QuoteForm::prefix`]
5969 /// and [`Self::wrap`] takes through [`crate::ast::QuoteForm::wrap`].
5970 /// Round-trip law (section-for-retraction with the outer projection
5971 /// sibling): `self.sexp_shape().as_unquote_form() == Some(self)` for
5972 /// every `self: UnquoteForm`.
5973 ///
5974 /// Pre-lift the (UnquoteForm variant, SexpShape variant) pairing had
5975 /// NO typed projection on this subset algebra — a template rewriter
5976 /// with an `UnquoteForm` marker in hand wanting the outer
5977 /// [`SexpShape`] identity (e.g., a future `tatara-check` predicate
5978 /// that filters diagnostics to "this rejection was on a template-
5979 /// substitution wrapper", a future LSP hover that renders a typed
5980 /// marker's outer-shape witness alongside its punctuation, a future
5981 /// macro-hygiene analyzer that keys on the outer shape of a captured
5982 /// substitution point) had to spell the two-step composition
5983 /// `uf.to_quote_form().sexp_shape()` at every callsite. Post-lift
5984 /// the composition binds at ONE typed-algebra method on the closed-
5985 /// set `UnquoteForm` algebra, and the (embed, project) pair with
5986 /// [`SexpShape::as_unquote_form`] forms an `Iso(UnquoteForm,
5987 /// UnquoteShape ⊂ SexpShape)` on the substitution-subset carving.
5988 ///
5989 /// Theory anchor: THEORY.md §V.1 — knowable platform; the (subset
5990 /// marker, outer shape) pairing becomes a TYPE projection on the
5991 /// substrate algebra rather than a per-callsite `.to_quote_form()
5992 /// .sexp_shape()` two-step. THEORY.md §II.1 invariant 2 — free
5993 /// middle; every consumer that has an `UnquoteForm` marker and
5994 /// wants the outer [`SexpShape`] routes through the SAME typed
5995 /// method. THEORY.md §VI.1 — generation over composition; the
5996 /// pairing emerges from ONE typed-algebra composition on the subset
5997 /// algebra rather than from parallel per-consumer per-variant
5998 /// literals.
5999 ///
6000 /// Frontier inspiration: MLIR's `mlir::OperationName` typed
6001 /// projection from a subset-op-family value into the parent
6002 /// `OperationName` taxonomy — the (subset op, taxonomic identity)
6003 /// pairing lives at ONE typed projection on the subset algebra,
6004 /// composed through the parent op-family's typed identity face.
6005 /// `UnquoteForm::sexp_shape` is the Rust-typed peer on the closed-
6006 /// set `UnquoteForm` algebra with [`SexpShape`] standing in for
6007 /// MLIR's parent `OperationName` taxonomy.
6008 #[must_use]
6009 pub fn sexp_shape(self) -> SexpShape {
6010 self.to_quote_form().sexp_shape()
6011 }
6012
6013 /// Project the 2-of-4 template-substitution subset marker into its
6014 /// canonical iac-forge interop tag — [`Self::Unquote`] →
6015 /// `"unquote"`, [`Self::Splice`] → `"unquote-splicing"`. The
6016 /// Common-Lisp-canonical tag string [`crate::ast::QuoteForm::iac_forge_tag`]
6017 /// projects on the superset carving, restricted through
6018 /// [`Self::to_quote_form`] to the substitution-subset carving.
6019 ///
6020 /// Composition law: `self.iac_forge_tag() ==
6021 /// self.to_quote_form().iac_forge_tag()` for every `self:
6022 /// UnquoteForm`. The body composes [`Self::to_quote_form`] (the
6023 /// typed 2-of-4 subset → superset projection) with
6024 /// [`crate::ast::QuoteForm::iac_forge_tag`] (the canonical 4-of-4
6025 /// interop-tag projection), so the two `&'static str` literals live
6026 /// at ONE canonical site (`QuoteForm::iac_forge_tag`'s
6027 /// Unquote/UnquoteSplice arms in `ast.rs`) — the (subset marker,
6028 /// canonical iac-forge tag string) pairing binds at ONE closed-set
6029 /// match on the superset algebra rather than at a parallel two-arm
6030 /// inline match table on this subset. Matches the posture
6031 /// [`Self::marker`] takes through [`crate::ast::QuoteForm::prefix`],
6032 /// [`Self::wrap`] takes through [`crate::ast::QuoteForm::wrap`],
6033 /// and [`Self::sexp_shape`] takes through
6034 /// [`crate::ast::QuoteForm::sexp_shape`] — the FOURTH
6035 /// composition-through-`to_quote_form` axis the closed-set subset
6036 /// algebra closes.
6037 ///
6038 /// The `&'static str` lifetime is load-bearing: every future iac-
6039 /// forge consumer with an `UnquoteForm` marker in hand projects
6040 /// through this method into the canonical 2-element-list head
6041 /// without an allocation, parallel to how [`Self::marker`] projects
6042 /// the reader-punctuation vocabulary. The tag stays intentionally
6043 /// disjoint from [`Self::marker`] (reader punctuation: `","` /
6044 /// `",@"`) AND from [`Self::sexp_shape`]'s
6045 /// [`crate::error::SexpShape::label`] rendering (diagnostic label:
6046 /// `"unquote"` / `"unquote-splice"`) at the substitution-subset's
6047 /// `Splice` arm — the CL canonical form REQUIRES the
6048 /// `unquote-splicing` spelling, while the substrate's diagnostic
6049 /// surface renders `unquote-splice`. The two projections key the
6050 /// SAME closed-set on TWO distinct boundaries; consolidating them
6051 /// would silently break either the iac-forge canonical-form round-
6052 /// trip OR the operator-facing diagnostic surface. That distinction
6053 /// is pinned bit-for-bit through the composition law: this method
6054 /// AGREES with [`crate::ast::QuoteForm::iac_forge_tag`] at every
6055 /// variant AND disagrees with [`crate::error::SexpShape::label`]
6056 /// at the `Splice` arm through the same closed-set typed algebra.
6057 ///
6058 /// Pre-lift the (UnquoteForm variant, iac-forge tag) pairing had
6059 /// NO typed projection on this subset algebra — a future consumer
6060 /// with an `UnquoteForm` marker wanting the canonical iac-forge
6061 /// tag (e.g. a future template rewriter emitting canonical-form
6062 /// diagnostics keyed on the substitution-subset marker, a future
6063 /// `tatara-check` predicate that renders `(check-iac-forge-tag
6064 /// ,x)` for a substitution-subset value, a future audit-trail
6065 /// metric jointly labeled by the interop tag on a template-
6066 /// substitution rejection) had to spell the two-step composition
6067 /// `uf.to_quote_form().iac_forge_tag()` at every callsite —
6068 /// exactly the composition the docstring for [`Self::wrap`]
6069 /// explicitly names as the NEXT deferred sweep this lift picks up.
6070 /// Post-lift the composition binds at ONE typed-algebra method on
6071 /// the closed-set `UnquoteForm` algebra sitting next to the three
6072 /// prior sibling projections ([`Self::marker`], [`Self::wrap`],
6073 /// [`Self::sexp_shape`]), closing the FOURTH
6074 /// composition-through-`to_quote_form` axis on the subset — every
6075 /// substrate-surface axis the superset algebra carries
6076 /// ([`crate::ast::QuoteForm::prefix`],
6077 /// [`crate::ast::QuoteForm::wrap`],
6078 /// [`crate::ast::QuoteForm::sexp_shape`],
6079 /// [`crate::ast::QuoteForm::iac_forge_tag`]) now has a matching
6080 /// composition on the substitution subset.
6081 ///
6082 /// Theory anchor: THEORY.md §V.1 — knowable platform; the (subset
6083 /// marker, canonical iac-forge tag) pairing becomes a TYPE
6084 /// projection on the substrate algebra rather than a per-callsite
6085 /// `.to_quote_form().iac_forge_tag()` two-step. THEORY.md §II.1
6086 /// invariant 2 — free middle; every consumer that has an
6087 /// `UnquoteForm` marker and wants the canonical iac-forge tag
6088 /// routes through the SAME typed method. THEORY.md §VI.1 —
6089 /// generation over composition; the pairing emerges from ONE
6090 /// typed-algebra composition on the subset algebra rather than
6091 /// from parallel per-consumer per-variant literals. A future
6092 /// third template-substitution marker (e.g. `,~` reverse-unquote)
6093 /// extends [`Self::ALL`] + [`Self::to_quote_form`]'s dispatch
6094 /// table in lockstep — rustc-enforced through the closed-set
6095 /// exhaustiveness — with THIS method inheriting the extension
6096 /// through the [`crate::ast::QuoteForm::iac_forge_tag`]
6097 /// composition site without a per-site edit.
6098 ///
6099 /// Frontier inspiration: Racket's `syntax-source-module` +
6100 /// `syntax->datum` interop chain paired with a substitution-subset
6101 /// pattern-class projection — the (subset pattern, canonical
6102 /// interop identity) pairing lives at ONE typed projection on the
6103 /// subset algebra, composed through the parent syntactic-form
6104 /// interop identity face. `UnquoteForm::iac_forge_tag` is the
6105 /// Rust-typed peer on the closed-set `UnquoteForm` algebra with
6106 /// [`crate::ast::QuoteForm::iac_forge_tag`] standing in for
6107 /// Racket's parent syntactic-form interop identity taxonomy.
6108 /// MLIR's `mlir::OperationName::getStringRef()` typed projection
6109 /// from a subset-op-family value into the canonical cross-crate
6110 /// op-identity string — the (subset op, canonical string) pairing
6111 /// lives at ONE typed projection on the subset algebra, composed
6112 /// through the parent op-family's typed identity face.
6113 /// `UnquoteForm::iac_forge_tag` is the Rust-typed peer on the
6114 /// closed-set `UnquoteForm` algebra with `iac_forge_tag` standing
6115 /// in for MLIR's `getStringRef` cross-boundary identity face.
6116 #[must_use]
6117 pub fn iac_forge_tag(self) -> &'static str {
6118 self.to_quote_form().iac_forge_tag()
6119 }
6120
6121 /// Project the 2-of-4 template-substitution subset marker into its
6122 /// canonical substrate diagnostic label — [`Self::Unquote`] →
6123 /// `"unquote"`, [`Self::Splice`] → `"unquote-splice"`. Feeds the
6124 /// same `SexpShape::label`-backed diagnostic surface consumers
6125 /// reach for on the parent [`crate::ast::QuoteForm::label`]
6126 /// projection, restricted through [`Self::to_quote_form`] to the
6127 /// substitution-subset carving.
6128 ///
6129 /// Composition law: `self.label() ==
6130 /// self.to_quote_form().label()` for every `self: UnquoteForm`.
6131 /// The body composes [`Self::to_quote_form`] (the typed 2-of-4
6132 /// subset → superset projection) with
6133 /// [`crate::ast::QuoteForm::label`] (the canonical 4-of-4
6134 /// diagnostic-label projection through
6135 /// [`crate::ast::QuoteForm::sexp_shape`] +
6136 /// [`SexpShape::label`]), so the two `&'static str` literals live
6137 /// at ONE canonical site ([`SexpShape::UNQUOTE_LABEL`] /
6138 /// [`SexpShape::UNQUOTE_SPLICE_LABEL`] in `error.rs`) — the
6139 /// (subset marker, canonical diagnostic label) pairing binds at
6140 /// ONE closed-set match on the superset algebra rather than at a
6141 /// parallel two-arm inline match table on this subset. Matches
6142 /// the posture [`Self::marker`] takes through
6143 /// [`crate::ast::QuoteForm::prefix`], [`Self::wrap`] takes through
6144 /// [`crate::ast::QuoteForm::wrap`], [`Self::sexp_shape`] takes
6145 /// through [`crate::ast::QuoteForm::sexp_shape`], and
6146 /// [`Self::iac_forge_tag`] takes through
6147 /// [`crate::ast::QuoteForm::iac_forge_tag`] — the FIFTH
6148 /// composition-through-`to_quote_form` axis the closed-set subset
6149 /// algebra closes.
6150 ///
6151 /// The `&'static str` lifetime is load-bearing: every future
6152 /// diagnostic consumer with an `UnquoteForm` marker in hand
6153 /// projects through this method into the canonical
6154 /// SexpShape-label vocabulary without an allocation, parallel to
6155 /// how [`Self::marker`] projects the reader-punctuation vocabulary
6156 /// and [`Self::iac_forge_tag`] projects the cross-crate iac-forge
6157 /// vocabulary. The diagnostic label stays intentionally distinct
6158 /// from [`Self::marker`] (reader punctuation: `","` / `",@"`) AND
6159 /// diverges INTENTIONALLY from [`Self::iac_forge_tag`] at the
6160 /// `Splice` arm — `label` renders `"unquote-splice"` (substrate
6161 /// diagnostic surface) while `iac_forge_tag` renders `"unquote-
6162 /// splicing"` (Common-Lisp canonical form). The typed method
6163 /// documents the boundary-distinct invariant structurally: a
6164 /// future "consolidation" that homogenizes the two axes would
6165 /// have to touch this method explicitly.
6166 ///
6167 /// Note: `#[closed_set(via = "marker")]` on the enum's derive
6168 /// wires the trait-side [`tatara_closed_set::ClosedSet::label`] projection to
6169 /// [`Self::marker`] (the reader-punctuation byte), NOT to this
6170 /// inherent `label` (which is the SexpShape-diagnostic label).
6171 /// The inherent surface takes precedence for direct `uf.label()`
6172 /// calls; generic consumers reaching through the trait
6173 /// (`<UnquoteForm as ClosedSet>::label(uf)`) still see the marker.
6174 /// This mirrors [`crate::ast::QuoteForm::label`]'s posture on the
6175 /// parent superset ([`crate::ast::QuoteForm`]'s trait `label` is
6176 /// wired via `#[closed_set(via = "prefix")]` to `prefix`, while
6177 /// its inherent `label` returns `sexp_shape().label()`) — the
6178 /// UnquoteForm subset now closes the SAME (inherent-diagnostic,
6179 /// trait-punctuation) split on its subset algebra.
6180 ///
6181 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
6182 /// (subset marker, canonical diagnostic label) pairing becomes a
6183 /// TYPE projection on the substrate algebra rather than a per-
6184 /// callsite `.to_quote_form().label()` two-step. THEORY.md §II.1
6185 /// invariant 2 — free middle; every consumer that has an
6186 /// `UnquoteForm` marker and wants the canonical diagnostic label
6187 /// routes through the SAME typed method. THEORY.md §VI.1 —
6188 /// generation over composition; the pairing emerges from ONE
6189 /// typed-algebra composition on the subset algebra rather than
6190 /// from parallel per-consumer per-variant literals. A future
6191 /// third template-substitution marker (e.g. `,~` reverse-unquote)
6192 /// extends [`Self::ALL`] + [`Self::to_quote_form`]'s dispatch
6193 /// table in lockstep — rustc-enforced through the closed-set
6194 /// exhaustiveness — with THIS method inheriting the extension
6195 /// through the [`crate::ast::QuoteForm::label`] composition site
6196 /// without a per-site edit.
6197 #[must_use]
6198 pub fn label(self) -> &'static str {
6199 self.to_quote_form().label()
6200 }
6201
6202 /// Stable, per-variant byte discriminator that paired with the
6203 /// substitution-subset outer-shape hash body builds the substrate's
6204 /// [`Hash for Sexp`](crate::ast::Sexp) projection at the two
6205 /// template-substitution arms — `5u8` for [`Self::Unquote`], `6u8`
6206 /// for [`Self::Splice`]. The byte values are load-bearing because
6207 /// the macro-expansion cache ([`crate::macro_expand::Expander`]'s
6208 /// cache) keys on the hash of `(macro_name, args)`, and every
6209 /// [`crate::ast::Sexp::Unquote`] / [`crate::ast::Sexp::UnquoteSplice`]
6210 /// wrapper participates in that hash through the superset's
6211 /// [`crate::ast::QuoteForm::hash_discriminator`] arms — changing a
6212 /// discriminator here silently invalidates every cached expansion
6213 /// across the substrate AND risks collision with the reserved bytes
6214 /// the other outer-Sexp carvings' arms use (`1u8` for the
6215 /// [`crate::ast::Sexp::Atom`] outer-carve marker, `{0, 2}` for the
6216 /// [`StructuralKind::hash_discriminator`] structural-residual
6217 /// carving, `{3, 4}` for the non-substitution quote-family arms
6218 /// [`crate::ast::QuoteForm::Quote`] and
6219 /// [`crate::ast::QuoteForm::Quasiquote`]).
6220 ///
6221 /// Composition law: `self.hash_discriminator() ==
6222 /// self.to_quote_form().hash_discriminator()` for every `self:
6223 /// UnquoteForm`. The body composes [`Self::to_quote_form`] (the
6224 /// typed 2-of-4 subset → superset projection) with
6225 /// [`crate::ast::QuoteForm::hash_discriminator`] (the canonical
6226 /// 4-of-4 cache-key byte projection), so the two `5u8`/`6u8`
6227 /// literals live at ONE canonical site
6228 /// ([`crate::ast::QuoteForm::hash_discriminator`]'s Unquote /
6229 /// UnquoteSplice arms in `ast.rs`) rather than at a parallel
6230 /// two-arm inline table on this subset. Matches the posture
6231 /// [`Self::marker`] takes through [`crate::ast::QuoteForm::prefix`],
6232 /// [`Self::wrap`] takes through [`crate::ast::QuoteForm::wrap`],
6233 /// [`Self::sexp_shape`] takes through
6234 /// [`crate::ast::QuoteForm::sexp_shape`], and
6235 /// [`Self::iac_forge_tag`] takes through
6236 /// [`crate::ast::QuoteForm::iac_forge_tag`] — the FIFTH
6237 /// composition-through-`to_quote_form` axis the closed-set subset
6238 /// algebra closes.
6239 ///
6240 /// Pre-lift the (UnquoteForm variant, cache-key byte) pairing had
6241 /// NO typed projection on this subset algebra — a future consumer
6242 /// with an [`UnquoteForm`] marker in hand wanting the outer-Sexp
6243 /// cache-key byte (e.g. a future template rewriter that keys a
6244 /// substitution-subset audit-trail metric on the
6245 /// [`Hash for Sexp`](crate::ast::Sexp) cache-key partition, a
6246 /// future `tatara-check` predicate that verifies the substitution-
6247 /// subset carving's cache-key partition sits inside `{5, 6}` at a
6248 /// substrate coherence gate, a future `TypedRewriter<UnquoteFormOp>`
6249 /// that decides cache invalidation on the substitution-subset
6250 /// discriminator) had to spell the two-step composition
6251 /// `uf.to_quote_form().hash_discriminator()` at every callsite.
6252 /// Post-lift the composition binds at ONE typed-algebra method on
6253 /// the closed-set [`UnquoteForm`] algebra sitting next to the four
6254 /// prior sibling projections ([`Self::marker`], [`Self::wrap`],
6255 /// [`Self::sexp_shape`], [`Self::iac_forge_tag`]), closing the
6256 /// FIFTH composition-through-`to_quote_form` axis on the subset
6257 /// — every substrate-surface axis the superset algebra carries
6258 /// ([`crate::ast::QuoteForm::prefix`],
6259 /// [`crate::ast::QuoteForm::wrap`],
6260 /// [`crate::ast::QuoteForm::sexp_shape`],
6261 /// [`crate::ast::QuoteForm::iac_forge_tag`],
6262 /// [`crate::ast::QuoteForm::hash_discriminator`]) now has a
6263 /// matching composition on the substitution subset.
6264 ///
6265 /// `pub(crate)` because the byte-discriminator surface is an
6266 /// implementation detail of the substrate's
6267 /// [`Hash for Sexp`](crate::ast::Sexp) cache-key contract; exposing
6268 /// it publicly would leak the cache-key shape through the API
6269 /// without enabling any external consumer the public projections
6270 /// ([`Self::marker`], [`Self::wrap`], [`Self::sexp_shape`],
6271 /// [`Self::iac_forge_tag`]) don't already serve. Same posture as
6272 /// [`crate::ast::QuoteForm::hash_discriminator`],
6273 /// [`StructuralKind::hash_discriminator`],
6274 /// [`crate::ast::AtomKind::hash_discriminator`], and
6275 /// [`SexpShape::hash_discriminator`] — every `hash_discriminator`
6276 /// projection on the substrate's typed-shape family is
6277 /// crate-private.
6278 ///
6279 /// Theory anchor: THEORY.md §V.1 — knowable platform; the (subset
6280 /// marker, cache-key byte) pairing becomes a TYPE projection on the
6281 /// substrate algebra rather than a per-callsite
6282 /// `.to_quote_form().hash_discriminator()` two-step. THEORY.md
6283 /// §II.1 invariant 2 — free middle; every consumer that has an
6284 /// [`UnquoteForm`] marker and wants the outer-Sexp cache-key byte
6285 /// routes through the SAME typed method, so a regression that
6286 /// drifts ONE consumer's byte from the others cannot reach the
6287 /// substrate's runtime cache. THEORY.md §VI.1 — generation over
6288 /// composition; the pairing emerges from ONE typed-algebra
6289 /// composition on the subset algebra rather than from parallel
6290 /// per-consumer per-variant literals. A future third template-
6291 /// substitution marker (e.g. `,~` reverse-unquote) extends
6292 /// [`Self::ALL`] + [`Self::to_quote_form`]'s dispatch table in
6293 /// lockstep — rustc-enforced through the closed-set exhaustiveness
6294 /// — with THIS method inheriting the extension through the
6295 /// [`crate::ast::QuoteForm::hash_discriminator`] composition site
6296 /// without a per-site edit.
6297 ///
6298 /// Frontier inspiration: MLIR's `mlir::TypeID::get<T>()` typed
6299 /// projection from a subset-op-family value into the canonical
6300 /// per-type identity byte the IR framework hashes on — the (subset
6301 /// op, canonical hash identity) pairing lives at ONE typed
6302 /// projection on the subset algebra, composed through the parent
6303 /// op-family's typed identity face. `UnquoteForm::hash_discriminator`
6304 /// is the Rust-typed peer on the closed-set [`UnquoteForm`]
6305 /// algebra with [`crate::ast::QuoteForm::hash_discriminator`]
6306 /// standing in for MLIR's parent op-family `TypeID` face.
6307 ///
6308 /// The `#[allow(dead_code)]` posture is deliberate: the substrate's
6309 /// current [`Hash for Sexp`](crate::ast::Sexp) body composes
6310 /// through [`crate::ast::QuoteForm::hash_discriminator`] at the
6311 /// four quote-family arms uniformly rather than splitting the
6312 /// dispatch into (2 non-substitution + 2 substitution) arms — so
6313 /// no non-test caller currently reaches THIS subset-algebra
6314 /// method's discriminator projection. The lift lands the
6315 /// substrate primitive so future consumers keyed on the
6316 /// substitution-subset carving specifically (a future
6317 /// [`crate::macro_expand::Expander`] cache-invalidation predicate
6318 /// that decides on the 2-of-4 substitution-subset only, a future
6319 /// `tatara-check` predicate `(check-substitution-subset-cache-key
6320 /// …)` that verifies the substitution-subset carving's cache-key
6321 /// partition sits inside `{5, 6}`, a future
6322 /// `TypedRewriter<UnquoteFormOp>` sweep that keys on the subset
6323 /// discriminator directly) bind to ONE typed algebra method
6324 /// rather than re-deriving the `.to_quote_form()
6325 /// .hash_discriminator()` composition per callsite. Matches the
6326 /// preemptive-primitive posture the prior-run [`Self::wrap`],
6327 /// [`Self::sexp_shape`], and [`Self::iac_forge_tag`] lifts
6328 /// carried before their downstream consumers materialized.
6329 #[must_use]
6330 #[allow(dead_code)]
6331 pub(crate) fn hash_discriminator(self) -> u8 {
6332 self.to_quote_form().hash_discriminator()
6333 }
6334}
6335
6336// `impl std::fmt::Display for UnquoteForm` + `impl std::str::FromStr
6337// for UnquoteForm` + `impl tatara_closed_set::ClosedSet for UnquoteForm` +
6338// `pub struct UnknownUnquoteForm(pub String)` are generated by
6339// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
6340// above. `label` delegates to the inherent `UnquoteForm::marker` via
6341// `#[closed_set(via = "marker")]` so the domain-canonical
6342// punctuation-marker projection (`","` / `",@"`) stays load-bearing at
6343// the inherent surface while the trait surface unifies every
6344// closed-set implementor's projection name onto `label`. The marker
6345// axis stays intentionally disjoint from the structural-axis
6346// `SexpShape` vocabulary (`"unquote"` / `"unquote-splice"`); the
6347// disjointness contract holds at the trait surface exactly because
6348// each implementor's `label` projects its own inherent
6349// axis-vocabulary. The `display` flag emits the substrate-wide
6350// `f.write_str(Self::marker(*self))` block.
6351// `#[closed_set(generate_unknown = "unquote form")]` emits the typed
6352// parse-rejection carrier with the substrate-wide `Debug + Clone +
6353// PartialEq + Eq + thiserror::Error` derives and the `#[error("unknown
6354// unquote form: {0}")]` annotation byte-for-byte; the explicit label
6355// matches the auto-derived `pascal_to_spaced_lowercase("UnquoteForm")`
6356// projection byte-for-byte but pins the pre-lift wording against any
6357// future change to the projection helper's behavior on this name. The
6358// FromStr decode is a linear sweep over `UnquoteForm::ALL` keyed on
6359// `marker`; round-trip + cross-axis rejection (`"unquote"` /
6360// `"unquote-splice"`) pinned by
6361// `unquote_form_marker_round_trips_through_from_str` +
6362// `unquote_form_from_str_rejects_sexp_shape_labels_on_template_marker_axis`.
6363
6364/// Closed-set identifier for a kwargs-path projection — the `form:` label
6365/// shape that a typed-entry kwarg failure renders into the `compile error
6366/// in {form}:` prefix of a `LispError::TypeMismatch` diagnostic. Encodes
6367/// the three reachable path shapes the kwargs gate emits — `:<key>` for a
6368/// named kwarg (`extract_string` / `extract_int` / etc. failure),
6369/// `:<key>[<idx>]` for the Nth item of a list-typed kwarg
6370/// (`extract_string_list` per-item failure), and `kwargs[<idx>]` for an
6371/// even-position slot that failed the "this-position-must-be-a-keyword"
6372/// gate before a key was known (`parse_kwargs` direct call) — as a typed
6373/// borrowed enum, so authoring tools (REPL, LSP, `tatara-check`) bind to
6374/// path-shape identity (`KwargPath::Item { .. }` etc.) rather than
6375/// substring-matching the rendered prefix.
6376///
6377/// Mirror at the kwargs-path-shape boundary of the prior-run
6378/// `MacroDefHead` (macro-definition-head closed set),
6379/// `CompilerSpecIoStage` (disk-persistence surface),
6380/// `TemplateInvariantKind` (bytecode-runtime surface), and `UnquoteForm`
6381/// (template-marker syntactic forms) closed-set lifts: those enums key
6382/// their respective rejection variants on a typed identity carried inside
6383/// the variant's data shape; this enum keys the THREE distinct `form:`
6384/// label shapes emitted by the kwarg-gate's typed-entry chain on a typed
6385/// path identity. The three `format!` literals that used to live inline
6386/// in `domain.rs::kwarg_form` / `kwarg_item_form` / `kwargs_pos_form`
6387/// (three byte-identical `format!` shapes, one per helper) collapse into
6388/// ONE `Display` impl on this enum — the canonical literals (`":<key>"`
6389/// / `":<key>[<idx>]"` / `"kwargs[<idx>]"`) live in ONE place, so a typo
6390/// in any one of the three shapes can never drift independent of the
6391/// others (THEORY.md §VI.1 three-times rule). Adding a fourth path shape
6392/// (e.g., `:<key>.<field>` for nested-struct kwarg failures or
6393/// `:<key>::<variant>` for sum-typed kwarg failures) requires extending
6394/// this enum, which rustc-enforces matching at the `Display` projection
6395/// site.
6396///
6397/// `KwargPath` owns its `key` payload as `String` so it can inhabit
6398/// `LispError::TypeMismatch.form` (and any future error variant) without
6399/// a borrow constraint. The owned shape is the typed-slot promotion the
6400/// prior-run `KwargPath` landing pre-staged: every projection site that
6401/// used to produce a `String` via `KwargPath::Named(key).to_string()` (the
6402/// three sibling helpers `kwarg_form` / `kwarg_item_form` /
6403/// `kwargs_pos_form` and the fourth `kwarg_deserialize_form` helper) now
6404/// produces a typed `KwargPath` value directly; `type_mismatch` and every
6405/// `TypeMismatch.form` consumer pattern-match on the variant identity
6406/// (`KwargPath::Item { key, idx }`, `KwargPath::Slot(idx)`, etc.) instead
6407/// of substring-parsing the rendered prefix.
6408///
6409/// `Copy` is dropped because `String` is not `Copy`; `Clone + Debug +
6410/// PartialEq + Eq` are retained (same posture as every other owned-data
6411/// `LispError` field). The closed-set structural-completeness floor is
6412/// unchanged — only the data ownership changed.
6413///
6414/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set of
6415/// kwargs-path shapes becomes a TYPE rather than three byte-identical
6416/// `format!` literals scattered across helper definitions. THEORY.md
6417/// §VI.1 — generation over composition; the typed enum lands the
6418/// structural-completeness floor for the kwargs-path surface, parallel
6419/// to how `CompilerSpecIoStage` lands it for the disk-persistence
6420/// surface, `MacroDefHead` for the macro-definition-head surface,
6421/// `TemplateInvariantKind` for the bytecode-runtime surface, and
6422/// `UnquoteForm` for the template-marker surface. THEORY.md §II.1
6423/// invariant 1 — typed entry; the kwargs-path's renderable identity is
6424/// part of the proof of WHICH kwarg-gate fired, and the typed enum makes
6425/// that identity first-class — now as load-bearing data on the
6426/// `TypeMismatch` variant rather than as a projection-to-String.
6427#[derive(Debug, Clone, PartialEq, Eq)]
6428pub enum KwargPath {
6429 /// `:<key>` — failure at a named kwarg (`extract_string`,
6430 /// `extract_int`, `extract_float`, `extract_bool`, etc.). The `key`
6431 /// is the offending kwarg's identifier, owned so the variant lives
6432 /// independent of the call frame.
6433 Named(String),
6434 /// `:<key>[<idx>]` — failure at the Nth item of a list-typed kwarg
6435 /// (`extract_string_list` per-item failure). The `key` is the
6436 /// containing kwarg's identifier (owned); `idx` is the 0-based item
6437 /// index inside that kwarg's list value.
6438 Item { key: String, idx: usize },
6439 /// `kwargs[<idx>]` — failure at the Nth slot of the kwargs slice
6440 /// before a key was known (`parse_kwargs`'s
6441 /// "this-position-must-be-a-keyword" gate firing on an even-position
6442 /// slot). `idx` is the 0-based position into the raw kwargs slice
6443 /// (not into a particular kwarg's value).
6444 Slot(usize),
6445}
6446
6447impl KwargPath {
6448 /// Owned constructor for the `:<key>` shape — used by every call site
6449 /// that has a `&str` borrow of the kwarg identifier and wants to lift
6450 /// it into the typed enum without an inline `.to_string()` projection.
6451 #[must_use]
6452 pub fn named(key: &str) -> Self {
6453 Self::Named(key.to_string())
6454 }
6455
6456 /// Owned constructor for the `:<key>[<idx>]` shape — sibling of
6457 /// `named`, threading the per-item index alongside the kwarg key.
6458 #[must_use]
6459 pub fn item(key: &str, idx: usize) -> Self {
6460 Self::Item {
6461 key: key.to_string(),
6462 idx,
6463 }
6464 }
6465
6466 /// Discriminator projection — strips the payload and returns the
6467 /// closed-set [`KwargPathKind`]. The same shape every sibling
6468 /// payload-carrying closed-set enum in the workspace projects through
6469 /// (e.g. `tatara_process::lifetime_clock::AutoTerminate::kind` →
6470 /// [`crate::error::KwargPath`]'s tatara-process cousin
6471 /// `AutoTerminateKind`, `TerminateReason::kind` →
6472 /// `TerminateReasonKind`, `crate::matrix::SelectStrategy::kind` →
6473 /// `SelectStrategyKind`).
6474 ///
6475 /// Consumers that group [`LispError::TypeMismatch`] /
6476 /// [`LispError::KwargDeserialize`] failures by path-shape
6477 /// CATEGORY rather than full path identity (failure-cluster metrics
6478 /// labelled `path_kind=named` / `path_kind=item` / `path_kind=slot`,
6479 /// an LSP that surfaces "this is a per-item failure" before drilling
6480 /// into the bracket-suffix, a future `tatara-check` diagnostic
6481 /// histogram that buckets by kind) project through this method
6482 /// instead of destructuring the variant and discarding the payload at
6483 /// every site. Adding a fourth path shape (e.g., `:<key>.<field>`
6484 /// for nested-struct kwarg failures or `:<key>::<variant>` for
6485 /// sum-typed kwarg failures) requires extending [`KwargPathKind`],
6486 /// which rustc-enforces matching at this projection.
6487 #[must_use]
6488 pub const fn kind(&self) -> KwargPathKind {
6489 match self {
6490 Self::Named(_) => KwargPathKind::Named,
6491 Self::Item { .. } => KwargPathKind::Item,
6492 Self::Slot(_) => KwargPathKind::Slot,
6493 }
6494 }
6495}
6496
6497impl std::fmt::Display for KwargPath {
6498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6499 match self {
6500 Self::Named(key) => write!(f, ":{key}"),
6501 Self::Item { key, idx } => write!(f, ":{key}[{idx}]"),
6502 Self::Slot(idx) => write!(f, "kwargs[{idx}]"),
6503 }
6504 }
6505}
6506
6507/// The closed set of [`KwargPath`] kinds — the discriminator view,
6508/// payload-stripped, that sibling closed-set lifts in this crate carry
6509/// (see [`SexpShape`], [`ExpectedKwargShape`], [`MacroDefHead`],
6510/// [`UnquoteForm`], [`CompilerSpecIoStage`], [`TemplateInvariantKind`]).
6511///
6512/// Mirrors the workspace-wide [`payload-carrier, payload-stripped kind]
6513/// pairing — `AutoTerminate` / `AutoTerminateKind`, `TerminateReason` /
6514/// `TerminateReasonKind`, `SelectStrategy` / `SelectStrategyKind`,
6515/// `ChannelVariant` / `ChannelKind`, `ArtifactVariant` / `ArtifactKind`,
6516/// `EncapsulationTarget` / `EncapsulationKind`. [`KwargPath`] owns the
6517/// per-variant payload (`key: String`, `idx: usize`); [`KwargPathKind`]
6518/// is the `Copy`-able discriminator view callers reach when they want
6519/// the CATEGORY without the payload.
6520///
6521/// Drives the `label` / [`Display`] / [`FromStr`] triad over [`Self::ALL`]
6522/// so a new variant added with an `ALL` entry automatically extends the
6523/// parser, the canonical wire-format projection, and any future
6524/// metrics-label / failure-cluster bucket that needs to enumerate the
6525/// kwargs-path categories. The `[Self; 3]` array literal forces the arity
6526/// so a fourth variant — a hypothetical `Field` for nested-struct kwarg
6527/// failures (`:<key>.<field>`) or `Variant` for sum-typed kwarg failures
6528/// (`:<key>::<variant>`) — cannot land without bumping the constant.
6529///
6530/// Theory anchor: THEORY.md §V.1 — knowable platform; the
6531/// payload-stripped kind becomes a TYPE rather than three byte-identical
6532/// `matches!` discriminator literals scattered across consumers.
6533/// THEORY.md §VI.1 — generation over composition; the typed kind enum
6534/// lands the structural-completeness floor for the path-shape category
6535/// surface, parallel to how [`KwargPath`] lands it for the path-identity
6536/// surface, [`ExpectedKwargShape`] for the expected-shape surface, etc.
6537#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
6538#[closed_set(via = "label", display, generate_unknown = "kwarg path kind")]
6539pub enum KwargPathKind {
6540 /// The kind-view of [`KwargPath::Named`] — `:<key>` failures at a
6541 /// named kwarg (typed-atom extractors, `Option<T>` paths).
6542 Named,
6543 /// The kind-view of [`KwargPath::Item`] — `:<key>[<idx>]` failures
6544 /// at the Nth item of a list-typed kwarg
6545 /// (`extract_string_list` per-item).
6546 Item,
6547 /// The kind-view of [`KwargPath::Slot`] — `kwargs[<idx>]` failures
6548 /// at a kwargs slice slot before a key was known
6549 /// (`parse_kwargs`'s slot-must-be-a-keyword gate).
6550 Slot,
6551}
6552
6553impl KwargPathKind {
6554 /// The closed set — single source of truth for [`Self::label`] /
6555 /// [`Display`] / [`FromStr`]. The `[Self; 3]` arity is forced at the
6556 /// declaration so a fourth variant cannot land without bumping the
6557 /// constant.
6558 pub const ALL: [Self; 3] = [Self::Named, Self::Item, Self::Slot];
6559
6560 /// Canonical `&'static str` category label for [`Self::Named`] —
6561 /// the `:<key>` failure category emitted by every typed-atom
6562 /// extractor (`extract_string`, `extract_int`, `extract_float`,
6563 /// `extract_bool`, and their `Option<T>` siblings). Sibling
6564 /// posture to [`Self::ITEM_LABEL`] (`"item"`) and
6565 /// [`Self::SLOT_LABEL`] (`"slot"`) on the same category-label
6566 /// algebra layer.
6567 ///
6568 /// Pre-lift the same `"named"` bytes lived inline at the
6569 /// [`Self::label`] match arm plus at the sibling truth-table tests
6570 /// (`kwarg_path_kind_all_is_unique_and_complete` sorted-labels
6571 /// pin, `kwarg_path_kind_label_does_not_overlap_kwarg_path_display_renderings`
6572 /// disjointness sweep) — the ≥2 PRIME-DIRECTIVE trigger. Post-lift
6573 /// the (`Named` variant, canonical `&'static str`) pairing binds
6574 /// at ONE `pub const` on the typed [`KwargPathKind`] algebra: the
6575 /// [`Self::label`] arm and every consumer that pins the exact
6576 /// bytes route through this constant. Sibling posture to the
6577 /// closed set of per-role canonical `&'static str` labels on the
6578 /// substrate's other closed-set outer algebras:
6579 /// [`MacroDefHead::DEFMACRO_KEYWORD`] (`"defmacro"`),
6580 /// [`crate::ast::Atom::TRUE_LITERAL`] (`"#t"`),
6581 /// [`crate::ast::QuoteForm::QUOTE_PREFIX`] (`"'"`),
6582 /// [`crate::macro_expand::MacroParams::REST_MARKER`] (`"&rest"`).
6583 pub const NAMED_LABEL: &'static str = "named";
6584
6585 /// Canonical `&'static str` category label for [`Self::Item`] —
6586 /// the `:<key>[<idx>]` failure category emitted by
6587 /// `extract_string_list`'s per-item gate. Sibling posture to
6588 /// [`Self::NAMED_LABEL`] (`"named"`) and [`Self::SLOT_LABEL`]
6589 /// (`"slot"`) on the same category-label algebra layer.
6590 ///
6591 /// Pre-lift the same `"item"` bytes lived inline at the
6592 /// [`Self::label`] match arm plus at the sibling truth-table
6593 /// tests. Post-lift the (`Item` variant, canonical `&'static str`)
6594 /// pairing binds at ONE `pub const` on the typed [`KwargPathKind`]
6595 /// algebra.
6596 pub const ITEM_LABEL: &'static str = "item";
6597
6598 /// Canonical `&'static str` category label for [`Self::Slot`] —
6599 /// the `kwargs[<idx>]` failure category emitted by
6600 /// `parse_kwargs`'s slot-must-be-a-keyword gate. Sibling posture
6601 /// to [`Self::NAMED_LABEL`] (`"named"`) and [`Self::ITEM_LABEL`]
6602 /// (`"item"`) on the same category-label algebra layer.
6603 ///
6604 /// Pre-lift the same `"slot"` bytes lived inline at the
6605 /// [`Self::label`] match arm plus at the sibling truth-table
6606 /// tests. Post-lift the (`Slot` variant, canonical `&'static str`)
6607 /// pairing binds at ONE `pub const` on the typed [`KwargPathKind`]
6608 /// algebra.
6609 pub const SLOT_LABEL: &'static str = "slot";
6610
6611 /// The closed set of three canonical `&'static str` category
6612 /// labels — the [`Self::NAMED_LABEL`] (`"named"`) typed-atom-
6613 /// extractor failure category followed by the [`Self::ITEM_LABEL`]
6614 /// (`"item"`) list-item failure category followed by the
6615 /// [`Self::SLOT_LABEL`] (`"slot"`) kwargs-slice-slot failure
6616 /// category. Canonical declaration order matches [`Self::ALL`] so
6617 /// `Self::LABELS[i] == Self::ALL[i].label()` element-wise —
6618 /// pinned by `kwarg_path_kind_labels_align_with_all_by_index`.
6619 ///
6620 /// Sibling posture to [`MacroDefHead::KEYWORDS`]
6621 /// (`[&'static str; 3]`) — every closed-set algebra now pins its
6622 /// label-projection ALL array at the declaration site via a
6623 /// forced-arity `[&'static str; N]` array whose length fails
6624 /// compilation if a new variant lands without being added to the
6625 /// set. Adding a hypothetical fourth category (a `Root` failure
6626 /// category for a future kwargs-root-level rejection, a `Tail`
6627 /// failure category for a future nested-list-tail extractor)
6628 /// extends [`Self::ALL`] ONCE + [`Self::label`] ONCE +
6629 /// [`Self::LABELS`] ONCE + adds ONE per-role label `pub const`
6630 /// — rustc's forced-arity check on the `[Self; N]` array +
6631 /// `[&'static str; N]` array pair enforces every downstream
6632 /// consumer picks up the extension mechanically.
6633 ///
6634 /// Future consumers that compose against [`Self::LABELS`]: an
6635 /// LSP / REPL completion provider surfacing every legal category
6636 /// name in a `path_kind=` metrics query bar
6637 /// (`Self::LABELS.iter()` is the ONE typed sweep over every legal
6638 /// kwarg-path category), a `tatara-check` coverage assertion
6639 /// (every workspace `.lisp` file's typed-atom rejection must
6640 /// classify to some entry of `Self::LABELS`), a Sekiban
6641 /// audit-trail metric jointly labeled by [`Self::label`] whose
6642 /// metric-label set IS `Self::LABELS` (e.g.
6643 /// `tatara_lisp_kwarg_type_mismatch_total{path_kind="named"}`).
6644 pub const LABELS: [&'static str; 3] = [Self::NAMED_LABEL, Self::ITEM_LABEL, Self::SLOT_LABEL];
6645
6646 /// Project the typed [`KwargPathKind`] to the canonical `&'static str`
6647 /// literal — lowercase byte-equal to the variant name (`"named"` /
6648 /// `"item"` / `"slot"`). The labels are kept distinct from the
6649 /// [`KwargPath::Display`] renderings (`":<key>"` / `":<key>[<idx>]"`
6650 /// / `"kwargs[<idx>]"`) because this projection names the CATEGORY,
6651 /// not the rendered identity — a metrics label `path_kind="named"`
6652 /// makes more sense at the kind boundary than a path-prefix template
6653 /// would.
6654 ///
6655 /// Body routes each arm through the per-role [`Self::NAMED_LABEL`] /
6656 /// [`Self::ITEM_LABEL`] / [`Self::SLOT_LABEL`] `pub const` so the
6657 /// three canonical `&'static str` labels live at ONE `pub const`
6658 /// per variant rather than at TWO sites (the per-role `pub const`
6659 /// AND an inline arm literal). Sibling posture to
6660 /// [`MacroDefHead::keyword`]'s arms routing through the per-role
6661 /// [`MacroDefHead::DEFMACRO_KEYWORD`] / [`MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD`]
6662 /// / [`MacroDefHead::DEFCHECK_KEYWORD`] constants.
6663 ///
6664 /// Same shape every sibling kind-projection in the workspace uses
6665 /// (`AutoTerminateKind::as_str`, `TerminateReasonKind::as_str`,
6666 /// [`SexpShape::label`], [`ExpectedKwargShape::label`]).
6667 #[must_use]
6668 pub const fn label(self) -> &'static str {
6669 match self {
6670 Self::Named => Self::NAMED_LABEL,
6671 Self::Item => Self::ITEM_LABEL,
6672 Self::Slot => Self::SLOT_LABEL,
6673 }
6674 }
6675}
6676
6677// `impl std::fmt::Display for KwargPathKind` + `impl std::str::FromStr
6678// for KwargPathKind` + `impl tatara_closed_set::ClosedSet for KwargPathKind` +
6679// `pub struct UnknownKwargPathKind(pub String)` are generated by
6680// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
6681// above. `label` delegates to the inherent `KwargPathKind::label` via
6682// `#[closed_set(via = "label")]` — the inherent name coincides with
6683// the trait method name here, but the delegation stays explicit so the
6684// SAME wiring shape applies whether the inherent projection is named
6685// `label` / `prefix` / `marker` / `keyword` / `as_str`. The `display`
6686// flag emits the substrate-wide `f.write_str(Self::label(*self))` block.
6687// `#[closed_set(generate_unknown = "kwarg path kind")]` emits the typed
6688// parse-rejection carrier with the substrate-wide `Debug + Clone +
6689// PartialEq + Eq + thiserror::Error` derives and the `#[error("unknown
6690// kwarg path kind: {0}")]` annotation byte-for-byte; the explicit label
6691// matches the auto-derived `pascal_to_spaced_lowercase("KwargPathKind")`
6692// projection byte-for-byte but pins the pre-lift wording. Round-trip +
6693// cross-axis rejection (path-rendering literals `":foo"` / `":foo[0]"` /
6694// `"kwargs[0]"`) pinned by
6695// `kwarg_path_kind_label_round_trips_through_from_str` +
6696// `unknown_kwarg_path_kind_carries_offending_input_verbatim`.
6697
6698/// Closed-set identifier for the `expected:` slot of a
6699/// `LispError::TypeMismatch` diagnostic — the seven reachable
6700/// expected-shape labels the typed-entry kwarg gate emits:
6701/// `Keyword` (the `parse_kwargs` slot-must-be-a-keyword gate),
6702/// `String` / `Int` / `Number` / `Bool` (the typed-atom extractors —
6703/// `extract_string`, `extract_int`, `extract_float`, `extract_bool`,
6704/// and their `Option` siblings, plus `extract_string_list`'s per-item
6705/// `string` gate), `List` (the `extract_vec_via_serde` outer-shape
6706/// gate), and `ListOfStrings` (the `extract_string_list` outer-shape
6707/// gate). Encoded as a typed enum so the closed set becomes
6708/// load-bearing data on `LispError::TypeMismatch.expected` rather than
6709/// a `&'static str` literal scattered across eleven call sites in
6710/// `domain.rs`.
6711///
6712/// Mirror at the expected-shape boundary of the prior-run `KwargPath`
6713/// (kwargs-path-shape closed set), `MacroDefHead` (macro-definition-
6714/// head closed set), `CompilerSpecIoStage` (disk-persistence surface),
6715/// `TemplateInvariantKind` (bytecode-runtime surface), and
6716/// `UnquoteForm` (template-marker syntactic forms) closed-set lifts:
6717/// those enums key their respective rejection variants on a typed
6718/// identity carried inside the variant's data shape; this enum keys
6719/// the SECOND slot (`expected`) of every `LispError::TypeMismatch`
6720/// site on a typed expected-shape identity, alongside the
6721/// already-typed `form: KwargPath`. After this lift the type-mismatch
6722/// variant's identity is fully closed-set typed in TWO of its three
6723/// slots — only `got: &'static str` remains as a `&'static str`
6724/// projection, and that slot's compile-time guarantee is sourced from
6725/// `crate::domain::sexp_type_name`'s exhaustive `Sexp` match.
6726///
6727/// Adding a future expected-shape (e.g. `Float` once `extract_float`
6728/// stops accepting integers, `Symbol` if a future extractor accepts
6729/// only `Sexp::Atom(Symbol)`, or a parameterized `ListOf(Box<Self>)`
6730/// for nested-typed-vec extractors) requires extending this enum,
6731/// which rustc-enforces matching at every projection site
6732/// (`label()`).
6733///
6734/// `label(self) -> &'static str` projects the typed `ExpectedKwargShape`
6735/// back to the canonical literal for `LispError::Display` rendering.
6736/// The `&'static str` lifetime is load-bearing: it lets the variant
6737/// project through this method into the `expected {expected}` slot of
6738/// the `#[error(...)]` annotation without an allocation, parallel to
6739/// how `MacroDefHead::keyword()`, `UnquoteForm::marker()`, and
6740/// `CompilerSpecIoStage::operation()` / `label()` feed their
6741/// respective `LispError::*` Display impls.
6742///
6743/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
6744/// of expected-shape labels becomes a TYPE rather than eleven
6745/// `&'static str` literal call sites scattered across the kwarg
6746/// extractors. A typo in any literal can never drift into the
6747/// diagnostic at runtime; a regression that drifts the expected-shape
6748/// label (e.g. a typo `"strin"` for `"string"`) becomes a type error
6749/// at the call site, not a runtime substring drift. THEORY.md §VI.1 —
6750/// generation over composition; the typed enum lands the structural-
6751/// completeness floor for the expected-shape surface, parallel to how
6752/// `KwargPath` lands it for the kwargs-path surface, `MacroDefHead`
6753/// for the macro-definition-head surface, `UnquoteForm` for the
6754/// template-marker surface, `CompilerSpecIoStage` for the disk-
6755/// persistence surface, and `TemplateInvariantKind` for the bytecode-
6756/// runtime surface. THEORY.md §II.1 invariant 1 — typed entry; the
6757/// expected-shape identity is part of the proof of WHICH typed-entry
6758/// kwarg gate fired, and the typed enum makes that identity first-
6759/// class as load-bearing data on the variant rather than as a
6760/// projection-to-String at the helper boundary.
6761#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
6762#[closed_set(via = "label", display, generate_unknown = "expected kwarg shape")]
6763pub enum ExpectedKwargShape {
6764 /// `"keyword"` — emitted by `parse_kwargs`'s
6765 /// "this-position-must-be-a-keyword" gate when an even-position
6766 /// slot in the kwargs slice isn't a `Sexp::Atom(Keyword(_))`.
6767 Keyword,
6768 /// `"string"` — emitted by `extract_string` /
6769 /// `extract_optional_string` (the kwarg's value isn't a
6770 /// `Sexp::Atom(Str(_))`) AND by `extract_string_list`'s per-item
6771 /// gate (an item inside a list-typed kwarg isn't a string).
6772 String,
6773 /// `"int"` — emitted by `extract_int` / `extract_optional_int`
6774 /// when the kwarg's value isn't a `Sexp::Atom(Int(_))`.
6775 Int,
6776 /// `"number"` — emitted by `extract_float` /
6777 /// `extract_optional_float` when the kwarg's value isn't a
6778 /// numeric atom. Wider than `Int`: `extract_float` accepts both
6779 /// `Sexp::Atom(Float(_))` and `Sexp::Atom(Int(_))` via
6780 /// `Sexp::as_float`, so the expected-shape label is the union
6781 /// "number" rather than the narrower "float".
6782 Number,
6783 /// `"bool"` — emitted by `extract_bool` / `extract_optional_bool`
6784 /// when the kwarg's value isn't a `Sexp::Atom(Bool(_))`.
6785 Bool,
6786 /// `"list"` — emitted by `extract_vec_via_serde`'s outer-shape
6787 /// gate when the kwarg's value isn't a `Sexp::List(_)`. Used by
6788 /// the universal-Deserialize fallthrough for any `Vec<T>` field.
6789 List,
6790 /// `"list of strings"` — emitted by `extract_string_list`'s
6791 /// outer-shape gate when the kwarg's value isn't a
6792 /// `Sexp::List(_)`. Wider than `List`: names the expected
6793 /// element-type so the diagnostic reads `expected list of
6794 /// strings, got string` instead of the bare `expected list, got
6795 /// string`. The per-item gate fires `String` (the narrower
6796 /// expected-shape for the element-type failure).
6797 ListOfStrings,
6798}
6799
6800impl ExpectedKwargShape {
6801 /// The closed set of seven reachable expected-kwarg shapes — single
6802 /// source of truth that drives the [`Self::label`] / [`fmt::Display`]
6803 /// projection AND the [`FromStr`] decode sweep keyed on
6804 /// [`Self::label`]. Adding a hypothetical eighth variant (e.g.
6805 /// `Float` once `extract_float` stops accepting integers, `Symbol`
6806 /// if a future extractor accepts only `Sexp::Atom(Symbol)`, or a
6807 /// parameterized `ListOf(Box<Self>)` for nested-typed-vec
6808 /// extractors) lands at one [`Self::ALL`] entry + one [`Self::label`]
6809 /// arm — exhaustively checked by the compiler (the `[Self; 7]`
6810 /// array literal forces the arity) AND by the per-variant
6811 /// truth-table tests below.
6812 ///
6813 /// Sibling closed-set lift to every other typed-shape enum the
6814 /// substrate carries: this crate's own [`SexpShape::ALL`] (the
6815 /// twelve reachable outer shapes the reader can produce — peer
6816 /// axis on the same `Sexp` algebra, whose vocabulary overlaps
6817 /// with this set on five of seven entries — `"keyword"`,
6818 /// `"string"`, `"int"`, `"bool"`, `"list"` — and does NOT overlap
6819 /// on two — `"number"` ⊎ `"list of strings"`; the overlap is
6820 /// intentional and pinned by the cross-axis tests), and across
6821 /// the workspace ([`MacroDefHead::ALL`], [`UnquoteForm::ALL`],
6822 /// [`crate::ast::AtomKind::ALL`], [`crate::ast::QuoteForm::ALL`],
6823 /// `ConditionKind::ALL`, `ProcessPhase::ALL`,
6824 /// `ProcessSignal::ALL`, `ChannelKind::ALL`, `IntentKind::ALL`,
6825 /// `LifetimeKind::ALL`, `RequestorKind::ALL`, `ReceiptKind::ALL`,
6826 /// …) every one of which paired its typed projection with `ALL`
6827 /// before this lift.
6828 ///
6829 /// Future consumers that compose against `ALL`: LSP / REPL
6830 /// completion for the operator-facing rendered expected-shape
6831 /// label (every `expected X, ...` substring in `LispError`'s
6832 /// rendered diagnostics keys on this set's projection through
6833 /// [`Self::label`]); `tatara-check` coverage assertions over
6834 /// which expected-shape variants reach a `TypeMismatch.expected`
6835 /// arm at all — the typed sweep replaces the per-call-site
6836 /// vocabulary of seven `&'static str` literals; any future
6837 /// audit-trail metric jointly labeled by [`Self::label`] (e.g.
6838 /// `tatara_lisp_type_mismatch_total{expected="number"}`) — the
6839 /// metric label set IS [`Self::ALL`] mapped through
6840 /// [`Self::label`].
6841 pub const ALL: [Self; 7] = [
6842 Self::Keyword,
6843 Self::String,
6844 Self::Int,
6845 Self::Number,
6846 Self::Bool,
6847 Self::List,
6848 Self::ListOfStrings,
6849 ];
6850
6851 /// Canonical `&'static str` bytes for the `Keyword` expected-shape —
6852 /// aliases [`SexpShape::KEYWORD_LABEL`] on the ExpectedKwargShape ⊂
6853 /// SexpShape 5-of-7 subset carving so the shared `"keyword"`
6854 /// vocabulary binds at ONE `pub const` on the parent-superset's
6855 /// `SexpShape::KEYWORD_LABEL` arm rather than at TWO independent
6856 /// literal-discipline sites (the per-role `pub const` here AND the
6857 /// per-role `pub const` on `SexpShape`). Per-role peer of
6858 /// `Self::Keyword` on the closed-set outer algebra; every consumer
6859 /// of the `"keyword"` bytes (Display, FromStr, the derive-
6860 /// generated `UnknownExpectedKwargShape` carrier, LSP completion,
6861 /// audit-trail metric labels) routes through THIS constant, so a
6862 /// spelling migration (a hypothetical port to `":keyword"`,
6863 /// `"kwarg-key"`, or a case-fold to `"Keyword"`) at
6864 /// [`SexpShape::KEYWORD_LABEL`] propagates HERE structurally at
6865 /// rustc time rather than through a parallel literal edit.
6866 ///
6867 /// Sibling posture to [`Self::STRING_LABEL`], [`Self::INT_LABEL`],
6868 /// [`Self::BOOL_LABEL`], [`Self::LIST_LABEL`] — the FIVE
6869 /// ExpectedKwargShape variants that share bytes with a matching
6870 /// [`SexpShape`] carving arm all bind through this same alias
6871 /// chain. The TWO non-overlapping siblings [`Self::NUMBER_LABEL`]
6872 /// (`"number"` — the wider numeric-union label distinct from
6873 /// [`SexpShape::FLOAT_LABEL`]'s `"float"`) and
6874 /// [`Self::LIST_OF_STRINGS_LABEL`] (`"list of strings"` — an
6875 /// element-typed refinement with no [`SexpShape`] peer) stay as
6876 /// direct literals since there is no superset arm to alias
6877 /// through. The 5-of-7 carving IS the intersection
6878 /// `ExpectedKwargShape::ALL ∩ SexpShape::ALL` on the
6879 /// `&'static str` label vocabulary.
6880 pub const KEYWORD_LABEL: &'static str = SexpShape::KEYWORD_LABEL;
6881
6882 /// Canonical `&'static str` bytes for the `String` expected-shape —
6883 /// aliases [`SexpShape::STRING_LABEL`] on the ExpectedKwargShape ⊂
6884 /// SexpShape 5-of-7 subset carving. Per-role peer of `Self::String`
6885 /// on the closed-set outer algebra. Emitted by `extract_string` /
6886 /// `extract_optional_string` AND by `extract_string_list`'s per-
6887 /// item gate. See [`Self::KEYWORD_LABEL`] for the alias-chain shape
6888 /// every sibling in the 5-of-7 carving shares.
6889 pub const STRING_LABEL: &'static str = SexpShape::STRING_LABEL;
6890
6891 /// Canonical `&'static str` bytes for the `Int` expected-shape —
6892 /// aliases [`SexpShape::INT_LABEL`] on the ExpectedKwargShape ⊂
6893 /// SexpShape 5-of-7 subset carving. Per-role peer of `Self::Int`
6894 /// on the closed-set outer algebra; the sibling
6895 /// [`Self::NUMBER_LABEL`] distinguishes the wider numeric-union
6896 /// case `extract_float` emits (which has NO [`SexpShape`] peer —
6897 /// [`SexpShape::FLOAT_LABEL`] is the narrower `"float"`). See
6898 /// [`Self::KEYWORD_LABEL`] for the alias-chain shape.
6899 pub const INT_LABEL: &'static str = SexpShape::INT_LABEL;
6900
6901 /// Canonical `&'static str` bytes for the `Number` expected-shape —
6902 /// emitted by `extract_float` / `extract_optional_float` when the
6903 /// kwarg's value isn't a numeric atom. The "number" naming (not
6904 /// "float") is load-bearing: `extract_float` accepts BOTH
6905 /// `Sexp::Atom(Float(_))` and `Sexp::Atom(Int(_))` via
6906 /// `Sexp::as_float`, so the expected-shape label names the union
6907 /// rather than the narrower Float element-type. INTENTIONALLY NOT
6908 /// aliased through [`SexpShape`]: the `SexpShape::Float` variant's
6909 /// label is `"float"` (the NARROWER element-type), not `"number"`,
6910 /// so this constant has NO peer on the parent-superset algebra to
6911 /// alias through. Divergence pinned by
6912 /// `sexp_shape_atom_carving_labels_align_with_expected_kwarg_shape_labels`'s
6913 /// `assert_ne!(SexpShape::FLOAT_LABEL, ExpectedKwargShape::NUMBER_LABEL)`
6914 /// disjointness pin — the TWO non-overlapping arms of the 7-arm
6915 /// closed set are the natural residual once the 5-of-7 subset
6916 /// carving aliases through [`SexpShape`].
6917 pub const NUMBER_LABEL: &'static str = "number";
6918
6919 /// Canonical `&'static str` bytes for the `Bool` expected-shape —
6920 /// aliases [`SexpShape::BOOL_LABEL`] on the ExpectedKwargShape ⊂
6921 /// SexpShape 5-of-7 subset carving. Per-role peer of `Self::Bool`
6922 /// on the closed-set outer algebra. Emitted by `extract_bool` /
6923 /// `extract_optional_bool` when the kwarg's value isn't a
6924 /// `Sexp::Atom(Bool(_))`. See [`Self::KEYWORD_LABEL`] for the
6925 /// alias-chain shape.
6926 pub const BOOL_LABEL: &'static str = SexpShape::BOOL_LABEL;
6927
6928 /// Canonical `&'static str` bytes for the `List` expected-shape —
6929 /// aliases [`SexpShape::LIST_LABEL`] on the ExpectedKwargShape ⊂
6930 /// SexpShape 5-of-7 subset carving. Per-role peer of `Self::List`
6931 /// on the closed-set outer algebra; the wider
6932 /// [`Self::LIST_OF_STRINGS_LABEL`] names the element-typed variant
6933 /// (which has NO [`SexpShape`] peer — the SexpShape algebra
6934 /// doesn't type-index list-of-strings as a distinct shape). See
6935 /// [`Self::KEYWORD_LABEL`] for the alias-chain shape.
6936 pub const LIST_LABEL: &'static str = SexpShape::LIST_LABEL;
6937
6938 /// Canonical `&'static str` bytes for the `ListOfStrings`
6939 /// expected-shape — emitted by `extract_string_list`'s outer-shape
6940 /// gate when the kwarg's value isn't a `Sexp::List(_)`. The
6941 /// `"list of strings"` naming is load-bearing: it names the
6942 /// element-type (String) so the diagnostic reads `expected list of
6943 /// strings, got string` instead of the ambiguous `expected list,
6944 /// got string`. The per-item gate fires `STRING_LABEL` (the
6945 /// narrower element-type failure).
6946 pub const LIST_OF_STRINGS_LABEL: &'static str = "list of strings";
6947
6948 /// Closed-set forced-arity ALL array over the canonical expected-
6949 /// shape `&'static str` bytes, in declaration order matching
6950 /// [`Self::ALL`] element-wise (pinned by
6951 /// `expected_kwarg_shape_labels_align_with_all_by_index`). Sibling
6952 /// posture to `KwargPathKind::LABELS: [&'static str; 3]`,
6953 /// `MacroDefHead::KEYWORDS: [&'static str; 3]`,
6954 /// `MacroParams::LAMBDA_LIST_KEYWORDS: [&'static str; 2]`,
6955 /// `Atom::BOOL_LITERALS: [&'static str; 2]`, and
6956 /// `QuoteForm::PREFIXES: [&'static str; 4]` — every closed-set
6957 /// outer projection on the substrate that carries an `&'static
6958 /// str`-per-variant label now pins its per-role canonical bytes
6959 /// at ONE `pub const` per role PLUS an ALL array for family-wide
6960 /// consumers.
6961 ///
6962 /// Future consumers that compose against `LABELS`: LSP completion
6963 /// surfacing the seven expected-shape labels in a
6964 /// `expected=` metrics query bar, a `tatara-check` coverage
6965 /// assertion sweeping `LABELS` over workspace `.lisp` files'
6966 /// kwarg-gate rejection sites, a Sekiban audit-trail metric
6967 /// labeled by the canonical expected-shape (e.g.
6968 /// `tatara_lisp_type_mismatch_total{expected="number"}`) whose
6969 /// metric-label set IS `LABELS`. Adding an eighth variant (e.g.
6970 /// `Float` once `extract_float` stops accepting integers,
6971 /// `Symbol`, or a parameterized `ListOfInts`) extends
6972 /// [`Self::ALL`] AND [`Self::LABELS`] AND [`Self::label`]'s arm
6973 /// AND one new per-role `pub const` in lockstep — rustc's forced-
6974 /// arity check on the two `[_; N]` arrays fails compilation if
6975 /// EITHER ALL array grows without the other.
6976 pub const LABELS: [&'static str; 7] = [
6977 Self::KEYWORD_LABEL,
6978 Self::STRING_LABEL,
6979 Self::INT_LABEL,
6980 Self::NUMBER_LABEL,
6981 Self::BOOL_LABEL,
6982 Self::LIST_LABEL,
6983 Self::LIST_OF_STRINGS_LABEL,
6984 ];
6985
6986 /// Project the typed `ExpectedKwargShape` to the canonical
6987 /// `&'static str` literal — feeds the `LispError::TypeMismatch`
6988 /// Display rendering via the `#[error(...)]` annotation. The
6989 /// `&'static str` lifetime is load-bearing: it lets the variant
6990 /// project through this method into the `expected {expected}` slot
6991 /// of the `#[error(...)]` annotation without an allocation,
6992 /// parallel to how `MacroDefHead::keyword()`,
6993 /// `UnquoteForm::marker()`, [`SexpShape::label`], and
6994 /// `CompilerSpecIoStage::operation()` / `label()` feed their
6995 /// respective `LispError::*` Display impls.
6996 ///
6997 /// Each arm routes through the per-role `pub const` on
6998 /// `impl Self` ([`Self::KEYWORD_LABEL`], [`Self::STRING_LABEL`],
6999 /// [`Self::INT_LABEL`], [`Self::NUMBER_LABEL`],
7000 /// [`Self::BOOL_LABEL`], [`Self::LIST_LABEL`],
7001 /// [`Self::LIST_OF_STRINGS_LABEL`]) so the seven canonical
7002 /// expected-shape byte-strings bind at ONE typed source of truth
7003 /// per role rather than as inline literals scattered across the
7004 /// `match` body. Sibling posture to
7005 /// `KwargPathKind::label`'s post-lift arms.
7006 ///
7007 /// The bidirectional contract is anchored by tests:
7008 /// `label_renders_canonical_string_for_every_variant` pins each
7009 /// variant's canonical literal so a typo in any arm fails-loudly,
7010 /// `display_matches_label_for_every_variant` pins
7011 /// Display-equals-label so the `#[error(...)]` annotation's
7012 /// `{expected}` slot projects byte-for-byte through this method,
7013 /// and `expected_kwarg_shape_label_round_trips_through_from_str`
7014 /// pins the `label` ↔ [`FromStr`] round-trip for every variant in
7015 /// [`Self::ALL`] so the typed surface and the rendered diagnostic
7016 /// literal cannot drift.
7017 #[must_use]
7018 pub fn label(self) -> &'static str {
7019 match self {
7020 Self::Keyword => Self::KEYWORD_LABEL,
7021 Self::String => Self::STRING_LABEL,
7022 Self::Int => Self::INT_LABEL,
7023 Self::Number => Self::NUMBER_LABEL,
7024 Self::Bool => Self::BOOL_LABEL,
7025 Self::List => Self::LIST_LABEL,
7026 Self::ListOfStrings => Self::LIST_OF_STRINGS_LABEL,
7027 }
7028 }
7029}
7030
7031// `impl std::fmt::Display for ExpectedKwargShape` +
7032// `impl std::str::FromStr for ExpectedKwargShape` +
7033// `impl tatara_closed_set::ClosedSet for ExpectedKwargShape` +
7034// `pub struct UnknownExpectedKwargShape(pub String)` are generated by
7035// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
7036// above. `label` delegates to the inherent `ExpectedKwargShape::label`
7037// via `#[closed_set(via = "label")]` — the inherent name coincides
7038// with the trait method name here; the delegation stays explicit so
7039// the SAME wiring shape applies whether the inherent projection is
7040// `label` / `prefix` / `marker` / `keyword` / `as_str`. The `display`
7041// flag emits the substrate-wide `f.write_str(Self::label(*self))`
7042// block. `#[closed_set(generate_unknown = "expected kwarg shape")]`
7043// emits the typed parse-rejection carrier with the substrate-wide
7044// `Debug + Clone + PartialEq + Eq + thiserror::Error` derives and the
7045// `#[error("unknown expected kwarg shape: {0}")]` annotation
7046// byte-for-byte; the explicit label matches the auto-derived
7047// `pascal_to_spaced_lowercase("ExpectedKwargShape")` projection
7048// byte-for-byte but pins the pre-lift wording. Round-trip + cross-axis
7049// rejection (`SexpShape` structural labels `"nil"` / `"symbol"` /
7050// `"float"` / `"quote"` / `"quasiquote"` / `"unquote"` /
7051// `"unquote-splice"`) pinned by
7052// `expected_kwarg_shape_label_round_trips_through_from_str` +
7053// `expected_kwarg_shape_from_str_accepts_only_canonical_labels`.
7054
7055/// Closed-set identifier for the outermost shape of a `Sexp` — the twelve
7056/// reachable shapes the reader can produce (`Nil` ⊎ `Symbol` ⊎ `Keyword` ⊎
7057/// `String` ⊎ `Int` ⊎ `Float` ⊎ `Bool` ⊎ `List` ⊎ `Quote` ⊎ `Quasiquote` ⊎
7058/// `Unquote` ⊎ `UnquoteSplice`). Carried as a typed slot on
7059/// `LispError::TypeMismatch.got` and `LispError::NamedFormNonSymbolName.got`
7060/// so authoring tools (REPL, LSP, `tatara-check`) bind to variant identity
7061/// (`SexpShape::Int` etc.) directly rather than substring-matching the
7062/// rendered `got` literal.
7063///
7064/// Mirror at the observed-shape boundary of the prior-run `KwargPath`
7065/// (kwargs-path-shape closed set), `ExpectedKwargShape` (kwarg-gate's
7066/// expected-shape closed set), `MacroDefHead` (macro-definition-head
7067/// closed set), `CompilerSpecIoStage` (disk-persistence surface),
7068/// `TemplateInvariantKind` (bytecode-runtime surface), and `UnquoteForm`
7069/// (template-marker syntactic forms) closed-set lifts: those enums key
7070/// their respective rejection variants on a typed identity carried inside
7071/// the variant's data shape; this enum keys the THIRD slot (`got`) of
7072/// every `LispError::TypeMismatch` site on a typed observed-shape identity
7073/// — alongside the already-typed `form: KwargPath` and
7074/// `expected: ExpectedKwargShape`. After this lift the type-mismatch
7075/// variant's identity is fully closed-set typed in ALL THREE of its slots
7076/// — no `&'static str` projection at any helper boundary, every reachable
7077/// identity encoded as a variant of a typed enum.
7078///
7079/// Adding a future `Sexp` variant (e.g. a hypothetical `Sexp::Vector` for
7080/// `#(...)` reader syntax, or `Sexp::Map` for `{...}`) requires extending
7081/// this enum, which rustc-enforces matching at every projection site
7082/// (`label()`, `crate::domain::sexp_shape`).
7083///
7084/// `label(self) -> &'static str` projects the typed `SexpShape` back to
7085/// the canonical literal for `LispError::Display` rendering. The
7086/// `&'static str` lifetime is load-bearing: it lets the variant project
7087/// through this method into the `got {got}` slot of the `#[error(...)]`
7088/// annotation without an allocation, parallel to how
7089/// `ExpectedKwargShape::label()`, `MacroDefHead::keyword()`,
7090/// `UnquoteForm::marker()`, and `CompilerSpecIoStage::operation()` /
7091/// `label()` feed their respective `LispError::*` Display impls.
7092///
7093/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set of
7094/// observed-Sexp shapes becomes a TYPE rather than a `&'static str`
7095/// projection through a string-keyed helper. A regression that drifts the
7096/// observed-shape label (e.g. a typo `"strin"` for `"string"`) becomes a
7097/// type error at the call site, not a runtime substring drift. THEORY.md
7098/// §VI.1 — generation over composition; the typed enum lands the
7099/// structural-completeness floor for the observed-shape surface, parallel
7100/// to how `ExpectedKwargShape` lands it for the expected-shape surface,
7101/// `KwargPath` for the kwargs-path surface, `MacroDefHead` for the
7102/// macro-definition-head surface, `UnquoteForm` for the template-marker
7103/// surface, `CompilerSpecIoStage` for the disk-persistence surface, and
7104/// `TemplateInvariantKind` for the bytecode-runtime surface. THEORY.md
7105/// §II.1 invariant 1 — typed entry; the observed-shape identity is part
7106/// of the proof of WHAT the typed-entry gate observed, and the typed enum
7107/// makes that identity first-class as load-bearing data on the variant
7108/// rather than as a projection-to-`&'static str` at the helper boundary.
7109#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
7110#[closed_set(via = "label", display, generate_unknown = "sexp shape")]
7111pub enum SexpShape {
7112 /// `"nil"` — `Sexp::Nil`.
7113 Nil,
7114 /// `"symbol"` — `Sexp::Atom(Symbol(_))`.
7115 Symbol,
7116 /// `"keyword"` — `Sexp::Atom(Keyword(_))`.
7117 Keyword,
7118 /// `"string"` — `Sexp::Atom(Str(_))`.
7119 String,
7120 /// `"int"` — `Sexp::Atom(Int(_))`.
7121 Int,
7122 /// `"float"` — `Sexp::Atom(Float(_))`.
7123 Float,
7124 /// `"bool"` — `Sexp::Atom(Bool(_))`.
7125 Bool,
7126 /// `"list"` — `Sexp::List(_)`.
7127 List,
7128 /// `"quote"` — `Sexp::Quote(_)`.
7129 Quote,
7130 /// `"quasiquote"` — `Sexp::Quasiquote(_)`.
7131 Quasiquote,
7132 /// `"unquote"` — `Sexp::Unquote(_)`.
7133 Unquote,
7134 /// `"unquote-splice"` — `Sexp::UnquoteSplice(_)`.
7135 UnquoteSplice,
7136}
7137
7138impl SexpShape {
7139 /// The closed set of reachable `Sexp` outermost shapes — single
7140 /// source of truth that drives the [`Self::label`] / [`fmt::Display`]
7141 /// projection AND the [`FromStr`] decode sweep keyed on
7142 /// [`Self::label`]. Adding a hypothetical thirteenth variant (e.g.
7143 /// `Vector` for `#(...)` reader syntax, `Map` for `{...}`, or
7144 /// `Char` for `#\x`) lands at one `ALL` entry + one `label` arm —
7145 /// exhaustively checked by the compiler (the `[Self; 12]` array
7146 /// literal forces the arity) AND by the per-variant truth-table
7147 /// tests below. Sibling closed-set lift to every other typed-shape
7148 /// enum the substrate carries: this crate's own [`UnquoteForm`]
7149 /// (the four template markers — the only other closed set on the
7150 /// `Sexp` algebra with `Sexp variant ↔ enum variant` parity), and
7151 /// the cross-crate `tatara-process` family
7152 /// (`ConditionKind::ALL`, `ProcessPhase::ALL`,
7153 /// `ProcessSignal::ALL`, `ChannelKind::ALL`, `IntentKind::ALL`,
7154 /// …) every one of which paired its typed projection with `ALL`
7155 /// before this lift.
7156 ///
7157 /// Future consumers that compose against `ALL`:
7158 /// - LSP / REPL completion for the operator-facing rendered
7159 /// shape label (every `expected X, got Y` substring in
7160 /// `LispError`'s rendered diagnostics keys on this set);
7161 /// - `tatara-check` coverage assertions over which `SexpShape`
7162 /// variants reach a `TypeMismatch.got` arm at all;
7163 /// - any future audit-trail metric jointly labeled by
7164 /// `SexpShape::label` (e.g.
7165 /// `tatara_lisp_type_mismatch_total{got="symbol"}`) — the
7166 /// metric label set IS [`Self::ALL`] mapped through
7167 /// [`Self::label`].
7168 pub const ALL: [Self; 12] = [
7169 Self::Nil,
7170 Self::Symbol,
7171 Self::Keyword,
7172 Self::String,
7173 Self::Int,
7174 Self::Float,
7175 Self::Bool,
7176 Self::List,
7177 Self::Quote,
7178 Self::Quasiquote,
7179 Self::Unquote,
7180 Self::UnquoteSplice,
7181 ];
7182
7183 /// Canonical `&'static str` bytes for the [`Self::Nil`] outer-shape —
7184 /// projects [`crate::ast::Sexp::Nil`] into the `got {got}` slot of every
7185 /// `LispError::*` Display rendering. Per-role peer of `Self::Nil` on
7186 /// the closed-set outer algebra.
7187 pub const NIL_LABEL: &'static str = "nil";
7188
7189 /// Canonical `&'static str` bytes for the [`Self::Symbol`] outer-shape —
7190 /// projects [`crate::ast::Sexp::Atom`] `(Symbol(_))` into diagnostic
7191 /// rendering. Per-role peer of `Self::Symbol`.
7192 pub const SYMBOL_LABEL: &'static str = "symbol";
7193
7194 /// Canonical `&'static str` bytes for the [`Self::Keyword`] outer-shape —
7195 /// projects [`crate::ast::Sexp::Atom`] `(Keyword(_))` into diagnostic
7196 /// rendering. Per-role peer of `Self::Keyword`; matches
7197 /// [`ExpectedKwargShape::KEYWORD_LABEL`] byte-for-byte — the cross-axis
7198 /// overlap on the same `Sexp` algebra is intentional and load-bearing,
7199 /// pinned by `sexp_shape_atom_carving_labels_align_with_expected_kwarg_shape_labels`.
7200 pub const KEYWORD_LABEL: &'static str = "keyword";
7201
7202 /// Canonical `&'static str` bytes for the [`Self::String`] outer-shape —
7203 /// projects [`crate::ast::Sexp::Atom`] `(Str(_))` into diagnostic
7204 /// rendering. Per-role peer of `Self::String`; matches
7205 /// [`ExpectedKwargShape::STRING_LABEL`] byte-for-byte.
7206 pub const STRING_LABEL: &'static str = "string";
7207
7208 /// Canonical `&'static str` bytes for the [`Self::Int`] outer-shape —
7209 /// projects [`crate::ast::Sexp::Atom`] `(Int(_))` into diagnostic
7210 /// rendering. Per-role peer of `Self::Int`; matches
7211 /// [`ExpectedKwargShape::INT_LABEL`] byte-for-byte.
7212 pub const INT_LABEL: &'static str = "int";
7213
7214 /// Canonical `&'static str` bytes for the [`Self::Float`] outer-shape —
7215 /// projects [`crate::ast::Sexp::Atom`] `(Float(_))` into diagnostic
7216 /// rendering. Per-role peer of `Self::Float`; distinct from
7217 /// [`ExpectedKwargShape::NUMBER_LABEL`] (which is the wider numeric-
7218 /// union label emitted at `extract_float` gates).
7219 pub const FLOAT_LABEL: &'static str = "float";
7220
7221 /// Canonical `&'static str` bytes for the [`Self::Bool`] outer-shape —
7222 /// projects [`crate::ast::Sexp::Atom`] `(Bool(_))` into diagnostic
7223 /// rendering. Per-role peer of `Self::Bool`; matches
7224 /// [`ExpectedKwargShape::BOOL_LABEL`] byte-for-byte.
7225 pub const BOOL_LABEL: &'static str = "bool";
7226
7227 /// Canonical `&'static str` bytes for the [`Self::List`] outer-shape —
7228 /// projects [`crate::ast::Sexp::List`] into diagnostic rendering.
7229 /// Per-role peer of `Self::List`; matches
7230 /// [`ExpectedKwargShape::LIST_LABEL`] byte-for-byte.
7231 pub const LIST_LABEL: &'static str = "list";
7232
7233 /// Canonical `&'static str` bytes for the [`Self::Quote`] outer-shape —
7234 /// projects [`crate::ast::Sexp::Quote`] into diagnostic rendering.
7235 /// Per-role peer of `Self::Quote`; matches
7236 /// [`crate::ast::QuoteForm::QUOTE_IAC_FORGE_TAG`] byte-for-byte — the
7237 /// cross-axis overlap on the shared quote-family sub-vocabulary is
7238 /// intentional and load-bearing, pinned by
7239 /// `sexp_shape_quote_carving_labels_align_with_quote_form_iac_forge_tags`.
7240 pub const QUOTE_LABEL: &'static str = "quote";
7241
7242 /// Canonical `&'static str` bytes for the [`Self::Quasiquote`] outer-
7243 /// shape — projects [`crate::ast::Sexp::Quasiquote`] into diagnostic
7244 /// rendering. Per-role peer of `Self::Quasiquote`; matches
7245 /// [`crate::ast::QuoteForm::QUASIQUOTE_IAC_FORGE_TAG`] byte-for-byte.
7246 pub const QUASIQUOTE_LABEL: &'static str = "quasiquote";
7247
7248 /// Canonical `&'static str` bytes for the [`Self::Unquote`] outer-shape
7249 /// — projects [`crate::ast::Sexp::Unquote`] into diagnostic rendering.
7250 /// Per-role peer of `Self::Unquote`; matches
7251 /// [`crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG`] byte-for-byte.
7252 pub const UNQUOTE_LABEL: &'static str = "unquote";
7253
7254 /// Canonical `&'static str` bytes for the [`Self::UnquoteSplice`]
7255 /// outer-shape — projects [`crate::ast::Sexp::UnquoteSplice`] into
7256 /// diagnostic rendering. Per-role peer of `Self::UnquoteSplice`;
7257 /// DIFFERS from [`crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG`]
7258 /// (`"unquote-splicing"`) by design — the SexpShape diagnostic layer
7259 /// uses the SHORTER `"unquote-splice"` bytes across every `LispError::*`
7260 /// Display rendering while the iac-forge canonical-form tag uses the
7261 /// LONGER `"unquote-splicing"` for cross-crate byte-identical
7262 /// interoperability. The two-byte spelling asymmetry is the ONE
7263 /// intentional exception to the quote-family cross-axis label
7264 /// alignment; pinned by
7265 /// `sexp_shape_unquote_splice_label_differs_from_quote_form_iac_forge_tag`.
7266 pub const UNQUOTE_SPLICE_LABEL: &'static str = "unquote-splice";
7267
7268 /// Closed-set forced-arity ALL array over the canonical outer-shape
7269 /// `&'static str` bytes, in declaration order matching [`Self::ALL`]
7270 /// element-wise (pinned by
7271 /// `sexp_shape_labels_align_with_all_by_index`). Sibling posture to
7272 /// [`ExpectedKwargShape::LABELS`] (`[&'static str; 7]`),
7273 /// [`KwargPathKind::LABELS`] (`[&'static str; 3]`),
7274 /// [`MacroDefHead::KEYWORDS`] (`[&'static str; 3]`),
7275 /// [`crate::ast::Atom::BOOL_LITERALS`] (`[&'static str; 2]`), and
7276 /// [`crate::ast::QuoteForm::PREFIXES`] (`[&'static str; 4]`) — every
7277 /// closed-set outer projection on the substrate that carries an
7278 /// `&'static str`-per-variant label now pins its per-role canonical
7279 /// bytes at ONE `pub const` per role PLUS an ALL array for
7280 /// family-wide consumers.
7281 ///
7282 /// Pre-lift the twelve canonical outer-shape label bytes lived inline
7283 /// at four sites: [`Self::label`]'s twelve match arms, plus three
7284 /// truth-table tests
7285 /// (`sexp_shape_label_renders_canonical_string_for_every_variant`,
7286 /// `sexp_shape_display_matches_label_for_every_variant`,
7287 /// `sexp_shape_label_round_trips_through_from_str`) enumerating the
7288 /// twelve `&'static str` literals byte-for-byte. Post-lift the
7289 /// per-role bytes bind at ONE `pub const` per role on the typed
7290 /// [`SexpShape`] algebra so every consumer routes through the typed
7291 /// constants — the label match arms, the truth-table tests, and every
7292 /// future consumer (LSP completion, `tatara-check` coverage
7293 /// assertion, Sekiban audit-trail metric labeled by the shape) picks
7294 /// up the same canonical bytes from ONE source of truth.
7295 ///
7296 /// Future consumers that compose against [`Self::LABELS`]: LSP / REPL
7297 /// completion surfacing every `got X` diagnostic label in a
7298 /// `got=` metrics query bar, a `tatara-check` coverage assertion
7299 /// sweeping [`Self::LABELS`] over workspace `.lisp` files' shape-
7300 /// rejection sites, a Sekiban audit-trail metric jointly labeled by
7301 /// the canonical shape (e.g.
7302 /// `tatara_lisp_type_mismatch_total{got="symbol"}`) whose metric-
7303 /// label set IS [`Self::LABELS`]. Adding a hypothetical thirteenth
7304 /// variant (`Vector` for `#(...)` reader syntax, `Map` for `{...}`,
7305 /// `Char` for `#\x`) extends [`Self::ALL`] AND [`Self::LABELS`] AND
7306 /// [`Self::label`]'s arm AND one new per-role `pub const` in
7307 /// lockstep — rustc's forced-arity check on the two `[_; N]` arrays
7308 /// fails compilation if EITHER ALL array grows without the other.
7309 ///
7310 /// Theory anchor: THEORY.md §III — the typescape; the twelve
7311 /// canonical outer-shape label bytes bind at ONE typed `[&'static
7312 /// str; 12]` array on the closed-set outer [`SexpShape`] algebra
7313 /// rather than as twelve inline literals scattered across four
7314 /// substrate sites. THEORY.md §V.1 — knowable platform; the family's
7315 /// cardinality becomes a TYPE-level constant on the substrate
7316 /// algebra rather than a per-consumer hand-rolled enumeration of the
7317 /// twelve labels. THEORY.md §VI.1 — generation over composition; the
7318 /// family-wide contract sweeps (alignment with `ALL`, pairwise
7319 /// disjointness, membership through `label()`) emerge from the
7320 /// composition of TWO substrate primitives (this `pub const` array +
7321 /// the twelve per-role `pub const *_LABEL`s) rather than as per-
7322 /// variant inline assertions the current pins duplicate structurally.
7323 pub const LABELS: [&'static str; 12] = [
7324 Self::NIL_LABEL,
7325 Self::SYMBOL_LABEL,
7326 Self::KEYWORD_LABEL,
7327 Self::STRING_LABEL,
7328 Self::INT_LABEL,
7329 Self::FLOAT_LABEL,
7330 Self::BOOL_LABEL,
7331 Self::LIST_LABEL,
7332 Self::QUOTE_LABEL,
7333 Self::QUASIQUOTE_LABEL,
7334 Self::UNQUOTE_LABEL,
7335 Self::UNQUOTE_SPLICE_LABEL,
7336 ];
7337
7338 /// Project the typed `SexpShape` to the canonical `&'static str`
7339 /// literal — feeds the `LispError::TypeMismatch` /
7340 /// `LispError::NamedFormNonSymbolName` Display rendering via the
7341 /// `#[error(...)]` annotation. The `&'static str` lifetime is
7342 /// load-bearing: it lets the variant project through this method into
7343 /// the `got {got}` slot without an allocation, parallel to how
7344 /// `ExpectedKwargShape::label()`, `MacroDefHead::keyword()`,
7345 /// `UnquoteForm::marker()`, and `CompilerSpecIoStage::operation()` /
7346 /// `label()` feed their respective `LispError::*` Display impls.
7347 ///
7348 /// Each arm routes through the per-role `pub const` on `impl Self`
7349 /// ([`Self::NIL_LABEL`], [`Self::SYMBOL_LABEL`],
7350 /// [`Self::KEYWORD_LABEL`], [`Self::STRING_LABEL`],
7351 /// [`Self::INT_LABEL`], [`Self::FLOAT_LABEL`], [`Self::BOOL_LABEL`],
7352 /// [`Self::LIST_LABEL`], [`Self::QUOTE_LABEL`],
7353 /// [`Self::QUASIQUOTE_LABEL`], [`Self::UNQUOTE_LABEL`],
7354 /// [`Self::UNQUOTE_SPLICE_LABEL`]) so the twelve canonical
7355 /// outer-shape byte-strings bind at ONE typed source of truth per
7356 /// role rather than as inline literals scattered across the `match`
7357 /// body. Sibling posture to
7358 /// [`ExpectedKwargShape::label`]'s post-lift arms.
7359 ///
7360 /// The bidirectional contract is anchored by tests:
7361 /// `sexp_shape_label_renders_canonical_string_for_every_variant` pins
7362 /// each variant's canonical literal so a typo in any arm fails-loudly,
7363 /// `sexp_shape_display_matches_label_for_every_variant` pins
7364 /// Display-equals-label so the `#[error(...)]` annotation's `{got}`
7365 /// slot projects byte-for-byte through this method, and
7366 /// `sexp_shape_label_round_trips_through_from_str` pins the
7367 /// `label` ↔ `FromStr` round-trip for every variant in
7368 /// [`Self::ALL`] so the typed surface and the rendered diagnostic
7369 /// literal cannot drift.
7370 #[must_use]
7371 pub fn label(self) -> &'static str {
7372 match self {
7373 Self::Nil => Self::NIL_LABEL,
7374 Self::Symbol => Self::SYMBOL_LABEL,
7375 Self::Keyword => Self::KEYWORD_LABEL,
7376 Self::String => Self::STRING_LABEL,
7377 Self::Int => Self::INT_LABEL,
7378 Self::Float => Self::FLOAT_LABEL,
7379 Self::Bool => Self::BOOL_LABEL,
7380 Self::List => Self::LIST_LABEL,
7381 Self::Quote => Self::QUOTE_LABEL,
7382 Self::Quasiquote => Self::QUASIQUOTE_LABEL,
7383 Self::Unquote => Self::UNQUOTE_LABEL,
7384 Self::UnquoteSplice => Self::UNQUOTE_SPLICE_LABEL,
7385 }
7386 }
7387
7388 /// Project the twelve-variant [`SexpShape`] back to its
7389 /// corresponding [`crate::ast::AtomKind`] iff the shape names an
7390 /// atomic-payload variant — `Symbol → Some(AtomKind::Symbol)`,
7391 /// `Keyword → Some(AtomKind::Keyword)`, `String → Some(AtomKind::Str)`,
7392 /// `Int → Some(AtomKind::Int)`, `Float → Some(AtomKind::Float)`,
7393 /// `Bool → Some(AtomKind::Bool)`, every other shape (`Nil`,
7394 /// `List`, every quote-family wrapper) `None`. The 6-of-12 carving
7395 /// of [`SexpShape`] that the inverse [`crate::ast::AtomKind::sexp_shape`]
7396 /// embed projection covers — naming the inverse closes the
7397 /// embed/project section on the (atomic-payload, outer-shape)
7398 /// algebra.
7399 ///
7400 /// Composition law (round-trip on the atom carving):
7401 /// `AtomKind::sexp_shape(k).as_atom_kind() == Some(k)` for every
7402 /// `k: AtomKind`. The non-atom shapes form the kernel of the
7403 /// projection — `SexpShape::List.as_atom_kind() == None`,
7404 /// `SexpShape::Nil.as_atom_kind() == None`, every quote-family
7405 /// variant also `None`. The two projections together form an
7406 /// `Iso(AtomKind, AtomShape ⊂ SexpShape)`: `as_atom_kind` is the
7407 /// section (every `AtomKind` round-trips through the embed),
7408 /// `sexp_shape` is the retraction (every atom-shape pre-image
7409 /// recovers the typed marker). Disjoint with [`Self::as_quote_form`]:
7410 /// for every variant in [`Self::ALL`], at most ONE of the two
7411 /// projections returns `Some` — the typed-shape lattice's two
7412 /// closed-set carvings partition the carve-able SexpShape variants
7413 /// (every variant is in exactly one of AtomShape, QuoteShape, or
7414 /// neither — the latter being `Nil` and `List`).
7415 ///
7416 /// Pre-lift the docstring on [`crate::ast::AtomKind::sexp_shape`]
7417 /// explicitly anticipated this dual:
7418 ///
7419 /// > "the dual projection `SexpShape::as_atom_kind(self) ->
7420 /// > Option<AtomKind>` is NOT currently provided because no
7421 /// > consumer needs it ... If a future authoring tool (LSP /
7422 /// > REPL / `tatara-check` typed-pattern matcher) wants to lift
7423 /// > the typed marker out of a diagnostic-side shape identity,
7424 /// > the dual lands as ONE new `match` over the closed set,
7425 /// > parallel to the structure here."
7426 ///
7427 /// Post-lift the dual lives at this method; the `AtomKind::sexp_shape`
7428 /// docstring's anticipation is satisfied. Two plausible future
7429 /// consumers the closed-set projection admits with no new
7430 /// boilerplate:
7431 /// * **LSP / REPL typed-shape filter** — given a
7432 /// `LispError::TypeMismatch { got: SexpShape, .. }`, project
7433 /// the offending shape through `as_atom_kind()`; iff `Some(k)`,
7434 /// the offending value was an atomic payload and the LSP can
7435 /// route through `AtomKind`'s diagnostic / completion surface
7436 /// (`AtomKind::label`, `AtomKind::ALL`-driven completion list)
7437 /// without re-deriving the 6-of-12 carving inline. Iff `None`,
7438 /// the surface dispatches to the structural (list / quote-family /
7439 /// nil) branch instead.
7440 /// * **`tatara-check` typed-pattern matcher** — a future
7441 /// `(check-shape-projects-to-atom-kind …)` substrate primitive
7442 /// binds to this projection rather than substring-matching the
7443 /// rendered label or duplicating the 6-of-12 carving on its
7444 /// own.
7445 /// * **Diagnostic-side audit-trail metric** — a future
7446 /// `tatara_lisp_type_mismatch_atom_kind_total{kind="symbol"}`
7447 /// metric reads the typed `AtomKind` directly off the rejected
7448 /// `SexpShape` via this projection, with the carving's
7449 /// correctness pinned at ONE site (the typed match here)
7450 /// rather than across N per-metric inline `match` arms.
7451 ///
7452 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
7453 /// (SexpShape variant, AtomKind variant) inverse pairing becomes a
7454 /// TYPE projection on the substrate algebra rather than an inline
7455 /// 6-of-12 `match` re-derived per call site. THEORY.md §II.1
7456 /// invariant 2 — free middle; the (atomic-payload, outer-shape)
7457 /// algebra now binds at TWO typed sites (`AtomKind::sexp_shape`
7458 /// for the embed, `SexpShape::as_atom_kind` for the project), each
7459 /// rebuilding the same closed-set pairing — a regression that
7460 /// drifts ONE direction from the other fails the
7461 /// `atom_kind_sexp_shape_round_trips_through_as_atom_kind` round-
7462 /// trip pin below. THEORY.md §VI.1 — generation over composition;
7463 /// the inverse pairing's ONE typed `match` displaces the speculative
7464 /// per-consumer 6-of-12 carving (LSP, REPL, `tatara-check`,
7465 /// metrics) the substrate's authoring surface roadmap anticipates.
7466 ///
7467 /// Frontier inspiration: MLIR's `mlir::dyn_cast<AtomicAttr>(attr)`
7468 /// — the typed soft-downcast from a generic `Attribute` to a
7469 /// narrower typed `AtomicAttr` interface IS the closed-set
7470 /// project direction; `SexpShape::as_atom_kind` is the
7471 /// unstructured-Rust peer on the substrate's typed-shape algebra,
7472 /// with [`crate::ast::AtomKind`] standing in for MLIR's
7473 /// `AtomicAttr` interface. Racket's `(syntax->datum stx)` paired
7474 /// with a closed-set predicate (`number?`, `symbol?`, `string?`,
7475 /// `boolean?`) — the typed predicate face IS the project direction
7476 /// on Racket's syntax-datum taxonomy; the substrate's
7477 /// `as_atom_kind` is the Rust-typed peer where the predicate
7478 /// surfaces the typed witness alongside the predicate verdict in
7479 /// ONE `Option<AtomKind>` projection.
7480 #[must_use]
7481 pub fn as_atom_kind(self) -> Option<crate::ast::AtomKind> {
7482 use crate::ast::AtomKind;
7483 match self {
7484 Self::Symbol => Some(AtomKind::Symbol),
7485 Self::Keyword => Some(AtomKind::Keyword),
7486 Self::String => Some(AtomKind::Str),
7487 Self::Int => Some(AtomKind::Int),
7488 Self::Float => Some(AtomKind::Float),
7489 Self::Bool => Some(AtomKind::Bool),
7490 Self::Nil
7491 | Self::List
7492 | Self::Quote
7493 | Self::Quasiquote
7494 | Self::Unquote
7495 | Self::UnquoteSplice => None,
7496 }
7497 }
7498
7499 /// Project the twelve-variant [`SexpShape`] back to its
7500 /// corresponding [`crate::ast::QuoteForm`] iff the shape names a
7501 /// homoiconic quote-family wrapper — `Quote → Some(QuoteForm::Quote)`,
7502 /// `Quasiquote → Some(QuoteForm::Quasiquote)`, `Unquote →
7503 /// Some(QuoteForm::Unquote)`, `UnquoteSplice →
7504 /// Some(QuoteForm::UnquoteSplice)`, every other shape (`Nil`,
7505 /// `List`, every atomic-payload variant) `None`. The 4-of-12
7506 /// carving of [`SexpShape`] that the inverse
7507 /// [`crate::ast::QuoteForm::sexp_shape`] embed projection covers —
7508 /// naming the inverse closes the embed/project section on the
7509 /// (quote-family, outer-shape) algebra, sibling to
7510 /// [`Self::as_atom_kind`] on the atomic-payload axis.
7511 ///
7512 /// Composition law (round-trip on the quote-family carving):
7513 /// `QuoteForm::sexp_shape(qf).as_quote_form() == Some(qf)` for
7514 /// every `qf: QuoteForm`. The non-quote-family shapes form the
7515 /// kernel of the projection — `SexpShape::List.as_quote_form() ==
7516 /// None`, `SexpShape::Nil.as_quote_form() == None`, every atomic-
7517 /// payload variant also `None`. The two projections together form
7518 /// an `Iso(QuoteForm, QuoteShape ⊂ SexpShape)`: `as_quote_form`
7519 /// is the section (every `QuoteForm` round-trips through the
7520 /// embed), `sexp_shape` is the retraction (every quote-shape
7521 /// pre-image recovers the typed marker). Disjoint with
7522 /// [`Self::as_atom_kind`]: for every variant in [`Self::ALL`], at
7523 /// most ONE of the two projections returns `Some` — the typed-
7524 /// shape lattice's two closed-set carvings partition the carve-
7525 /// able SexpShape variants (every variant is in exactly one of
7526 /// AtomShape, QuoteShape, or neither — the latter being `Nil`
7527 /// and `List`).
7528 ///
7529 /// Pre-lift the docstring on [`crate::ast::QuoteForm::sexp_shape`]
7530 /// explicitly anticipated this dual:
7531 ///
7532 /// > "the dual projection `SexpShape::as_quote_form(self) ->
7533 /// > Option<QuoteForm>` is NOT currently provided because no
7534 /// > consumer needs it ... If a future authoring tool (LSP /
7535 /// > REPL / `tatara-check` typed-pattern matcher) wants to lift
7536 /// > the typed marker out of a diagnostic-side shape identity,
7537 /// > the dual lands as ONE new match on the closed set, parallel
7538 /// > to the structure here."
7539 ///
7540 /// Post-lift the dual lives at this method; the
7541 /// `QuoteForm::sexp_shape` docstring's anticipation is satisfied.
7542 /// The same plausible future consumers [`Self::as_atom_kind`]
7543 /// documents apply on the quote-family axis — a typed-shape
7544 /// filter that narrows a diagnostic to "this rejection was on a
7545 /// homoiconic prefix-wrapper" iff `as_quote_form().is_some()`
7546 /// binds to ONE projection rather than re-deriving the 4-of-12
7547 /// carving inline. A future `tatara-check` predicate
7548 /// `(check-shape-projects-to-quote-form …)` reads the typed
7549 /// `QuoteForm` directly off a rejected `SexpShape` via this
7550 /// projection.
7551 ///
7552 /// Theory anchor: same as [`Self::as_atom_kind`]. THEORY.md §V.1
7553 /// (knowable platform; the inverse pairing is a TYPE projection
7554 /// rather than an inline 4-of-12 `match`), THEORY.md §II.1
7555 /// invariant 2 (free middle; the embed/project pair binds at TWO
7556 /// typed sites — `QuoteForm::sexp_shape` for the embed,
7557 /// `SexpShape::as_quote_form` for the project — both rebuilding
7558 /// the same closed-set pairing), THEORY.md §VI.1 (generation
7559 /// over composition; the inverse 4-of-12 carving lifts to ONE
7560 /// typed `match`).
7561 ///
7562 /// Frontier inspiration: same as [`Self::as_atom_kind`] — MLIR's
7563 /// `mlir::dyn_cast<QuoteAttr>(attr)` typed soft-downcast on the
7564 /// quote-family carving of a closed-set attribute union, with
7565 /// Racket's `(or (quote-syntax? stx) (quasiquote-syntax? stx)
7566 /// (unquote-syntax? stx) (unquote-splicing-syntax? stx))` as the
7567 /// closed-form predicate-family sibling whose Rust-typed peer
7568 /// surfaces the typed witness alongside the predicate verdict in
7569 /// ONE `Option<QuoteForm>` projection.
7570 #[must_use]
7571 pub fn as_quote_form(self) -> Option<crate::ast::QuoteForm> {
7572 use crate::ast::QuoteForm;
7573 match self {
7574 Self::Quote => Some(QuoteForm::Quote),
7575 Self::Quasiquote => Some(QuoteForm::Quasiquote),
7576 Self::Unquote => Some(QuoteForm::Unquote),
7577 Self::UnquoteSplice => Some(QuoteForm::UnquoteSplice),
7578 Self::Nil
7579 | Self::List
7580 | Self::Symbol
7581 | Self::Keyword
7582 | Self::String
7583 | Self::Int
7584 | Self::Float
7585 | Self::Bool => None,
7586 }
7587 }
7588
7589 /// Project the twelve-variant [`SexpShape`] back to its
7590 /// corresponding [`UnquoteForm`] iff the shape names a template-
7591 /// substitution wrapper — `Unquote → Some(UnquoteForm::Unquote)`,
7592 /// `UnquoteSplice → Some(UnquoteForm::Splice)`, every other shape
7593 /// (`Quote`, `Quasiquote`, `Nil`, `List`, every atomic-payload
7594 /// variant) `None`. The 2-of-12 carving of [`SexpShape`] that the
7595 /// inverse [`UnquoteForm::sexp_shape`] embed projection covers —
7596 /// naming the inverse closes the embed/project section on the
7597 /// (template-substitution subset, outer-shape) algebra, sibling of
7598 /// the already-closed ([`crate::ast::AtomKind::sexp_shape`],
7599 /// [`Self::as_atom_kind`]) dual on the 6-of-12 atomic-payload
7600 /// carving AND ([`crate::ast::QuoteForm::sexp_shape`],
7601 /// [`Self::as_quote_form`]) dual on the 4-of-12 quote-family
7602 /// carving.
7603 ///
7604 /// Composition law (routes through the quote-family superset):
7605 /// `self.as_unquote_form() == self.as_quote_form().and_then(
7606 /// crate::ast::QuoteForm::as_unquote_form)` for every `self:
7607 /// SexpShape`. The (outer shape, subset marker) pairing binds at
7608 /// TWO existing closed-set matches ([`Self::as_quote_form`] +
7609 /// [`crate::ast::QuoteForm::as_unquote_form`]) rather than at a
7610 /// parallel twelve-arm inline match table here — matching the
7611 /// posture [`UnquoteForm::sexp_shape`] takes through
7612 /// [`crate::ast::QuoteForm::sexp_shape`] on the embed direction.
7613 /// Round-trip law (retraction on the substitution-subset carving):
7614 /// `UnquoteForm::sexp_shape(uf).as_unquote_form() == Some(uf)` for
7615 /// every `uf: UnquoteForm`. The two projections together form an
7616 /// `Iso(UnquoteForm, UnquoteShape ⊂ SexpShape)`:
7617 /// [`Self::as_unquote_form`] is the section (every `UnquoteForm`
7618 /// round-trips through the embed), [`UnquoteForm::sexp_shape`] is
7619 /// the retraction (every substitution-subset shape pre-image
7620 /// recovers the typed marker).
7621 ///
7622 /// Disjoint with [`Self::as_atom_kind`] AND properly subsumed by
7623 /// [`Self::as_quote_form`]: for every variant in [`Self::ALL`], at
7624 /// most ONE of ([`Self::as_atom_kind`], [`Self::as_quote_form`])
7625 /// returns `Some` (the substrate's typed-shape lattice's two closed-
7626 /// set carvings partition the carve-able SexpShape variants); and
7627 /// [`Self::as_unquote_form`] returns `Some` iff [`Self::as_quote_form`]
7628 /// returns `Some(qf)` AND `qf.as_unquote_form()` returns `Some(_)`
7629 /// (the 2-of-4 template-substitution subset of the 4-of-12 quote-
7630 /// family carving). The two quote-family shapes that lie OUTSIDE
7631 /// the substitution subset (`Quote`, `Quasiquote`) form the kernel
7632 /// of this projection but NOT of [`Self::as_quote_form`] — the
7633 /// nested subset relationship makes the projections composable.
7634 ///
7635 /// Pre-lift the (SexpShape variant, UnquoteForm variant) pairing
7636 /// had NO typed projection at this closed-set boundary — a
7637 /// consumer with a `SexpShape` in hand (a `LispError::TypeMismatch
7638 /// .got` slot's outer-shape identity, a rejected shape from a
7639 /// typed-entry gate) wanting to narrow to "was this rejection on a
7640 /// template-substitution wrapper?" had to spell the two-step
7641 /// composition `shape.as_quote_form().and_then(QuoteForm::as_unquote_form)`
7642 /// inline. Post-lift the composition binds at ONE typed-algebra
7643 /// method on the outer [`SexpShape`] algebra, and the pair with
7644 /// [`UnquoteForm::sexp_shape`] closes the embed/project algebra
7645 /// dual on the substitution-subset carving.
7646 ///
7647 /// Theory anchor: THEORY.md §V.1 — knowable platform; the inverse
7648 /// 2-of-12 carving is a TYPE projection on the substrate algebra
7649 /// rather than an inline `.as_quote_form().and_then(...)` two-step.
7650 /// THEORY.md §II.1 invariant 2 — free middle; the embed/project
7651 /// pair binds at TWO typed sites — [`UnquoteForm::sexp_shape`] for
7652 /// the embed, [`Self::as_unquote_form`] for the project — both
7653 /// composing through the parent quote-family carving's closed
7654 /// pair. THEORY.md §VI.1 — generation over composition; the
7655 /// inverse 2-of-12 carving lifts to ONE typed projection.
7656 ///
7657 /// Frontier inspiration: MLIR's `mlir::dyn_cast<UnquoteFamilyOp>(op)`
7658 /// typed soft-downcast on the substitution-subset carving of a
7659 /// closed-set operation union — the (op, typed identity) pairing
7660 /// lives at ONE typed projection composed through the parent
7661 /// op-family's typed downcast. [`Self::as_unquote_form`] is the
7662 /// Rust-typed peer on the [`SexpShape`] closed set, with
7663 /// [`crate::ast::QuoteForm::as_unquote_form`] standing in for the
7664 /// parent-family-to-subset composition and this projection lifting
7665 /// the parent-to-parent [`Self::as_quote_form`] composition into a
7666 /// single typed method the consumer surface binds against directly.
7667 #[must_use]
7668 pub fn as_unquote_form(self) -> Option<UnquoteForm> {
7669 self.as_quote_form()
7670 .and_then(crate::ast::QuoteForm::as_unquote_form)
7671 }
7672
7673 /// Project the twelve-variant [`SexpShape`] back to its corresponding
7674 /// [`StructuralKind`] iff the shape names a structural-residual
7675 /// wrapper — `Nil → Some(StructuralKind::Nil)`, `List →
7676 /// Some(StructuralKind::List)`, every other shape (`Symbol`,
7677 /// `Keyword`, `String`, `Int`, `Float`, `Bool`, `Quote`,
7678 /// `Quasiquote`, `Unquote`, `UnquoteSplice`) `None`. The 2-of-12
7679 /// carving of [`SexpShape`] that the inverse
7680 /// [`StructuralKind::sexp_shape`] embed projection covers — naming
7681 /// the inverse closes the (embed, project) algebra dual on the
7682 /// (structural-residual subset, outer-shape) algebra, THIRD sibling
7683 /// of the already-closed ([`crate::ast::AtomKind::sexp_shape`],
7684 /// [`Self::as_atom_kind`]) dual on the 6-of-12 atomic-payload
7685 /// carving AND ([`crate::ast::QuoteForm::sexp_shape`],
7686 /// [`Self::as_quote_form`]) dual on the 4-of-12 quote-family
7687 /// carving.
7688 ///
7689 /// Closes the typed-shape lattice's partition: every `SexpShape`
7690 /// projects through EXACTLY ONE of [`Self::as_atom_kind`],
7691 /// [`Self::as_quote_form`], [`Self::as_structural_kind`] as
7692 /// `Some(_)`, and the three carvings cover `SexpShape::ALL`
7693 /// disjointly (6 + 4 + 2 = 12). The partition is total AND
7694 /// disjoint on the outer-shape closed set — pre-lift the
7695 /// "residual" side of the partition lived at ONE inline
7696 /// `!matches!(shape, SexpShape::Nil | SexpShape::List)` assertion
7697 /// inside the test-module carving-disjointness pin (the runtime
7698 /// witness the substrate maintained by test discipline); post-lift
7699 /// the residual is a TYPED CARVING binding at ONE typed-algebra
7700 /// method, symmetric with the two already-closed carvings that own
7701 /// the atomic-payload and quote-family sides of the same partition.
7702 ///
7703 /// Composition law (partition-total): for every `self: SexpShape`,
7704 /// EXACTLY ONE of `self.as_atom_kind().is_some()`,
7705 /// `self.as_quote_form().is_some()`,
7706 /// `self.as_structural_kind().is_some()` is `true`. Round-trip law
7707 /// (retraction on the structural-residual subset carving):
7708 /// `StructuralKind::sexp_shape(sk).as_structural_kind() ==
7709 /// Some(sk)` for every `sk: StructuralKind`. The two projections
7710 /// together form an `Iso(StructuralKind, StructuralShape ⊂
7711 /// SexpShape)`: [`Self::as_structural_kind`] is the section (every
7712 /// `StructuralKind` round-trips through the embed),
7713 /// [`StructuralKind::sexp_shape`] is the retraction (every
7714 /// structural-residual shape pre-image recovers the typed marker).
7715 ///
7716 /// Disjoint with [`Self::as_atom_kind`] AND [`Self::as_quote_form`]:
7717 /// for every variant in [`Self::ALL`], at most ONE of the THREE
7718 /// projections returns `Some`. The structural-residual subset
7719 /// (`Nil` and `List`) is the KERNEL of both the atomic-payload and
7720 /// quote-family projections and is EXACTLY the IMAGE of this one —
7721 /// the three projections partition [`Self::ALL`] injectively.
7722 ///
7723 /// Pre-lift the (SexpShape variant, StructuralKind variant) pairing
7724 /// had NO typed projection at this closed-set boundary — a consumer
7725 /// with a `SexpShape` in hand (a `LispError::TypeMismatch.got`
7726 /// slot's outer-shape identity, a rejected shape from a typed-entry
7727 /// gate) wanting to narrow to "was this rejection on the structural
7728 /// residual (neither atomic nor quote-family)?" had to spell the
7729 /// two-step negation `shape.as_atom_kind().is_none() &&
7730 /// shape.as_quote_form().is_none()` OR the inline `matches!(shape,
7731 /// SexpShape::Nil | SexpShape::List)` at the callsite. Post-lift
7732 /// the composition binds at ONE typed-algebra method on the outer
7733 /// [`SexpShape`] algebra, and the pair with
7734 /// [`StructuralKind::sexp_shape`] closes the embed/project algebra
7735 /// dual on the structural-residual carving, completing the typed-
7736 /// shape lattice's third and final closed-set carving.
7737 ///
7738 /// Two plausible future consumers the closed-set projection admits
7739 /// with no new boilerplate:
7740 /// * **`tatara-check` typed-shape filter** — a future
7741 /// `(check-shape-projects-to-structural …)` substrate primitive
7742 /// binds to this projection rather than re-deriving the 2-of-12
7743 /// carving inline or negating the two sibling projections. A
7744 /// `LispError::TypeMismatch { got: SexpShape, .. }` whose
7745 /// offending shape projects through
7746 /// `as_structural_kind() == Some(_)` is a rejection on the
7747 /// structural residual (an empty [`crate::ast::Sexp::Nil`]
7748 /// marker or a bare [`crate::ast::Sexp::List`] shape) rather
7749 /// than on an atomic-payload OR quote-family wrapper.
7750 /// * **LSP / REPL typed-shape completion** — a future authoring
7751 /// surface offering completions filtered by carving axis binds
7752 /// to `StructuralKind::ALL` for the "structural" completion
7753 /// column rather than re-deriving `Nil | List` inline.
7754 ///
7755 /// Theory anchor: THEORY.md §V.1 — knowable platform; the inverse
7756 /// 2-of-12 residual carving is a TYPE projection on the substrate
7757 /// algebra rather than an inline `matches!(shape, SexpShape::Nil |
7758 /// SexpShape::List)` two-arm predicate. THEORY.md §II.1 invariant
7759 /// 2 — free middle; the embed/project pair binds at TWO typed
7760 /// sites — [`StructuralKind::sexp_shape`] for the embed,
7761 /// [`Self::as_structural_kind`] for the project — completing the
7762 /// third and final closed-set carving of the outer-shape algebra.
7763 /// THEORY.md §VI.1 — generation over composition; the residual
7764 /// 2-of-12 carving lifts to ONE typed projection AND makes the
7765 /// partition-total invariant (every `SexpShape` is in EXACTLY ONE
7766 /// of the three carvings) a TYPED THEOREM rather than a runtime
7767 /// `matches!` assertion.
7768 ///
7769 /// Frontier inspiration: MLIR's `mlir::dyn_cast<StructuralOp>(op)`
7770 /// typed soft-downcast on the residual carving of a closed-set
7771 /// operation union — the (op, typed identity) pairing lives at ONE
7772 /// typed projection on the outer op-family algebra, sibling of the
7773 /// atomic-payload and quote-family carvings' typed downcasts.
7774 /// [`Self::as_structural_kind`] is the Rust-typed peer on the
7775 /// [`SexpShape`] closed set, completing the three-carving partition
7776 /// the substrate's typed-shape lattice carries. Racket's
7777 /// `(or (null? datum) (pair? datum))` closed-form predicate on the
7778 /// (nil-or-list) residual whose Rust-typed peer surfaces the typed
7779 /// witness alongside the predicate verdict in ONE
7780 /// `Option<StructuralKind>` projection.
7781 #[must_use]
7782 pub fn as_structural_kind(self) -> Option<StructuralKind> {
7783 match self {
7784 Self::Nil => Some(StructuralKind::Nil),
7785 Self::List => Some(StructuralKind::List),
7786 Self::Symbol
7787 | Self::Keyword
7788 | Self::String
7789 | Self::Int
7790 | Self::Float
7791 | Self::Bool
7792 | Self::Quote
7793 | Self::Quasiquote
7794 | Self::Unquote
7795 | Self::UnquoteSplice => None,
7796 }
7797 }
7798
7799 /// Project the twelve-variant [`SexpShape`] into the canonical
7800 /// iac-forge interop tag string iff the shape names a homoiconic
7801 /// quote-family wrapper — `Quote → Some("quote")`, `Quasiquote →
7802 /// Some("quasiquote")`, `Unquote → Some("unquote")`, `UnquoteSplice
7803 /// → Some("unquote-splicing")`, every other shape (`Nil`, `List`,
7804 /// every atomic-payload variant) `None`. The 4-of-12 partial
7805 /// projection on the closed-set [`SexpShape`] algebra that surfaces
7806 /// [`crate::ast::QuoteForm::iac_forge_tag`]'s cross-crate canonical-
7807 /// form tag surface at the outer-shape algebra level.
7808 ///
7809 /// Composition law: `self.iac_forge_tag() ==
7810 /// self.as_quote_form().map(crate::ast::QuoteForm::iac_forge_tag)`
7811 /// for every `self: SexpShape` — the (outer shape, canonical iac-
7812 /// forge tag) pairing binds through the pre-existing quote-family
7813 /// carving ([`Self::as_quote_form`]) composed with the closed-set
7814 /// tag projection ([`crate::ast::QuoteForm::iac_forge_tag`]) rather
7815 /// than at a parallel four-arm inline match table here — matching
7816 /// the posture [`Self::as_unquote_form`] takes through
7817 /// [`crate::ast::QuoteForm::as_unquote_form`] on the 2-of-4
7818 /// template-substitution subset carving. `None` on every non-quote-
7819 /// family shape (six atomic-payload variants + `Nil` + `List`) — the
7820 /// eight-shape kernel of the projection matches the kernel of
7821 /// [`Self::as_quote_form`] exactly.
7822 ///
7823 /// The SIXTH consumer of the outer-shape algebra's shape-level
7824 /// projection surface, sibling of [`Self::label`] (operator-facing
7825 /// diagnostic-label surface, all twelve arms), [`Self::hash_discriminator`]
7826 /// (outer-`Sexp` cache-key discriminator byte, all twelve arms
7827 /// → `{0..=6}`), [`Self::as_atom_kind`] (6-of-12 atomic-payload
7828 /// carving), [`Self::as_quote_form`] (4-of-12 quote-family carving),
7829 /// [`Self::as_structural_kind`] (2-of-12 structural-residual
7830 /// carving), and [`Self::as_unquote_form`] (2-of-12 template-
7831 /// substitution subset carving composed through the parent quote-
7832 /// family carving). Post-lift the outer-shape algebra closes over
7833 /// seven typed projection surfaces, each rebuilt from the SAME
7834 /// closed twelve-arm set — a regression that drifts ONE projection
7835 /// against the algebra's shape-identity fails-loudly at rustc's
7836 /// exhaustiveness rather than silently at runtime.
7837 ///
7838 /// Pre-lift the (SexpShape variant, canonical iac-forge tag) pairing
7839 /// had no typed projection at the outer-shape algebra level — a
7840 /// consumer with a `SexpShape` in hand (a `LispError::TypeMismatch
7841 /// .got` slot's outer-shape identity, a rejected shape from a
7842 /// typed-entry gate, an LSP / REPL / audit-trail metric keyed on the
7843 /// observed outer shape) wanting the cross-crate iac-forge canonical
7844 /// tag had to re-embed the shape into a quote-family typed marker
7845 /// via [`Self::as_quote_form`] then compose through
7846 /// [`crate::ast::QuoteForm::iac_forge_tag`] inline. Post-lift the
7847 /// composition binds at ONE method on the outer [`SexpShape`]
7848 /// algebra — the (outer shape, canonical iac-forge tag) pairing lives
7849 /// at ONE typed projection matching the shape-level algebra sweep
7850 /// the six sibling projections established.
7851 ///
7852 /// The `Option<&'static str>` return shape is the natural typed peer
7853 /// of [`crate::ast::QuoteForm::iac_forge_tag`]'s total `&'static str`
7854 /// return: the quote-family superset TOTALS on the four-arm closed
7855 /// set, and the outer twelve-arm closed set's projection PARTIALIZES
7856 /// on the eight non-quote-family shapes. Same shape as
7857 /// [`Self::as_atom_kind`] / [`Self::as_quote_form`] /
7858 /// [`Self::as_structural_kind`] / [`Self::as_unquote_form`] — each
7859 /// carving lifts a total sub-algebra projection to a partial outer-
7860 /// shape projection with `None` on the residual arms, so a consumer
7861 /// keying on "iff this outer shape has a canonical iac-forge tag"
7862 /// binds against the closed set's typed image directly rather than
7863 /// substring-matching the shape label.
7864 ///
7865 /// The `&'static str` lifetime is load-bearing: every iac-forge
7866 /// consumer projects through this method into the canonical
7867 /// 2-element-list head without an allocation, parallel to how
7868 /// [`crate::ast::QuoteForm::iac_forge_tag`] on the sub-carving,
7869 /// [`Self::label`] on the outer-shape diagnostic surface,
7870 /// [`crate::ast::QuoteForm::prefix`] on the Display / reader
7871 /// prefix surface, and [`UnquoteForm::marker`] on the template
7872 /// marker axis project their respective closed sets. A future
7873 /// homoiconic prefix-wrapper (e.g. hypothetical `,~` reverse-
7874 /// unquote) extends [`crate::ast::QuoteForm`] AND [`Self`]'s
7875 /// closed set together — rustc binds the iac-forge canonical-form
7876 /// surface to the extended algebra at ONE arm each through
7877 /// exhaustiveness, with the composition through [`Self::as_quote_form`]
7878 /// pulling the new variant's tag into this projection mechanically.
7879 ///
7880 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
7881 /// (SexpShape variant, canonical iac-forge tag) pairing becomes a
7882 /// TYPE projection on the outer-shape algebra rather than an inline
7883 /// `.as_quote_form().map(...)` two-step at every consumer. A typo
7884 /// or swap at the projection site is no longer a runtime tag drift
7885 /// but a compile error against the typed composition — the
7886 /// `SexpShape` ↔ `QuoteForm` ↔ tag string chain is rustc-enforced
7887 /// end-to-end. THEORY.md §II.1 invariant 2 — free middle; the
7888 /// (outer shape, canonical iac-forge tag) pairing now binds at ONE
7889 /// site on the outer-shape algebra, composing through the pre-
7890 /// existing four-arm sub-carving's tag projection rather than
7891 /// duplicating the four-arm match here. THEORY.md §VI.1 —
7892 /// generation over composition; the outer-shape algebra's typed
7893 /// projection surface closes over SEVEN methods (label,
7894 /// hash_discriminator, four carvings, iac_forge_tag) each keyed on
7895 /// the SAME closed twelve-arm set.
7896 ///
7897 /// Frontier inspiration: MLIR's `mlir::TypeSwitch<T>` typed
7898 /// pattern-match combinator on a closed union — narrowing to a
7899 /// specific typed case yields the typed downcast + operations on
7900 /// that case in ONE typed step. [`Self::iac_forge_tag`] is the
7901 /// Rust-typed peer where the "narrow to quote-family" step
7902 /// ([`Self::as_quote_form`]) composes with the "read the case's
7903 /// canonical tag" step ([`crate::ast::QuoteForm::iac_forge_tag`])
7904 /// into ONE outer-shape projection. Racket's `(match shape
7905 /// [(? quote-shape? qs) (quote-shape->iac-forge-tag qs)] [_ #f])`
7906 /// closed-form match on the shape-taxonomy union — the outer
7907 /// pattern's partial projection to the canonical form IS the shape
7908 /// this method lifts to a typed Rust closed-set match.
7909 #[must_use]
7910 pub fn iac_forge_tag(self) -> Option<&'static str> {
7911 self.as_quote_form()
7912 .map(crate::ast::QuoteForm::iac_forge_tag)
7913 }
7914
7915 /// Project the twelve-variant [`SexpShape`] into the canonical
7916 /// reader-punctuation prefix string iff the shape names a homoiconic
7917 /// quote-family wrapper — `Quote → Some("'")`, `Quasiquote →
7918 /// Some("`")`, `Unquote → Some(",")`, `UnquoteSplice → Some(",@")`,
7919 /// every other shape (`Nil`, `List`, every atomic-payload variant)
7920 /// `None`. The 4-of-12 partial projection on the closed-set
7921 /// [`SexpShape`] algebra that surfaces
7922 /// [`crate::ast::QuoteForm::prefix`]'s reader-punctuation vocabulary
7923 /// at the outer-shape algebra level.
7924 ///
7925 /// Composition law: `self.prefix() ==
7926 /// self.as_quote_form().map(crate::ast::QuoteForm::prefix)` for every
7927 /// `self: SexpShape` — the (outer shape, reader-punctuation prefix)
7928 /// pairing binds through the pre-existing quote-family carving
7929 /// ([`Self::as_quote_form`]) composed with the closed-set prefix
7930 /// projection ([`crate::ast::QuoteForm::prefix`]) rather than at a
7931 /// parallel four-arm inline match table here — matching the posture
7932 /// [`Self::iac_forge_tag`] takes through
7933 /// [`crate::ast::QuoteForm::iac_forge_tag`] on the cross-crate
7934 /// canonical-form tag axis and [`Self::as_unquote_form`] takes
7935 /// through [`crate::ast::QuoteForm::as_unquote_form`] on the 2-of-4
7936 /// template-substitution subset carving. `None` on every non-quote-
7937 /// family shape (six atomic-payload variants + `Nil` + `List`) — the
7938 /// eight-shape kernel of the projection matches the kernel of
7939 /// [`Self::as_quote_form`] AND [`Self::iac_forge_tag`] byte-for-byte.
7940 ///
7941 /// The EIGHTH consumer of the outer-shape algebra's shape-level
7942 /// projection surface, sibling of [`Self::label`] (operator-facing
7943 /// diagnostic-label surface, all twelve arms),
7944 /// [`Self::hash_discriminator`] (outer-`Sexp` cache-key discriminator
7945 /// byte, all twelve arms → `{0..=6}`), [`Self::as_atom_kind`]
7946 /// (6-of-12 atomic-payload carving), [`Self::as_quote_form`]
7947 /// (4-of-12 quote-family carving), [`Self::as_structural_kind`]
7948 /// (2-of-12 structural-residual carving), [`Self::as_unquote_form`]
7949 /// (2-of-12 template-substitution subset carving composed through
7950 /// the parent quote-family carving), and [`Self::iac_forge_tag`]
7951 /// (4-of-12 cross-crate canonical-form tag projection). Post-lift
7952 /// the outer-shape algebra closes over EIGHT typed projection
7953 /// surfaces, each rebuilt from the SAME closed twelve-arm set — a
7954 /// regression that drifts ONE projection against the algebra's
7955 /// shape-identity fails-loudly at rustc's exhaustiveness rather than
7956 /// silently at runtime.
7957 ///
7958 /// Pre-lift the (SexpShape variant, reader-punctuation prefix)
7959 /// pairing had no typed projection at the outer-shape algebra level
7960 /// — a consumer with a [`SexpShape`] in hand (a
7961 /// [`LispError::TypeMismatch`]'s `got` slot's outer-shape identity,
7962 /// a rejected shape from a typed-entry gate, an LSP hover / REPL
7963 /// completion / audit-trail metric keyed on the observed outer
7964 /// shape) wanting the reader's canonical homoiconic prefix character
7965 /// had to re-embed the shape into a quote-family typed marker via
7966 /// [`Self::as_quote_form`] then compose through
7967 /// [`crate::ast::QuoteForm::prefix`] inline. Post-lift the
7968 /// composition binds at ONE method on the outer [`SexpShape`]
7969 /// algebra — the (outer shape, reader-punctuation prefix) pairing
7970 /// lives at ONE typed projection matching the shape-level algebra
7971 /// sweep the seven sibling projections established.
7972 ///
7973 /// The `Option<&'static str>` return shape is the natural typed peer
7974 /// of [`crate::ast::QuoteForm::prefix`]'s total `&'static str`
7975 /// return: the quote-family superset TOTALS on the four-arm closed
7976 /// set, and the outer twelve-arm closed set's projection PARTIALIZES
7977 /// on the eight non-quote-family shapes. Same shape as
7978 /// [`Self::as_atom_kind`] / [`Self::as_quote_form`] /
7979 /// [`Self::as_structural_kind`] / [`Self::as_unquote_form`] /
7980 /// [`Self::iac_forge_tag`] — each carving lifts a total sub-algebra
7981 /// projection to a partial outer-shape projection with `None` on
7982 /// the residual arms, so a consumer keying on "iff this outer shape
7983 /// has a canonical reader-punctuation prefix" binds against the
7984 /// closed set's typed image directly rather than substring-matching
7985 /// the shape label.
7986 ///
7987 /// The reader-punctuation vocabulary this method projects (`"'"` /
7988 /// `` "`" `` / `","` / `",@"`) is INTENTIONALLY DISJOINT from the
7989 /// two sibling `&'static str` projection axes:
7990 ///
7991 /// * [`Self::iac_forge_tag`] — cross-crate canonical form
7992 /// (`"quote"` / `"quasiquote"` / `"unquote"` /
7993 /// `"unquote-splicing"`), BLAKE3 attestation keys, render-cache
7994 /// shape (load-bearing for byte-identical inter-crate compatibility
7995 /// with the iac-forge ecosystem).
7996 /// * [`Self::label`] — operator-facing diagnostic label (`"quote"` /
7997 /// `"quasiquote"` / `"unquote"` / `"unquote-splice"`),
7998 /// [`LispError::TypeMismatch`]'s `got` rendering, REPL / LSP
7999 /// shape-of-witness surface.
8000 ///
8001 /// This method projects the reader's SOURCE-TEXT vocabulary — the
8002 /// four punctuation characters that appear literally in Lisp source
8003 /// at each variant's homoiconic prefix. The three closed-set
8004 /// projections key the SAME twelve-arm shape algebra on THREE
8005 /// distinct `&'static str` vocabularies (source-punctuation,
8006 /// diagnostic-label, cross-crate canonical-form); consolidating any
8007 /// two would silently break either the reader round-trip, the
8008 /// operator-facing diagnostic surface, OR the iac-forge attestation
8009 /// pipeline. The three vocabularies' distinctness is pinned bit-for-
8010 /// bit through the composition law across the closed-set typed
8011 /// algebra.
8012 ///
8013 /// The `&'static str` lifetime is load-bearing: every reader / LSP
8014 /// / REPL / diagnostic consumer projects through this method into
8015 /// the canonical prefix character without an allocation, parallel to
8016 /// how [`crate::ast::QuoteForm::prefix`] on the sub-carving,
8017 /// [`Self::iac_forge_tag`] on the cross-crate canonical-form axis,
8018 /// [`Self::label`] on the outer-shape diagnostic surface, and
8019 /// [`UnquoteForm::marker`] on the template marker axis project their
8020 /// respective closed sets. A future homoiconic prefix-wrapper (e.g.
8021 /// hypothetical `,~` reverse-unquote) extends
8022 /// [`crate::ast::QuoteForm`] AND [`Self`]'s closed set together —
8023 /// rustc binds the reader-punctuation surface to the extended
8024 /// algebra at ONE arm each through exhaustiveness, with the
8025 /// composition through [`Self::as_quote_form`] pulling the new
8026 /// variant's prefix into this projection mechanically.
8027 ///
8028 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
8029 /// (SexpShape variant, reader-punctuation prefix) pairing becomes a
8030 /// TYPE projection on the outer-shape algebra rather than an inline
8031 /// `.as_quote_form().map(...)` two-step at every consumer. A typo
8032 /// or swap at the projection site is no longer a runtime prefix
8033 /// drift but a compile error against the typed composition — the
8034 /// `SexpShape` ↔ `QuoteForm` ↔ prefix character chain is
8035 /// rustc-enforced end-to-end. THEORY.md §II.1 invariant 2 — free
8036 /// middle; the (outer shape, reader-punctuation prefix) pairing now
8037 /// binds at ONE site on the outer-shape algebra, composing through
8038 /// the pre-existing four-arm sub-carving's prefix projection rather
8039 /// than duplicating the four-arm match here. THEORY.md §VI.1 —
8040 /// generation over composition; the outer-shape algebra's typed
8041 /// projection surface closes over EIGHT methods (label,
8042 /// hash_discriminator, four carvings, iac_forge_tag, prefix) each
8043 /// keyed on the SAME closed twelve-arm set.
8044 ///
8045 /// Frontier inspiration: MLIR's `mlir::TypeSwitch<T>` typed
8046 /// pattern-match combinator on a closed union — narrowing to a
8047 /// specific typed case yields the typed downcast + operations on
8048 /// that case in ONE typed step. [`Self::prefix`] is the Rust-typed
8049 /// peer where the "narrow to quote-family" step
8050 /// ([`Self::as_quote_form`]) composes with the "read the case's
8051 /// reader-punctuation prefix" step
8052 /// ([`crate::ast::QuoteForm::prefix`]) into ONE outer-shape
8053 /// projection. Racket's `(match shape [(? quote-shape? qs)
8054 /// (quote-shape->reader-prefix qs)] [_ #f])` closed-form match on
8055 /// the shape-taxonomy union — the outer pattern's partial projection
8056 /// to the source-punctuation form IS the shape this method lifts to
8057 /// a typed Rust closed-set match.
8058 #[must_use]
8059 pub fn prefix(self) -> Option<&'static str> {
8060 self.as_quote_form().map(crate::ast::QuoteForm::prefix)
8061 }
8062
8063 /// Project the typed [`SexpShape`] into the outer-`Sexp`
8064 /// [`Hash for Sexp`](crate::ast::Sexp) cache-key discriminator byte
8065 /// `{0..=6}` — the SHAPE-LEVEL peer of
8066 /// [`crate::ast::Sexp::hash_discriminator`] one algebra level up.
8067 /// The twelve outer-shape arms collapse to seven outer bytes: the six
8068 /// atomic-payload arms (`Symbol`, `Keyword`, `String`, `Int`, `Float`,
8069 /// `Bool`) all project to the outer Atom marker byte `1u8` (their
8070 /// per-atom-kind inner byte lives nested inside
8071 /// [`Hash for Atom`](crate::ast::Atom) via
8072 /// [`crate::ast::AtomKind::hash_discriminator`]'s `{0..=5}` payload
8073 /// carving, NOT surfaced here); the two structural-residual arms
8074 /// (`Nil`, `List`) delegate through
8075 /// [`StructuralKind::hash_discriminator`] for `{0, 2}`; the four
8076 /// quote-family arms (`Quote`, `Quasiquote`, `Unquote`,
8077 /// `UnquoteSplice`) delegate through
8078 /// [`crate::ast::QuoteForm::hash_discriminator`] for `{3..=6}`. The
8079 /// byte partition is byte-identical to
8080 /// [`crate::ast::Sexp::hash_discriminator`]'s outer sweep, so the
8081 /// substrate's macro-expansion cache
8082 /// ([`crate::macro_expand::Expander`]'s cache) key stays bit-for-bit
8083 /// stable across the shape-level lift.
8084 ///
8085 /// Composition law: `sexp.hash_discriminator() ==
8086 /// sexp.shape().hash_discriminator()` for every `sexp: &Sexp` — the
8087 /// outer-`Sexp` cache-key discriminator method routes through
8088 /// [`crate::ast::Sexp::shape`] into THIS method, which in turn
8089 /// routes into the three closed-set carvings' discriminator
8090 /// methods. Post-lift the outer-`Sexp` cache-key algebra closes at
8091 /// FIVE typed layers:
8092 /// 1. [`crate::ast::Sexp::hash_discriminator`] — 7-arm outer
8093 /// dispatch on the outer `Sexp` algebra;
8094 /// 2. [`Self::hash_discriminator`] — 12-arm shape-level dispatch on
8095 /// the [`SexpShape`] algebra (this method);
8096 /// 3. [`StructuralKind::hash_discriminator`] — 2-arm
8097 /// structural-residual sub-carving for `{0, 2}`;
8098 /// 4. [`crate::ast::QuoteForm::hash_discriminator`] — 4-arm
8099 /// quote-family sub-carving for `{3..=6}`;
8100 /// 5. [`crate::ast::AtomKind::hash_discriminator`] — 6-arm
8101 /// atomic-payload sub-carving for `{0..=5}` (nested inside
8102 /// `Hash for Atom` under outer byte `1u8`, NOT surfaced through
8103 /// this shape-level method).
8104 ///
8105 /// Pre-lift the outer-`Sexp` cache-key algebra bottomed out at the
8106 /// three sub-carvings' discriminator methods directly via
8107 /// [`crate::ast::Sexp::hash_discriminator`]'s seven arms — no
8108 /// intermediate shape-level projection existed. Consumers that had a
8109 /// typed [`SexpShape`] identity in hand (a
8110 /// `LispError::TypeMismatch.got` slot, a rejected shape from a
8111 /// typed-entry gate, a future authoring-tool audit-trail metric keyed
8112 /// on the observed shape) wanting the outer cache-key byte had to
8113 /// re-embed the typed shape into a [`crate::ast::Sexp`] value and
8114 /// route through the outer method — an unnecessary allocation. Post-
8115 /// lift the shape-level projection is a NAMED PRIMITIVE on the closed-
8116 /// set [`SexpShape`] algebra sitting next to the three sibling shape-
8117 /// level projections ([`Self::label`], [`Self::as_atom_kind`],
8118 /// [`Self::as_quote_form`], [`Self::as_structural_kind`],
8119 /// [`Self::as_unquote_form`]) — the outer cache-key partition
8120 /// becomes a typed identity on the shape algebra rather than a
8121 /// projection through the outer-`Sexp` value carrier.
8122 ///
8123 /// `pub(crate)` because the byte-discriminator surface is an
8124 /// implementation detail of the substrate's `Hash for Sexp` cache-
8125 /// key contract; exposing it publicly would leak the cache-key shape
8126 /// through the API without enabling any external consumer the public
8127 /// projections ([`Self::label`], [`Self::as_atom_kind`],
8128 /// [`Self::as_quote_form`], [`Self::as_structural_kind`]) don't
8129 /// already serve. Same posture as
8130 /// [`crate::ast::Sexp::hash_discriminator`],
8131 /// [`crate::ast::AtomKind::hash_discriminator`],
8132 /// [`crate::ast::QuoteForm::hash_discriminator`], and
8133 /// [`StructuralKind::hash_discriminator`].
8134 ///
8135 /// A future thirteenth [`SexpShape`] variant (e.g. `Vector` for
8136 /// `#(...)` reader syntax, `Map` for `{...}`, `Char` for `#\x`)
8137 /// picks a fresh cache-key byte outside `{0..=6}` (e.g. `7u8`),
8138 /// extends either [`StructuralKind`] / [`crate::ast::AtomKind`] /
8139 /// [`crate::ast::QuoteForm`] (if it names an existing sub-carving)
8140 /// or a fresh sub-algebra, and lands at ONE new arm here — rustc
8141 /// binds the consistency through exhaustiveness over the closed
8142 /// [`SexpShape`] enum.
8143 ///
8144 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
8145 /// (SexpShape variant, outer cache-key byte) pairing becomes a TYPE
8146 /// projection on the substrate's shape algebra rather than requiring
8147 /// a re-embed into [`crate::ast::Sexp`] followed by an outer
8148 /// dispatch. A typo or swap at the shape-level projection site is
8149 /// a compile error against the typed projection rather than a
8150 /// silent runtime cache-key drift. THEORY.md §II.1 invariant 2 —
8151 /// free middle; the outer-`Sexp` cache-key algebra now closes over
8152 /// FIVE typed layers (outer, shape, three sub-carvings) with rustc-
8153 /// enforced consistency across each — a regression that drifts ONE
8154 /// layer's discriminator bytes from the others cannot reach the
8155 /// substrate's runtime cache. THEORY.md §VI.1 — generation over
8156 /// composition; the shape-level projection is the missing algebra
8157 /// layer between the outer-`Sexp` and the three sub-carvings — the
8158 /// four pre-existing typed layers become a full FIVE-layer typed
8159 /// composition through ONE new named projection.
8160 #[must_use]
8161 pub(crate) fn hash_discriminator(self) -> u8 {
8162 match self {
8163 Self::Nil => StructuralKind::Nil.hash_discriminator(),
8164 Self::List => StructuralKind::List.hash_discriminator(),
8165 Self::Symbol | Self::Keyword | Self::String | Self::Int | Self::Float | Self::Bool => {
8166 crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR
8167 }
8168 Self::Quote => crate::ast::QuoteForm::Quote.hash_discriminator(),
8169 Self::Quasiquote => crate::ast::QuoteForm::Quasiquote.hash_discriminator(),
8170 Self::Unquote => crate::ast::QuoteForm::Unquote.hash_discriminator(),
8171 Self::UnquoteSplice => crate::ast::QuoteForm::UnquoteSplice.hash_discriminator(),
8172 }
8173 }
8174
8175 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Nil`]
8176 /// outer shape — aliases [`StructuralKind::NIL_HASH_DISCRIMINATOR`]
8177 /// on the StructuralKind ⊂ SexpShape carving so the marker-level
8178 /// byte binds at ONE `pub(crate) const` on the SUB-CARVING's
8179 /// per-role primitive rather than at TWO sites (the sub-carving's
8180 /// per-role byte AND a parallel outer-shape inline literal). The
8181 /// canonical source of the byte lives on the sub-carving; this
8182 /// outer-shape peer alias projects it into the twelve-arm
8183 /// [`SexpShape`] algebra at zero-drift cost.
8184 ///
8185 /// Sibling posture to the twelve peer per-role `*_LABEL` aliases
8186 /// on [`Self`] — the `Nil` outer-shape per-role LABEL is canonical
8187 /// on [`Self`] and is aliased INTO [`StructuralKind::NIL_LABEL`];
8188 /// this per-role HASH_DISCRIMINATOR alias runs in the OPPOSITE
8189 /// direction because the outer-`Sexp` cache-key byte's canonical
8190 /// source is the sub-carving (which the outer `Sexp` dispatch
8191 /// composes through). The load-bearing invariant across the two
8192 /// axes is byte-identity: the outer-shape peer alias equals the
8193 /// sub-carving's canonical byte at rustc-time through the
8194 /// `const` initializer.
8195 pub(crate) const NIL_HASH_DISCRIMINATOR: u8 = StructuralKind::NIL_HASH_DISCRIMINATOR;
8196
8197 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Symbol`]
8198 /// outer shape — aliases [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`]
8199 /// on the AtomKind ⊂ SexpShape carving. Every atomic-payload outer
8200 /// shape shares this outer marker byte because the per-atom-kind
8201 /// specialisation lives at the NESTED INNER
8202 /// [`crate::ast::AtomKind::HASH_DISCRIMINATORS`] `{0..=5}` carve
8203 /// inside `Hash for Atom`, NOT surfaced through this outer shape-
8204 /// level projection.
8205 pub(crate) const SYMBOL_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
8206
8207 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Keyword`]
8208 /// outer shape — aliases [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`]
8209 /// on the AtomKind ⊂ SexpShape carving. Six-way collapse peer of
8210 /// [`Self::SYMBOL_HASH_DISCRIMINATOR`].
8211 pub(crate) const KEYWORD_HASH_DISCRIMINATOR: u8 =
8212 crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
8213
8214 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::String`]
8215 /// outer shape — aliases [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`].
8216 /// Six-way collapse peer.
8217 pub(crate) const STRING_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
8218
8219 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Int`]
8220 /// outer shape — aliases [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`].
8221 /// Six-way collapse peer.
8222 pub(crate) const INT_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
8223
8224 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Float`]
8225 /// outer shape — aliases [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`].
8226 /// Six-way collapse peer.
8227 pub(crate) const FLOAT_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
8228
8229 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Bool`]
8230 /// outer shape — aliases [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`].
8231 /// Six-way collapse peer of the atomic-outer sextet.
8232 pub(crate) const BOOL_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
8233
8234 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::List`]
8235 /// outer shape — aliases [`StructuralKind::LIST_HASH_DISCRIMINATOR`]
8236 /// on the StructuralKind ⊂ SexpShape carving. Structural-residual
8237 /// peer of [`Self::NIL_HASH_DISCRIMINATOR`]; the gap at `1u8`
8238 /// between the two structural-residual bytes reserves the outer
8239 /// atomic-carve byte the six-way atomic-outer sextet collapses to.
8240 pub(crate) const LIST_HASH_DISCRIMINATOR: u8 = StructuralKind::LIST_HASH_DISCRIMINATOR;
8241
8242 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Quote`]
8243 /// outer shape — aliases [`crate::ast::QuoteForm::QUOTE_HASH_DISCRIMINATOR`]
8244 /// on the QuoteForm ⊂ SexpShape carving.
8245 pub(crate) const QUOTE_HASH_DISCRIMINATOR: u8 = crate::ast::QuoteForm::QUOTE_HASH_DISCRIMINATOR;
8246
8247 /// Canonical `u8` outer-`Sexp` cache-key byte for the
8248 /// [`Self::Quasiquote`] outer shape — aliases
8249 /// [`crate::ast::QuoteForm::QUASIQUOTE_HASH_DISCRIMINATOR`].
8250 pub(crate) const QUASIQUOTE_HASH_DISCRIMINATOR: u8 =
8251 crate::ast::QuoteForm::QUASIQUOTE_HASH_DISCRIMINATOR;
8252
8253 /// Canonical `u8` outer-`Sexp` cache-key byte for the [`Self::Unquote`]
8254 /// outer shape — aliases [`crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR`].
8255 pub(crate) const UNQUOTE_HASH_DISCRIMINATOR: u8 =
8256 crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR;
8257
8258 /// Canonical `u8` outer-`Sexp` cache-key byte for the
8259 /// [`Self::UnquoteSplice`] outer shape — aliases
8260 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR`].
8261 pub(crate) const UNQUOTE_SPLICE_HASH_DISCRIMINATOR: u8 =
8262 crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR;
8263
8264 /// Closed-set forced-arity ALL array over the canonical outer-`Sexp`
8265 /// cache-key `u8` bytes, in declaration order matching [`Self::ALL`]
8266 /// element-wise (pinned by
8267 /// `sexp_shape_hash_discriminators_align_with_all_by_index`).
8268 /// Sibling posture to [`Self::LABELS`] (`[&'static str; 12]` — the
8269 /// diagnostic-label axis on the same twelve-variant closed set) AND
8270 /// to the sub-carvings' HASH_DISCRIMINATORS peers:
8271 /// [`StructuralKind::HASH_DISCRIMINATORS`] (`[u8; 2]` — `{0, 2}`),
8272 /// [`crate::ast::QuoteForm::HASH_DISCRIMINATORS`] (`[u8; 4]` — `{3..=6}`),
8273 /// [`crate::ast::AtomKind::HASH_DISCRIMINATORS`] (`[u8; 6]` — the
8274 /// NESTED INNER `{0..=5}` bytes inside `Hash for Atom` under the
8275 /// outer `1u8` marker), and [`crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR`]
8276 /// (scalar `1u8` — the six-way outer-collapse byte). This lift joins
8277 /// the outer-shape closed set to the family-wide ALL-array posture
8278 /// every sibling closed set already carries, closing the SIXTH
8279 /// per-family axis on the [`SexpShape`] algebra (ALL, LABELS,
8280 /// as_atom_kind, as_quote_form, as_structural_kind, plus this).
8281 ///
8282 /// Pre-lift the twelve outer-shape cache-key bytes had NO family-wide
8283 /// forced-arity primitive on the outer-shape algebra — a consumer
8284 /// wanting to sweep the outer partition through a `zip(SexpShape::ALL,
8285 /// bytes)` had to compose the twelve bytes inline by dispatching
8286 /// through [`Self::hash_discriminator`] at runtime OR by re-deriving
8287 /// the twelve-arm collapse against the three sub-carvings'
8288 /// HASH_DISCRIMINATORS arrays + the atomic outer-marker byte at every
8289 /// call site. Post-lift the twelve bytes bind at ONE typed `[u8; 12]`
8290 /// primitive on the outer-shape algebra composed from the sub-
8291 /// carvings' canonical bytes — a future LSP / REPL completion bar
8292 /// keyed on the shape's cache-key byte, a `tatara-check` coverage
8293 /// sweep over the outer partition, or a Sekiban audit-trail metric
8294 /// jointly labeled by the outer-shape byte picks up the same
8295 /// canonical bytes from ONE source of truth.
8296 ///
8297 /// Composition law (pinned by
8298 /// `sexp_shape_hash_discriminators_align_with_hash_discriminator_by_index`):
8299 /// for every `i in 0..12`, `Self::HASH_DISCRIMINATORS[i] ==
8300 /// Self::ALL[i].hash_discriminator()`. Sibling to the peer alignment
8301 /// pins on the three sub-carvings' HASH_DISCRIMINATORS arrays — this
8302 /// pin closes the family-wide alignment at the OUTER twelve-arm
8303 /// algebra so a regression that reorders ONE array without reordering
8304 /// the other silently misaligns every `zip(ALL, HASH_DISCRIMINATORS)`
8305 /// consumer.
8306 ///
8307 /// Partition closure (pinned by
8308 /// `sexp_shape_hash_discriminators_cover_outer_partition_zero_through_six`):
8309 /// the twelve bytes collect to exactly `{0..=6}` as a set — the
8310 /// same outer-partition space [`Self::hash_discriminator`] surjects
8311 /// onto. Sibling to the pre-existing
8312 /// `sexp_shape_hash_discriminator_covers_outer_partition_zero_through_six`
8313 /// projection-driven pin — that pin closes the partition through
8314 /// the projection method; this pin closes it at the array level so
8315 /// a regression that drifts the array's contents without changing
8316 /// the method surfaces here.
8317 ///
8318 /// The `#[allow(dead_code)]` posture matches
8319 /// [`StructuralKind::HASH_DISCRIMINATORS`] /
8320 /// [`crate::ast::QuoteForm::HASH_DISCRIMINATORS`] /
8321 /// [`crate::ast::AtomKind::HASH_DISCRIMINATORS`]: the substrate's
8322 /// current [`Self::hash_discriminator`] body dispatches through the
8323 /// twelve-arm per-role per-variant match rather than through an
8324 /// [`Self::HASH_DISCRIMINATORS`] index lookup (patterns cannot be
8325 /// array-indexing expressions in the current const-fn grammar); the
8326 /// lift lands the substrate primitive so future consumers keyed on
8327 /// the whole outer-shape cache-key algebra (a future `tatara-check`
8328 /// predicate `(check-outer-shape-partition-injective …)`; a future
8329 /// `TypedRewriter<ShapeOp>` sweep zipping ALL / LABELS /
8330 /// HASH_DISCRIMINATORS in lockstep) pick it up through ONE typed
8331 /// primitive.
8332 ///
8333 /// A hypothetical thirteenth [`SexpShape`] variant (`Vector` for
8334 /// `#(...)`, `Map` for `{...}`, `Char` for `#\x`) extends [`Self::ALL`]
8335 /// AND [`Self::LABELS`] AND [`Self::HASH_DISCRIMINATORS`] AND adds
8336 /// ONE new per-role `pub(crate) const *_HASH_DISCRIMINATOR` alias in
8337 /// lockstep — rustc's forced-arity check on the three `[_; N]`
8338 /// arrays fails compilation if ONE array grows without the others,
8339 /// closing the extensibility gap the pre-lift outer-shape algebra
8340 /// silently allowed at the [`Self::hash_discriminator`] match body.
8341 ///
8342 /// Theory anchor: THEORY.md §III — the typescape; the twelve
8343 /// canonical outer-shape cache-key bytes bind at ONE typed
8344 /// `[u8; 12]` array on the closed-set outer [`SexpShape`] algebra
8345 /// rather than at zero-primitive-plus-runtime-dispatch through the
8346 /// three sub-carvings' twelve-arm collapse. THEORY.md §V.1 —
8347 /// knowable platform; the family's cardinality becomes a
8348 /// TYPE-level constant on the substrate algebra rather than a
8349 /// per-consumer runtime dispatch through the match table. THEORY.md
8350 /// §V.3 — three-pillar attestation; the outer-shape cache-key
8351 /// partition is the substrate's outer `Sexp` `intent_hash`
8352 /// composition axis — binding the twelve-arm ALL array on the typed
8353 /// algebra makes attestation-key drift a compile error rather than
8354 /// a silent BLAKE3 mis-hash. THEORY.md §VI.1 — generation over
8355 /// composition; the family-wide contract sweeps (alignment with
8356 /// `ALL`, membership through [`Self::hash_discriminator`], partition
8357 /// closure onto `{0..=6}`) emerge from the composition of ONE
8358 /// substrate primitive (this `pub(crate) const` array) rather than
8359 /// as per-variant inline assertions at each call site.
8360 #[allow(dead_code)]
8361 pub(crate) const HASH_DISCRIMINATORS: [u8; 12] = [
8362 Self::NIL_HASH_DISCRIMINATOR,
8363 Self::SYMBOL_HASH_DISCRIMINATOR,
8364 Self::KEYWORD_HASH_DISCRIMINATOR,
8365 Self::STRING_HASH_DISCRIMINATOR,
8366 Self::INT_HASH_DISCRIMINATOR,
8367 Self::FLOAT_HASH_DISCRIMINATOR,
8368 Self::BOOL_HASH_DISCRIMINATOR,
8369 Self::LIST_HASH_DISCRIMINATOR,
8370 Self::QUOTE_HASH_DISCRIMINATOR,
8371 Self::QUASIQUOTE_HASH_DISCRIMINATOR,
8372 Self::UNQUOTE_HASH_DISCRIMINATOR,
8373 Self::UNQUOTE_SPLICE_HASH_DISCRIMINATOR,
8374 ];
8375}
8376
8377// `impl std::fmt::Display for SexpShape` + `impl std::str::FromStr for
8378// SexpShape` + `impl tatara_closed_set::ClosedSet for SexpShape` +
8379// `pub struct UnknownSexpShape(pub String)` are generated by
8380// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
8381// above. `label` delegates to the inherent `SexpShape::label` via
8382// `#[closed_set(via = "label")]` — the inherent name coincides with
8383// the trait method name here; the delegation stays explicit so the
8384// SAME wiring shape applies whether the inherent projection is `label`
8385// / `prefix` / `marker` / `keyword` / `as_str`. The `display` flag
8386// emits the substrate-wide `f.write_str(Self::label(*self))` block.
8387// `#[closed_set(generate_unknown = "sexp shape")]` emits the typed
8388// parse-rejection carrier with the substrate-wide `Debug + Clone +
8389// PartialEq + Eq + thiserror::Error` derives and the `#[error("unknown
8390// sexp shape: {0}")]` annotation byte-for-byte; the explicit label
8391// matches the auto-derived `pascal_to_spaced_lowercase("SexpShape")`
8392// projection byte-for-byte but pins the pre-lift wording. Round-trip
8393// + cross-axis rejection (`ExpectedKwargShape` labels `"number"` /
8394// `"list of strings"` whose vocabulary partially overlaps SexpShape on
8395// five of seven entries) pinned by
8396// `sexp_shape_label_round_trips_through_from_str` +
8397// `sexp_shape_from_str_accepts_only_canonical_labels`.
8398
8399/// Closed-set identifier for the STRUCTURAL-RESIDUAL carving of the
8400/// twelve-variant [`SexpShape`] outer-shape lattice — the 2-of-12
8401/// subset that names the two outermost shapes lying outside BOTH
8402/// the atomic-payload carving ([`crate::ast::AtomKind`], 6-of-12) and
8403/// the quote-family carving ([`crate::ast::QuoteForm`], 4-of-12):
8404/// [`crate::ast::Sexp::Nil`] (the empty-result marker projecting to
8405/// [`SexpShape::Nil`]) and [`crate::ast::Sexp::List`] (the
8406/// payload-bearing container projecting to [`SexpShape::List`]).
8407///
8408/// Third-and-final closed-set carving of [`SexpShape`], sibling of
8409/// [`crate::ast::AtomKind`] (the 6-of-12 atomic-payload carving) and
8410/// [`crate::ast::QuoteForm`] (the 4-of-12 quote-family carving). The
8411/// three carvings partition [`SexpShape::ALL`] disjointly (6 + 4 + 2 =
8412/// 12) — every outer shape is in EXACTLY ONE carving, and the
8413/// partition-total invariant becomes a TYPED THEOREM through the
8414/// three sibling projections
8415/// ([`SexpShape::as_atom_kind`], [`SexpShape::as_quote_form`],
8416/// [`SexpShape::as_structural_kind`]) rather than through a runtime
8417/// `matches!(shape, SexpShape::Nil | SexpShape::List)` predicate that
8418/// the substrate maintained by test discipline at the carving-
8419/// disjointness pin pre-lift.
8420///
8421/// Mirror at the residual-carving boundary of the prior-run
8422/// [`crate::ast::AtomKind`] (atomic-payload closed set),
8423/// [`crate::ast::QuoteForm`] (quote-family closed set), and
8424/// [`UnquoteForm`] (template-substitution subset closed set) lifts:
8425/// those enums key their respective carvings on a typed identity
8426/// carried inside their variant listings; this enum keys the RESIDUAL
8427/// carving that lies outside all three of them on the same
8428/// [`SexpShape`] outer-shape lattice, closing the three-carving
8429/// partition.
8430///
8431/// Theory anchor: THEORY.md §V.1 — knowable platform; the residual
8432/// carving becomes a TYPE rather than an inline
8433/// `matches!(shape, SexpShape::Nil | SexpShape::List)` predicate the
8434/// substrate maintained by callsite discipline. THEORY.md §VI.1 —
8435/// generation over composition; the typed enum lands the structural-
8436/// completeness floor for the residual outer-shape surface, parallel
8437/// to how [`crate::ast::AtomKind`] lands it for the atomic-payload
8438/// carving and [`crate::ast::QuoteForm`] lands it for the quote-family
8439/// carving. THEORY.md §II.1 invariant 1 — typed entry; the
8440/// (structural-residual identity, outer shape) pairing is first-class
8441/// load-bearing data on the substrate algebra rather than a runtime
8442/// negation of the two sibling carvings.
8443#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
8444#[closed_set(via = "label", display, generate_unknown = "structural kind")]
8445pub enum StructuralKind {
8446 /// `[`crate::ast::Sexp::Nil`]` — the empty-result marker projecting
8447 /// to [`SexpShape::Nil`]. `"nil"` diagnostic label (byte-for-byte
8448 /// identical to [`SexpShape::Nil`]'s label under the
8449 /// [`Self::sexp_shape`] composition).
8450 Nil,
8451 /// `[`crate::ast::Sexp::List(_)`]` — the payload-bearing container
8452 /// projecting to [`SexpShape::List`]. `"list"` diagnostic label
8453 /// (byte-for-byte identical to [`SexpShape::List`]'s label under
8454 /// the [`Self::sexp_shape`] composition).
8455 List,
8456}
8457
8458impl StructuralKind {
8459 /// The closed set of two structural-residual outer shapes — single
8460 /// source of truth that drives the [`Self::label`] / [`fmt::Display`]
8461 /// projection AND the [`FromStr`] decode sweep keyed on
8462 /// [`Self::label`]. Adding a hypothetical third structural-residual
8463 /// variant (e.g. `Vector` for `#(...)` reader syntax, `Map` for
8464 /// `{...}`, or `Char` for `#\x` — each of which extends
8465 /// [`SexpShape::ALL`] AND joins THIS enum's residual carving iff
8466 /// it lies outside BOTH the atomic-payload carving and the
8467 /// quote-family carving) lands at one [`Self::ALL`] entry + one
8468 /// [`Self::sexp_shape`] arm + one [`SexpShape::as_structural_kind`]
8469 /// arm — exhaustively checked by the compiler (the `[Self; 2]`
8470 /// array literal forces the arity) AND by the per-variant truth-
8471 /// table tests below.
8472 ///
8473 /// Sibling closed-set lift to every other typed-shape enum the
8474 /// substrate carries: this crate's own [`SexpShape::ALL`] (the
8475 /// twelve reachable outer shapes — superset of this residual's
8476 /// two via [`Self::sexp_shape`]), [`crate::ast::AtomKind::ALL`]
8477 /// (the six atomic-payload kinds — peer 6-of-12 carving of
8478 /// [`SexpShape`]), [`crate::ast::QuoteForm::ALL`] (the four
8479 /// homoiconic prefix-wrappers — peer 4-of-12 carving of
8480 /// [`SexpShape`]), [`UnquoteForm::ALL`] (the two template-
8481 /// substitution markers — proper 2-of-4 subset of [`crate::ast::QuoteForm`]),
8482 /// and the cross-crate `tatara-process` family (`ConditionKind::ALL`,
8483 /// `ProcessPhase::ALL`, `ProcessSignal::ALL`, `ChannelKind::ALL`,
8484 /// `IntentKind::ALL`, `LifetimeKind::ALL`, `RequestorKind::ALL`,
8485 /// `ReceiptKind::ALL`, …) every one of which paired its typed
8486 /// projection with `ALL` before this lift.
8487 ///
8488 /// Future consumers that compose against `ALL`: LSP / REPL
8489 /// completion for the operator-facing rendered residual-shape
8490 /// label under the "structural" carving-axis column; a future
8491 /// `tatara-check` predicate `(check-shape-projects-to-structural
8492 /// …)` that filters diagnostics to "was this rejection on the
8493 /// structural residual (neither atomic nor quote-family)?"; any
8494 /// future audit-trail metric jointly labeled by [`Self::label`]
8495 /// (e.g. `tatara_lisp_structural_shape_total{shape="list"}`) —
8496 /// the metric label set IS [`Self::ALL`] mapped through
8497 /// [`Self::label`]; any future structural rewriter (typed analogue
8498 /// of MLIR's `op.walk<StructuralOp>()`) that wants to sweep over
8499 /// every residual shape in a typed sequence.
8500 pub const ALL: [Self; 2] = [Self::Nil, Self::List];
8501
8502 /// Canonical `&'static str` bytes for the [`Self::Nil`] structural-
8503 /// residual marker — aliases [`SexpShape::NIL_LABEL`] on the
8504 /// StructuralKind ⊂ SexpShape carving so the marker-level per-role
8505 /// bytes bind at ONE `pub const` on the parent superset's residual
8506 /// arm rather than at TWO sites (the per-role `pub const` AND a
8507 /// parallel inline literal). Per-role peer of `Self::Nil` on the
8508 /// closed-set structural-residual algebra; consumers reach for
8509 /// `StructuralKind::NIL_LABEL` when the caller has a variant in
8510 /// hand at compile time and wants the canonical diagnostic bytes
8511 /// without runtime dispatch through [`Self::label`].
8512 ///
8513 /// Sibling posture to the peer 6-of-12 atomic-payload carving's
8514 /// per-role aliases ([`crate::ast::AtomKind::SYMBOL_LABEL`] …
8515 /// [`crate::ast::AtomKind::BOOL_LABEL`] — every one an alias of
8516 /// its [`SexpShape`] peer) and the peer 4-of-12 quote-family
8517 /// carving's per-role prefixes on [`crate::ast::QuoteForm`] — the
8518 /// third and final closed-set carving of [`SexpShape`] now
8519 /// surfaces its per-role marker bytes through the same alias-chain
8520 /// shape rather than through the composition
8521 /// [`Self::sexp_shape`] + [`SexpShape::label`] alone.
8522 pub const NIL_LABEL: &'static str = SexpShape::NIL_LABEL;
8523
8524 /// Canonical `&'static str` bytes for the [`Self::List`]
8525 /// structural-residual marker — aliases [`SexpShape::LIST_LABEL`]
8526 /// on the StructuralKind ⊂ SexpShape carving. Per-role peer of
8527 /// `Self::List`; matches [`ExpectedKwargShape::LIST_LABEL`] byte-
8528 /// for-byte through the SexpShape canonical site (the same
8529 /// cross-axis overlap [`SexpShape::LIST_LABEL`]'s docstring pins).
8530 pub const LIST_LABEL: &'static str = SexpShape::LIST_LABEL;
8531
8532 /// Closed-set forced-arity ALL array over the canonical structural-
8533 /// residual marker `&'static str` bytes, in declaration order
8534 /// matching [`Self::ALL`] element-wise (pinned by
8535 /// `structural_kind_labels_align_with_all_by_index`). Sibling
8536 /// posture to [`SexpShape::LABELS`] (`[&'static str; 12]` — the
8537 /// superset carving this StructuralKind subset embeds into),
8538 /// [`crate::ast::AtomKind::LABELS`] (`[&'static str; 6]` — the
8539 /// peer 6-of-12 atomic-payload carving's ALL array),
8540 /// [`ExpectedKwargShape::LABELS`] (`[&'static str; 7]`),
8541 /// [`KwargPathKind::LABELS`] (`[&'static str; 3]`),
8542 /// [`MacroDefHead::KEYWORDS`] (`[&'static str; 3]`),
8543 /// [`crate::ast::Atom::BOOL_LITERALS`] (`[&'static str; 2]`), and
8544 /// [`crate::ast::QuoteForm::PREFIXES`] (`[&'static str; 4]`) —
8545 /// every closed-set outer projection on the substrate that carries
8546 /// an `&'static str`-per-variant label now pins its per-role
8547 /// canonical bytes at ONE `pub const` per role PLUS an ALL array
8548 /// for family-wide consumers.
8549 ///
8550 /// Pre-lift the two structural-residual marker bytes had NO per-
8551 /// role primitive on this closed-set algebra — a consumer with a
8552 /// `StructuralKind` variant in hand at compile time reaching for
8553 /// the canonical diagnostic bytes had to spell
8554 /// `StructuralKind::Nil.label()` (runtime dispatch through the
8555 /// composition [`Self::sexp_shape`] + [`SexpShape::label`]) OR
8556 /// reach across the algebra boundary into
8557 /// [`SexpShape::NIL_LABEL`] and re-derive the StructuralKind ⊂
8558 /// SexpShape variant pairing at the call site. Post-lift the TWO
8559 /// canonical bytes bind at ONE `pub const` per role on the typed
8560 /// [`StructuralKind`] algebra AND at [`Self::LABELS`] as a family-
8561 /// wide forced-arity array — a future LSP / REPL completion bar
8562 /// keyed on `StructuralKind::LABELS` for the "structural" carving-
8563 /// axis column, a `tatara-check` coverage sweep over the residual
8564 /// arms of a `TypeMismatch.got` corpus, or a Sekiban audit-trail
8565 /// metric jointly labeled by the structural marker
8566 /// (`tatara_lisp_structural_shape_total{kind="list"}`) reads
8567 /// through the typed constants on this subset algebra without
8568 /// re-deriving the 2-of-12 carving inline.
8569 ///
8570 /// Each entry is byte-for-byte identical to the corresponding
8571 /// [`SexpShape`] residual arm — an intentional cross-axis overlap
8572 /// pinned by
8573 /// `structural_kind_per_role_labels_alias_sexp_shape_per_role_labels_byte_for_byte`
8574 /// so a future label rename on EITHER side (a SexpShape `"nil"` →
8575 /// `"empty"` drift, or a StructuralKind rename that skips the
8576 /// alias) fails-loudly at the alias test rather than as a silent
8577 /// operator-facing vocabulary fracture. Adding a hypothetical
8578 /// third structural-residual variant (e.g. `Vector` for `#(...)`
8579 /// reader syntax, `Map` for `{...}`) extends [`Self::ALL`] AND
8580 /// [`Self::LABELS`] AND adds ONE per-role `pub const` alias in
8581 /// lockstep — rustc's forced-arity check on the two `[_; N]`
8582 /// arrays fails compilation if EITHER ALL array grows without the
8583 /// other.
8584 ///
8585 /// Theory anchor: THEORY.md §III — the typescape; the two
8586 /// canonical structural-residual marker bytes bind at ONE typed
8587 /// `[&'static str; 2]` array on the closed-set StructuralKind
8588 /// algebra rather than at zero-primitive-on-this-subset-plus-two-
8589 /// inline-lookups scattered across the substrate. THEORY.md §V.1
8590 /// — knowable platform; the family's cardinality becomes a TYPE-
8591 /// level constant on the substrate algebra rather than a per-
8592 /// consumer runtime dispatch through the composition. THEORY.md
8593 /// §VI.1 — generation over composition; the family-wide contract
8594 /// sweeps (alignment with `ALL`, pairwise disjointness, membership
8595 /// through [`Self::label`]) emerge from the composition of TWO
8596 /// substrate primitives (this `pub const` array + the two per-
8597 /// role `pub const *_LABEL` aliases) rather than as per-variant
8598 /// inline assertions duplicated at each call site.
8599 pub const LABELS: [&'static str; 2] = [Self::NIL_LABEL, Self::LIST_LABEL];
8600
8601 /// Project the typed `StructuralKind` to the canonical `&'static
8602 /// str` diagnostic label — `"nil"` for [`Self::Nil`], `"list"` for
8603 /// [`Self::List`]. Each label is byte-for-byte identical to the
8604 /// corresponding [`SexpShape`] variant's label — and post-lift
8605 /// this agreement is STRUCTURAL rather than a two-literal-
8606 /// discipline site pinned by a cross-projection test.
8607 ///
8608 /// Composition law: `StructuralKind::label(sk) ==
8609 /// StructuralKind::sexp_shape(sk).label()` for every `sk:
8610 /// StructuralKind`. Body composes [`Self::sexp_shape`] (the typed
8611 /// projection lifting each StructuralKind variant into its peer
8612 /// [`SexpShape`] variant) with [`SexpShape::label`] (the canonical
8613 /// `&'static str` projection on the superset's twelve-variant
8614 /// closed set), so the two residual-arm labels live at ONE
8615 /// canonical site ([`SexpShape::label`]) rather than at TWO
8616 /// ([`SexpShape::label`] AND a parallel two-arm match here).
8617 ///
8618 /// Same lift posture as the sibling
8619 /// [`crate::ast::AtomKind::label`] ⊂ [`SexpShape::label`]
8620 /// composition (via [`crate::ast::AtomKind::sexp_shape`]) and
8621 /// [`UnquoteForm::marker`] ⊂ [`crate::ast::QuoteForm::prefix`]
8622 /// composition (via [`UnquoteForm::to_quote_form`]): the typed
8623 /// projection sits on the value, and the consumer composes
8624 /// through the existing structural pairing rather than re-
8625 /// deriving the per-variant literal. The `&'static str` lifetime
8626 /// is load-bearing: the composition allocates nothing at runtime
8627 /// ([`Self::sexp_shape`] returns a `Copy` value and
8628 /// [`SexpShape::label`] yields `&'static str`).
8629 ///
8630 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
8631 /// StructuralKind ⊂ SexpShape label-vocabulary containment
8632 /// becomes a TYPED CONSEQUENCE of the [`Self::sexp_shape`] +
8633 /// [`SexpShape::label`] composition rather than literal discipline
8634 /// at two sites. THEORY.md §VI.1 — generation over composition;
8635 /// the two residual-arm labels live at ONE canonical site
8636 /// ([`SexpShape::label`]) and this method generates its identity
8637 /// through the typed-projection composition. THEORY.md §II.1
8638 /// invariant 2 — free middle; consumers of the [`StructuralKind`]
8639 /// algebra route through ONE typed closed-set projection family
8640 /// with no per-consumer literal duplication.
8641 #[must_use]
8642 pub fn label(self) -> &'static str {
8643 self.sexp_shape().label()
8644 }
8645
8646 /// Project the typed marker into its matching [`SexpShape`]
8647 /// variant — `Nil → SexpShape::Nil`, `List → SexpShape::List`.
8648 /// ONE projection on the closed-set structural-residual algebra
8649 /// that binds the (StructuralKind variant, SexpShape variant)
8650 /// pairing at ONE site on the typed algebra rather than at two
8651 /// byte-identical inline arms scattered across consumers. Direct
8652 /// sibling to [`crate::ast::AtomKind::sexp_shape`] (the 6-of-12
8653 /// atomic-payload carving's embed) and
8654 /// [`crate::ast::QuoteForm::sexp_shape`] (the 4-of-12 quote-family
8655 /// carving's embed) — all three embeds land at ONE typed method
8656 /// per closed-set carving, symmetric across the partition.
8657 ///
8658 /// Bidirectional dual: the inverse projection
8659 /// [`SexpShape::as_structural_kind`] (12→2, partial) covers the
8660 /// 2-of-12 carving of [`SexpShape`] this embed reaches. The pair
8661 /// `(StructuralKind::sexp_shape, SexpShape::as_structural_kind)`
8662 /// forms an `Iso(StructuralKind, StructuralShape ⊂ SexpShape)`:
8663 /// every typed marker round-trips through the embed
8664 /// (`StructuralKind::sexp_shape(sk).as_structural_kind() ==
8665 /// Some(sk)` for every `sk: StructuralKind`), every structural-
8666 /// residual shape pre-image recovers the typed marker. The ten
8667 /// non-residual shapes (`Symbol`, `Keyword`, `String`, `Int`,
8668 /// `Float`, `Bool`, `Quote`, `Quasiquote`, `Unquote`,
8669 /// `UnquoteSplice`) form the kernel of the inverse —
8670 /// `as_structural_kind` returns `None` for them.
8671 ///
8672 /// Composition law (partition-total, jointly with the two sibling
8673 /// carvings): for every `shape: SexpShape`, EXACTLY ONE of
8674 /// `shape.as_atom_kind()`, `shape.as_quote_form()`,
8675 /// `shape.as_structural_kind()` is `Some(_)`. Pinned by the joint
8676 /// partition test in this module, so a regression that drifts any
8677 /// of the three carvings' partition-membership (an
8678 /// [`crate::ast::AtomKind::sexp_shape`] arm, a
8679 /// [`crate::ast::QuoteForm::sexp_shape`] arm, or this
8680 /// [`Self::sexp_shape`] arm) surfaces immediately.
8681 ///
8682 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
8683 /// (StructuralKind variant, SexpShape variant) pairing becomes a
8684 /// TYPE projection on the substrate algebra rather than a runtime
8685 /// `matches!(shape, SexpShape::Nil | SexpShape::List)` predicate.
8686 /// A typo or swap at the shape-projection site is no longer a
8687 /// runtime drift but a compile error against the typed projection.
8688 /// THEORY.md §II.1 invariant 2 — free middle; the (embed, project)
8689 /// pair binds at TWO typed sites — [`Self::sexp_shape`] for the
8690 /// embed, [`SexpShape::as_structural_kind`] for the project —
8691 /// completing the third and final closed-set carving of the
8692 /// outer-shape algebra. THEORY.md §VI.1 — generation over
8693 /// composition; the two residual arms lift to ONE typed method,
8694 /// and the partition-total invariant across the three carvings
8695 /// becomes a TYPED THEOREM rather than a test assertion.
8696 #[must_use]
8697 pub fn sexp_shape(self) -> SexpShape {
8698 match self {
8699 Self::Nil => Self::NIL_SHAPE,
8700 Self::List => Self::LIST_SHAPE,
8701 }
8702 }
8703
8704 /// Canonical [`SexpShape`] embed target for the [`Self::Nil`]
8705 /// structural-residual arm on the StructuralKind ⊂ SexpShape
8706 /// 2-of-12 carving — [`SexpShape::Nil`]. Per-role peer of
8707 /// `Self::Nil` on the closed-set structural-residual → outer-shape
8708 /// embed axis; consumers with a `StructuralKind` variant in hand
8709 /// at compile time bind the canonical embed target through ONE
8710 /// typed `pub const` per role rather than through runtime dispatch
8711 /// via [`Self::sexp_shape`] or by re-deriving the StructuralKind ⊂
8712 /// SexpShape variant pairing inline.
8713 ///
8714 /// Sibling posture to [`Self::NIL_LABEL`] (the per-role diagnostic
8715 /// label alias) and [`Self::NIL_HASH_DISCRIMINATOR`] (the per-role
8716 /// outer-Sexp cache-key byte) on the closed-set StructuralKind
8717 /// algebra — each closes a distinct per-role sub-vocabulary axis
8718 /// on the StructuralKind carving. This constant closes the THIRD
8719 /// per-role axis on [`StructuralKind`] (the `SexpShape`-embed
8720 /// axis, paired with the pre-existing `&'static str`
8721 /// diagnostic-label axis + the `u8` cache-key axis) at ONE typed
8722 /// alias through the peer superset variant on the [`SexpShape`]
8723 /// closed set.
8724 ///
8725 /// Sibling posture to the peer 6-of-12 atomic-payload carving's
8726 /// per-role SHAPE aliases
8727 /// ([`crate::ast::AtomKind::SYMBOL_SHAPE`] …
8728 /// [`crate::ast::AtomKind::BOOL_SHAPE`]) and the peer 4-of-12
8729 /// quote-family carving's per-role SHAPE aliases
8730 /// ([`crate::ast::QuoteForm::QUOTE_SHAPE`] …
8731 /// [`crate::ast::QuoteForm::UNQUOTE_SPLICE_SHAPE`]). Post-lift the
8732 /// SexpShape's per-carving embed-target axis is uniformly surfaced
8733 /// through per-role `pub const *_SHAPE` aliases on every
8734 /// sub-carving that carries a bidirectional (embed, project)
8735 /// `Iso(_, _ ⊂ SexpShape)` — `AtomKind` (6), `QuoteForm` (4), and
8736 /// now `StructuralKind` (2) — closing the third and final peer of
8737 /// the twelve-arm superset.
8738 pub const NIL_SHAPE: SexpShape = SexpShape::Nil;
8739
8740 /// Canonical [`SexpShape`] embed target for the [`Self::List`]
8741 /// structural-residual arm on the StructuralKind ⊂ SexpShape
8742 /// 2-of-12 carving — [`SexpShape::List`]. Per-role peer of
8743 /// `Self::List`. See [`Self::NIL_SHAPE`] for the alias-chain shape
8744 /// every sibling shares.
8745 pub const LIST_SHAPE: SexpShape = SexpShape::List;
8746
8747 /// Closed-set forced-arity ALL array over the canonical
8748 /// [`SexpShape`] embed targets on the StructuralKind ⊂ SexpShape
8749 /// 2-of-12 carving, in declaration order matching [`Self::ALL`]
8750 /// element-wise (pinned by
8751 /// `structural_kind_shapes_align_with_all_by_index`). Sibling
8752 /// posture to [`Self::LABELS`] (`[&'static str; 2]` — per-role
8753 /// diagnostic bytes) and [`Self::HASH_DISCRIMINATORS`] (`[u8; 2]`
8754 /// — per-role outer-Sexp cache-key bytes) on the SAME closed-set
8755 /// StructuralKind algebra; where those two arrays lift per-role
8756 /// `&'static str` and `u8` sub-vocabularies onto the substrate,
8757 /// this array lifts the per-role [`SexpShape`] embed-target
8758 /// sub-vocabulary at the same `[_; 2]` forced arity.
8759 ///
8760 /// Sibling posture to [`crate::ast::AtomKind::SHAPES`]
8761 /// (`[SexpShape; 6]`) and [`crate::ast::QuoteForm::SHAPES`]
8762 /// (`[SexpShape; 4]`) — the two peer carvings' family-wide
8763 /// embed-target arrays on the AtomKind ⊂ SexpShape 6-of-12 and
8764 /// QuoteForm ⊂ SexpShape 4-of-12 carvings. Together the THREE
8765 /// `SHAPES` arrays (6 + 4 + 2 = 12) cover EVERY sub-carving of
8766 /// [`SexpShape`] that carries a bidirectional `Iso(_, _ ⊂
8767 /// SexpShape)` at the same `[SexpShape; N]` forced-arity shape —
8768 /// a family-wide sweep zipping every carving's `ALL` + `SHAPES` in
8769 /// lockstep now closes over the FULL 12-arm outer-shape algebra at
8770 /// ONE typed pair-of-arrays per carving. This lift closes the
8771 /// THIRD and FINAL peer array — post-lift the substrate's every
8772 /// closed-set carving of [`SexpShape`] has its embed-target
8773 /// sub-vocabulary surfaced through a per-role `pub const` + ALL
8774 /// array uniformly.
8775 ///
8776 /// Pre-lift the two [`SexpShape`] embed targets had NO per-role
8777 /// primitive on this closed-set algebra — a consumer with a
8778 /// [`StructuralKind`] variant in hand at compile time reaching for
8779 /// the canonical embed target had to spell
8780 /// `StructuralKind::Nil.sexp_shape()` (runtime dispatch through
8781 /// the two-arm match body) OR re-derive the StructuralKind ⊂
8782 /// SexpShape variant pairing at the call site by importing both
8783 /// enums and spelling `SexpShape::Nil` inline. Post-lift the TWO
8784 /// canonical embed targets bind at ONE `pub const` per role on the
8785 /// typed [`StructuralKind`] algebra AND at [`Self::SHAPES`] as a
8786 /// family-wide forced-arity array — a future LSP / REPL completion
8787 /// bar keyed on `StructuralKind::SHAPES` for the "which SexpShape
8788 /// does this StructuralKind embed into?" outer-shape column, a
8789 /// `tatara-check` coverage sweep zipping `StructuralKind::ALL` /
8790 /// `LABELS` / `HASH_DISCRIMINATORS` / `SHAPES` in lockstep for a
8791 /// family-wide (variant, label, byte, embed-target) quadruple
8792 /// render, or a Sekiban audit-trail metric jointly labeled by the
8793 /// embed-target's SexpShape identity reads through the typed
8794 /// constants on this subset algebra without re-deriving the 2-of-12
8795 /// carving inline.
8796 ///
8797 /// Round-trip identity with the inverse projection
8798 /// [`SexpShape::as_structural_kind`]: for every index `i`,
8799 /// `Self::SHAPES[i].as_structural_kind() == Some(Self::ALL[i])`
8800 /// (pinned by
8801 /// `structural_kind_shapes_align_with_all_by_index_through_as_structural_kind`)
8802 /// — the embed / project section closes as a family-wide array-
8803 /// indexed law rather than as a per-variant assertion sweep.
8804 /// Adding a hypothetical third structural-residual variant (e.g.
8805 /// `Vector` for `#(...)` reader syntax, `Map` for `{...}`, or
8806 /// `Char` for `#\x` — each of which extends [`SexpShape::ALL`]
8807 /// AND joins THIS carving iff it lies outside BOTH the
8808 /// atomic-payload and quote-family carvings) extends [`Self::ALL`]
8809 /// AND [`Self::SHAPES`] AND [`SexpShape::ALL`] AND adds ONE
8810 /// per-role `pub const *_SHAPE` in lockstep — rustc's forced-arity
8811 /// check on the two `[_; N]` arrays fails compilation if EITHER
8812 /// ALL array grows without the other, AND the peer
8813 /// [`SexpShape::as_structural_kind`] arm must grow in lockstep to
8814 /// preserve the round-trip identity.
8815 ///
8816 /// Theory anchor: THEORY.md §III — the typescape; the two
8817 /// canonical [`SexpShape`] embed targets bind at ONE typed
8818 /// `[SexpShape; 2]` array on the closed-set StructuralKind algebra
8819 /// rather than at zero-primitive-on-this-subset-plus-two-inline-
8820 /// lookups scattered across the substrate. Closes the THIRD
8821 /// per-role `pub const` axis on the StructuralKind carving
8822 /// alongside the pre-existing LABELS + HASH_DISCRIMINATORS axes.
8823 /// THEORY.md §V.1 — knowable platform; the family's cardinality
8824 /// becomes a TYPE-level constant on the substrate algebra rather
8825 /// than a per-consumer runtime dispatch through the composition. A
8826 /// regression that drifts the (StructuralKind variant, SexpShape
8827 /// variant) pairing at ONE of the three typed sites (per-role
8828 /// alias, family-wide array, projection method) fails-loudly at
8829 /// the array-indexed sweep rather than as a silent operator-facing
8830 /// diagnostic drift. THEORY.md §II.1 invariant 2 — free middle;
8831 /// the (embed, project) pair binds at THREE typed sites now — the
8832 /// projection method [`Self::sexp_shape`], this family-wide array,
8833 /// AND the peer inverse [`SexpShape::as_structural_kind`] — with
8834 /// rustc-enforced consistency across all three. THEORY.md §VI.1
8835 /// — generation over composition; the family-wide contract sweeps
8836 /// (alignment with `ALL`, round-trip through
8837 /// `as_structural_kind`, membership through `sexp_shape`, pairwise
8838 /// injectivity across the two embed targets) emerge from the
8839 /// composition of TWO substrate primitives (this `pub const`
8840 /// array + the two per-role `pub const *_SHAPE` aliases) rather
8841 /// than as per-variant inline assertions duplicated at each call
8842 /// site.
8843 pub const SHAPES: [SexpShape; 2] = [Self::NIL_SHAPE, Self::LIST_SHAPE];
8844
8845 /// Stable, per-variant byte discriminator that paired with the
8846 /// residual outer-shape hash body builds the substrate's
8847 /// [`Hash for Sexp`](crate::ast::Sexp) projection at the two
8848 /// structural-residual arms — `0u8` for [`Self::Nil`], `2u8` for
8849 /// [`Self::List`]. The byte values are load-bearing because the
8850 /// macro-expansion cache ([`crate::macro_expand::Expander`]'s cache)
8851 /// keys on the hash of `(macro_name, args)`, and every `Sexp`
8852 /// participates in that hash — changing a discriminator silently
8853 /// invalidates every cached expansion across the substrate AND
8854 /// risks collision with the reserved bytes the other two
8855 /// closed-set carvings' arms use (`1u8` for the [`crate::ast::Sexp::Atom`]
8856 /// outer-carve marker, `3..=6` for the quote-family via
8857 /// [`crate::ast::QuoteForm::hash_discriminator`]).
8858 ///
8859 /// The closed set ensures the two arms partition `{0, 2}`
8860 /// injectively. Disjointness across the outer-Sexp discriminator
8861 /// space is structural rather than overlap-induced hash
8862 /// collision: [`crate::ast::Sexp::Atom(_)`]'s outer-carve byte
8863 /// (`1u8`, single-arm literal inside [`Hash for Sexp`](crate::ast::Sexp))
8864 /// AND [`crate::ast::QuoteForm::hash_discriminator`]'s
8865 /// `{3, 4, 5, 6}` partition sit around this carving's `{0, 2}`
8866 /// with a compile-time-checked prefix-uniqueness contract
8867 /// [`Hash for Sexp`](crate::ast::Sexp)'s outer match maintains
8868 /// through the exhaustive closed-set arm coverage. A future
8869 /// structural-residual, quote-family, or atomic-kind extension
8870 /// must extend both this method's arms AND the sibling
8871 /// discriminator methods in lockstep, with rustc binding the
8872 /// consistency through exhaustiveness over each closed enum.
8873 ///
8874 /// The SIBLING of the two prior-run discriminator lifts on the
8875 /// same outer-Sexp Hash body: [`crate::ast::AtomKind::hash_discriminator`]
8876 /// carries the six-variant atomic-payload carve's inner cache-key
8877 /// bytes at the nested `Atom::hash` level, and
8878 /// [`crate::ast::QuoteForm::hash_discriminator`] carries the four-
8879 /// variant quote-family carve's outer-Sexp cache-key bytes at the
8880 /// same `Hash for Sexp` body. Post-lift the SAME shape lands at
8881 /// the structural-residual axis — every carving now surfaces its
8882 /// hash-projection through ONE typed method rather than through
8883 /// per-arm inline byte literals at the [`Hash for Sexp`](crate::ast::Sexp)
8884 /// body. The three carvings' `hash_discriminator` methods form
8885 /// the joint bit-map onto the outer-Sexp cache-key space, and
8886 /// their partition-disjointness is the substrate's cache-key
8887 /// prefix-uniqueness contract.
8888 ///
8889 /// `pub(crate)` because the byte-discriminator surface is an
8890 /// implementation detail of the substrate's [`Hash for Sexp`](crate::ast::Sexp)
8891 /// cache-key contract; exposing it publicly would leak the
8892 /// cache-key shape through the API without enabling any external
8893 /// consumer the public projections ([`Self::label`],
8894 /// [`Self::sexp_shape`]) don't already serve. Same posture as
8895 /// [`crate::ast::AtomKind::hash_discriminator`] and
8896 /// [`crate::ast::QuoteForm::hash_discriminator`].
8897 ///
8898 /// Theory anchor: THEORY.md §V.1 — knowable platform; the
8899 /// (StructuralKind variant, cache-key byte) pairing becomes a TYPE
8900 /// projection on the substrate algebra rather than two `0u8`/`2u8`
8901 /// inline literals in [`Hash for Sexp`](crate::ast::Sexp). A typo
8902 /// or swap at the hash-projection site is no longer a runtime
8903 /// cache-key drift but a compile error against the typed
8904 /// projection. THEORY.md §II.1 invariant 2 — free middle; ALL
8905 /// THREE closed-set carvings (atomic-payload via
8906 /// [`crate::ast::AtomKind::hash_discriminator`], quote-family via
8907 /// [`crate::ast::QuoteForm::hash_discriminator`], structural-
8908 /// residual via this method) now route through a typed
8909 /// closed-set match family, so a regression that drifts ONE
8910 /// carving's discriminator bytes from the others cannot reach
8911 /// the substrate's runtime cache. THEORY.md §VI.1 — generation
8912 /// over composition; the two `0u8`/`2u8` residual-arm literals
8913 /// appeared inline at [`Hash for Sexp`](crate::ast::Sexp) — past
8914 /// the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
8915 /// named, and post-lift this method's two arms are the ONE
8916 /// canonical site the outer-Sexp Hash body composes through.
8917 /// Canonical `u8` cache-key byte for [`Self::Nil`]'s
8918 /// [`Self::hash_discriminator`] arm — `0`. Per-role peer of
8919 /// [`Self::Nil`] on the closed-set structural-residual cache-key-
8920 /// byte axis; consumers reach for
8921 /// `StructuralKind::NIL_HASH_DISCRIMINATOR` when the caller has a
8922 /// variant in hand at compile time and wants the canonical byte
8923 /// without runtime dispatch through [`Self::hash_discriminator`].
8924 /// The byte is load-bearing because the macro-expansion cache
8925 /// ([`crate::macro_expand::Expander`]'s cache) keys on
8926 /// [`Hash for Sexp`](crate::ast::Sexp), and any renumbering
8927 /// silently invalidates every cached expansion AND risks collision
8928 /// with the reserved bytes the other two closed-set carvings' arms
8929 /// use (`1u8` for the [`crate::ast::Sexp::Atom`] outer-carve
8930 /// marker, `3..=6` for the quote-family via
8931 /// [`crate::ast::QuoteForm::hash_discriminator`]).
8932 ///
8933 /// Sibling posture to
8934 /// [`crate::ast::AtomKind::SYMBOL_HASH_DISCRIMINATOR`] on the
8935 /// atomic-payload sub-carving of the SAME outer-`Sexp` Hash body
8936 /// AND to [`crate::ast::QuoteForm::QUOTE_HASH_DISCRIMINATOR`] on
8937 /// the quote-family sub-carving — the third and final closed-set
8938 /// carving of [`SexpShape`] now surfaces its per-role cache-key
8939 /// bytes at ONE `pub(crate) const` per variant PLUS a family-wide
8940 /// [`Self::HASH_DISCRIMINATORS`] array.
8941 ///
8942 /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
8943 /// preserves proofs; the alias-chain composition law
8944 /// `StructuralKind::HASH_DISCRIMINATORS[i] ==
8945 /// StructuralKind::ALL[i].hash_discriminator()` binds the family-
8946 /// wide array to the projection method at rustc time, pinned by
8947 /// byte equality. THEORY.md §III — the typescape; the two
8948 /// canonical cache-key bytes bind at ONE `pub(crate) const` per
8949 /// role on the typed algebra rather than as inline `u8` literals
8950 /// in the [`Self::hash_discriminator`] match arms.
8951 pub(crate) const NIL_HASH_DISCRIMINATOR: u8 = 0;
8952
8953 /// Canonical `u8` cache-key byte for [`Self::List`]'s
8954 /// [`Self::hash_discriminator`] arm — `2`. Sibling of
8955 /// [`Self::NIL_HASH_DISCRIMINATOR`] on the closed-set per-role
8956 /// structural-residual cache-key-byte axis; see
8957 /// [`Self::NIL_HASH_DISCRIMINATOR`] for the algebra-level round-
8958 /// trip + disjointness contracts every sibling shares.
8959 ///
8960 /// The gap at `1u8` between [`Self::NIL_HASH_DISCRIMINATOR`] and
8961 /// this constant is intentional and load-bearing: it reserves the
8962 /// outer-carve byte [`crate::ast::Sexp::Atom(_)`] uses at the
8963 /// outer [`Hash for Sexp`](crate::ast::Sexp) body — the joint
8964 /// partition `{0, 1, 2, 3, 4, 5, 6}` binds bit-for-bit across the
8965 /// three closed-set carvings AND the atomic-carve outer marker.
8966 pub(crate) const LIST_HASH_DISCRIMINATOR: u8 = 2;
8967
8968 /// Closed-set forced-arity ALL array over the canonical
8969 /// structural-residual cache-key `u8` bytes, in declaration order
8970 /// matching [`Self::ALL`] element-wise (pinned by
8971 /// `structural_kind_hash_discriminators_align_with_all_by_index`).
8972 /// Sibling posture to [`Self::LABELS`] (`[&'static str; 2]` — the
8973 /// diagnostic-label `&'static str` axis on the SAME closed set),
8974 /// [`crate::ast::AtomKind::HASH_DISCRIMINATORS`] (`[u8; 6]` — the
8975 /// atomic-payload sub-carving's cache-key-byte peer at the nested
8976 /// [`crate::ast::Atom`] Hash level), and
8977 /// [`crate::ast::QuoteForm::HASH_DISCRIMINATORS`] (`[u8; 4]` —
8978 /// the quote-family sub-carving's cache-key-byte peer at the same
8979 /// outer [`Hash for Sexp`](crate::ast::Sexp) body). Every closed-
8980 /// set carving of [`SexpShape`] now pins its per-role canonical
8981 /// cache-key bytes at ONE `pub(crate) const` per role PLUS an ALL
8982 /// array for family-wide consumers.
8983 ///
8984 /// Pre-lift the two cache-key bytes had NO per-role primitive on
8985 /// this closed-set algebra — a consumer with a [`StructuralKind`]
8986 /// variant in hand at compile time reaching for the canonical byte
8987 /// had to spell `StructuralKind::Nil.hash_discriminator()`
8988 /// (runtime dispatch through the match arm) OR reach across into
8989 /// the inline `0u8` at the pre-lift match arm's [`Self::Nil`]
8990 /// branch and re-derive the (variant, byte) pairing at the call
8991 /// site. Post-lift the TWO canonical bytes bind at ONE
8992 /// `pub(crate) const` per role on the typed [`StructuralKind`]
8993 /// algebra AND at [`Self::HASH_DISCRIMINATORS`] as a family-wide
8994 /// forced-arity array — a future substrate-facing cache-key
8995 /// introspection tool (a `tatara-check` predicate `(check-outer-
8996 /// sexp-cache-key-partition-injective …)` that asserts the joint
8997 /// `{0, 1, 2, 3, 4, 5, 6}` partition structurally by sweeping the
8998 /// three carvings' `HASH_DISCRIMINATORS` arrays, a Sekiban audit-
8999 /// trail metric jointly labeled by the structural-residual cache-
9000 /// key partition, a future `TypedRewriter<StructuralKindOp>` sweep
9001 /// zipping ALL / LABELS / HASH_DISCRIMINATORS in lockstep for a
9002 /// family-wide (variant, label, byte) triple render) reads
9003 /// through the typed constants without re-deriving the two-arm
9004 /// carving inline.
9005 ///
9006 /// Each entry is byte-for-byte identical to the pre-lift inline
9007 /// `u8` literal at the corresponding [`Self::hash_discriminator`]
9008 /// arm — pinned by
9009 /// `structural_kind_hash_discriminators_pin_legacy_cache_key_bytes`
9010 /// so a regression that drifts ONE `pub(crate) const` from its
9011 /// pre-lift byte silently invalidates every cached expansion of
9012 /// a [`crate::ast::Sexp::Nil`] / [`crate::ast::Sexp::List`]
9013 /// participating in [`crate::macro_expand::Expander::cache`],
9014 /// fails-loudly at the alias test rather than at a silent cache
9015 /// mis-hash. Adding a hypothetical third structural-residual
9016 /// variant (e.g. `Vector` for `#(...)` reader syntax, `Map` for
9017 /// `{...}`) extends [`Self::ALL`] AND [`Self::HASH_DISCRIMINATORS`]
9018 /// AND adds ONE per-role `pub(crate) const` in lockstep — rustc's
9019 /// forced-arity check on the two `[_; N]` arrays fails
9020 /// compilation if EITHER array grows without the other, closing
9021 /// the extensibility gap that pre-lift silently allowed a
9022 /// discriminator collision on `7u8` (the next free byte on the
9023 /// outer [`Sexp`](crate::ast::Sexp) cache-key space) or an
9024 /// accidental re-use of the `1u8` outer-carve byte reserved for
9025 /// [`crate::ast::Sexp::Atom(_)`].
9026 ///
9027 /// Theory anchor: THEORY.md §III — the typescape; the two
9028 /// canonical cache-key bytes bind at ONE typed `[u8; 2]` array on
9029 /// the closed-set [`StructuralKind`] algebra rather than at zero-
9030 /// primitive-plus-two-inline-`u8`-literals scattered across the
9031 /// [`Self::hash_discriminator`] match arms. THEORY.md §V.1 —
9032 /// knowable platform; the family's cardinality becomes a TYPE-
9033 /// level constant on the substrate algebra rather than a per-
9034 /// consumer runtime dispatch through the match table. THEORY.md
9035 /// §V.3 — three-pillar attestation; the cache-key partition is
9036 /// the substrate's outer [`Sexp`](crate::ast::Sexp) `intent_hash`
9037 /// composition axis for every structural-residual arm — binding
9038 /// the two bytes on the typed algebra makes attestation-key drift
9039 /// a compile error rather than a silent BLAKE3 mis-hash.
9040 /// THEORY.md §VI.1 — generation over composition; the family-
9041 /// wide contract sweeps (alignment with `ALL`, pairwise
9042 /// disjointness, membership through [`Self::hash_discriminator`])
9043 /// emerge from the composition of TWO substrate primitives (this
9044 /// `pub(crate) const` array + the two per-role
9045 /// `pub(crate) const *_HASH_DISCRIMINATOR` aliases) rather than
9046 /// as per-variant inline assertions duplicated at each call site.
9047 ///
9048 /// The `#[allow(dead_code)]` posture matches
9049 /// [`crate::ast::AtomKind::HASH_DISCRIMINATORS`] and
9050 /// [`crate::ast::QuoteForm::HASH_DISCRIMINATORS`]: the substrate's
9051 /// current [`Hash for Sexp`](crate::ast::Sexp) body composes
9052 /// through the per-variant [`Self::hash_discriminator`] projection
9053 /// arm-by-arm rather than sweeping the family-wide array, so no
9054 /// non-test caller currently reaches this ALL array directly. The
9055 /// lift lands the substrate primitive so future consumers keyed
9056 /// on the whole family (a future
9057 /// [`crate::macro_expand::Expander`] cache-warmup pass that
9058 /// hashes the structural-residual byte-set upfront, a future
9059 /// `tatara-check` predicate `(check-outer-sexp-cache-key-
9060 /// partition-injective …)` that verifies the joint
9061 /// `{0, 1, 2, 3, 4, 5, 6}` partition structurally, a future
9062 /// `TypedRewriter<StructuralKindOp>` sweep zipping ALL / LABELS /
9063 /// HASH_DISCRIMINATORS in lockstep for a family-wide (variant,
9064 /// label, byte) triple render) bind to ONE `[u8; 2]` primitive
9065 /// rather than re-deriving the array inline per callsite.
9066 #[allow(dead_code)]
9067 pub(crate) const HASH_DISCRIMINATORS: [u8; 2] =
9068 [Self::NIL_HASH_DISCRIMINATOR, Self::LIST_HASH_DISCRIMINATOR];
9069
9070 #[must_use]
9071 pub(crate) fn hash_discriminator(self) -> u8 {
9072 match self {
9073 Self::Nil => Self::NIL_HASH_DISCRIMINATOR,
9074 Self::List => Self::LIST_HASH_DISCRIMINATOR,
9075 }
9076 }
9077}
9078
9079// `impl std::fmt::Display for StructuralKind` + `impl std::str::FromStr
9080// for StructuralKind` + `impl tatara_closed_set::ClosedSet for StructuralKind` +
9081// `pub struct UnknownStructuralKind(pub String)` are generated by
9082// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
9083// above. `label` delegates to the inherent `StructuralKind::label` via
9084// `#[closed_set(via = "label")]` — the inherent name coincides with
9085// the trait method name here; the delegation stays explicit so the
9086// SAME wiring shape applies whether the inherent projection is `label`
9087// / `prefix` / `marker` / `keyword` / `as_str`. The `display` flag
9088// emits the substrate-wide `f.write_str(Self::label(*self))` block.
9089// `#[closed_set(generate_unknown = "structural kind")]` emits the typed
9090// parse-rejection carrier with the substrate-wide `Debug + Clone +
9091// PartialEq + Eq + thiserror::Error` derives and the `#[error("unknown
9092// structural kind: {0}")]` annotation byte-for-byte; the explicit label
9093// matches the auto-derived `pascal_to_spaced_lowercase("StructuralKind")`
9094// projection byte-for-byte but pins the pre-lift wording. Round-trip +
9095// cross-axis rejection (the label vocabulary partially overlaps
9096// [`SexpShape`]'s labels on the two residual entries `"nil"` and
9097// `"list"`; the disjointness contract is that a `FromStr` on THIS
9098// closed set MUST reject the ten non-residual `SexpShape` labels since
9099// they lie outside the carving) pinned by
9100// `structural_kind_label_round_trips_through_from_str` +
9101// `structural_kind_from_str_rejects_non_structural_sexp_shape_labels`.
9102
9103/// Typed witness of an offending `Sexp` at a typed-entry rejection
9104/// boundary — the joint identity (shape + literal) the substrate's
9105/// diagnostic surface owes the operator. Pairs the closed-set
9106/// `SexpShape` projection (the twelve reachable Sexp outermost shapes
9107/// the reader can produce) with the `Sexp::Display` projection (the
9108/// literal value the operator wrote: `5`, `:foo`, `(list 1 2)`,
9109/// `notify-ref`, etc.).
9110///
9111/// Mirror at the offending-value boundary of the prior-run
9112/// `SexpShape` (typed-shape closed set), `ExpectedKwargShape`
9113/// (expected-shape closed set), `KwargPath` (kwargs-path shapes),
9114/// `MacroDefHead` (macro-definition-head closed set), `UnquoteForm`
9115/// (template-marker syntactic forms), `CompilerSpecIoStage`
9116/// (disk-persistence surface), and `TemplateInvariantKind`
9117/// (bytecode-runtime surface) closed-set lifts: those enums key
9118/// rejection variants on a typed identity carried inside the
9119/// variant's data shape; `SexpWitness` keys the OFFENDING-VALUE side
9120/// (the `got: String` Sexp::Display slots on `NonSymbolUnquoteTarget`,
9121/// `SpliceOutsideList`, `NonSymbolParam`, `RestParamMissingName`,
9122/// `DefmacroNonSymbolName`, `DefmacroNonListParams`,
9123/// `MissingHeadSymbol`, and any future variant taking a `&Sexp` at
9124/// the helper boundary) on a typed joint identity so authoring tools
9125/// (REPL, LSP, `tatara-check`) bind to BOTH `witness.shape` (the
9126/// structural identity — pattern-matchable on `SexpShape::List` etc.)
9127/// AND `witness.display` (the literal value — renderable verbatim)
9128/// without losing either side.
9129///
9130/// Before this struct landed, the six error-builder helpers in
9131/// `macro_expand.rs` (`non_symbol_unquote_target`, `splice_outside_list`,
9132/// `non_symbol_param`, `rest_param_missing_name`,
9133/// `defmacro_non_symbol_name`, `defmacro_non_list_params`) and one
9134/// in `domain.rs` (`missing_head_err`'s caller) each projected `&Sexp
9135/// → String` via `Sexp::to_string()` at the boundary — the structural
9136/// `SexpShape` was lost. After this primitive lands, every offending-
9137/// value variant slot that takes a `SexpWitness` carries the typed
9138/// shape AND the literal jointly in ONE owned value the variant lives
9139/// independent of the call frame on.
9140///
9141/// The byte-for-byte rendering contract is preserved: `Display` for
9142/// `SexpWitness` writes only the `display` field, so a variant whose
9143/// `#[error(...)]` annotation projects through `{got}` renders
9144/// byte-identically to the legacy `got: String` shape — every
9145/// downstream substring-grep consumer (`tatara-check`, REPL) passes
9146/// unchanged. The gain is structural: tools that pattern-match on
9147/// `witness.shape == SexpShape::List` now bind to the typed identity
9148/// directly instead of substring-parsing the rendered literal.
9149///
9150/// `Clone + Debug + PartialEq + Eq` are retained (same posture as
9151/// every other owned-data `LispError` field); `Copy` is dropped
9152/// because the `display: String` is not `Copy`. When a future run
9153/// gives `Sexp` source spans, `pos: Option<usize>` lands here in ONE
9154/// place and every offending-value site picks up positional rendering
9155/// with no per-variant edit — the same future-proofing posture
9156/// `KwargPath`, `SexpShape`, and `ExpectedKwargShape` already carry.
9157///
9158/// Theory anchor: THEORY.md §V.1 — knowable platform; the offending-
9159/// value's joint identity (structural shape + renderable literal)
9160/// becomes a TYPE rather than a `String` projection at the helper
9161/// boundary that discards the shape. After this primitive lands the
9162/// substrate's understanding of "the offending Sexp at a typed-entry
9163/// rejection" lives in ONE typed struct the diagnostic promotions
9164/// hang off of. THEORY.md §VI.1 — generation over composition; seven
9165/// inline `got.to_string()` projections at error-builder boundaries
9166/// (six in `macro_expand.rs`, one in `domain.rs::missing_head_err`'s
9167/// caller) is past the three-times-rule trigger. THEORY.md §II.1
9168/// invariant 1 — typed entry; the offending Sexp's identity is part
9169/// of the proof of WHAT the typed-entry gate rejected, and the typed
9170/// witness makes both halves of that identity (shape + literal)
9171/// load-bearing data on the variant rather than the literal-only
9172/// `String` projection the legacy shape carried.
9173#[derive(Debug, Clone, PartialEq, Eq)]
9174pub struct SexpWitness {
9175 /// Structural identity — the typed-shape projection (`SexpShape::Int`,
9176 /// `SexpShape::List`, etc.). Pattern-matchable; future `pos: Option<usize>`
9177 /// promotions land alongside this field once `Sexp` carries spans.
9178 pub shape: SexpShape,
9179 /// Renderable identity — the `Sexp::Display` projection (`"5"`,
9180 /// `"(list 1 2)"`, `":foo"`, etc.). Owned so the witness lives
9181 /// independent of the call frame and crosses thread boundaries
9182 /// cleanly. Feeds the `#[error(...)]` annotation's `{got}` slot
9183 /// via `SexpWitness`'s `Display` impl.
9184 pub display: String,
9185}
9186
9187impl SexpWitness {
9188 /// Owned constructor — pairs a typed `SexpShape` with an owned
9189 /// `String` projection of the offending `Sexp::Display`. Used by
9190 /// the `sexp_witness(&Sexp)` projection helper in `domain.rs`;
9191 /// hand-written `TataraDomain` impls that need to construct a
9192 /// witness at their own call boundary route through this
9193 /// constructor.
9194 #[must_use]
9195 pub fn new(shape: SexpShape, display: impl Into<String>) -> Self {
9196 Self {
9197 shape,
9198 display: display.into(),
9199 }
9200 }
9201}
9202
9203impl std::fmt::Display for SexpWitness {
9204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9205 // Render the literal only — the byte-for-byte legacy rendering
9206 // of every `got: String` variant slot that projected through
9207 // `Sexp::Display`. Authoring tools that substring-grep on the
9208 // rendered diagnostic see no drift; tools that pattern-match
9209 // on the variant's `SexpWitness`-shaped `got` slot bind to
9210 // `witness.shape` directly.
9211 f.write_str(&self.display)
9212 }
9213}
9214
9215fn unbound_hint_suffix(prefix: UnquoteForm, hint: Option<&str>) -> String {
9216 match hint {
9217 Some(h) => format!("; did you mean {}{h}?", prefix.marker()),
9218 None => String::new(),
9219 }
9220}
9221
9222/// Renders the bare parenthetical suffix shared by EVERY `*_suffix`
9223/// diagnostic helper: a single leading space, an open paren, the
9224/// already-formatted `body`, and a close paren — ` ({body})`.
9225///
9226/// This is the lowest layer of the suffix-wrapping algebra. Four helpers
9227/// append a parenthetical structural-detail clause to a `#[error(...)]`
9228/// prefix and ALL FOUR share this exact skeleton — the leading space and the
9229/// parens:
9230/// * `unknown_among_suffix` wraps a `did you mean …?; …` / bare candidate
9231/// clause (the kwarg + registry gates);
9232/// * `rest_param_missing_name_suffix` wraps the `rest marker at position …`
9233/// clause;
9234/// * `missing_head_symbol_suffix` wraps the `got …` / `empty list` clause.
9235///
9236/// Owning the leading-space-and-parens HERE means it cannot drift across the
9237/// four renderers: a regression that drops the leading space at one site,
9238/// moves a paren, or doubles a space is structurally impossible because there
9239/// is exactly ONE wrapping implementation. Each helper keeps ONLY its own
9240/// body-construction and binds it to this primitive.
9241fn paren_suffix(body: &str) -> String {
9242 format!(" ({body})")
9243}
9244
9245/// Renders the parenthetical "unknown X among a known set" suffix shared by
9246/// the kwarg gate (`UnknownKwarg`) and the registry gate
9247/// (`UnknownDomainKeyword`). `hint` is the already-formatted near-miss
9248/// suggestion (`:foo` for kwargs, `(foo ...)` for registry keywords); `body`
9249/// is the already-formatted candidate clause (`allowed: :a, :b` /
9250/// `registered: x, y` / `no domains registered`).
9251///
9252/// This layer owns ONLY the `did you mean {hint}?; ` join when a hint is
9253/// present, so the two gates whose docs declare they "share ONE structural
9254/// shape" cannot drift apart in that join. The bare-parenthetical wrapping —
9255/// the leading space and the parens — is delegated to `paren_suffix`, the one
9256/// skeleton every `*_suffix` helper binds to.
9257fn unknown_among_suffix(hint: Option<&str>, body: &str) -> String {
9258 match hint {
9259 Some(h) => paren_suffix(&format!("did you mean {h}?; {body}")),
9260 None => paren_suffix(body),
9261 }
9262}
9263
9264fn unknown_kwarg_suffix(hint: Option<&str>, allowed: &[String]) -> String {
9265 let allowed_list = allowed
9266 .iter()
9267 .map(|s| format!(":{s}"))
9268 .collect::<Vec<_>>()
9269 .join(", ");
9270 unknown_among_suffix(
9271 hint.map(|h| format!(":{h}")).as_deref(),
9272 &format!("allowed: {allowed_list}"),
9273 )
9274}
9275
9276fn rest_param_missing_name_suffix(rest_position: usize, got: Option<&str>) -> String {
9277 let body = match got {
9278 Some(g) => format!("rest marker at position {rest_position}, got {g}"),
9279 None => format!("rest marker at position {rest_position}, none provided"),
9280 };
9281 paren_suffix(&body)
9282}
9283
9284fn rest_param_trailing_tokens_suffix(rest_position: usize, extra: usize, first: &str) -> String {
9285 paren_suffix(&format!(
9286 "rest marker at position {rest_position}, {extra} trailing after name, first: {first}"
9287 ))
9288}
9289
9290fn optional_marker_repeated_suffix(first_position: usize, second_position: usize) -> String {
9291 paren_suffix(&format!(
9292 "first &optional at position {first_position}, second at position {second_position}"
9293 ))
9294}
9295
9296fn optional_param_malformed_suffix(
9297 position: usize,
9298 reason: OptionalParamMalformedReason,
9299) -> String {
9300 paren_suffix(&format!("position {position}, {}", reason.label()))
9301}
9302
9303fn missing_head_symbol_suffix(got: Option<&str>) -> String {
9304 let body = match got {
9305 Some(g) => format!("got {g}"),
9306 None => "empty list".to_string(),
9307 };
9308 paren_suffix(&body)
9309}
9310
9311fn unknown_domain_keyword_suffix(hint: Option<&str>, registered: &[String]) -> String {
9312 let body = if registered.is_empty() {
9313 "no domains registered".to_string()
9314 } else {
9315 format!("registered: {}", registered.join(", "))
9316 };
9317 unknown_among_suffix(hint.map(|h| format!("({h} ...)")).as_deref(), &body)
9318}
9319
9320impl LispError {
9321 /// Byte offset of the failure into the source, when locatable.
9322 ///
9323 /// A PROJECTION of [`LispError::span`] — `span().map(|s| s.start)` and
9324 /// nothing else. Pre-lift this carried its own copy of the exhaustive
9325 /// positional/non-positional classification, so a new variant had to
9326 /// be classified TWICE and the two matches were free to disagree about
9327 /// whether an error is locatable. Post-lift `span` is the single
9328 /// classification site; this is the point-shaped view of it, kept
9329 /// because `format_diagnostic`'s `--> label:line:col` header names one
9330 /// byte, not a range.
9331 #[must_use]
9332 pub fn position(&self) -> Option<usize> {
9333 self.span().map(|span| span.start)
9334 }
9335
9336 /// Source `Span` of the failure, when locatable.
9337 ///
9338 /// THE classification site: a variant is locatable iff it appears in
9339 /// the `Some(..)` arms below. Variants without a location (`Type`,
9340 /// `Compile`, …) return `None`, so callers render a snippet only when
9341 /// the substrate has the information to do so; adding a variant
9342 /// without classifying it here is a non-exhaustive-match build
9343 /// failure, not a silently-positionless error.
9344 ///
9345 /// The range — not just the start — is what lets
9346 /// `crate::diagnostic::format_diagnostic` underline the offending
9347 /// FORM rather than its first byte. A zero-width span is legal and
9348 /// means "point here" (see `Eof`); `caret_run` clamps it up to one
9349 /// caret.
9350 #[must_use]
9351 pub fn span(&self) -> Option<Span> {
9352 match self {
9353 // `UnexpectedChar` predates the span lift and is not
9354 // constructed anywhere in the workspace today (measured: the
9355 // declaration, this arm, and one unit pin). Its span is
9356 // nonetheless exact rather than invented — a `char` occupies
9357 // `len_utf8()` bytes from the offset it was found at.
9358 Self::UnexpectedChar(c, pos) => Some(Span::new(*pos, pos + c.len_utf8())),
9359 Self::UnterminatedString(span) => Some(*span),
9360 Self::UnmatchedParen { span }
9361 | Self::UnmatchedOpenParen { span }
9362 | Self::Eof { span } => Some(*span),
9363 Self::InvalidNumber(_)
9364 | Self::UnknownSymbol(_)
9365 | Self::Type { .. }
9366 | Self::Compile { .. }
9367 | Self::TypeMismatch { .. }
9368 | Self::HeadMismatch { .. }
9369 | Self::Unknown { .. }
9370 | Self::Missing(_)
9371 | Self::OddKwargs { .. }
9372 | Self::UnboundTemplateVar { .. }
9373 | Self::DuplicateKwarg { .. }
9374 | Self::MissingKwarg { .. }
9375 | Self::UnknownKwarg { .. }
9376 | Self::UnknownDomainKeyword { .. }
9377 | Self::NonSymbolUnquoteTarget { .. }
9378 | Self::SpliceOutsideList { .. }
9379 | Self::MissingMacroArg { .. }
9380 | Self::TooManyMacroArgs { .. }
9381 | Self::NonSymbolParam { .. }
9382 | Self::RestParamMissingName { .. }
9383 | Self::RestParamTrailingTokens { .. }
9384 | Self::OptionalMarkerRepeated { .. }
9385 | Self::OptionalParamMalformed { .. }
9386 | Self::DefmacroArity { .. }
9387 | Self::DefmacroNonSymbolName { .. }
9388 | Self::DefmacroNonListParams { .. }
9389 | Self::NotAListForm { .. }
9390 | Self::MissingHeadSymbol { .. }
9391 | Self::NamedFormMissingName { .. }
9392 | Self::NamedFormNonSymbolName { .. }
9393 | Self::RewriterNonList { .. }
9394 | Self::DomainSerialize { .. }
9395 | Self::KwargDeserialize { .. }
9396 | Self::CompilerSpecIo { .. }
9397 | Self::TemplateInvariant { .. }
9398 | Self::ExpansionDepthExceeded { .. }
9399 | Self::ExpansionSizeExceeded { .. }
9400 | Self::MacroBodySizeExceeded { .. }
9401 | Self::RegisteredMacrosExceeded { .. }
9402 | Self::MacroArityExceeded { .. } => None,
9403 }
9404 }
9405}
9406
9407#[cfg(test)]
9408mod tests {
9409 use super::{
9410 missing_head_symbol_suffix, optional_marker_repeated_suffix,
9411 optional_param_malformed_suffix, paren_suffix, rest_param_missing_name_suffix,
9412 rest_param_trailing_tokens_suffix, unknown_among_suffix, unknown_domain_keyword_suffix,
9413 unknown_kwarg_suffix, CompilerSpecIoStage, ExpectedKwargShape, KwargPath, KwargPathKind,
9414 LispError, MacroDefHead, OptionalParamMalformedReason, SexpShape, SexpWitness,
9415 StructuralKind, TemplateInvariantKind, UnknownExpectedKwargShape, UnknownKwargPathKind,
9416 UnknownMacroDefHead, UnknownSexpShape, UnknownUnquoteForm, UnquoteForm,
9417 };
9418 use crate::span::Span;
9419
9420 #[test]
9421 fn position_extracts_offset_from_positional_variants() {
9422 assert_eq!(LispError::UnexpectedChar('?', 7).position(), Some(7));
9423 assert_eq!(
9424 LispError::UnterminatedString(Span::new(11, 15)).position(),
9425 Some(11)
9426 );
9427 assert_eq!(
9428 LispError::UnmatchedParen {
9429 span: Span::new(3, 4)
9430 }
9431 .position(),
9432 Some(3)
9433 );
9434 assert_eq!(
9435 LispError::UnmatchedOpenParen {
9436 span: Span::new(0, 6)
9437 }
9438 .position(),
9439 Some(0)
9440 );
9441 assert_eq!(
9442 LispError::Eof {
9443 span: Span::new(42, 42)
9444 }
9445 .position(),
9446 Some(42)
9447 );
9448 }
9449
9450 #[test]
9451 fn span_carries_the_full_range_position_projects_the_start_of() {
9452 // `position()` is now `span().map(|s| s.start)` — one
9453 // classification, two views. Pin BOTH views on the same values so
9454 // a regression that re-splits them (a variant locatable through
9455 // one accessor but not the other) fails here rather than showing
9456 // up as a diagnostic that renders a header without a caret.
9457 let cases = [
9458 (
9459 LispError::UnterminatedString(Span::new(11, 15)),
9460 Span::new(11, 15),
9461 ),
9462 (
9463 LispError::UnmatchedParen {
9464 span: Span::new(3, 4),
9465 },
9466 Span::new(3, 4),
9467 ),
9468 (
9469 LispError::UnmatchedOpenParen {
9470 span: Span::new(0, 6),
9471 },
9472 Span::new(0, 6),
9473 ),
9474 (
9475 LispError::Eof {
9476 span: Span::new(42, 42),
9477 },
9478 Span::new(42, 42),
9479 ),
9480 // `UnexpectedChar` still stores a bare offset; its span is
9481 // derived from the char's own UTF-8 width, so a multi-byte
9482 // char reports the bytes it actually occupies.
9483 (LispError::UnexpectedChar('?', 7), Span::new(7, 8)),
9484 (LispError::UnexpectedChar('é', 7), Span::new(7, 9)),
9485 ];
9486 for (err, expected) in cases {
9487 assert_eq!(err.span(), Some(expected), "span mismatch for {err:?}");
9488 assert_eq!(
9489 err.position(),
9490 Some(expected.start),
9491 "position must project span.start for {err:?}"
9492 );
9493 }
9494 }
9495
9496 #[test]
9497 fn span_is_none_exactly_where_position_is_none() {
9498 // The other half of the one-classification invariant: a
9499 // non-locatable error is non-locatable through both views.
9500 let err = LispError::Compile {
9501 form: ":threshold".into(),
9502 message: "expected number".into(),
9503 };
9504 assert_eq!(err.span(), None);
9505 assert_eq!(err.position(), None);
9506 }
9507
9508 #[test]
9509 fn position_is_none_for_non_positional_variants() {
9510 assert_eq!(
9511 LispError::OddKwargs {
9512 dangling: ":query".into()
9513 }
9514 .position(),
9515 None
9516 );
9517 assert_eq!(LispError::Missing("name").position(), None);
9518 assert_eq!(LispError::InvalidNumber("nan".into()).position(), None);
9519 assert_eq!(LispError::UnknownSymbol("foo".into()).position(), None);
9520 assert_eq!(
9521 LispError::Type {
9522 expected: "int",
9523 got: "string".into()
9524 }
9525 .position(),
9526 None
9527 );
9528 assert_eq!(
9529 LispError::Compile {
9530 form: ":x".into(),
9531 message: "bad".into()
9532 }
9533 .position(),
9534 None
9535 );
9536 assert_eq!(
9537 LispError::TypeMismatch {
9538 form: KwargPath::named("x"),
9539 expected: ExpectedKwargShape::String,
9540 got: SexpShape::Int,
9541 }
9542 .position(),
9543 None
9544 );
9545 assert_eq!(
9546 LispError::HeadMismatch {
9547 keyword: "defmonitor",
9548 got: "not-a-monitor".into(),
9549 }
9550 .position(),
9551 None
9552 );
9553 assert_eq!(
9554 LispError::Unknown {
9555 category: "domain",
9556 value: "defx".into()
9557 }
9558 .position(),
9559 None
9560 );
9561 assert_eq!(
9562 LispError::UnboundTemplateVar {
9563 prefix: UnquoteForm::Unquote,
9564 name: "xx".into(),
9565 hint: Some("x".into()),
9566 }
9567 .position(),
9568 None
9569 );
9570 assert_eq!(
9571 LispError::UnboundTemplateVar {
9572 prefix: UnquoteForm::Splice,
9573 name: "ys".into(),
9574 hint: None,
9575 }
9576 .position(),
9577 None
9578 );
9579 assert_eq!(
9580 LispError::DuplicateKwarg { key: "name".into() }.position(),
9581 None
9582 );
9583 assert_eq!(
9584 LispError::MissingKwarg { key: "name".into() }.position(),
9585 None
9586 );
9587 assert_eq!(
9588 LispError::UnknownKwarg {
9589 key: "tthreshold".into(),
9590 hint: Some("threshold".into()),
9591 allowed: vec!["name".into(), "threshold".into()],
9592 }
9593 .position(),
9594 None
9595 );
9596 assert_eq!(
9597 LispError::UnknownDomainKeyword {
9598 keyword: "defmoniter".into(),
9599 hint: Some("defmonitor".into()),
9600 registered: vec!["defalertpolicy".into(), "defmonitor".into()],
9601 }
9602 .position(),
9603 None
9604 );
9605 assert_eq!(
9606 LispError::NonSymbolUnquoteTarget {
9607 prefix: UnquoteForm::Unquote,
9608 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
9609 }
9610 .position(),
9611 None
9612 );
9613 assert_eq!(
9614 LispError::NonSymbolUnquoteTarget {
9615 prefix: UnquoteForm::Splice,
9616 got: SexpWitness::new(SexpShape::Int, "5"),
9617 }
9618 .position(),
9619 None
9620 );
9621 assert_eq!(
9622 LispError::SpliceOutsideList {
9623 got: SexpWitness::new(SexpShape::Symbol, "xs"),
9624 }
9625 .position(),
9626 None
9627 );
9628 assert_eq!(
9629 LispError::SpliceOutsideList {
9630 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
9631 }
9632 .position(),
9633 None
9634 );
9635 assert_eq!(
9636 LispError::MissingMacroArg {
9637 macro_name: "wrap".into(),
9638 param: "b".into(),
9639 }
9640 .position(),
9641 None
9642 );
9643 assert_eq!(
9644 LispError::MissingMacroArg {
9645 macro_name: "call".into(),
9646 param: "f".into(),
9647 }
9648 .position(),
9649 None
9650 );
9651 assert_eq!(
9652 LispError::TooManyMacroArgs {
9653 macro_name: "pair".into(),
9654 expected: 2,
9655 got: 3,
9656 }
9657 .position(),
9658 None
9659 );
9660 assert_eq!(
9661 LispError::TooManyMacroArgs {
9662 macro_name: "wrap".into(),
9663 expected: 1,
9664 got: 5,
9665 }
9666 .position(),
9667 None
9668 );
9669 assert_eq!(
9670 LispError::NonSymbolParam {
9671 position: 0,
9672 got: SexpWitness::new(SexpShape::Int, "5"),
9673 }
9674 .position(),
9675 None
9676 );
9677 assert_eq!(
9678 LispError::NonSymbolParam {
9679 position: 2,
9680 got: SexpWitness::new(SexpShape::List, "(nested)"),
9681 }
9682 .position(),
9683 None
9684 );
9685 assert_eq!(
9686 LispError::RestParamMissingName {
9687 rest_position: 1,
9688 got: None,
9689 }
9690 .position(),
9691 None
9692 );
9693 assert_eq!(
9694 LispError::RestParamMissingName {
9695 rest_position: 0,
9696 got: Some(SexpWitness::new(SexpShape::Int, "5")),
9697 }
9698 .position(),
9699 None
9700 );
9701 assert_eq!(
9702 LispError::OptionalParamMalformed {
9703 position: 1,
9704 got: SexpWitness::new(SexpShape::List, "()"),
9705 reason: OptionalParamMalformedReason::EmptyList,
9706 }
9707 .position(),
9708 None
9709 );
9710 assert_eq!(
9711 LispError::OptionalParamMalformed {
9712 position: 3,
9713 got: SexpWitness::new(SexpShape::List, "(x 1 2)"),
9714 reason: OptionalParamMalformedReason::ExtraElements { length: 3 },
9715 }
9716 .position(),
9717 None
9718 );
9719 assert_eq!(
9720 LispError::DefmacroArity {
9721 head: MacroDefHead::Defmacro,
9722 arity: 1,
9723 }
9724 .position(),
9725 None
9726 );
9727 assert_eq!(
9728 LispError::DefmacroArity {
9729 head: MacroDefHead::Defcheck,
9730 arity: 3,
9731 }
9732 .position(),
9733 None
9734 );
9735 assert_eq!(
9736 LispError::DefmacroNonSymbolName {
9737 head: MacroDefHead::Defmacro,
9738 got: SexpWitness::new(SexpShape::Int, "5"),
9739 }
9740 .position(),
9741 None
9742 );
9743 assert_eq!(
9744 LispError::DefmacroNonListParams {
9745 head: MacroDefHead::Defmacro,
9746 got: SexpWitness::new(SexpShape::Symbol, "x"),
9747 }
9748 .position(),
9749 None
9750 );
9751 assert_eq!(
9752 LispError::DefmacroNonListParams {
9753 head: MacroDefHead::Defcheck,
9754 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
9755 }
9756 .position(),
9757 None
9758 );
9759 assert_eq!(
9760 LispError::DefmacroNonSymbolName {
9761 head: MacroDefHead::DefpointTemplate,
9762 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
9763 }
9764 .position(),
9765 None
9766 );
9767 assert_eq!(
9768 LispError::NotAListForm {
9769 keyword: "defmonitor",
9770 }
9771 .position(),
9772 None
9773 );
9774 assert_eq!(
9775 LispError::NotAListForm {
9776 keyword: "defpoint",
9777 }
9778 .position(),
9779 None
9780 );
9781 assert_eq!(
9782 LispError::MissingHeadSymbol {
9783 keyword: "defmonitor",
9784 got: None,
9785 }
9786 .position(),
9787 None
9788 );
9789 assert_eq!(
9790 LispError::MissingHeadSymbol {
9791 keyword: "defpoint",
9792 got: Some(SexpWitness::new(SexpShape::Int, "5")),
9793 }
9794 .position(),
9795 None
9796 );
9797 assert_eq!(
9798 LispError::NamedFormMissingName {
9799 keyword: "defpoint",
9800 }
9801 .position(),
9802 None
9803 );
9804 assert_eq!(
9805 LispError::NamedFormMissingName {
9806 keyword: "defalertpolicy",
9807 }
9808 .position(),
9809 None
9810 );
9811 assert_eq!(
9812 LispError::NamedFormNonSymbolName {
9813 keyword: "defpoint",
9814 got: SexpShape::Int,
9815 }
9816 .position(),
9817 None
9818 );
9819 assert_eq!(
9820 LispError::NamedFormNonSymbolName {
9821 keyword: "defalertpolicy",
9822 got: SexpShape::List,
9823 }
9824 .position(),
9825 None
9826 );
9827 assert_eq!(
9828 LispError::RewriterNonList {
9829 keyword: "defmonitor",
9830 got: SexpWitness::new(SexpShape::Int, "42"),
9831 }
9832 .position(),
9833 None
9834 );
9835 assert_eq!(
9836 LispError::DomainSerialize {
9837 keyword: "defmonitor",
9838 message: "expected value at line 1 column 1".into(),
9839 }
9840 .position(),
9841 None
9842 );
9843 assert_eq!(
9844 LispError::DomainSerialize {
9845 keyword: "defalertpolicy",
9846 message: "key must be a string".into(),
9847 }
9848 .position(),
9849 None
9850 );
9851 assert_eq!(
9852 LispError::KwargDeserialize {
9853 path: KwargPath::named("level"),
9854 message: "unknown variant `NotASeverity`".into(),
9855 }
9856 .position(),
9857 None
9858 );
9859 assert_eq!(
9860 LispError::KwargDeserialize {
9861 path: KwargPath::item("steps", 1),
9862 message: "invalid type: integer `7`, expected a string".into(),
9863 }
9864 .position(),
9865 None
9866 );
9867 assert_eq!(
9868 LispError::CompilerSpecIo {
9869 stage: super::CompilerSpecIoStage::RealizeToDiskSerialize,
9870 message: "expected struct CompilerSpec".into(),
9871 }
9872 .position(),
9873 None
9874 );
9875 assert_eq!(
9876 LispError::CompilerSpecIo {
9877 stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
9878 message: "expected value at line 1 column 1".into(),
9879 }
9880 .position(),
9881 None
9882 );
9883 }
9884
9885 #[test]
9886 fn missing_head_symbol_display_with_empty_list_renders_legacy_prefix_and_empty_marker() {
9887 // `()` — list[0] doesn't exist. The variant renders the
9888 // legacy prefix `compile error in {keyword}: missing head
9889 // symbol` byte-for-byte AND names the structural reason
9890 // `(empty list)` parenthetically — same posture as how
9891 // `RestParamMissingName` renders `(rest marker at position
9892 // N, none provided)` and how `UnknownDomainKeyword` renders
9893 // `(no domains registered)` for the empty-side case. A
9894 // regression that drops either fragment fails-loudly here.
9895 let err = LispError::MissingHeadSymbol {
9896 keyword: "defmonitor",
9897 got: None,
9898 };
9899 assert_eq!(
9900 format!("{err}"),
9901 "compile error in defmonitor: missing head symbol (empty list)"
9902 );
9903 }
9904
9905 #[test]
9906 fn missing_head_symbol_display_with_int_got_renders_legacy_prefix_and_got() {
9907 // `(5 …)` — list[0] is the int `5`, not a symbol. The variant
9908 // renders both the keyword AND the offending head's
9909 // `Sexp::Display` projection — both fields are first-class
9910 // structural data, not embedded substrings of `message`. The
9911 // prefix `compile error in {keyword}: missing head symbol`
9912 // matches the legacy `Compile { form: keyword.to_string(),
9913 // message: "missing head symbol" }` byte-for-byte; the
9914 // structural detail (`(got 5)`) is appended.
9915 let err = LispError::MissingHeadSymbol {
9916 keyword: "defmonitor",
9917 got: Some(SexpWitness::new(SexpShape::Int, "5")),
9918 };
9919 assert_eq!(
9920 format!("{err}"),
9921 "compile error in defmonitor: missing head symbol (got 5)"
9922 );
9923 }
9924
9925 #[test]
9926 fn missing_head_symbol_display_carries_keyword_atom_got_unchanged() {
9927 // `(:foo …)` — list[0] is a keyword atom. `Sexp::Display` for
9928 // `Atom::Keyword(s)` writes `:s`; pin that the variant's
9929 // Display passes the keyword form through unchanged so an
9930 // LSP that surfaces "you wrote `:foo` where a head symbol
9931 // was expected" gains the literal keyword value as data, no
9932 // re-parsing required.
9933 let err = LispError::MissingHeadSymbol {
9934 keyword: "defalertpolicy",
9935 got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
9936 };
9937 assert_eq!(
9938 format!("{err}"),
9939 "compile error in defalertpolicy: missing head symbol (got :foo)"
9940 );
9941 }
9942
9943 #[test]
9944 fn missing_head_symbol_display_carries_string_got_unchanged() {
9945 // `Sexp::Display` for `Atom::Str(s)` writes `"s"` (with quotes);
9946 // pin that the variant's Display passes the string form through
9947 // unchanged so an LSP that surfaces "you wrote `\"name\"` where
9948 // a head symbol was expected" gains the literal value as data,
9949 // no re-parsing required.
9950 let err = LispError::MissingHeadSymbol {
9951 keyword: "defmonitor",
9952 got: Some(SexpWitness::new(SexpShape::String, "\"name\"")),
9953 };
9954 assert_eq!(
9955 format!("{err}"),
9956 "compile error in defmonitor: missing head symbol (got \"name\")"
9957 );
9958 }
9959
9960 #[test]
9961 fn missing_head_symbol_display_carries_nested_list_got_unchanged() {
9962 // `((nested) …)` — list[0] is itself a list. The nested form
9963 // round-trips through `Sexp::Display` into the variant's `got`
9964 // slot unchanged so the operator sees what they wrote.
9965 let err = LispError::MissingHeadSymbol {
9966 keyword: "defmonitor",
9967 got: Some(SexpWitness::new(SexpShape::List, "(nested)")),
9968 };
9969 assert_eq!(
9970 format!("{err}"),
9971 "compile error in defmonitor: missing head symbol (got (nested))"
9972 );
9973 }
9974
9975 #[test]
9976 fn missing_head_symbol_display_preserves_legacy_substring_for_message_grep() {
9977 // Pin the legacy substring — `"missing head symbol"` — as a
9978 // separate assertion so a regression that drifts the wording
9979 // (e.g., to "no head symbol" or "head must be a symbol")
9980 // fails-loudly here even if the appended parenthetical changes
9981 // shape. The substring is what consumers downstream
9982 // (`tatara-check`, the REPL) substring-match on today; the
9983 // prefix matches the legacy `Compile { form:
9984 // keyword.to_string(), message: "missing head symbol" }`
9985 // byte-for-byte.
9986 let err = LispError::MissingHeadSymbol {
9987 keyword: "defmonitor",
9988 got: None,
9989 };
9990 let msg = format!("{err}");
9991 assert!(
9992 msg.contains("missing head symbol"),
9993 "expected legacy substring in message, got: {msg}"
9994 );
9995 assert!(
9996 msg.contains("compile error in defmonitor:"),
9997 "expected legacy form-label prefix in message, got: {msg}"
9998 );
9999 }
10000
10001 #[test]
10002 fn missing_head_symbol_got_carries_typed_witness_through_variant_slot() {
10003 // Pin the structural binding AND the Display projection on
10004 // `LispError::MissingHeadSymbol.got`. After this lift the
10005 // variant's typed slot is `Option<SexpWitness>` — the joint
10006 // `SexpWitness` identity (the same primitive `SpliceOutsideList.got`,
10007 // `NonSymbolUnquoteTarget.got`, `NonSymbolParam.got`,
10008 // `DefmacroNonSymbolName.got`, `DefmacroNonListParams.got`,
10009 // and `RestParamMissingName.got` already carry) wrapped in
10010 // `Option` because the head slot bifurcates structurally
10011 // between "missing entirely" (`None`, empty list) and
10012 // "present but malformed" (`Some(SexpWitness)`, non-symbol
10013 // head). The typed witness lands on the `Some` arm only. A
10014 // regression that re-collapses to a free-form `Option<String>`
10015 // got slot (losing the rustc-enforced closed-set guarantee
10016 // on shape identity at the outer typed-entry gate's
10017 // non-symbol-head rejection variant) fails-loudly here.
10018 // Display via `SexpWitness::Display` writes only the `display`
10019 // field so the rendered `(got <display>)` clause is
10020 // byte-for-byte identical to the pre-lift `Option<String>`
10021 // shape; downstream substring-grep consumers (`tatara-check`,
10022 // REPL) see no drift.
10023 let err = LispError::MissingHeadSymbol {
10024 keyword: "defmonitor",
10025 got: Some(SexpWitness::new(SexpShape::Int, "5")),
10026 };
10027 match &err {
10028 LispError::MissingHeadSymbol { keyword, got } => {
10029 assert_eq!(*keyword, "defmonitor");
10030 let witness = got.as_ref().expect("got must be Some");
10031 assert_eq!(witness.shape, SexpShape::Int);
10032 assert_eq!(witness.display, "5");
10033 }
10034 other => panic!("expected MissingHeadSymbol, got {other:?}"),
10035 }
10036 assert_eq!(
10037 format!("{err}"),
10038 "compile error in defmonitor: missing head symbol (got 5)"
10039 );
10040 }
10041
10042 #[test]
10043 fn missing_head_symbol_got_distinguishes_int_from_keyword_at_variant_slot() {
10044 // Pin the typed-shape bifurcation at the variant slot — `5`
10045 // (int atom at the head slot) and `:foo` (keyword atom at
10046 // the head slot) BOTH route to `MissingHeadSymbol` on the
10047 // `Some` arm, but the typed `got.shape` slot distinguishes
10048 // them structurally — `SexpShape::Int` vs.
10049 // `SexpShape::Keyword`. Sibling pin for the same structural-
10050 // shape-bifurcation property
10051 // `rest_param_missing_name_got_distinguishes_int_from_keyword_at_variant_slot`
10052 // pins on `RestParamMissingName`. A regression that erases
10053 // the typed shape (e.g., reverts to `got: Option<String>`)
10054 // would lose this distinction — tooling that wants to surface
10055 // "you wrote an int `5` where a head symbol was expected" vs.
10056 // "you wrote a keyword `:foo` where a head symbol was
10057 // expected (did you mean `foo`?)" would have to substring-
10058 // grep the `display` field, brittle.
10059 let err_int = LispError::MissingHeadSymbol {
10060 keyword: "defmonitor",
10061 got: Some(SexpWitness::new(SexpShape::Int, "5")),
10062 };
10063 let err_kw = LispError::MissingHeadSymbol {
10064 keyword: "defalertpolicy",
10065 got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
10066 };
10067 let (int_shape, kw_shape) = (
10068 match &err_int {
10069 LispError::MissingHeadSymbol { got: Some(w), .. } => w.shape,
10070 _ => unreachable!(),
10071 },
10072 match &err_kw {
10073 LispError::MissingHeadSymbol { got: Some(w), .. } => w.shape,
10074 _ => unreachable!(),
10075 },
10076 );
10077 assert_ne!(
10078 int_shape, kw_shape,
10079 "Int and Keyword witnesses must remain structurally distinct at the variant slot",
10080 );
10081 assert_eq!(int_shape, SexpShape::Int);
10082 assert_eq!(kw_shape, SexpShape::Keyword);
10083 }
10084
10085 #[test]
10086 fn missing_head_symbol_and_rest_param_gate_share_one_witness_primitive() {
10087 // Pin that ALL SEVEN Sexp-display-source `got` slots in the
10088 // substrate (`SpliceOutsideList`, `NonSymbolUnquoteTarget`,
10089 // `NonSymbolParam`, `DefmacroNonSymbolName`,
10090 // `DefmacroNonListParams`, `RestParamMissingName`,
10091 // `MissingHeadSymbol`) carry the SAME typed `SexpWitness`
10092 // primitive — the closed set of "offending inner Sexp"
10093 // identities is bound by ONE typed primitive across SEVEN
10094 // rejection surfaces: the template-gate's `,X/,@X` pair, the
10095 // defmacro-syntax-gate's `parse_params` walker (BOTH
10096 // non-symbol-param AND post-`&rest`-non-symbol-follower
10097 // rejection points), BOTH of the defmacro-syntax-gate's outer
10098 // `macro_def_from` rejection points (name-symbol AND
10099 // param-list), AND the outer `compile_from_sexp` typed-entry
10100 // gate's non-symbol-head rejection point. With this lift
10101 // EVERY `Sexp::Display`-source `got` slot in the substrate is
10102 // structurally unified end-to-end. The `Option`-wrap on
10103 // `MissingHeadSymbol.got` and `RestParamMissingName.got` is
10104 // the bifurcation between "missing entirely" and "present but
10105 // malformed"; the typed witness rides on the `Some` arm and
10106 // is structurally identical to the other five variants' got
10107 // slots. A regression that diverges the slot type on any one
10108 // variant (e.g., re-collapses `MissingHeadSymbol.got` to
10109 // `Option<String>` while leaving the others typed) fails-
10110 // loudly here because the assignment round-trips the witness
10111 // across all seven slot types. Sibling pin to
10112 // `rest_param_missing_name_and_macro_def_gate_share_one_witness_primitive`
10113 // — extending the typed-identity unification contract from
10114 // six slots to seven, closing the contract.
10115 let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
10116 let missing_head = LispError::MissingHeadSymbol {
10117 keyword: "defmonitor",
10118 got: Some(same_witness.clone()),
10119 };
10120 let rest_param_missing_name = LispError::RestParamMissingName {
10121 rest_position: 0,
10122 got: Some(same_witness.clone()),
10123 };
10124 let defmacro_non_list_params = LispError::DefmacroNonListParams {
10125 head: MacroDefHead::Defmacro,
10126 got: same_witness.clone(),
10127 };
10128 let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
10129 head: MacroDefHead::Defmacro,
10130 got: same_witness.clone(),
10131 };
10132 let non_symbol_param = LispError::NonSymbolParam {
10133 position: 0,
10134 got: same_witness.clone(),
10135 };
10136 let non_symbol_target = LispError::NonSymbolUnquoteTarget {
10137 prefix: UnquoteForm::Unquote,
10138 got: same_witness.clone(),
10139 };
10140 let splice_outside = LispError::SpliceOutsideList {
10141 got: same_witness.clone(),
10142 };
10143 match (
10144 &missing_head,
10145 &rest_param_missing_name,
10146 &defmacro_non_list_params,
10147 &defmacro_non_symbol_name,
10148 &non_symbol_param,
10149 &non_symbol_target,
10150 &splice_outside,
10151 ) {
10152 (
10153 LispError::MissingHeadSymbol { got: Some(a), .. },
10154 LispError::RestParamMissingName { got: Some(b), .. },
10155 LispError::DefmacroNonListParams { got: c, .. },
10156 LispError::DefmacroNonSymbolName { got: d, .. },
10157 LispError::NonSymbolParam { got: e, .. },
10158 LispError::NonSymbolUnquoteTarget { got: f, .. },
10159 LispError::SpliceOutsideList { got: g },
10160 ) => {
10161 assert_eq!(a.shape, b.shape);
10162 assert_eq!(b.shape, c.shape);
10163 assert_eq!(c.shape, d.shape);
10164 assert_eq!(d.shape, e.shape);
10165 assert_eq!(e.shape, f.shape);
10166 assert_eq!(f.shape, g.shape);
10167 assert_eq!(a.display, b.display);
10168 assert_eq!(b.display, c.display);
10169 assert_eq!(c.display, d.display);
10170 assert_eq!(d.display, e.display);
10171 assert_eq!(e.display, f.display);
10172 assert_eq!(f.display, g.display);
10173 assert_eq!(*a, same_witness);
10174 assert_eq!(*b, same_witness);
10175 assert_eq!(*c, same_witness);
10176 assert_eq!(*d, same_witness);
10177 assert_eq!(*e, same_witness);
10178 assert_eq!(*f, same_witness);
10179 assert_eq!(*g, same_witness);
10180 }
10181 _ => unreachable!(),
10182 }
10183 }
10184
10185 #[test]
10186 fn not_a_list_form_display_renders_legacy_compile_shape() {
10187 // Pin the rendered diagnostic byte-for-byte against the
10188 // legacy `Compile { form: keyword.to_string(), message:
10189 // "expected list form" }` shape. Authoring tools that
10190 // substring-grep on the rendered message see no drift; tools
10191 // that pattern-match on the variant gain structural binding.
10192 let err = LispError::NotAListForm {
10193 keyword: "defmonitor",
10194 };
10195 assert_eq!(
10196 format!("{err}"),
10197 "compile error in defmonitor: expected list form"
10198 );
10199 }
10200
10201 #[test]
10202 fn not_a_list_form_display_carries_keyword_unchanged() {
10203 // Pin path-uniformity across distinct keywords — every
10204 // `TataraDomain` impl funnels through `not_a_list_form_err`
10205 // with its own `Self::KEYWORD`, so the variant's `keyword`
10206 // slot must round-trip every literal the derive macro
10207 // accepts. A regression that drops or rewrites the keyword
10208 // (e.g., lowercasing, stripping the `def` prefix) fails-
10209 // loudly here.
10210 let err = LispError::NotAListForm {
10211 keyword: "defpoint",
10212 };
10213 assert_eq!(
10214 format!("{err}"),
10215 "compile error in defpoint: expected list form"
10216 );
10217 }
10218
10219 #[test]
10220 fn not_a_list_form_display_preserves_legacy_substring_for_message_grep() {
10221 // Pin the legacy substring — `"expected list form"` — as a
10222 // separate assertion so a regression that drifts the
10223 // wording (e.g., to "expected list" or "must be a list")
10224 // fails-loudly here even if the prefix changes shape.
10225 // The substring is what consumers downstream
10226 // (`tatara-check`, the REPL) substring-match on today; the
10227 // prefix matches the legacy `Compile { form:
10228 // keyword.to_string(), message: "expected list form" }`
10229 // byte-for-byte.
10230 let err = LispError::NotAListForm {
10231 keyword: "defalertpolicy",
10232 };
10233 let msg = format!("{err}");
10234 assert!(
10235 msg.contains("expected list form"),
10236 "expected legacy substring in message, got: {msg}"
10237 );
10238 assert!(
10239 msg.contains("compile error in defalertpolicy:"),
10240 "expected legacy form-label prefix in message, got: {msg}"
10241 );
10242 }
10243
10244 #[test]
10245 fn unknown_kwarg_display_with_hint_renders_did_you_mean_then_allowed_list() {
10246 // The variant renders byte-for-byte the same string the legacy
10247 // `Compile { form: ":tthreshold", message: "unknown keyword (did
10248 // you mean :threshold?; allowed: :a, :b, :c)" }` shape produced,
10249 // so authoring tools (REPL, LSP, `tatara-check`) that
10250 // substring-match on the rendered diagnostic see no drift; tools
10251 // that pattern-match on the variant gain structural binding.
10252 let err = LispError::UnknownKwarg {
10253 key: "tthreshold".into(),
10254 hint: Some("threshold".into()),
10255 allowed: vec!["name".into(), "query".into(), "threshold".into()],
10256 };
10257 assert_eq!(
10258 format!("{err}"),
10259 "compile error in :tthreshold: unknown keyword \
10260 (did you mean :threshold?; allowed: :name, :query, :threshold)"
10261 );
10262 }
10263
10264 #[test]
10265 fn unknown_kwarg_display_without_hint_renders_allowed_list_only() {
10266 // No hint: the rendered message has the allowed list but no `did
10267 // you mean` clause. A wrong hint is worse than no hint — the
10268 // slot stays empty unless `suggest` ranks a candidate within
10269 // the bounded edit distance.
10270 let err = LispError::UnknownKwarg {
10271 key: "totally-unrelated".into(),
10272 hint: None,
10273 allowed: vec!["name".into(), "query".into(), "threshold".into()],
10274 };
10275 assert_eq!(
10276 format!("{err}"),
10277 "compile error in :totally-unrelated: unknown keyword \
10278 (allowed: :name, :query, :threshold)"
10279 );
10280 }
10281
10282 #[test]
10283 fn unknown_kwarg_display_carries_kebab_case_keys_unchanged() {
10284 // `:notify-ref`, `:window-seconds`, every kebab-cased kwarg name
10285 // round-trips through both the offending-key slot AND the
10286 // allowed-list slot unchanged. Pinning this contract means a
10287 // regression that camelCases or lowercases either side fails-
10288 // loudly here.
10289 let err = LispError::UnknownKwarg {
10290 key: "windou-seconds".into(),
10291 hint: Some("window-seconds".into()),
10292 allowed: vec!["notify-ref".into(), "window-seconds".into()],
10293 };
10294 assert_eq!(
10295 format!("{err}"),
10296 "compile error in :windou-seconds: unknown keyword \
10297 (did you mean :window-seconds?; allowed: :notify-ref, :window-seconds)"
10298 );
10299 }
10300
10301 #[test]
10302 fn duplicate_kwarg_display_matches_legacy_compile_shape() {
10303 // The variant renders byte-for-byte the same string the legacy
10304 // `Compile { form: ":name", message: "duplicate keyword" }` shape
10305 // produced, so authoring tools (REPL, LSP, `tatara-check`) that
10306 // substring-match on the rendered diagnostic see no drift; tools
10307 // that pattern-match on the variant gain structural binding.
10308 let err = LispError::DuplicateKwarg { key: "name".into() };
10309 assert_eq!(
10310 format!("{err}"),
10311 "compile error in :name: duplicate keyword"
10312 );
10313 }
10314
10315 #[test]
10316 fn duplicate_kwarg_display_carries_kebab_case_keys_unchanged() {
10317 // `:notify-ref`, `:window-seconds`, every kebab-cased kwarg name
10318 // round-trips through the variant's Display unchanged. Pinning
10319 // this contract means a regression that camelCases or lowercases
10320 // the key in the rendered message fails-loudly.
10321 let err = LispError::DuplicateKwarg {
10322 key: "notify-ref".into(),
10323 };
10324 assert_eq!(
10325 format!("{err}"),
10326 "compile error in :notify-ref: duplicate keyword"
10327 );
10328 }
10329
10330 #[test]
10331 fn missing_kwarg_display_matches_legacy_compile_shape() {
10332 // The variant renders byte-for-byte the same string the legacy
10333 // `Compile { form: ":threshold", message: "required but not provided" }`
10334 // shape produced, so authoring tools (REPL, LSP, `tatara-check`) that
10335 // substring-match on the rendered diagnostic see no drift; tools
10336 // that pattern-match on the variant gain structural binding.
10337 let err = LispError::MissingKwarg {
10338 key: "threshold".into(),
10339 };
10340 assert_eq!(
10341 format!("{err}"),
10342 "compile error in :threshold: required but not provided"
10343 );
10344 }
10345
10346 #[test]
10347 fn missing_kwarg_display_carries_kebab_case_keys_unchanged() {
10348 // `:notify-ref`, `:window-seconds`, every kebab-cased kwarg name
10349 // round-trips through the variant's Display unchanged. Pinning
10350 // this contract means a regression that camelCases or lowercases
10351 // the key in the rendered message fails-loudly.
10352 let err = LispError::MissingKwarg {
10353 key: "notify-ref".into(),
10354 };
10355 assert_eq!(
10356 format!("{err}"),
10357 "compile error in :notify-ref: required but not provided"
10358 );
10359 }
10360
10361 #[test]
10362 fn unbound_template_var_display_without_hint_matches_legacy_compile_shape() {
10363 // Without a hint the variant renders byte-for-byte the same string
10364 // the legacy `Compile { form: ",x", message: "unbound" }` shape
10365 // produced, so authoring tools that substring-match on the rendered
10366 // diagnostic see no drift.
10367 let err = LispError::UnboundTemplateVar {
10368 prefix: UnquoteForm::Unquote,
10369 name: "y".into(),
10370 hint: None,
10371 };
10372 assert_eq!(format!("{err}"), "compile error in ,y: unbound");
10373 }
10374
10375 #[test]
10376 fn unbound_template_var_display_appends_hint_suffix_when_present() {
10377 // With a hint the message gains a `"; did you mean ,X?"` suffix —
10378 // the prefix is preserved in the hint so the operator can copy-paste
10379 // the suggestion verbatim.
10380 let err = LispError::UnboundTemplateVar {
10381 prefix: UnquoteForm::Unquote,
10382 name: "xs".into(),
10383 hint: Some("x".into()),
10384 };
10385 assert_eq!(
10386 format!("{err}"),
10387 "compile error in ,xs: unbound; did you mean ,x?"
10388 );
10389 }
10390
10391 #[test]
10392 fn unknown_domain_keyword_display_with_hint_renders_did_you_mean_then_registered_list() {
10393 // The variant names the offending keyword's full call shape
10394 // (`(defmoniter ...)`), the structural near-miss in the same call
10395 // shape (`(defmonitor ...)`), and the sorted registered set.
10396 // Authoring tools that pattern-match on the variant gain
10397 // structural binding to `keyword` / `hint` / `registered` —
10398 // tools that substring-match on the rendered diagnostic see a
10399 // stable shape.
10400 let err = LispError::UnknownDomainKeyword {
10401 keyword: "defmoniter".into(),
10402 hint: Some("defmonitor".into()),
10403 registered: vec![
10404 "defalertpolicy".into(),
10405 "defmonitor".into(),
10406 "defnotify".into(),
10407 ],
10408 };
10409 assert_eq!(
10410 format!("{err}"),
10411 "unknown domain keyword: (defmoniter ...) \
10412 (did you mean (defmonitor ...)?; \
10413 registered: defalertpolicy, defmonitor, defnotify)"
10414 );
10415 }
10416
10417 #[test]
10418 fn unknown_domain_keyword_display_without_hint_renders_registered_list_only() {
10419 // No near-miss within the bounded edit distance: the rendered
10420 // message has the registered list but no `did you mean` clause.
10421 // A wrong hint is worse than no hint — the slot stays empty
10422 // unless `suggest_keyword` ranks a candidate within the bound.
10423 let err = LispError::UnknownDomainKeyword {
10424 keyword: "totally-unrelated".into(),
10425 hint: None,
10426 registered: vec!["defalertpolicy".into(), "defmonitor".into()],
10427 };
10428 assert_eq!(
10429 format!("{err}"),
10430 "unknown domain keyword: (totally-unrelated ...) \
10431 (registered: defalertpolicy, defmonitor)"
10432 );
10433 }
10434
10435 #[test]
10436 fn unknown_domain_keyword_display_with_empty_registry_renders_no_domains_registered() {
10437 // The substrate names the structural reason — the registry has
10438 // no candidates at all — instead of a misleading empty
10439 // `registered: ` suffix. A typo against an empty registry is
10440 // unambiguously a registry-seeding bug, not a near-miss.
10441 let err = LispError::UnknownDomainKeyword {
10442 keyword: "defmonitor".into(),
10443 hint: None,
10444 registered: vec![],
10445 };
10446 assert_eq!(
10447 format!("{err}"),
10448 "unknown domain keyword: (defmonitor ...) (no domains registered)"
10449 );
10450 }
10451
10452 #[test]
10453 fn unknown_domain_keyword_display_carries_kebab_case_keywords_unchanged() {
10454 // Kebab-cased domain keywords (`defalert-policy`,
10455 // `defprocess-spec`) round-trip through the offending-keyword
10456 // slot AND the registered-list slot unchanged. Pinning this
10457 // contract means a regression that camelCases or lowercases
10458 // either side fails-loudly here.
10459 let err = LispError::UnknownDomainKeyword {
10460 keyword: "defalert-policiy".into(),
10461 hint: Some("defalert-policy".into()),
10462 registered: vec!["defalert-policy".into(), "defprocess-spec".into()],
10463 };
10464 assert_eq!(
10465 format!("{err}"),
10466 "unknown domain keyword: (defalert-policiy ...) \
10467 (did you mean (defalert-policy ...)?; \
10468 registered: defalert-policy, defprocess-spec)"
10469 );
10470 }
10471
10472 #[test]
10473 fn unknown_among_suffix_owns_the_parenthetical_wrapping_skeleton() {
10474 // The Some-arm owns the `did you mean {hint}?; {body}` join; both arms
10475 // delegate the bare leading-space-and-parens to `paren_suffix`. Both
10476 // gates whose docs declare they "share ONE structural shape" wrap
10477 // through this skeleton, so a regression that drifts the join fails
10478 // here; one that drifts the bare wrapping fails in
10479 // `every_suffix_helper_wraps_through_one_paren_primitive`.
10480 assert_eq!(
10481 unknown_among_suffix(Some(":threshold"), "allowed: :name, :threshold"),
10482 " (did you mean :threshold?; allowed: :name, :threshold)"
10483 );
10484 assert_eq!(
10485 unknown_among_suffix(None, "allowed: :name, :threshold"),
10486 " (allowed: :name, :threshold)"
10487 );
10488 }
10489
10490 #[test]
10491 fn optional_param_malformed_display_renders_typed_reason_in_suffix() {
10492 // The variant's Display threads BOTH the offending list literal
10493 // (`got` via SexpWitness's Display projection) AND the typed
10494 // `reason`'s label into the rendered message — the prefix names
10495 // the malformed-spec failure mode, the parenthetical suffix names
10496 // position + reason. Authoring tools (REPL, LSP, `tatara-check`)
10497 // pattern-match on the variant for structural binding; tools that
10498 // substring-match see a stable shape parallel to the existing
10499 // `OptionalMarkerRepeated` Display.
10500 let empty = LispError::OptionalParamMalformed {
10501 position: 1,
10502 got: SexpWitness::new(SexpShape::List, "()"),
10503 reason: OptionalParamMalformedReason::EmptyList,
10504 };
10505 assert_eq!(
10506 format!("{empty}"),
10507 "compile error in defmacro params: malformed &optional spec, got () (position 1, empty list)"
10508 );
10509 let missing = LispError::OptionalParamMalformed {
10510 position: 2,
10511 got: SexpWitness::new(SexpShape::List, "(x)"),
10512 reason: OptionalParamMalformedReason::MissingDefault,
10513 };
10514 assert_eq!(
10515 format!("{missing}"),
10516 "compile error in defmacro params: malformed &optional spec, got (x) (position 2, missing default)"
10517 );
10518 let extra = LispError::OptionalParamMalformed {
10519 position: 3,
10520 got: SexpWitness::new(SexpShape::List, "(x 1 2)"),
10521 reason: OptionalParamMalformedReason::ExtraElements { length: 3 },
10522 };
10523 assert_eq!(
10524 format!("{extra}"),
10525 "compile error in defmacro params: malformed &optional spec, got (x 1 2) (position 3, 3 elements (need 2))"
10526 );
10527 let nonsym = LispError::OptionalParamMalformed {
10528 position: 0,
10529 got: SexpWitness::new(SexpShape::List, "(5 default)"),
10530 reason: OptionalParamMalformedReason::NonSymbolName,
10531 };
10532 assert_eq!(
10533 format!("{nonsym}"),
10534 "compile error in defmacro params: malformed &optional spec, got (5 default) (position 0, name not a symbol)"
10535 );
10536 }
10537
10538 #[test]
10539 fn optional_param_malformed_reason_static_labels_pin_legacy_label_bytes() {
10540 // Each per-role `pub const *_LABEL: &'static str` on the closed-set
10541 // `OptionalParamMalformedReason` algebra binds byte-for-byte to the
10542 // pre-lift inline literal at `label()`'s matching arm — so a
10543 // regression that renames ONE constant silently fractures the
10544 // operator-facing rejection wording. The three static-subset arms
10545 // route through the typed constants; the fourth arm
10546 // (`ExtraElements`) formats a `usize` payload dynamically and is
10547 // excluded from this pin.
10548 assert_eq!(OptionalParamMalformedReason::EMPTY_LIST_LABEL, "empty list");
10549 assert_eq!(
10550 OptionalParamMalformedReason::MISSING_DEFAULT_LABEL,
10551 "missing default"
10552 );
10553 assert_eq!(
10554 OptionalParamMalformedReason::NON_SYMBOL_NAME_LABEL,
10555 "name not a symbol"
10556 );
10557
10558 // The `label()` projection routes through the typed per-role
10559 // constants — pinned by string equality against each constant.
10560 assert_eq!(
10561 OptionalParamMalformedReason::EmptyList.label(),
10562 OptionalParamMalformedReason::EMPTY_LIST_LABEL
10563 );
10564 assert_eq!(
10565 OptionalParamMalformedReason::MissingDefault.label(),
10566 OptionalParamMalformedReason::MISSING_DEFAULT_LABEL
10567 );
10568 assert_eq!(
10569 OptionalParamMalformedReason::NonSymbolName.label(),
10570 OptionalParamMalformedReason::NON_SYMBOL_NAME_LABEL
10571 );
10572
10573 // The dynamic `ExtraElements` arm is EXCLUDED from the static
10574 // subset — its label is `format!("{length} elements (need 2)")`
10575 // and cannot bind to a `&'static str` constant. The three
10576 // per-role constants close the STATIC subset; the fourth arm
10577 // stays inline at `label()`.
10578 assert_eq!(
10579 OptionalParamMalformedReason::ExtraElements { length: 4 }.label(),
10580 "4 elements (need 2)"
10581 );
10582
10583 // Family-wide `STATIC_LABELS` array closes the 3-of-4 carving —
10584 // `[&'static str; 3]` with rustc-enforced arity.
10585 assert_eq!(OptionalParamMalformedReason::STATIC_LABELS.len(), 3);
10586 assert_eq!(
10587 OptionalParamMalformedReason::STATIC_LABELS,
10588 [
10589 OptionalParamMalformedReason::EMPTY_LIST_LABEL,
10590 OptionalParamMalformedReason::MISSING_DEFAULT_LABEL,
10591 OptionalParamMalformedReason::NON_SYMBOL_NAME_LABEL,
10592 ]
10593 );
10594
10595 // Pairwise disjointness across the static subset — a future
10596 // rename that collides `MISSING_DEFAULT_LABEL` onto the
10597 // `EMPTY_LIST_LABEL` bytes silently merges two distinct
10598 // rejection wordings into one; the disjointness pin fails-
10599 // loudly at rustc-test time rather than at silent operator-
10600 // facing vocabulary fracture.
10601 let labels = OptionalParamMalformedReason::STATIC_LABELS;
10602 for i in 0..labels.len() {
10603 for j in (i + 1)..labels.len() {
10604 assert_ne!(
10605 labels[i], labels[j],
10606 "OptionalParamMalformedReason::STATIC_LABELS[{i}] and [{j}] collide on `{}`",
10607 labels[i]
10608 );
10609 }
10610 }
10611 }
10612
10613 #[test]
10614 fn optional_param_spec_arity_pins_canonical_accept_target() {
10615 // The canonical accept-arity of the `&optional (NAME DEFAULT)`
10616 // list-spec is exactly TWO elements — a symbol head naming the
10617 // optional plus a default form. Pinning the const here means a
10618 // regression that bumps the accept-arity (e.g. an evaluator
10619 // lands and CL's `(name default supplied-p)` shape becomes
10620 // admissible) MUST also update this pin — which forces the
10621 // `ExtraElements` label's `"need N"` suffix + every truth-table
10622 // test's rendered-diagnostic assertion to be re-derived in
10623 // lockstep rather than silently drifting the accept target from
10624 // its operator-facing wording.
10625 assert_eq!(OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY, 2);
10626 }
10627
10628 #[test]
10629 fn classify_arity_projects_arity_to_typed_rejection_reason() {
10630 // The typed classifier closes the arity-to-reason projection at
10631 // ONE typed function on the algebra — arity 0 → EmptyList,
10632 // arity 1 → MissingDefault, arity == accept-target → None,
10633 // arity ≥3 → ExtraElements{length}. The fourth variant
10634 // NonSymbolName is NOT arity-derivable and stays at the
10635 // caller's head-symbol gate.
10636 use OptionalParamMalformedReason as R;
10637 assert_eq!(R::classify_arity(0), Some(R::EmptyList));
10638 assert_eq!(R::classify_arity(1), Some(R::MissingDefault));
10639 // Accept-arity: the classifier returns None because arity alone
10640 // does not decide the head-symbol gate.
10641 assert_eq!(R::classify_arity(R::OPTIONAL_PARAM_SPEC_ARITY), None);
10642 assert_eq!(R::classify_arity(2), None);
10643 // ExtraElements payload carries the offending arity verbatim so
10644 // the operator-facing diagnostic can name "N elements (need 2)"
10645 // exactly. Pin arity-3, arity-4, and arity-17 to close the
10646 // range of the ≥3 arm.
10647 assert_eq!(R::classify_arity(3), Some(R::ExtraElements { length: 3 }));
10648 assert_eq!(R::classify_arity(4), Some(R::ExtraElements { length: 4 }));
10649 assert_eq!(R::classify_arity(17), Some(R::ExtraElements { length: 17 }));
10650 }
10651
10652 #[test]
10653 fn extra_elements_label_derives_target_arity_from_typed_constant() {
10654 // The `ExtraElements` arm's dynamic label suffix embeds
10655 // [`OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY`]
10656 // via `format!("{length} elements (need {})", …)` — pin that a
10657 // future bump to the accept-arity propagates through the label
10658 // in lockstep rather than leaving the suffix stuck at the pre-
10659 // lift literal `2`. The bytes `"3 elements (need 2)"` /
10660 // `"7 elements (need 2)"` are the current cross-product of
10661 // (offending arity, accept-arity) values.
10662 assert_eq!(
10663 OptionalParamMalformedReason::ExtraElements { length: 3 }.label(),
10664 "3 elements (need 2)"
10665 );
10666 assert_eq!(
10667 OptionalParamMalformedReason::ExtraElements { length: 7 }.label(),
10668 "7 elements (need 2)"
10669 );
10670 // Cross-check: the `2` in `"need 2"` IS the typed constant, not
10671 // a bare literal — a rename of the const to a different value
10672 // fails this assertion with a clear diff.
10673 assert!(OptionalParamMalformedReason::ExtraElements { length: 3 }
10674 .label()
10675 .ends_with(&format!(
10676 "(need {})",
10677 OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY
10678 )));
10679 }
10680
10681 #[test]
10682 fn label_static_projects_static_subset_to_typed_option() {
10683 // The typed STATIC-subset projection carves the 4-arm closed set
10684 // along the same 3-of-4 axis STATIC_LABELS enumerates — three
10685 // arms whose diagnostic bytes bind at a `pub const &'static str`
10686 // (`Some(bytes)`) and one dynamic arm (`ExtraElements`) whose
10687 // bytes format a `usize` payload (`None`).
10688 //
10689 // Pre-lift the STATIC-subset selector was three inline
10690 // `matches!(reason, EmptyList | MissingDefault | NonSymbolName)`
10691 // gates duplicated across consumers (`label`'s three static match
10692 // arms, plus any downstream site that wanted a zero-allocation
10693 // path over the static subset). Post-lift the carving binds at
10694 // ONE typed function on the algebra with rustc-enforced exhaust-
10695 // iveness — a hypothetical fifth static arm extends the match
10696 // body AND `STATIC_LABELS` in lockstep.
10697 use OptionalParamMalformedReason as R;
10698 assert_eq!(R::EmptyList.label_static(), Some(R::EMPTY_LIST_LABEL));
10699 assert_eq!(
10700 R::MissingDefault.label_static(),
10701 Some(R::MISSING_DEFAULT_LABEL)
10702 );
10703 assert_eq!(
10704 R::NonSymbolName.label_static(),
10705 Some(R::NON_SYMBOL_NAME_LABEL)
10706 );
10707 // ExtraElements returns None regardless of the `length` payload —
10708 // the projection is payload-invariant on the dynamic arm.
10709 assert_eq!(R::ExtraElements { length: 3 }.label_static(), None);
10710 assert_eq!(R::ExtraElements { length: 0 }.label_static(), None);
10711 assert_eq!(R::ExtraElements { length: 42 }.label_static(), None);
10712
10713 // Composition with `label()`: for every static arm,
10714 // `label_static().unwrap().to_string() == label()` byte-for-byte
10715 // — the `label()` body threads through `label_static` for the
10716 // static subset rather than re-deriving the per-role match arms
10717 // inline.
10718 for reason in [R::EmptyList, R::MissingDefault, R::NonSymbolName] {
10719 assert_eq!(reason.label_static().unwrap().to_string(), reason.label());
10720 }
10721 }
10722
10723 #[test]
10724 fn label_static_matches_static_labels_array_in_declaration_order() {
10725 // The three `Some(&'static str)` results, taken in the closed
10726 // set's declaration order (EmptyList, MissingDefault,
10727 // NonSymbolName — same order as STATIC_LABELS), equal
10728 // STATIC_LABELS byte-for-byte. This pins the carving axis: the
10729 // `label_static` projection and the `STATIC_LABELS` array carve
10730 // the same 3-of-4 subset in the same order. A future rename that
10731 // reorders one against the other (e.g. swaps two variants in
10732 // `label_static`'s match without updating `STATIC_LABELS`) fails
10733 // here.
10734 use OptionalParamMalformedReason as R;
10735 let projected = [
10736 R::EmptyList.label_static().unwrap(),
10737 R::MissingDefault.label_static().unwrap(),
10738 R::NonSymbolName.label_static().unwrap(),
10739 ];
10740 assert_eq!(projected, R::STATIC_LABELS);
10741 }
10742
10743 #[test]
10744 fn label_static_composes_with_classify_arity_over_the_3_of_4_carving() {
10745 // The two typed subset projections carve the same closed set
10746 // along overlapping 3-of-4 axes:
10747 //
10748 // * `classify_arity(n)` returns `Option<Self>` — `None` at
10749 // accept-arity, `Some(reason)` for the 3-of-4 arity-derivable
10750 // rejections (EmptyList, MissingDefault, ExtraElements).
10751 // * `label_static(reason)` returns `Option<&'static str>` —
10752 // `Some(bytes)` for the 3-of-4 static arms (EmptyList,
10753 // MissingDefault, NonSymbolName), `None` for ExtraElements.
10754 //
10755 // The 2-of-4 intersection (EmptyList, MissingDefault) is the
10756 // arms both carvings agree on: classify_arity emits them AND
10757 // label_static gives them a `&'static str`. Composition pins:
10758 // for arity 0 or 1 the pipeline yields a `Some(&'static str)`.
10759 use OptionalParamMalformedReason as R;
10760 assert_eq!(
10761 R::classify_arity(0).and_then(R::label_static),
10762 Some(R::EMPTY_LIST_LABEL)
10763 );
10764 assert_eq!(
10765 R::classify_arity(1).and_then(R::label_static),
10766 Some(R::MISSING_DEFAULT_LABEL)
10767 );
10768 // At accept-arity, `classify_arity` returns None — the
10769 // composition short-circuits.
10770 assert_eq!(
10771 R::classify_arity(R::OPTIONAL_PARAM_SPEC_ARITY).and_then(R::label_static),
10772 None
10773 );
10774 // At arity ≥3, `classify_arity` returns
10775 // `Some(ExtraElements{length})` and `label_static` returns None
10776 // — the composition also short-circuits, because the two
10777 // carvings' `None` arms disagree (arity-derivable but NOT
10778 // static-labeled).
10779 assert_eq!(R::classify_arity(3).and_then(R::label_static), None);
10780 assert_eq!(R::classify_arity(17).and_then(R::label_static), None);
10781 }
10782
10783 #[test]
10784 fn label_delegates_static_arms_through_label_static() {
10785 // The `label()` body composes `label_static()` (the STATIC-subset
10786 // projection) with an `unwrap_or_else` fallthrough onto the
10787 // dynamic `ExtraElements` format arm. Pin that the three static
10788 // arms' `label()` bytes ARE the `label_static()` bytes with a
10789 // `.to_string()` allocation, and the fourth arm's `label()`
10790 // bytes ARE the format arm's rendering. A regression that
10791 // re-inlines the per-role `.to_string()` at `label()` (drifting
10792 // from the `label_static` composition) still passes the
10793 // per-variant byte-equality assertions above but fails this
10794 // composition pin.
10795 use OptionalParamMalformedReason as R;
10796 // Static arms — `label()` bytes route through `label_static`.
10797 for (variant, static_bytes) in [
10798 (R::EmptyList, R::EMPTY_LIST_LABEL),
10799 (R::MissingDefault, R::MISSING_DEFAULT_LABEL),
10800 (R::NonSymbolName, R::NON_SYMBOL_NAME_LABEL),
10801 ] {
10802 assert_eq!(variant.label_static(), Some(static_bytes));
10803 assert_eq!(variant.label(), static_bytes.to_string());
10804 }
10805 // Dynamic arm — `label_static()` is None; `label()` falls
10806 // through to the `ExtraElements` format arm.
10807 let extra = R::ExtraElements { length: 5 };
10808 assert_eq!(extra.label_static(), None);
10809 assert_eq!(
10810 extra.label(),
10811 format!("5 elements (need {})", R::OPTIONAL_PARAM_SPEC_ARITY)
10812 );
10813 }
10814
10815 #[test]
10816 fn optional_param_malformed_reason_extra_elements_descriptor_pins_canonical_noun_bytes() {
10817 // Pin the per-role `pub const EXTRA_ELEMENTS_DESCRIPTOR` bytes
10818 // byte-for-byte. The `"elements"` noun IS the shared format
10819 // template's `{descriptor}` slot binding for the ExtraElements
10820 // arm — a regression that renames the noun (e.g. to `"items"`
10821 // for a hypothetical general-list-shape evaluator) surfaces
10822 // here rather than at silent operator-facing diagnostic drift.
10823 assert_eq!(
10824 OptionalParamMalformedReason::EXTRA_ELEMENTS_DESCRIPTOR,
10825 "elements"
10826 );
10827 }
10828
10829 #[test]
10830 fn optional_param_malformed_reason_dynamic_descriptors_forced_arity_pins_1_of_4_carving() {
10831 // Pin `DYNAMIC_DESCRIPTORS.len() == 1` — the DYNAMIC subset of
10832 // the four-arm closed set carves exactly ONE arm
10833 // (`ExtraElements`). The peer STATIC subset carves THREE arms
10834 // (`EmptyList`, `MissingDefault`, `NonSymbolName`). The
10835 // partition sums to four: 3 + 1 == 4 == R::ALL analogue count.
10836 assert_eq!(OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS.len(), 1);
10837 // Element-wise: the single dynamic descriptor equals the per-
10838 // role const byte-for-byte. Reordering the array against the
10839 // per-role const fails here.
10840 assert_eq!(
10841 OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
10842 [OptionalParamMalformedReason::EXTRA_ELEMENTS_DESCRIPTOR]
10843 );
10844 }
10845
10846 #[test]
10847 fn descriptor_dynamic_projects_dynamic_subset_to_typed_option() {
10848 // The typed DYNAMIC-subset projection carves the 4-arm closed
10849 // set along the same 1-of-4 axis DYNAMIC_DESCRIPTORS
10850 // enumerates — one arm whose diagnostic bytes format through
10851 // the shared `"{length} {descriptor} (need {})"` template
10852 // (`Some(descriptor)`) and three arms whose full bytes bind at
10853 // a `&'static str` on the STATIC axis (`None`).
10854 use OptionalParamMalformedReason as R;
10855 assert_eq!(
10856 R::ExtraElements { length: 3 }.descriptor_dynamic(),
10857 Some(R::EXTRA_ELEMENTS_DESCRIPTOR)
10858 );
10859 // ExtraElements descriptor is payload-invariant — same
10860 // descriptor regardless of the `length` payload.
10861 assert_eq!(
10862 R::ExtraElements { length: 0 }.descriptor_dynamic(),
10863 Some(R::EXTRA_ELEMENTS_DESCRIPTOR)
10864 );
10865 assert_eq!(
10866 R::ExtraElements { length: 42 }.descriptor_dynamic(),
10867 Some(R::EXTRA_ELEMENTS_DESCRIPTOR)
10868 );
10869 // STATIC arms return None on the descriptor axis.
10870 assert_eq!(R::EmptyList.descriptor_dynamic(), None);
10871 assert_eq!(R::MissingDefault.descriptor_dynamic(), None);
10872 assert_eq!(R::NonSymbolName.descriptor_dynamic(), None);
10873 }
10874
10875 #[test]
10876 fn descriptor_dynamic_matches_dynamic_descriptors_array_in_declaration_order() {
10877 // The single `Some(&'static str)` result, taken in the closed
10878 // set's DYNAMIC declaration order (ExtraElements — same order
10879 // as DYNAMIC_DESCRIPTORS), equals DYNAMIC_DESCRIPTORS byte-for-
10880 // byte. Pins the carving axis: the `descriptor_dynamic`
10881 // projection and the `DYNAMIC_DESCRIPTORS` array carve the
10882 // same 1-of-4 subset in the same order.
10883 use OptionalParamMalformedReason as R;
10884 let projected = [R::ExtraElements { length: 5 }.descriptor_dynamic().unwrap()];
10885 assert_eq!(projected, R::DYNAMIC_DESCRIPTORS);
10886 }
10887
10888 #[test]
10889 fn label_static_and_descriptor_dynamic_partition_the_closed_set() {
10890 // Truth-table pin of the STATIC ⊕ DYNAMIC partition contract:
10891 // for every legal variant, EXACTLY ONE of `label_static(self)`
10892 // and `descriptor_dynamic(self)` returns `Some`. The two
10893 // projections carve the four-arm closed set into disjoint
10894 // covering halves without overlap.
10895 use OptionalParamMalformedReason as R;
10896 for reason in [
10897 R::EmptyList,
10898 R::MissingDefault,
10899 R::NonSymbolName,
10900 R::ExtraElements { length: 3 },
10901 R::ExtraElements { length: 0 },
10902 R::ExtraElements { length: 99 },
10903 ] {
10904 let static_hit = reason.label_static().is_some();
10905 let dynamic_hit = reason.descriptor_dynamic().is_some();
10906 assert!(
10907 static_hit ^ dynamic_hit,
10908 "STATIC ⊕ DYNAMIC partition violated for {reason:?}: \
10909 label_static={static_hit}, descriptor_dynamic={dynamic_hit}",
10910 );
10911 }
10912 }
10913
10914 #[test]
10915 fn label_dynamic_composes_descriptor_arity_and_payload_through_shared_template() {
10916 // `label_dynamic()` returns `Some(String)` for the DYNAMIC arm
10917 // formatted through the shared `"{length} {descriptor} (need
10918 // {})"` template — the descriptor binds to
10919 // EXTRA_ELEMENTS_DESCRIPTOR, the arity suffix binds to
10920 // OPTIONAL_PARAM_SPEC_ARITY, and the length binds to the
10921 // enum's dynamic payload. The three STATIC arms return None.
10922 use OptionalParamMalformedReason as R;
10923 assert_eq!(R::EmptyList.label_dynamic(), None);
10924 assert_eq!(R::MissingDefault.label_dynamic(), None);
10925 assert_eq!(R::NonSymbolName.label_dynamic(), None);
10926 // Payload-varying: the `length` slot binds each variant's
10927 // payload verbatim.
10928 assert_eq!(
10929 R::ExtraElements { length: 3 }.label_dynamic(),
10930 Some("3 elements (need 2)".to_string())
10931 );
10932 assert_eq!(
10933 R::ExtraElements { length: 7 }.label_dynamic(),
10934 Some("7 elements (need 2)".to_string())
10935 );
10936 assert_eq!(
10937 R::ExtraElements { length: 0 }.label_dynamic(),
10938 Some("0 elements (need 2)".to_string())
10939 );
10940 // Cross-check: the composition threads OPTIONAL_PARAM_SPEC_ARITY
10941 // rather than a bare `2` literal — a rename of the arity const
10942 // propagates through this projection in lockstep.
10943 assert_eq!(
10944 R::ExtraElements { length: 4 }.label_dynamic().unwrap(),
10945 format!(
10946 "4 {} (need {})",
10947 R::EXTRA_ELEMENTS_DESCRIPTOR,
10948 R::OPTIONAL_PARAM_SPEC_ARITY
10949 )
10950 );
10951 }
10952
10953 #[test]
10954 fn label_composes_static_and_dynamic_projections_over_the_full_partition() {
10955 // Post-lift `label()` composes `label_static().map(str::to_string)
10956 // .or_else(|| label_dynamic()).expect(...)` — the STATIC ⊕
10957 // DYNAMIC partition contract guarantees exactly ONE projection
10958 // returns `Some` per variant, so the expect arm is
10959 // structurally unreachable. Pin that every variant's `label()`
10960 // bytes equal exactly what the composed projection yields.
10961 use OptionalParamMalformedReason as R;
10962 for (reason, expected) in [
10963 (R::EmptyList, R::EMPTY_LIST_LABEL.to_string()),
10964 (R::MissingDefault, R::MISSING_DEFAULT_LABEL.to_string()),
10965 (R::NonSymbolName, R::NON_SYMBOL_NAME_LABEL.to_string()),
10966 (
10967 R::ExtraElements { length: 3 },
10968 "3 elements (need 2)".to_string(),
10969 ),
10970 (
10971 R::ExtraElements { length: 42 },
10972 "42 elements (need 2)".to_string(),
10973 ),
10974 ] {
10975 assert_eq!(reason.label(), expected);
10976 // Composition pin: `label()` bytes equal whichever
10977 // projection returned Some.
10978 let composed = reason
10979 .label_static()
10980 .map(str::to_string)
10981 .or_else(|| reason.label_dynamic())
10982 .unwrap();
10983 assert_eq!(reason.label(), composed);
10984 }
10985 }
10986
10987 #[test]
10988 fn paren_suffix_owns_the_bare_parenthetical_skeleton() {
10989 // The lowest layer of the suffix-wrapping algebra: one leading space,
10990 // open paren, body, close paren. A regression that drops the leading
10991 // space, doubles it, or moves a paren fails here.
10992 assert_eq!(paren_suffix("got 5"), " (got 5)");
10993 assert_eq!(paren_suffix(""), " ()");
10994 }
10995
10996 #[test]
10997 fn every_suffix_helper_wraps_through_one_paren_primitive() {
10998 // All seven `*_suffix` helpers delegate their bare ` (…)` wrapping to
10999 // `paren_suffix`; only their body-construction stays helper-specific.
11000 // Pinning that each helper's output EQUALS `paren_suffix` applied with
11001 // that helper's body means a re-inlined divergent skeleton in ANY of
11002 // them (e.g. a dropped leading space, a moved paren) fails-loudly.
11003 // Covers both arms of the multi-arm helpers.
11004
11005 // unknown_among_suffix — the `did you mean …?; …` join layer.
11006 assert_eq!(
11007 unknown_among_suffix(Some(":t"), "allowed: :name"),
11008 paren_suffix("did you mean :t?; allowed: :name")
11009 );
11010 assert_eq!(
11011 unknown_among_suffix(None, "allowed: :name"),
11012 paren_suffix("allowed: :name")
11013 );
11014
11015 // rest_param_missing_name_suffix — the `rest marker at position …` body.
11016 assert_eq!(
11017 rest_param_missing_name_suffix(1, Some("5")),
11018 paren_suffix("rest marker at position 1, got 5")
11019 );
11020 assert_eq!(
11021 rest_param_missing_name_suffix(1, None),
11022 paren_suffix("rest marker at position 1, none provided")
11023 );
11024
11025 // rest_param_trailing_tokens_suffix — the `… trailing after name` body.
11026 assert_eq!(
11027 rest_param_trailing_tokens_suffix(1, 2, "extra"),
11028 paren_suffix("rest marker at position 1, 2 trailing after name, first: extra")
11029 );
11030
11031 // missing_head_symbol_suffix — the `got …` / `empty list` body.
11032 assert_eq!(missing_head_symbol_suffix(Some("5")), paren_suffix("got 5"));
11033 assert_eq!(missing_head_symbol_suffix(None), paren_suffix("empty list"));
11034
11035 // optional_marker_repeated_suffix — the two-marker-position body.
11036 assert_eq!(
11037 optional_marker_repeated_suffix(1, 3),
11038 paren_suffix("first &optional at position 1, second at position 3")
11039 );
11040
11041 // optional_param_malformed_suffix — the position+reason-label body.
11042 // Both `&'static` (the three string-only arms) and formatted
11043 // (the `ExtraElements{length}` arm) cases route through the same
11044 // `paren_suffix` wrapping.
11045 assert_eq!(
11046 optional_param_malformed_suffix(1, OptionalParamMalformedReason::EmptyList),
11047 paren_suffix("position 1, empty list")
11048 );
11049 assert_eq!(
11050 optional_param_malformed_suffix(
11051 2,
11052 OptionalParamMalformedReason::ExtraElements { length: 3 }
11053 ),
11054 paren_suffix("position 2, 3 elements (need 2)")
11055 );
11056 }
11057
11058 #[test]
11059 fn unknown_kwarg_and_domain_suffixes_share_one_wrapping_primitive() {
11060 // Both gates delegate their parenthetical wrapping to
11061 // `unknown_among_suffix`; only the hint-formatting (`:foo` vs
11062 // `(foo ...)`) and the body-construction (`allowed:` vs `registered:`
11063 // / `no domains registered`) stay gate-specific. Pinning that each
11064 // gate's output EQUALS the primitive applied with that gate's
11065 // formatted hint + body means a re-inlined divergent skeleton in
11066 // either gate fails-loudly. Covers all four arms: kwarg-with-hint,
11067 // kwarg-without-hint, domain-with-hint, domain-empty-registry.
11068 let allowed = vec!["name".to_string(), "threshold".to_string()];
11069 assert_eq!(
11070 unknown_kwarg_suffix(Some("threshold"), &allowed),
11071 unknown_among_suffix(Some(":threshold"), "allowed: :name, :threshold")
11072 );
11073 assert_eq!(
11074 unknown_kwarg_suffix(None, &allowed),
11075 unknown_among_suffix(None, "allowed: :name, :threshold")
11076 );
11077
11078 let registered = vec!["defmonitor".to_string(), "defnotify".to_string()];
11079 assert_eq!(
11080 unknown_domain_keyword_suffix(Some("defmonitor"), ®istered),
11081 unknown_among_suffix(
11082 Some("(defmonitor ...)"),
11083 "registered: defmonitor, defnotify"
11084 )
11085 );
11086 assert_eq!(
11087 unknown_domain_keyword_suffix(None, &[]),
11088 unknown_among_suffix(None, "no domains registered")
11089 );
11090 }
11091
11092 #[test]
11093 fn non_symbol_unquote_target_display_renders_canonical_type_mismatch_shape() {
11094 // `,(list 1 2)` — the inner is a list, not a symbol. The variant
11095 // names the syntactic marker (`,`), the expected shape (`symbol` —
11096 // the only form a no-evaluator template can substitute), and the
11097 // offending literal (`(list 1 2)`) as first-class fields. Authoring
11098 // tools that pattern-match on the variant gain structural binding;
11099 // tools that substring-match on the rendered diagnostic see a
11100 // stable shape parallel to the existing `TypeMismatch` variant.
11101 let err = LispError::NonSymbolUnquoteTarget {
11102 prefix: UnquoteForm::Unquote,
11103 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
11104 };
11105 assert_eq!(
11106 format!("{err}"),
11107 "compile error in ,: expected symbol, got (list 1 2)"
11108 );
11109 }
11110
11111 #[test]
11112 fn non_symbol_unquote_target_display_preserves_splice_prefix() {
11113 // Splice marker rides through the `prefix` field; the rendered
11114 // diagnostic is `,@`, not `,`. The operator never has to translate
11115 // `,` ↔ `,@` mentally — same posture as `UnboundTemplateVar`.
11116 let err = LispError::NonSymbolUnquoteTarget {
11117 prefix: UnquoteForm::Splice,
11118 got: SexpWitness::new(SexpShape::Int, "5"),
11119 };
11120 assert_eq!(
11121 format!("{err}"),
11122 "compile error in ,@: expected symbol, got 5"
11123 );
11124 }
11125
11126 #[test]
11127 fn non_symbol_unquote_target_display_carries_keyword_atom_unchanged() {
11128 // `,:foo` — the inner is a keyword atom. The `:foo` form
11129 // round-trips through `SexpWitness::Display` (writing the
11130 // `display` field verbatim) into the variant's `got` slot
11131 // unchanged, so the operator sees what they wrote. The typed
11132 // `got.shape` slot independently carries `SexpShape::Keyword`
11133 // so tooling that wants the structural identity binds without
11134 // re-parsing.
11135 let err = LispError::NonSymbolUnquoteTarget {
11136 prefix: UnquoteForm::Unquote,
11137 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
11138 };
11139 assert_eq!(
11140 format!("{err}"),
11141 "compile error in ,: expected symbol, got :foo"
11142 );
11143 }
11144
11145 #[test]
11146 fn splice_outside_list_display_renders_legacy_substring_with_offending_form() {
11147 // `,@xs` at the body's top level — there is no containing list to
11148 // splice into. The variant names the offending inner (`xs`) as a
11149 // first-class typed witness so authoring tools (REPL, LSP,
11150 // `tatara-check`) gain structural binding to BOTH `got.shape`
11151 // (typed `SexpShape::Symbol` here) AND `got.display` (the literal
11152 // `"xs"`); tools that substring-match on the rendered diagnostic
11153 // still see the legacy `"\`,@\` may only appear inside a list"`
11154 // substring verbatim because `SexpWitness::Display` writes only
11155 // the `display` field.
11156 let err = LispError::SpliceOutsideList {
11157 got: SexpWitness::new(SexpShape::Symbol, "xs"),
11158 };
11159 assert_eq!(
11160 format!("{err}"),
11161 "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
11162 );
11163 }
11164
11165 #[test]
11166 fn splice_outside_list_display_carries_list_literal_unchanged() {
11167 // The offending inner is a list literal — `,@(list 1 2)` — so the
11168 // operator sees the literal value they wrote in the parenthetical,
11169 // not just a type-name. The typed `SexpShape::List` rides the
11170 // variant slot alongside the `Sexp::Display` projection; tools
11171 // can now pattern-match on `got.shape == SexpShape::List`
11172 // without re-parsing the rendered diagnostic.
11173 let err = LispError::SpliceOutsideList {
11174 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
11175 };
11176 assert_eq!(
11177 format!("{err}"),
11178 "compile error in ,@: `,@` may only appear inside a list (got ,@(list 1 2))"
11179 );
11180 }
11181
11182 #[test]
11183 fn splice_outside_list_display_carries_kebab_case_symbol_unchanged() {
11184 // `,@notify-ref` — kebab-cased symbol round-trips through the
11185 // variant's `got.display` slot unchanged. Pinning this contract
11186 // means a regression that camelCases or lowercases the offending
11187 // form fails-loudly here.
11188 let err = LispError::SpliceOutsideList {
11189 got: SexpWitness::new(SexpShape::Symbol, "notify-ref"),
11190 };
11191 assert_eq!(
11192 format!("{err}"),
11193 "compile error in ,@: `,@` may only appear inside a list (got ,@notify-ref)"
11194 );
11195 }
11196
11197 #[test]
11198 fn splice_outside_list_display_preserves_legacy_substring_for_message_grep() {
11199 // Pin the legacy substring as a separate assertion so a regression
11200 // that drifts the wording (e.g., to "outside a list" or "without a
11201 // containing list") fails-loudly here even if the parenthetical
11202 // changes shape. The substring is what consumers downstream
11203 // (tatara-check, the REPL) substring-match on today.
11204 let err = LispError::SpliceOutsideList {
11205 got: SexpWitness::new(SexpShape::Symbol, "xs"),
11206 };
11207 let msg = format!("{err}");
11208 assert!(
11209 msg.contains("`,@` may only appear inside a list"),
11210 "expected legacy substring in message, got: {msg}"
11211 );
11212 assert!(
11213 msg.contains("(got ,@xs)"),
11214 "expected offending-form parenthetical in message, got: {msg}"
11215 );
11216 }
11217
11218 // ── SexpWitness: typed joint-identity lift for offending-Sexp slots ──
11219 //
11220 // The `SexpWitness { shape: SexpShape, display: String }` typed
11221 // primitive lands as the first promotion of a `got: String`
11222 // Sexp::Display-projection slot to a typed joint identity. Pins:
11223 // (a) `SexpWitness::Display` writes only the `display` field
11224 // (byte-for-byte legacy rendering preserved); (b) the struct's
11225 // `shape` field is pattern-matchable for tooling that wants
11226 // structural binding without re-parsing the literal; (c) the
11227 // `SpliceOutsideList.got` slot's typed shape is now load-bearing
11228 // — a regression that re-collapses `got` to a free-form `String`
11229 // loses the rustc-enforced typed-shape guarantee.
11230
11231 #[test]
11232 fn sexp_witness_display_writes_only_the_display_field() {
11233 // Pin the byte-for-byte rendering contract: `SexpWitness`'s
11234 // `Display` impl writes ONLY the `display` field, NOT the
11235 // shape label. This is the load-bearing posture for the
11236 // `#[error("...(got ,@{got})")]` annotation on
11237 // `SpliceOutsideList` — the parenthetical reads `(got ,@xs)`
11238 // verbatim, not `(got ,@symbol xs)`. A regression that adds
11239 // a shape prefix to Display fails-loudly here AND at every
11240 // legacy-rendering test downstream.
11241 let w = SexpWitness::new(SexpShape::Symbol, "xs");
11242 assert_eq!(format!("{w}"), "xs");
11243 let w = SexpWitness::new(SexpShape::List, "(list 1 2)");
11244 assert_eq!(format!("{w}"), "(list 1 2)");
11245 let w = SexpWitness::new(SexpShape::Int, "5");
11246 assert_eq!(format!("{w}"), "5");
11247 let w = SexpWitness::new(SexpShape::Keyword, ":foo");
11248 assert_eq!(format!("{w}"), ":foo");
11249 }
11250
11251 #[test]
11252 fn sexp_witness_carries_both_shape_and_display_jointly() {
11253 // Pin the joint-identity contract: `SexpWitness` carries BOTH
11254 // halves of the offending-value identity (typed `SexpShape`
11255 // AND literal `Sexp::Display` projection) in ONE owned value.
11256 // Tools that pattern-match on `witness.shape` bind to the
11257 // structural identity; tools that render via `{witness}` get
11258 // the literal value. Neither half is recoverable from the
11259 // other (a `Sexp::Display` projection of `"5"` could be Int
11260 // or Symbol — substring parsing can't recover the structural
11261 // identity reliably), so the typed witness is the canonical
11262 // source for both.
11263 let w = SexpWitness::new(SexpShape::Int, "5");
11264 assert_eq!(w.shape, SexpShape::Int);
11265 assert_eq!(w.display, "5");
11266 // The literal `"5"` would substring-grep the same as a hand-
11267 // written symbol named `5`, but the typed shape distinguishes
11268 // them structurally — a regression that drops the shape slot
11269 // would collapse this distinction.
11270 let w_sym = SexpWitness::new(SexpShape::Symbol, "5");
11271 assert_eq!(w_sym.shape, SexpShape::Symbol);
11272 assert_eq!(w_sym.display, "5");
11273 assert_ne!(
11274 w, w_sym,
11275 "Witnesses with same display but different shape must NOT be equal — typed shape is load-bearing data",
11276 );
11277 }
11278
11279 #[test]
11280 fn splice_outside_list_got_carries_typed_witness_through_variant_slot() {
11281 // Pin the structural binding on `LispError::SpliceOutsideList.got`
11282 // — a regression that re-introduces a `String`-shaped got slot
11283 // (collapsing the typed witness back into a free-form literal)
11284 // fails-loudly here. After this lift the variant's typed slot
11285 // is the joint `SexpWitness` identity; the Display projection
11286 // through `SexpWitness::Display` writes only the `display`
11287 // field so the rendered `(got ,@<display>)` parenthetical is
11288 // byte-for-byte identical to the legacy `got: String` shape.
11289 let err = LispError::SpliceOutsideList {
11290 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
11291 };
11292 match &err {
11293 LispError::SpliceOutsideList { got } => {
11294 assert_eq!(got.shape, SexpShape::List);
11295 assert_eq!(got.display, "(list 1 2)");
11296 }
11297 other => panic!("expected SpliceOutsideList, got {other:?}"),
11298 }
11299 assert_eq!(
11300 format!("{err}"),
11301 "compile error in ,@: `,@` may only appear inside a list (got ,@(list 1 2))"
11302 );
11303 }
11304
11305 #[test]
11306 fn splice_outside_list_got_distinguishes_symbol_from_list_at_variant_slot() {
11307 // Pin the typed-shape bifurcation at the variant slot: a
11308 // `,@xs` (symbol unquote-splice outside list) and a `,@(list 1 2)`
11309 // (list-literal unquote-splice outside list) BOTH route to
11310 // `SpliceOutsideList`, but the typed `got.shape` slot
11311 // distinguishes them structurally — `SexpShape::Symbol` vs.
11312 // `SexpShape::List`. A regression that erases the typed
11313 // shape (e.g., reverts to `got: String`) would lose this
11314 // distinction — tooling that wants to surface "you wrote a
11315 // symbol `,@xs` outside a list; bind `xs` to a list first"
11316 // vs. "you wrote a list literal `,@(list 1 2)` outside a
11317 // list; nest it inside `(outer ,@(...))`" would have to
11318 // substring-grep the `display` field, brittle.
11319 let err_sym = LispError::SpliceOutsideList {
11320 got: SexpWitness::new(SexpShape::Symbol, "xs"),
11321 };
11322 let err_list = LispError::SpliceOutsideList {
11323 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
11324 };
11325 let (sym_shape, list_shape) = (
11326 match &err_sym {
11327 LispError::SpliceOutsideList { got } => got.shape,
11328 _ => unreachable!(),
11329 },
11330 match &err_list {
11331 LispError::SpliceOutsideList { got } => got.shape,
11332 _ => unreachable!(),
11333 },
11334 );
11335 assert_ne!(
11336 sym_shape, list_shape,
11337 "Symbol and List witnesses must remain structurally distinct at the variant slot",
11338 );
11339 assert_eq!(sym_shape, SexpShape::Symbol);
11340 assert_eq!(list_shape, SexpShape::List);
11341 }
11342
11343 #[test]
11344 fn non_symbol_unquote_target_got_carries_typed_witness_through_variant_slot() {
11345 // Sibling pin to
11346 // `splice_outside_list_got_carries_typed_witness_through_variant_slot`
11347 // for the template-gate's OTHER `,X/,@X` rejection variant.
11348 // After this lift `LispError::NonSymbolUnquoteTarget.got` is
11349 // the typed joint `SexpWitness` identity — the same
11350 // primitive `SpliceOutsideList.got` already carries. The two
11351 // template-gate `,X/,@X` rejection variants now share ONE
11352 // typed witness identity at their `got` slot; authoring tools
11353 // bind on `got.shape` AND `got.display` jointly across both
11354 // sites rather than substring-grepping a free-form String on
11355 // each. A regression that re-collapses `got` to `String` loses
11356 // the rustc-enforced closed-set guarantee on shape identity
11357 // here.
11358 let err = LispError::NonSymbolUnquoteTarget {
11359 prefix: UnquoteForm::Unquote,
11360 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
11361 };
11362 match &err {
11363 LispError::NonSymbolUnquoteTarget { prefix, got } => {
11364 assert_eq!(*prefix, UnquoteForm::Unquote);
11365 assert_eq!(got.shape, SexpShape::List);
11366 assert_eq!(got.display, "(list 1 2)");
11367 }
11368 other => panic!("expected NonSymbolUnquoteTarget, got {other:?}"),
11369 }
11370 assert_eq!(
11371 format!("{err}"),
11372 "compile error in ,: expected symbol, got (list 1 2)"
11373 );
11374 }
11375
11376 #[test]
11377 fn non_symbol_unquote_target_got_distinguishes_int_from_keyword_at_variant_slot() {
11378 // Pin the typed-shape bifurcation at the variant slot — `,5`
11379 // (int atom in unquote slot) and `,:foo` (keyword atom in
11380 // unquote slot) BOTH route to `NonSymbolUnquoteTarget`, but
11381 // the typed `got.shape` slot distinguishes them structurally
11382 // — `SexpShape::Int` vs. `SexpShape::Keyword`. Sibling pin
11383 // for the same structural-shape-bifurcation property
11384 // `splice_outside_list_got_distinguishes_symbol_from_list_at_variant_slot`
11385 // pins on `SpliceOutsideList`. A regression that erases the
11386 // typed shape (e.g., reverts to `got: String`) would lose
11387 // this distinction — tooling that wants to surface "you wrote
11388 // an int `,5` where a symbol was expected; only symbols are
11389 // substitutable in templates" vs. "you wrote a keyword `,:foo`
11390 // where a symbol was expected; keywords aren't substitutable
11391 // (did you mean `,foo`?)" would have to substring-grep the
11392 // `display` field, brittle.
11393 let err_int = LispError::NonSymbolUnquoteTarget {
11394 prefix: UnquoteForm::Unquote,
11395 got: SexpWitness::new(SexpShape::Int, "5"),
11396 };
11397 let err_kw = LispError::NonSymbolUnquoteTarget {
11398 prefix: UnquoteForm::Unquote,
11399 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
11400 };
11401 let (int_shape, kw_shape) = (
11402 match &err_int {
11403 LispError::NonSymbolUnquoteTarget { got, .. } => got.shape,
11404 _ => unreachable!(),
11405 },
11406 match &err_kw {
11407 LispError::NonSymbolUnquoteTarget { got, .. } => got.shape,
11408 _ => unreachable!(),
11409 },
11410 );
11411 assert_ne!(
11412 int_shape, kw_shape,
11413 "Int and Keyword witnesses must remain structurally distinct at the variant slot",
11414 );
11415 assert_eq!(int_shape, SexpShape::Int);
11416 assert_eq!(kw_shape, SexpShape::Keyword);
11417 }
11418
11419 #[test]
11420 fn non_symbol_unquote_target_and_splice_outside_list_share_one_witness_primitive() {
11421 // Pin that BOTH template-gate `,X/,@X` rejection variants
11422 // (`NonSymbolUnquoteTarget` AND `SpliceOutsideList`) carry
11423 // the SAME typed `SexpWitness` primitive at their `got` slot
11424 // — the closed set of "offending inner Sexp" identities is
11425 // bound by ONE typed primitive across both rejection
11426 // surfaces. A regression that diverges the slot type on one
11427 // variant (e.g., re-collapses NonSymbolUnquoteTarget.got to
11428 // String while leaving SpliceOutsideList.got as
11429 // SexpWitness) fails-loudly here because the assignment
11430 // round-trips the witness across both slot types.
11431 let same_witness = SexpWitness::new(SexpShape::List, "(list 1 2)");
11432 let non_symbol_target = LispError::NonSymbolUnquoteTarget {
11433 prefix: UnquoteForm::Splice,
11434 got: same_witness.clone(),
11435 };
11436 let splice_outside = LispError::SpliceOutsideList {
11437 got: same_witness.clone(),
11438 };
11439 match (&non_symbol_target, &splice_outside) {
11440 (
11441 LispError::NonSymbolUnquoteTarget { got: lhs_got, .. },
11442 LispError::SpliceOutsideList { got: rhs_got },
11443 ) => {
11444 assert_eq!(lhs_got.shape, rhs_got.shape);
11445 assert_eq!(lhs_got.display, rhs_got.display);
11446 assert_eq!(*lhs_got, same_witness);
11447 assert_eq!(*rhs_got, same_witness);
11448 }
11449 _ => unreachable!(),
11450 }
11451 }
11452
11453 #[test]
11454 fn non_symbol_param_got_carries_typed_witness_through_variant_slot() {
11455 // Pin the structural binding AND the Display projection on
11456 // `LispError::NonSymbolParam.got`. After this lift the variant's
11457 // typed slot is the joint `SexpWitness` identity — the same
11458 // primitive `SpliceOutsideList.got` and
11459 // `NonSymbolUnquoteTarget.got` already carry. A regression that
11460 // re-collapses `got` to `String` (losing the rustc-enforced
11461 // closed-set guarantee on shape identity) fails-loudly here.
11462 // The Display projection through `SexpWitness::Display` writes
11463 // only the `display` field so the rendered `at position X, got
11464 // <display>` clause is byte-for-byte identical to the legacy
11465 // `got: String` shape; downstream substring-grep consumers
11466 // (`tatara-check`, REPL) see no drift.
11467 let err = LispError::NonSymbolParam {
11468 position: 1,
11469 got: SexpWitness::new(SexpShape::Int, "5"),
11470 };
11471 match &err {
11472 LispError::NonSymbolParam { position, got } => {
11473 assert_eq!(*position, 1);
11474 assert_eq!(got.shape, SexpShape::Int);
11475 assert_eq!(got.display, "5");
11476 }
11477 other => panic!("expected NonSymbolParam, got {other:?}"),
11478 }
11479 assert_eq!(
11480 format!("{err}"),
11481 "compile error in defmacro params: expected symbol at \
11482 position 1, got 5"
11483 );
11484 }
11485
11486 #[test]
11487 fn non_symbol_param_got_distinguishes_int_from_keyword_at_variant_slot() {
11488 // Pin the typed-shape bifurcation at the variant slot — `5`
11489 // (int atom at a param-list position) and `:foo` (keyword atom
11490 // at a param-list position) BOTH route to `NonSymbolParam`, but
11491 // the typed `got.shape` slot distinguishes them structurally —
11492 // `SexpShape::Int` vs. `SexpShape::Keyword`. Sibling pin for
11493 // the same structural-shape-bifurcation property
11494 // `non_symbol_unquote_target_got_distinguishes_int_from_keyword_at_variant_slot`
11495 // pins on `NonSymbolUnquoteTarget`. A regression that erases
11496 // the typed shape (e.g., reverts to `got: String`) would lose
11497 // this distinction — tooling that wants to surface "you wrote
11498 // an int `5` where a symbol was expected at param-list position
11499 // 0" vs. "you wrote a keyword `:foo` where a symbol was expected
11500 // at param-list position 0 (did you mean `foo`?)" would have to
11501 // substring-grep the `display` field, brittle.
11502 let err_int = LispError::NonSymbolParam {
11503 position: 0,
11504 got: SexpWitness::new(SexpShape::Int, "5"),
11505 };
11506 let err_kw = LispError::NonSymbolParam {
11507 position: 0,
11508 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
11509 };
11510 let (int_shape, kw_shape) = (
11511 match &err_int {
11512 LispError::NonSymbolParam { got, .. } => got.shape,
11513 _ => unreachable!(),
11514 },
11515 match &err_kw {
11516 LispError::NonSymbolParam { got, .. } => got.shape,
11517 _ => unreachable!(),
11518 },
11519 );
11520 assert_ne!(
11521 int_shape, kw_shape,
11522 "Int and Keyword witnesses must remain structurally distinct at the variant slot",
11523 );
11524 assert_eq!(int_shape, SexpShape::Int);
11525 assert_eq!(kw_shape, SexpShape::Keyword);
11526 }
11527
11528 #[test]
11529 fn non_symbol_param_and_template_gate_share_one_witness_primitive() {
11530 // Pin that ALL THREE Sexp-display-source `got` slots in the
11531 // substrate (`NonSymbolParam`, `NonSymbolUnquoteTarget`,
11532 // `SpliceOutsideList`) carry the SAME typed `SexpWitness`
11533 // primitive — the closed set of "offending inner Sexp"
11534 // identities is bound by ONE typed primitive across the three
11535 // rejection surfaces (the defmacro-syntax-gate's `parse_params`
11536 // walker AND the template-gate's `,X/,@X` pair). A regression
11537 // that diverges the slot type on any one variant (e.g.,
11538 // re-collapses `NonSymbolParam.got` to `String` while leaving
11539 // the template-gate variants typed) fails-loudly here because
11540 // the assignment round-trips the witness across all three slot
11541 // types. Sibling pin to
11542 // `non_symbol_unquote_target_and_splice_outside_list_share_one_witness_primitive`
11543 // — extending the typed-identity unification contract from
11544 // two slots to three.
11545 let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
11546 let non_symbol_param = LispError::NonSymbolParam {
11547 position: 0,
11548 got: same_witness.clone(),
11549 };
11550 let non_symbol_target = LispError::NonSymbolUnquoteTarget {
11551 prefix: UnquoteForm::Unquote,
11552 got: same_witness.clone(),
11553 };
11554 let splice_outside = LispError::SpliceOutsideList {
11555 got: same_witness.clone(),
11556 };
11557 match (&non_symbol_param, &non_symbol_target, &splice_outside) {
11558 (
11559 LispError::NonSymbolParam { got: a, .. },
11560 LispError::NonSymbolUnquoteTarget { got: b, .. },
11561 LispError::SpliceOutsideList { got: c },
11562 ) => {
11563 assert_eq!(a.shape, b.shape);
11564 assert_eq!(b.shape, c.shape);
11565 assert_eq!(a.display, b.display);
11566 assert_eq!(b.display, c.display);
11567 assert_eq!(*a, same_witness);
11568 assert_eq!(*b, same_witness);
11569 assert_eq!(*c, same_witness);
11570 }
11571 _ => unreachable!(),
11572 }
11573 }
11574
11575 #[test]
11576 fn defmacro_non_symbol_name_got_carries_typed_witness_through_variant_slot() {
11577 // Pin the structural binding AND the Display projection on
11578 // `LispError::DefmacroNonSymbolName.got`. After this lift the
11579 // variant's typed slot is the joint `SexpWitness` identity —
11580 // the same primitive `SpliceOutsideList.got`,
11581 // `NonSymbolUnquoteTarget.got`, and `NonSymbolParam.got`
11582 // already carry. A regression that re-collapses `got` to
11583 // `String` (losing the rustc-enforced closed-set guarantee on
11584 // shape identity at the defmacro-syntax-gate's name-slot
11585 // rejection variant) fails-loudly here. The Display projection
11586 // through `SexpWitness::Display` writes only the `display`
11587 // field so the rendered `compile error in {head}: expected
11588 // name symbol, got <display>` clause is byte-for-byte
11589 // identical to the legacy `got: String` shape; downstream
11590 // substring-grep consumers (`tatara-check`, REPL) see no
11591 // drift.
11592 let err = LispError::DefmacroNonSymbolName {
11593 head: MacroDefHead::Defmacro,
11594 got: SexpWitness::new(SexpShape::Int, "5"),
11595 };
11596 match &err {
11597 LispError::DefmacroNonSymbolName { head, got } => {
11598 assert_eq!(*head, MacroDefHead::Defmacro);
11599 assert_eq!(got.shape, SexpShape::Int);
11600 assert_eq!(got.display, "5");
11601 }
11602 other => panic!("expected DefmacroNonSymbolName, got {other:?}"),
11603 }
11604 assert_eq!(
11605 format!("{err}"),
11606 "compile error in defmacro: expected name symbol, got 5"
11607 );
11608 }
11609
11610 #[test]
11611 fn defmacro_non_symbol_name_got_distinguishes_int_from_keyword_at_variant_slot() {
11612 // Pin the typed-shape bifurcation at the variant slot — `5`
11613 // (int atom at the defmacro name slot) and `:foo` (keyword
11614 // atom at the defmacro name slot) BOTH route to
11615 // `DefmacroNonSymbolName`, but the typed `got.shape` slot
11616 // distinguishes them structurally — `SexpShape::Int`
11617 // vs. `SexpShape::Keyword`. Sibling pin for the same
11618 // structural-shape-bifurcation property
11619 // `non_symbol_param_got_distinguishes_int_from_keyword_at_variant_slot`
11620 // pins on `NonSymbolParam` and
11621 // `non_symbol_unquote_target_got_distinguishes_int_from_keyword_at_variant_slot`
11622 // pins on `NonSymbolUnquoteTarget`. A regression that erases
11623 // the typed shape (e.g., reverts to `got: String`) would lose
11624 // this distinction — tooling that wants to surface "you wrote
11625 // an int `5` where a name symbol was expected" vs. "you wrote
11626 // a keyword `:foo` where a name symbol was expected (did you
11627 // mean `foo`?)" would have to substring-grep the `display`
11628 // field, brittle.
11629 let err_int = LispError::DefmacroNonSymbolName {
11630 head: MacroDefHead::Defmacro,
11631 got: SexpWitness::new(SexpShape::Int, "5"),
11632 };
11633 let err_kw = LispError::DefmacroNonSymbolName {
11634 head: MacroDefHead::Defmacro,
11635 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
11636 };
11637 let (int_shape, kw_shape) = (
11638 match &err_int {
11639 LispError::DefmacroNonSymbolName { got, .. } => got.shape,
11640 _ => unreachable!(),
11641 },
11642 match &err_kw {
11643 LispError::DefmacroNonSymbolName { got, .. } => got.shape,
11644 _ => unreachable!(),
11645 },
11646 );
11647 assert_ne!(
11648 int_shape, kw_shape,
11649 "Int and Keyword witnesses must remain structurally distinct at the variant slot",
11650 );
11651 assert_eq!(int_shape, SexpShape::Int);
11652 assert_eq!(kw_shape, SexpShape::Keyword);
11653 }
11654
11655 #[test]
11656 fn defmacro_non_symbol_name_and_param_gate_share_one_witness_primitive() {
11657 // Pin that ALL FOUR Sexp-display-source `got` slots in the
11658 // substrate (`SpliceOutsideList`, `NonSymbolUnquoteTarget`,
11659 // `NonSymbolParam`, `DefmacroNonSymbolName`) carry the SAME
11660 // typed `SexpWitness` primitive — the closed set of
11661 // "offending inner Sexp" identities is bound by ONE typed
11662 // primitive across FOUR rejection surfaces: the
11663 // template-gate's `,X/,@X` pair, the defmacro-syntax-gate's
11664 // `parse_params` walker, AND the defmacro-syntax-gate's
11665 // outer name-slot rejection. A regression that diverges the
11666 // slot type on any one variant (e.g., re-collapses
11667 // `DefmacroNonSymbolName.got` to `String` while leaving the
11668 // others typed) fails-loudly here because the assignment
11669 // round-trips the witness across all four slot types. Sibling
11670 // pin to `non_symbol_param_and_template_gate_share_one_witness_primitive`
11671 // — extending the typed-identity unification contract from
11672 // three slots to four.
11673 let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
11674 let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
11675 head: MacroDefHead::Defmacro,
11676 got: same_witness.clone(),
11677 };
11678 let non_symbol_param = LispError::NonSymbolParam {
11679 position: 0,
11680 got: same_witness.clone(),
11681 };
11682 let non_symbol_target = LispError::NonSymbolUnquoteTarget {
11683 prefix: UnquoteForm::Unquote,
11684 got: same_witness.clone(),
11685 };
11686 let splice_outside = LispError::SpliceOutsideList {
11687 got: same_witness.clone(),
11688 };
11689 match (
11690 &defmacro_non_symbol_name,
11691 &non_symbol_param,
11692 &non_symbol_target,
11693 &splice_outside,
11694 ) {
11695 (
11696 LispError::DefmacroNonSymbolName { got: a, .. },
11697 LispError::NonSymbolParam { got: b, .. },
11698 LispError::NonSymbolUnquoteTarget { got: c, .. },
11699 LispError::SpliceOutsideList { got: d },
11700 ) => {
11701 assert_eq!(a.shape, b.shape);
11702 assert_eq!(b.shape, c.shape);
11703 assert_eq!(c.shape, d.shape);
11704 assert_eq!(a.display, b.display);
11705 assert_eq!(b.display, c.display);
11706 assert_eq!(c.display, d.display);
11707 assert_eq!(*a, same_witness);
11708 assert_eq!(*b, same_witness);
11709 assert_eq!(*c, same_witness);
11710 assert_eq!(*d, same_witness);
11711 }
11712 _ => unreachable!(),
11713 }
11714 }
11715
11716 #[test]
11717 fn defmacro_non_list_params_got_carries_typed_witness_through_variant_slot() {
11718 // Pin the structural binding AND the Display projection on
11719 // `LispError::DefmacroNonListParams.got`. After this lift the
11720 // variant's typed slot is the joint `SexpWitness` identity —
11721 // the same primitive `SpliceOutsideList.got`,
11722 // `NonSymbolUnquoteTarget.got`, `NonSymbolParam.got`, and
11723 // `DefmacroNonSymbolName.got` already carry. A regression that
11724 // re-collapses `got` to `String` (losing the rustc-enforced
11725 // closed-set guarantee on shape identity at the defmacro-
11726 // syntax-gate's param-list-slot rejection variant) fails-loudly
11727 // here. The Display projection through `SexpWitness::Display`
11728 // writes only the `display` field so the rendered `compile
11729 // error in {head}: expected param list, got <display>` clause
11730 // is byte-for-byte identical to the legacy `got: String`
11731 // shape; downstream substring-grep consumers (`tatara-check`,
11732 // REPL) see no drift.
11733 let err = LispError::DefmacroNonListParams {
11734 head: MacroDefHead::Defmacro,
11735 got: SexpWitness::new(SexpShape::Symbol, "x"),
11736 };
11737 match &err {
11738 LispError::DefmacroNonListParams { head, got } => {
11739 assert_eq!(*head, MacroDefHead::Defmacro);
11740 assert_eq!(got.shape, SexpShape::Symbol);
11741 assert_eq!(got.display, "x");
11742 }
11743 other => panic!("expected DefmacroNonListParams, got {other:?}"),
11744 }
11745 assert_eq!(
11746 format!("{err}"),
11747 "compile error in defmacro: expected param list, got x"
11748 );
11749 }
11750
11751 #[test]
11752 fn defmacro_non_list_params_got_distinguishes_symbol_from_int_at_variant_slot() {
11753 // Pin the typed-shape bifurcation at the variant slot — `x`
11754 // (symbol atom at the defmacro param-list slot) and `5`
11755 // (int atom at the defmacro param-list slot) BOTH route to
11756 // `DefmacroNonListParams`, but the typed `got.shape` slot
11757 // distinguishes them structurally — `SexpShape::Symbol`
11758 // vs. `SexpShape::Int`. Sibling pin for the same
11759 // structural-shape-bifurcation property
11760 // `defmacro_non_symbol_name_got_distinguishes_int_from_keyword_at_variant_slot`
11761 // pins on `DefmacroNonSymbolName` and
11762 // `non_symbol_param_got_distinguishes_int_from_keyword_at_variant_slot`
11763 // pins on `NonSymbolParam`. A regression that erases the typed
11764 // shape (e.g., reverts to `got: String`) would lose this
11765 // distinction — tooling that wants to surface "you wrote a
11766 // symbol `x` where a param list was expected (did you mean
11767 // `(x)`?)" vs. "you wrote an int `5` where a param list was
11768 // expected" would have to substring-grep the `display` field,
11769 // brittle. The symbol-vs-int bifurcation matters at THIS slot
11770 // (not the int-vs-keyword bifurcation pinned at the
11771 // name-slot variant) because the most common authoring
11772 // mistake at the param-list slot is to forget the wrapping
11773 // parens — `(defmacro f x body)` instead of `(defmacro f (x)
11774 // body)` — so the symbol shape is the natural sibling to
11775 // distinguish from numeric typos.
11776 let err_sym = LispError::DefmacroNonListParams {
11777 head: MacroDefHead::Defmacro,
11778 got: SexpWitness::new(SexpShape::Symbol, "x"),
11779 };
11780 let err_int = LispError::DefmacroNonListParams {
11781 head: MacroDefHead::Defmacro,
11782 got: SexpWitness::new(SexpShape::Int, "5"),
11783 };
11784 let (sym_shape, int_shape) = (
11785 match &err_sym {
11786 LispError::DefmacroNonListParams { got, .. } => got.shape,
11787 _ => unreachable!(),
11788 },
11789 match &err_int {
11790 LispError::DefmacroNonListParams { got, .. } => got.shape,
11791 _ => unreachable!(),
11792 },
11793 );
11794 assert_ne!(
11795 sym_shape, int_shape,
11796 "Symbol and Int witnesses must remain structurally distinct at the variant slot",
11797 );
11798 assert_eq!(sym_shape, SexpShape::Symbol);
11799 assert_eq!(int_shape, SexpShape::Int);
11800 }
11801
11802 #[test]
11803 fn defmacro_non_list_params_and_name_gate_share_one_witness_primitive() {
11804 // Pin that ALL FIVE Sexp-display-source `got` slots in the
11805 // substrate (`SpliceOutsideList`, `NonSymbolUnquoteTarget`,
11806 // `NonSymbolParam`, `DefmacroNonSymbolName`,
11807 // `DefmacroNonListParams`) carry the SAME typed `SexpWitness`
11808 // primitive — the closed set of "offending inner Sexp"
11809 // identities is bound by ONE typed primitive across FIVE
11810 // rejection surfaces: the template-gate's `,X/,@X` pair, the
11811 // defmacro-syntax-gate's `parse_params` walker, AND BOTH of
11812 // the defmacro-syntax-gate's outer `macro_def_from` rejection
11813 // points (name-symbol AND param-list — the second and third of
11814 // the three `macro_def_from` gates). A regression that
11815 // diverges the slot type on any one variant (e.g., re-collapses
11816 // `DefmacroNonListParams.got` to `String` while leaving the
11817 // others typed) fails-loudly here because the assignment
11818 // round-trips the witness across all five slot types. Sibling
11819 // pin to `defmacro_non_symbol_name_and_param_gate_share_one_witness_primitive`
11820 // — extending the typed-identity unification contract from
11821 // four slots to five, completing structural unification of the
11822 // entire `macro_def_from` rejection chain at the
11823 // `Sexp::Display`-source `got` slot (every offending inner
11824 // Sexp value that `macro_def_from` rejects now carries the
11825 // SAME typed witness, regardless of which of the three gates
11826 // — arity, name-symbol, param-list — fired).
11827 let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
11828 let defmacro_non_list_params = LispError::DefmacroNonListParams {
11829 head: MacroDefHead::Defmacro,
11830 got: same_witness.clone(),
11831 };
11832 let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
11833 head: MacroDefHead::Defmacro,
11834 got: same_witness.clone(),
11835 };
11836 let non_symbol_param = LispError::NonSymbolParam {
11837 position: 0,
11838 got: same_witness.clone(),
11839 };
11840 let non_symbol_target = LispError::NonSymbolUnquoteTarget {
11841 prefix: UnquoteForm::Unquote,
11842 got: same_witness.clone(),
11843 };
11844 let splice_outside = LispError::SpliceOutsideList {
11845 got: same_witness.clone(),
11846 };
11847 match (
11848 &defmacro_non_list_params,
11849 &defmacro_non_symbol_name,
11850 &non_symbol_param,
11851 &non_symbol_target,
11852 &splice_outside,
11853 ) {
11854 (
11855 LispError::DefmacroNonListParams { got: a, .. },
11856 LispError::DefmacroNonSymbolName { got: b, .. },
11857 LispError::NonSymbolParam { got: c, .. },
11858 LispError::NonSymbolUnquoteTarget { got: d, .. },
11859 LispError::SpliceOutsideList { got: e },
11860 ) => {
11861 assert_eq!(a.shape, b.shape);
11862 assert_eq!(b.shape, c.shape);
11863 assert_eq!(c.shape, d.shape);
11864 assert_eq!(d.shape, e.shape);
11865 assert_eq!(a.display, b.display);
11866 assert_eq!(b.display, c.display);
11867 assert_eq!(c.display, d.display);
11868 assert_eq!(d.display, e.display);
11869 assert_eq!(*a, same_witness);
11870 assert_eq!(*b, same_witness);
11871 assert_eq!(*c, same_witness);
11872 assert_eq!(*d, same_witness);
11873 assert_eq!(*e, same_witness);
11874 }
11875 _ => unreachable!(),
11876 }
11877 }
11878
11879 #[test]
11880 fn rest_param_missing_name_got_carries_typed_witness_through_variant_slot() {
11881 // Pin the structural binding AND the Display projection on
11882 // `LispError::RestParamMissingName.got`. After this lift the
11883 // variant's typed slot is `Option<SexpWitness>` — the joint
11884 // `SexpWitness` identity (the same primitive `SpliceOutsideList.got`,
11885 // `NonSymbolUnquoteTarget.got`, `NonSymbolParam.got`,
11886 // `DefmacroNonSymbolName.got`, and `DefmacroNonListParams.got`
11887 // already carry) wrapped in `Option` because the post-`&rest`
11888 // follower slot bifurcates structurally between "missing
11889 // entirely" (`None`) and "present but malformed"
11890 // (`Some(SexpWitness)`). The typed witness lands on the `Some`
11891 // arm only. A regression that re-collapses to a free-form
11892 // `Option<String>` got slot (losing the rustc-enforced
11893 // closed-set guarantee on shape identity at the
11894 // `parse_params` walker's `&rest`-follower rejection variant)
11895 // fails-loudly here. The Display projection through
11896 // `SexpWitness::Display` writes only the `display` field so
11897 // the rendered `(rest marker at position {rest_position}, got
11898 // <display>)` clause is byte-for-byte identical to the
11899 // pre-lift `Option<String>` shape; downstream substring-grep
11900 // consumers (`tatara-check`, REPL) see no drift.
11901 let err = LispError::RestParamMissingName {
11902 rest_position: 1,
11903 got: Some(SexpWitness::new(SexpShape::Int, "5")),
11904 };
11905 match &err {
11906 LispError::RestParamMissingName { rest_position, got } => {
11907 assert_eq!(*rest_position, 1);
11908 let witness = got.as_ref().expect("got must be Some");
11909 assert_eq!(witness.shape, SexpShape::Int);
11910 assert_eq!(witness.display, "5");
11911 }
11912 other => panic!("expected RestParamMissingName, got {other:?}"),
11913 }
11914 assert_eq!(
11915 format!("{err}"),
11916 "compile error in defmacro params: &rest needs a name \
11917 (rest marker at position 1, got 5)"
11918 );
11919 }
11920
11921 #[test]
11922 fn rest_param_missing_name_got_distinguishes_int_from_keyword_at_variant_slot() {
11923 // Pin the typed-shape bifurcation at the variant slot — `5`
11924 // (int atom at the post-`&rest` follower slot) and `:foo`
11925 // (keyword atom at the post-`&rest` follower slot) BOTH route
11926 // to `RestParamMissingName` on the `Some` arm, but the typed
11927 // `got.shape` slot distinguishes them structurally —
11928 // `SexpShape::Int` vs. `SexpShape::Keyword`. Sibling pin for
11929 // the same structural-shape-bifurcation property
11930 // `non_symbol_param_got_distinguishes_int_from_keyword_at_variant_slot`
11931 // pins on `NonSymbolParam` and
11932 // `defmacro_non_symbol_name_got_distinguishes_int_from_keyword_at_variant_slot`
11933 // pins on `DefmacroNonSymbolName`. A regression that erases
11934 // the typed shape (e.g., reverts to `got: Option<String>`)
11935 // would lose this distinction — tooling that wants to surface
11936 // "you wrote an int `5` where a rest-name was expected" vs.
11937 // "you wrote a keyword `:foo` where a rest-name was expected
11938 // (did you mean `foo`?)" would have to substring-grep the
11939 // `display` field, brittle.
11940 let err_int = LispError::RestParamMissingName {
11941 rest_position: 0,
11942 got: Some(SexpWitness::new(SexpShape::Int, "5")),
11943 };
11944 let err_kw = LispError::RestParamMissingName {
11945 rest_position: 0,
11946 got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
11947 };
11948 let (int_shape, kw_shape) = (
11949 match &err_int {
11950 LispError::RestParamMissingName { got: Some(w), .. } => w.shape,
11951 _ => unreachable!(),
11952 },
11953 match &err_kw {
11954 LispError::RestParamMissingName { got: Some(w), .. } => w.shape,
11955 _ => unreachable!(),
11956 },
11957 );
11958 assert_ne!(
11959 int_shape, kw_shape,
11960 "Int and Keyword witnesses must remain structurally distinct at the variant slot",
11961 );
11962 assert_eq!(int_shape, SexpShape::Int);
11963 assert_eq!(kw_shape, SexpShape::Keyword);
11964 }
11965
11966 #[test]
11967 fn rest_param_missing_name_and_macro_def_gate_share_one_witness_primitive() {
11968 // Pin that ALL SIX Sexp-display-source `got` slots in the
11969 // substrate (`SpliceOutsideList`, `NonSymbolUnquoteTarget`,
11970 // `NonSymbolParam`, `DefmacroNonSymbolName`,
11971 // `DefmacroNonListParams`, `RestParamMissingName`) carry the
11972 // SAME typed `SexpWitness` primitive — the closed set of
11973 // "offending inner Sexp" identities is bound by ONE typed
11974 // primitive across SIX rejection surfaces: the template-gate's
11975 // `,X/,@X` pair, the defmacro-syntax-gate's `parse_params`
11976 // walker (BOTH non-symbol-param AND post-`&rest`-non-symbol-
11977 // follower rejection points), AND BOTH of the
11978 // defmacro-syntax-gate's outer `macro_def_from` rejection
11979 // points (name-symbol AND param-list). The `Option`-wrap on
11980 // `RestParamMissingName.got` is the bifurcation between
11981 // "missing entirely" and "present but malformed"; the typed
11982 // witness rides on the `Some` arm and is structurally identical
11983 // to the other five variants' got slots. A regression that
11984 // diverges the slot type on any one variant (e.g., re-collapses
11985 // `RestParamMissingName.got` to `Option<String>` while leaving
11986 // the others typed) fails-loudly here because the assignment
11987 // round-trips the witness across all six slot types. Sibling
11988 // pin to
11989 // `defmacro_non_list_params_and_name_gate_share_one_witness_primitive`
11990 // — extending the typed-identity unification contract from
11991 // five slots to six.
11992 let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
11993 let rest_param_missing_name = LispError::RestParamMissingName {
11994 rest_position: 0,
11995 got: Some(same_witness.clone()),
11996 };
11997 let defmacro_non_list_params = LispError::DefmacroNonListParams {
11998 head: MacroDefHead::Defmacro,
11999 got: same_witness.clone(),
12000 };
12001 let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
12002 head: MacroDefHead::Defmacro,
12003 got: same_witness.clone(),
12004 };
12005 let non_symbol_param = LispError::NonSymbolParam {
12006 position: 0,
12007 got: same_witness.clone(),
12008 };
12009 let non_symbol_target = LispError::NonSymbolUnquoteTarget {
12010 prefix: UnquoteForm::Unquote,
12011 got: same_witness.clone(),
12012 };
12013 let splice_outside = LispError::SpliceOutsideList {
12014 got: same_witness.clone(),
12015 };
12016 match (
12017 &rest_param_missing_name,
12018 &defmacro_non_list_params,
12019 &defmacro_non_symbol_name,
12020 &non_symbol_param,
12021 &non_symbol_target,
12022 &splice_outside,
12023 ) {
12024 (
12025 LispError::RestParamMissingName { got: Some(a), .. },
12026 LispError::DefmacroNonListParams { got: b, .. },
12027 LispError::DefmacroNonSymbolName { got: c, .. },
12028 LispError::NonSymbolParam { got: d, .. },
12029 LispError::NonSymbolUnquoteTarget { got: e, .. },
12030 LispError::SpliceOutsideList { got: f },
12031 ) => {
12032 assert_eq!(a.shape, b.shape);
12033 assert_eq!(b.shape, c.shape);
12034 assert_eq!(c.shape, d.shape);
12035 assert_eq!(d.shape, e.shape);
12036 assert_eq!(e.shape, f.shape);
12037 assert_eq!(a.display, b.display);
12038 assert_eq!(b.display, c.display);
12039 assert_eq!(c.display, d.display);
12040 assert_eq!(d.display, e.display);
12041 assert_eq!(e.display, f.display);
12042 assert_eq!(*a, same_witness);
12043 assert_eq!(*b, same_witness);
12044 assert_eq!(*c, same_witness);
12045 assert_eq!(*d, same_witness);
12046 assert_eq!(*e, same_witness);
12047 assert_eq!(*f, same_witness);
12048 }
12049 _ => unreachable!(),
12050 }
12051 }
12052
12053 #[test]
12054 fn missing_macro_arg_display_matches_legacy_compile_shape() {
12055 // The variant renders byte-for-byte the same string the legacy
12056 // `Compile { form: format!("call to {macro_name}"), message:
12057 // format!("missing required arg: {param}") }` shape produced, so
12058 // authoring tools (REPL, LSP, `tatara-check`) that substring-match
12059 // on the rendered diagnostic see no drift; tools that pattern-match
12060 // on the variant gain structural binding to `macro_name` and
12061 // `param`.
12062 let err = LispError::MissingMacroArg {
12063 macro_name: "wrap".into(),
12064 param: "b".into(),
12065 };
12066 assert_eq!(
12067 format!("{err}"),
12068 "compile error in call to wrap: missing required arg: b"
12069 );
12070 }
12071
12072 #[test]
12073 fn missing_macro_arg_display_carries_kebab_case_names_unchanged() {
12074 // Both `macro_name` and `param` round-trip through the variant's
12075 // Display unchanged. Pinning this contract means a regression that
12076 // camelCases or lowercases either side fails-loudly here.
12077 let err = LispError::MissingMacroArg {
12078 macro_name: "wrap-twice".into(),
12079 param: "notify-ref".into(),
12080 };
12081 assert_eq!(
12082 format!("{err}"),
12083 "compile error in call to wrap-twice: \
12084 missing required arg: notify-ref"
12085 );
12086 }
12087
12088 #[test]
12089 fn missing_macro_arg_display_preserves_legacy_substring_for_message_grep() {
12090 // Pin the legacy substring as a separate assertion so a regression
12091 // that drifts the wording (e.g., to "missing arg" or "no arg
12092 // provided") fails-loudly here even if the head clause changes
12093 // shape. The substring is what consumers downstream
12094 // (tatara-check, the REPL) substring-match on today.
12095 let err = LispError::MissingMacroArg {
12096 macro_name: "f".into(),
12097 param: "x".into(),
12098 };
12099 let msg = format!("{err}");
12100 assert!(
12101 msg.contains("missing required arg: x"),
12102 "expected legacy substring in message, got: {msg}"
12103 );
12104 assert!(
12105 msg.contains("call to f"),
12106 "expected call-to clause in message, got: {msg}"
12107 );
12108 }
12109
12110 #[test]
12111 fn non_symbol_param_display_carries_position_and_got() {
12112 // The variant renders both the failing position (0-based index
12113 // within the param list) AND the offending element via
12114 // `Sexp::Display` — both fields are first-class structural data,
12115 // not embedded substrings of `message`. A regression that drops
12116 // either field from the rendered diagnostic fails-loudly here.
12117 let err = LispError::NonSymbolParam {
12118 position: 1,
12119 got: SexpWitness::new(SexpShape::Int, "5"),
12120 };
12121 assert_eq!(
12122 format!("{err}"),
12123 "compile error in defmacro params: \
12124 expected symbol at position 1, got 5"
12125 );
12126 }
12127
12128 #[test]
12129 fn non_symbol_param_display_preserves_legacy_substring_for_message_grep() {
12130 // Pin the legacy substrings — `"defmacro params"` and `"expected
12131 // symbol"` — as separate assertions so a regression that drifts
12132 // either fragment fails-loudly here even if the appended position
12133 // / got clause changes shape. The substrings are what consumers
12134 // downstream substring-match on today; the prefix matches the
12135 // legacy `Compile { form: "defmacro params", message: "expected
12136 // symbol" }` byte-for-byte.
12137 let err = LispError::NonSymbolParam {
12138 position: 0,
12139 got: SexpWitness::new(SexpShape::List, "(nested)"),
12140 };
12141 let msg = format!("{err}");
12142 assert!(
12143 msg.contains("defmacro params"),
12144 "expected legacy form label in message, got: {msg}"
12145 );
12146 assert!(
12147 msg.contains("expected symbol"),
12148 "expected legacy substring in message, got: {msg}"
12149 );
12150 }
12151
12152 #[test]
12153 fn non_symbol_param_display_carries_keyword_got_unchanged() {
12154 // `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pin that
12155 // the variant's Display passes the keyword form through
12156 // unchanged so an LSP that surfaces "you wrote `:k` where a
12157 // symbol was expected" gains the literal value as data, no
12158 // re-parsing required.
12159 let err = LispError::NonSymbolParam {
12160 position: 2,
12161 got: SexpWitness::new(SexpShape::Keyword, ":k"),
12162 };
12163 assert_eq!(
12164 format!("{err}"),
12165 "compile error in defmacro params: \
12166 expected symbol at position 2, got :k"
12167 );
12168 }
12169
12170 #[test]
12171 fn rest_param_missing_name_display_with_got_renders_marker_position_and_got() {
12172 // `(defmacro f (a &rest 5) …)` — `&rest` at param-list position 1,
12173 // followed by `5` at position 2. The variant renders both the
12174 // marker's position AND the offending follower via `Sexp::Display`
12175 // — both are first-class structural data, not embedded substrings
12176 // of `message`. A regression that drops either field from the
12177 // rendered diagnostic fails-loudly here.
12178 let err = LispError::RestParamMissingName {
12179 rest_position: 1,
12180 got: Some(SexpWitness::new(SexpShape::Int, "5")),
12181 };
12182 assert_eq!(
12183 format!("{err}"),
12184 "compile error in defmacro params: &rest needs a name \
12185 (rest marker at position 1, got 5)"
12186 );
12187 }
12188
12189 #[test]
12190 fn rest_param_missing_name_display_without_got_renders_marker_position_only() {
12191 // `(defmacro f (a &rest))` — `&rest` at param-list position 1, no
12192 // follower at all. The variant renders the marker position and
12193 // names the absence structurally (`none provided`) instead of a
12194 // misleading empty / partial parenthetical. Sibling of how
12195 // `UnknownDomainKeyword` renders `(no domains registered)` for
12196 // the empty-registry case — the structural reason is named.
12197 let err = LispError::RestParamMissingName {
12198 rest_position: 1,
12199 got: None,
12200 };
12201 assert_eq!(
12202 format!("{err}"),
12203 "compile error in defmacro params: &rest needs a name \
12204 (rest marker at position 1, none provided)"
12205 );
12206 }
12207
12208 #[test]
12209 fn rest_param_missing_name_display_preserves_legacy_substring_for_message_grep() {
12210 // Pin the legacy substrings — `"defmacro params"` and `"&rest
12211 // needs a name"` — as separate assertions so a regression that
12212 // drifts either fragment fails-loudly here even if the appended
12213 // marker / got clause changes shape. The substrings are what
12214 // consumers downstream substring-match on today; the prefix
12215 // matches the legacy `Compile { form: "defmacro params",
12216 // message: "&rest needs a name" }` byte-for-byte.
12217 let err = LispError::RestParamMissingName {
12218 rest_position: 0,
12219 got: None,
12220 };
12221 let msg = format!("{err}");
12222 assert!(
12223 msg.contains("defmacro params"),
12224 "expected legacy form label in message, got: {msg}"
12225 );
12226 assert!(
12227 msg.contains("&rest needs a name"),
12228 "expected legacy substring in message, got: {msg}"
12229 );
12230 }
12231
12232 #[test]
12233 fn rest_param_missing_name_display_carries_keyword_got_unchanged() {
12234 // `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pin that the
12235 // variant's Display passes the keyword form through unchanged so
12236 // an LSP that surfaces "you wrote `:foo` where a rest-name was
12237 // expected" gains the literal keyword value as data, no
12238 // re-parsing required.
12239 let err = LispError::RestParamMissingName {
12240 rest_position: 2,
12241 got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
12242 };
12243 assert_eq!(
12244 format!("{err}"),
12245 "compile error in defmacro params: &rest needs a name \
12246 (rest marker at position 2, got :foo)"
12247 );
12248 }
12249
12250 #[test]
12251 fn defmacro_arity_display_with_defmacro_head_renders_arity_and_legacy_template() {
12252 // The variant renders both the head keyword AND the actual
12253 // arity — both fields are first-class structural data, not
12254 // embedded substrings of `message`. The example template
12255 // `(defmacro name (params) body)` stays the literal `defmacro`
12256 // (not the head) — matching the legacy form's behavior so
12257 // authoring tools that substring-grep on the rendered
12258 // diagnostic see no drift. A regression that drops either
12259 // field from the rendered diagnostic fails-loudly here.
12260 let err = LispError::DefmacroArity {
12261 head: MacroDefHead::Defmacro,
12262 arity: 1,
12263 };
12264 assert_eq!(
12265 format!("{err}"),
12266 "compile error in defmacro: (defmacro name (params) body) required \
12267 (got 1 elements, need 4)"
12268 );
12269 }
12270
12271 #[test]
12272 fn defmacro_arity_display_carries_defpoint_template_head_unchanged() {
12273 // Pin that the head slot accepts every literal the call-site
12274 // matches! gate admits — `defpoint-template` is the second
12275 // head keyword `macro_def_from` recognizes. The example
12276 // template literal stays `(defmacro name (params) body)` even
12277 // for non-defmacro heads (matching the legacy behavior); the
12278 // prefix `compile error in defpoint-template:` carries the
12279 // actual head so an LSP that wants to point at "your
12280 // defpoint-template form is missing elements" gains the head
12281 // as data.
12282 let err = LispError::DefmacroArity {
12283 head: MacroDefHead::DefpointTemplate,
12284 arity: 2,
12285 };
12286 assert_eq!(
12287 format!("{err}"),
12288 "compile error in defpoint-template: \
12289 (defmacro name (params) body) required \
12290 (got 2 elements, need 4)"
12291 );
12292 }
12293
12294 #[test]
12295 fn defmacro_arity_display_carries_defcheck_head_unchanged() {
12296 // Sibling for the `defcheck` head; rounds out the three-head-
12297 // keyword coverage so the variant renders identically across
12298 // `defmacro` / `defpoint-template` / `defcheck` (modulo the
12299 // head literal in the prefix).
12300 let err = LispError::DefmacroArity {
12301 head: MacroDefHead::Defcheck,
12302 arity: 3,
12303 };
12304 assert_eq!(
12305 format!("{err}"),
12306 "compile error in defcheck: (defmacro name (params) body) required \
12307 (got 3 elements, need 4)"
12308 );
12309 }
12310
12311 #[test]
12312 fn defmacro_arity_display_preserves_legacy_substring_for_message_grep() {
12313 // Pin the legacy substring — `"(defmacro name (params) body)
12314 // required"` — as a separate assertion so a regression that
12315 // drifts the example template fails-loudly here even if the
12316 // appended `got X elements, need 4` clause changes shape. The
12317 // substring is what consumers downstream substring-match on
12318 // today; the prefix matches the legacy `Compile { form:
12319 // head.to_string(), message: "(defmacro name (params) body)
12320 // required" }` byte-for-byte.
12321 let err = LispError::DefmacroArity {
12322 head: MacroDefHead::Defmacro,
12323 arity: 0,
12324 };
12325 let msg = format!("{err}");
12326 assert!(
12327 msg.contains("(defmacro name (params) body) required"),
12328 "expected legacy template substring in message, got: {msg}"
12329 );
12330 assert!(
12331 msg.contains("compile error in defmacro:"),
12332 "expected legacy form-label prefix in message, got: {msg}"
12333 );
12334 }
12335
12336 #[test]
12337 fn defmacro_non_symbol_name_display_with_int_got_renders_legacy_prefix_and_got() {
12338 // `(defmacro 5 () body)` — list[1] is `5`, not a symbol. The
12339 // variant renders both the head keyword AND the offending
12340 // `Sexp::Display` projection — both fields are first-class
12341 // structural data, not embedded substrings of `message`. The
12342 // prefix `compile error in defmacro: expected name symbol`
12343 // matches the legacy `Compile { form: "defmacro", message:
12344 // "expected name symbol" }` byte-for-byte; the structural
12345 // detail (`, got 5`) is appended. A regression that drops
12346 // either field from the rendered diagnostic fails-loudly here.
12347 let err = LispError::DefmacroNonSymbolName {
12348 head: MacroDefHead::Defmacro,
12349 got: SexpWitness::new(SexpShape::Int, "5"),
12350 };
12351 assert_eq!(
12352 format!("{err}"),
12353 "compile error in defmacro: expected name symbol, got 5"
12354 );
12355 }
12356
12357 #[test]
12358 fn defmacro_non_symbol_name_display_carries_defpoint_template_head_unchanged() {
12359 // Pin that the head slot accepts every literal the call-site
12360 // matches! gate admits — `defpoint-template` is the second
12361 // head keyword `macro_def_from` recognizes. The prefix
12362 // `compile error in defpoint-template:` carries the actual
12363 // head so an LSP that wants to point at "your defpoint-
12364 // template form's name slot isn't a symbol" gains the head
12365 // as data.
12366 let err = LispError::DefmacroNonSymbolName {
12367 head: MacroDefHead::DefpointTemplate,
12368 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
12369 };
12370 assert_eq!(
12371 format!("{err}"),
12372 "compile error in defpoint-template: expected name symbol, got :foo"
12373 );
12374 }
12375
12376 #[test]
12377 fn defmacro_non_symbol_name_display_carries_defcheck_head_unchanged() {
12378 // Sibling for the `defcheck` head; rounds out the three-head-
12379 // keyword coverage so the variant renders identically across
12380 // `defmacro` / `defpoint-template` / `defcheck` (modulo the
12381 // head literal in the prefix).
12382 let err = LispError::DefmacroNonSymbolName {
12383 head: MacroDefHead::Defcheck,
12384 got: SexpWitness::new(SexpShape::List, "(nested)"),
12385 };
12386 assert_eq!(
12387 format!("{err}"),
12388 "compile error in defcheck: expected name symbol, got (nested)"
12389 );
12390 }
12391
12392 #[test]
12393 fn defmacro_non_symbol_name_display_carries_string_got_unchanged() {
12394 // `Sexp::Display` for `Atom::String(s)` writes `"s"` (with
12395 // quotes); pin that the variant's Display passes the string
12396 // form through unchanged so an LSP that surfaces "you wrote
12397 // `\"name\"` where a name symbol was expected" gains the
12398 // literal value as data, no re-parsing required.
12399 let err = LispError::DefmacroNonSymbolName {
12400 head: MacroDefHead::Defmacro,
12401 got: SexpWitness::new(SexpShape::String, "\"name\""),
12402 };
12403 assert_eq!(
12404 format!("{err}"),
12405 "compile error in defmacro: expected name symbol, got \"name\""
12406 );
12407 }
12408
12409 #[test]
12410 fn defmacro_non_symbol_name_display_preserves_legacy_substring_for_message_grep() {
12411 // Pin the legacy substring — `"expected name symbol"` — as a
12412 // separate assertion so a regression that drifts the wording
12413 // (e.g., to "expected symbol" or "name must be a symbol")
12414 // fails-loudly here even if the appended `, got X` clause
12415 // changes shape. The substring is what consumers downstream
12416 // (tatara-check, the REPL) substring-match on today; the
12417 // prefix matches the legacy `Compile { form: head.to_string(),
12418 // message: "expected name symbol" }` byte-for-byte.
12419 let err = LispError::DefmacroNonSymbolName {
12420 head: MacroDefHead::Defmacro,
12421 got: SexpWitness::new(SexpShape::Int, "5"),
12422 };
12423 let msg = format!("{err}");
12424 assert!(
12425 msg.contains("expected name symbol"),
12426 "expected legacy substring in message, got: {msg}"
12427 );
12428 assert!(
12429 msg.contains("compile error in defmacro:"),
12430 "expected legacy form-label prefix in message, got: {msg}"
12431 );
12432 }
12433
12434 #[test]
12435 fn defmacro_non_list_params_display_with_symbol_got_renders_legacy_prefix_and_got() {
12436 // `(defmacro f x body)` — list[2] is the symbol `x`, not a
12437 // list. The variant renders both the head keyword AND the
12438 // offending `Sexp::Display` projection — both fields are
12439 // first-class structural data, not embedded substrings of
12440 // `message`. The prefix `compile error in defmacro: expected
12441 // param list` matches the legacy `Compile { form: "defmacro",
12442 // message: "expected param list" }` byte-for-byte; the
12443 // structural detail (`, got x`) is appended. A regression that
12444 // drops either field from the rendered diagnostic fails-loudly
12445 // here.
12446 let err = LispError::DefmacroNonListParams {
12447 head: MacroDefHead::Defmacro,
12448 got: SexpWitness::new(SexpShape::Symbol, "x"),
12449 };
12450 assert_eq!(
12451 format!("{err}"),
12452 "compile error in defmacro: expected param list, got x"
12453 );
12454 }
12455
12456 #[test]
12457 fn defmacro_non_list_params_display_carries_defpoint_template_head_unchanged() {
12458 // Pin that the head slot accepts every literal the call-site
12459 // matches! gate admits — `defpoint-template` is the second
12460 // head keyword `macro_def_from` recognizes. The prefix
12461 // `compile error in defpoint-template:` carries the actual
12462 // head so an LSP that wants to point at "your defpoint-
12463 // template form's param-list slot isn't a list" gains the
12464 // head as data.
12465 let err = LispError::DefmacroNonListParams {
12466 head: MacroDefHead::DefpointTemplate,
12467 got: SexpWitness::new(SexpShape::Int, "5"),
12468 };
12469 assert_eq!(
12470 format!("{err}"),
12471 "compile error in defpoint-template: expected param list, got 5"
12472 );
12473 }
12474
12475 #[test]
12476 fn defmacro_non_list_params_display_carries_defcheck_head_unchanged() {
12477 // Sibling for the `defcheck` head; rounds out the three-head-
12478 // keyword coverage so the variant renders identically across
12479 // `defmacro` / `defpoint-template` / `defcheck` (modulo the
12480 // head literal in the prefix).
12481 let err = LispError::DefmacroNonListParams {
12482 head: MacroDefHead::Defcheck,
12483 got: SexpWitness::new(SexpShape::Keyword, ":k"),
12484 };
12485 assert_eq!(
12486 format!("{err}"),
12487 "compile error in defcheck: expected param list, got :k"
12488 );
12489 }
12490
12491 #[test]
12492 fn defmacro_non_list_params_display_carries_string_got_unchanged() {
12493 // `Sexp::Display` for `Atom::String(s)` writes `"s"` (with
12494 // quotes); pin that the variant's Display passes the string
12495 // form through unchanged so an LSP that surfaces "you wrote
12496 // `\"params\"` where a param list was expected" gains the
12497 // literal value as data, no re-parsing required.
12498 let err = LispError::DefmacroNonListParams {
12499 head: MacroDefHead::Defmacro,
12500 got: SexpWitness::new(SexpShape::String, "\"params\""),
12501 };
12502 assert_eq!(
12503 format!("{err}"),
12504 "compile error in defmacro: expected param list, got \"params\""
12505 );
12506 }
12507
12508 #[test]
12509 fn defmacro_non_list_params_display_preserves_legacy_substring_for_message_grep() {
12510 // Pin the legacy substring — `"expected param list"` — as a
12511 // separate assertion so a regression that drifts the wording
12512 // (e.g., to "expected list" or "params must be a list")
12513 // fails-loudly here even if the appended `, got X` clause
12514 // changes shape. The substring is what consumers downstream
12515 // (tatara-check, the REPL) substring-match on today; the
12516 // prefix matches the legacy `Compile { form: head.to_string(),
12517 // message: "expected param list" }` byte-for-byte.
12518 let err = LispError::DefmacroNonListParams {
12519 head: MacroDefHead::Defmacro,
12520 got: SexpWitness::new(SexpShape::Symbol, "x"),
12521 };
12522 let msg = format!("{err}");
12523 assert!(
12524 msg.contains("expected param list"),
12525 "expected legacy substring in message, got: {msg}"
12526 );
12527 assert!(
12528 msg.contains("compile error in defmacro:"),
12529 "expected legacy form-label prefix in message, got: {msg}"
12530 );
12531 }
12532
12533 #[test]
12534 fn unbound_template_var_display_preserves_splice_prefix_in_hint() {
12535 // Splice marker rides through both the form and the suggestion; the
12536 // operator never has to translate `,` ↔ `,@` mentally.
12537 let err = LispError::UnboundTemplateVar {
12538 prefix: UnquoteForm::Splice,
12539 name: "rsts".into(),
12540 hint: Some("rest".into()),
12541 };
12542 assert_eq!(
12543 format!("{err}"),
12544 "compile error in ,@rsts: unbound; did you mean ,@rest?"
12545 );
12546 }
12547
12548 #[test]
12549 fn named_form_missing_name_display_renders_legacy_compile_shape() {
12550 // `(defpoint)` — list.len() == 1 (just the keyword, no NAME). The
12551 // variant renders byte-for-byte the same string the legacy
12552 // `Compile { form: "defpoint", message: "expected (defpoint NAME …)"
12553 // }` shape produced, so authoring tools (REPL, LSP, `tatara-check`)
12554 // that substring-match on the rendered diagnostic see no drift;
12555 // tools that pattern-match on the variant gain structural binding
12556 // to `keyword`.
12557 let err = LispError::NamedFormMissingName {
12558 keyword: "defpoint",
12559 };
12560 assert_eq!(
12561 format!("{err}"),
12562 "compile error in defpoint: expected (defpoint NAME …)"
12563 );
12564 }
12565
12566 #[test]
12567 fn named_form_missing_name_display_carries_defalertpolicy_keyword_unchanged() {
12568 // Pin path-uniformity across distinct keywords — every
12569 // `compile_named` caller funnels through `NamedFormMissingName`
12570 // with its own `T::KEYWORD`, so the variant's `keyword` slot
12571 // must round-trip every literal the derive macro accepts. A
12572 // regression that drops or rewrites the keyword (e.g.,
12573 // lowercasing, stripping the `def` prefix) fails-loudly here.
12574 let err = LispError::NamedFormMissingName {
12575 keyword: "defalertpolicy",
12576 };
12577 assert_eq!(
12578 format!("{err}"),
12579 "compile error in defalertpolicy: expected (defalertpolicy NAME …)"
12580 );
12581 }
12582
12583 #[test]
12584 fn named_form_missing_name_display_carries_kebab_case_keyword_unchanged() {
12585 // Kebab-cased domain keywords (`defprocess-spec`, `defalert-policy`)
12586 // round-trip through both occurrences of the keyword in the rendered
12587 // diagnostic — the prefix `compile error in {keyword}:` AND the
12588 // example template `(... NAME …)`. Pinning this contract means a
12589 // regression that camelCases either occurrence fails-loudly here.
12590 let err = LispError::NamedFormMissingName {
12591 keyword: "defprocess-spec",
12592 };
12593 assert_eq!(
12594 format!("{err}"),
12595 "compile error in defprocess-spec: expected (defprocess-spec NAME …)"
12596 );
12597 }
12598
12599 #[test]
12600 fn named_form_missing_name_display_preserves_unicode_ellipsis_byte_for_byte() {
12601 // The legacy `format!("expected ({} NAME …)", T::KEYWORD)` shape used
12602 // the Unicode horizontal-ellipsis character (U+2026), not the ASCII
12603 // three-dot sequence `...`. Pin the codepoint exactly so a regression
12604 // that replaces `…` with `...` fails-loudly here — consumers
12605 // downstream that substring-match on `"…"` would silently miss every
12606 // future occurrence otherwise.
12607 let err = LispError::NamedFormMissingName {
12608 keyword: "defmonitor",
12609 };
12610 let msg = format!("{err}");
12611 assert!(
12612 msg.contains('\u{2026}'),
12613 "expected Unicode horizontal-ellipsis (U+2026) in message, got: {msg}"
12614 );
12615 assert!(
12616 !msg.contains("..."),
12617 "expected no ASCII three-dot sequence in message, got: {msg}"
12618 );
12619 }
12620
12621 #[test]
12622 fn named_form_missing_name_display_preserves_legacy_substring_for_message_grep() {
12623 // Pin the legacy substring — `"expected ({keyword} NAME …)"` — as a
12624 // separate assertion so a regression that drifts the wording (e.g.,
12625 // to "expected NAME after keyword" or "missing positional name")
12626 // fails-loudly here. The substring is what consumers downstream
12627 // (`tatara-check`, the REPL) substring-match on today; the prefix
12628 // matches the legacy `Compile { form: T::KEYWORD.to_string(),
12629 // message: format!("expected ({} NAME …)", T::KEYWORD) }`
12630 // byte-for-byte.
12631 let err = LispError::NamedFormMissingName {
12632 keyword: "defmonitor",
12633 };
12634 let msg = format!("{err}");
12635 assert!(
12636 msg.contains("expected (defmonitor NAME"),
12637 "expected legacy form-label prefix in message, got: {msg}"
12638 );
12639 assert!(
12640 msg.contains("compile error in defmonitor:"),
12641 "expected legacy form-label prefix in message, got: {msg}"
12642 );
12643 }
12644
12645 #[test]
12646 fn named_form_non_symbol_name_display_renders_legacy_prefix_with_int_got() {
12647 // `(defpoint 5 …)` — list[1] is the int `5`. The variant renders
12648 // the legacy prefix `compile error in {keyword}: positional NAME
12649 // must be a symbol or string` byte-for-byte AND appends the
12650 // structural detail `(got int)` parenthetically — same posture as
12651 // how `MissingHeadSymbol` appends `(got 5)` and how
12652 // `RestParamMissingName` appends `(rest marker at position N,
12653 // got X)`. The `got` slot is the typed `SexpShape` enum sourced
12654 // from `sexp_shape`; pin the Int-arm rendering (via
12655 // `SexpShape::Display` to the canonical `"int"` literal) as the
12656 // canonical example.
12657 let err = LispError::NamedFormNonSymbolName {
12658 keyword: "defpoint",
12659 got: SexpShape::Int,
12660 };
12661 assert_eq!(
12662 format!("{err}"),
12663 "compile error in defpoint: positional NAME must be a symbol or string (got int)"
12664 );
12665 }
12666
12667 #[test]
12668 fn named_form_non_symbol_name_display_carries_keyword_got_unchanged() {
12669 // `(defpoint :foo …)` — list[1] is a `:foo` keyword. Pin
12670 // path-uniformity across distinct `SexpShape` variants: the
12671 // `got` slot is `SexpShape::Keyword` (the typed projection from
12672 // `sexp_shape(Sexp::Atom(Atom::Keyword(_)))`), threaded into
12673 // the parenthetical via `SexpShape::Display` -> "keyword".
12674 let err = LispError::NamedFormNonSymbolName {
12675 keyword: "defpoint",
12676 got: SexpShape::Keyword,
12677 };
12678 assert_eq!(
12679 format!("{err}"),
12680 "compile error in defpoint: positional NAME must be a symbol or string (got keyword)"
12681 );
12682 }
12683
12684 #[test]
12685 fn named_form_non_symbol_name_display_carries_list_got_unchanged() {
12686 // `(defpoint (nested) …)` — list[1] is a nested list. Pin the
12687 // `SexpShape::List` variant round-trips into the variant's
12688 // `got` slot unchanged so an LSP that surfaces "you wrote a
12689 // nested list where a NAME symbol was expected" gains the
12690 // structural shape as data, no re-parsing required.
12691 let err = LispError::NamedFormNonSymbolName {
12692 keyword: "defalertpolicy",
12693 got: SexpShape::List,
12694 };
12695 assert_eq!(
12696 format!("{err}"),
12697 "compile error in defalertpolicy: positional NAME must be a symbol or string (got list)"
12698 );
12699 }
12700
12701 #[test]
12702 fn named_form_non_symbol_name_display_carries_kebab_case_keyword_unchanged() {
12703 // Kebab-cased domain keywords (`defprocess-spec`, `defalert-policy`)
12704 // round-trip through the rendered diagnostic's `compile error in
12705 // {keyword}:` prefix unchanged. A regression that camelCases the
12706 // keyword fails-loudly here.
12707 let err = LispError::NamedFormNonSymbolName {
12708 keyword: "defprocess-spec",
12709 got: SexpShape::Int,
12710 };
12711 assert_eq!(
12712 format!("{err}"),
12713 "compile error in defprocess-spec: positional NAME must be a symbol or string (got int)"
12714 );
12715 }
12716
12717 #[test]
12718 fn named_form_non_symbol_name_display_preserves_legacy_substring_for_message_grep() {
12719 // Pin the legacy substring — `"positional NAME must be a symbol
12720 // or string"` — as a separate assertion so a regression that
12721 // drifts the wording (e.g., to "NAME must be a symbol", "NAME
12722 // slot wrong-typed") fails-loudly here even if the appended
12723 // parenthetical changes shape. The substring is what consumers
12724 // downstream (`tatara-check`, the REPL) substring-match on
12725 // today; the prefix matches the legacy `Compile { form:
12726 // T::KEYWORD.to_string(), message: "positional NAME must be a
12727 // symbol or string" }` byte-for-byte.
12728 let err = LispError::NamedFormNonSymbolName {
12729 keyword: "defmonitor",
12730 got: SexpShape::Int,
12731 };
12732 let msg = format!("{err}");
12733 assert!(
12734 msg.contains("positional NAME must be a symbol or string"),
12735 "expected legacy substring in message, got: {msg}"
12736 );
12737 assert!(
12738 msg.contains("compile error in defmonitor:"),
12739 "expected legacy form-label prefix in message, got: {msg}"
12740 );
12741 }
12742
12743 // ── RewriterNonList: typed-exit structural-variant lift ─────────
12744 //
12745 // `rewriter_non_list_err::<T>` (the typed-exit gate of
12746 // `rewrite_typed::<T>`'s round-trip) was promoted from the
12747 // `LispError::Compile`-shaped triple to the structural
12748 // `LispError::RewriterNonList { keyword, got }` variant. The
12749 // tests below pin: (a) Display matches the legacy `"compile error
12750 // in {keyword}: rewriter must return a list; got {got}"` shape
12751 // byte-for-byte across representative `got` renderings (int,
12752 // symbol, nil rendered as "()", quoted form); (b) the legacy
12753 // substring `"rewriter must return a list; got "` and the legacy
12754 // prefix `"compile error in {keyword}:"` both survive the lift
12755 // unchanged for substring-grep consumers; (c) kebab-case keywords
12756 // thread unchanged; (d) `position()` is `None` today (lands as
12757 // one branch when source spans arrive).
12758
12759 #[test]
12760 fn rewriter_non_list_display_renders_legacy_shape_with_int_got() {
12761 // `Sexp::int(42)` projects to `Sexp::Display = "42"`. The variant
12762 // renders the legacy `"compile error in {keyword}: rewriter must
12763 // return a list; got {got}"` shape byte-for-byte — same wording
12764 // as the pre-lift `Compile`-shaped triple.
12765 let err = LispError::RewriterNonList {
12766 keyword: "defmonitor",
12767 got: SexpWitness::new(SexpShape::Int, "42"),
12768 };
12769 assert_eq!(
12770 format!("{err}"),
12771 "compile error in defmonitor: rewriter must return a list; got 42"
12772 );
12773 }
12774
12775 #[test]
12776 fn rewriter_non_list_display_carries_symbol_got_unchanged() {
12777 // `Sexp::symbol("not-a-list")` projects to `"not-a-list"`. Pin
12778 // path-uniformity across distinct `Sexp::Display` outputs: the
12779 // typed `got` slot threads the value-rendering into the
12780 // diagnostic unchanged via `SexpWitness::Display` (which writes
12781 // only the `display` field).
12782 let err = LispError::RewriterNonList {
12783 keyword: "defmonitor",
12784 got: SexpWitness::new(SexpShape::Symbol, "not-a-list"),
12785 };
12786 assert_eq!(
12787 format!("{err}"),
12788 "compile error in defmonitor: rewriter must return a list; got not-a-list"
12789 );
12790 }
12791
12792 #[test]
12793 fn rewriter_non_list_display_carries_nil_got_as_paren_paren() {
12794 // `Sexp::Nil` projects to `"()"` per the `Sexp::Display`
12795 // contract — NOT `"nil"`. Pin the contract so a regression
12796 // that drifts `Sexp::Nil`'s Display fails-loudly here even
12797 // before reaching the rewriter end-to-end test.
12798 let err = LispError::RewriterNonList {
12799 keyword: "defmonitor",
12800 got: SexpWitness::new(SexpShape::Nil, "()"),
12801 };
12802 assert_eq!(
12803 format!("{err}"),
12804 "compile error in defmonitor: rewriter must return a list; got ()"
12805 );
12806 }
12807
12808 #[test]
12809 fn rewriter_non_list_display_carries_kebab_case_keyword_unchanged() {
12810 // Kebab-cased domain keywords (`defprocess-spec`,
12811 // `defalert-policy`) round-trip through the rendered
12812 // diagnostic's `compile error in {keyword}:` prefix unchanged.
12813 // A regression that camelCases the keyword fails-loudly here.
12814 let err = LispError::RewriterNonList {
12815 keyword: "defprocess-spec",
12816 got: SexpWitness::new(SexpShape::Int, "7"),
12817 };
12818 assert_eq!(
12819 format!("{err}"),
12820 "compile error in defprocess-spec: rewriter must return a list; got 7"
12821 );
12822 }
12823
12824 #[test]
12825 fn rewriter_non_list_display_preserves_legacy_substring_for_message_grep() {
12826 // Pin the legacy substring — `"rewriter must return a list;
12827 // got "` — as a separate assertion so a regression that drifts
12828 // the wording (e.g., to "rewriter returned non-list", "expected
12829 // list output") fails-loudly here. The substring is what
12830 // consumers downstream (`tatara-check`, the REPL) substring-
12831 // match on; the prefix matches the legacy `Compile { form:
12832 // T::KEYWORD.to_string(), message: format!("rewriter must
12833 // return a list; got {other}") }` byte-for-byte.
12834 let err = LispError::RewriterNonList {
12835 keyword: "defmonitor",
12836 got: SexpWitness::new(SexpShape::Int, "42"),
12837 };
12838 let msg = format!("{err}");
12839 assert!(
12840 msg.contains("rewriter must return a list; got "),
12841 "expected legacy substring in message, got: {msg}"
12842 );
12843 assert!(
12844 msg.contains("compile error in defmonitor:"),
12845 "expected legacy form-label prefix in message, got: {msg}"
12846 );
12847 }
12848
12849 #[test]
12850 fn rewriter_non_list_position_is_none_today() {
12851 // Until `Sexp` carries source positions, the variant's
12852 // `position()` returns `None`. Pin the contract: a future run
12853 // that adds `pos: Option<usize>` lands inside `SexpWitness` in
12854 // ONE place and `rewrite_typed`'s rewriter-output rejection
12855 // picks up the span automatically because it routes through
12856 // one helper (`rewriter_non_list_err`).
12857 let err = LispError::RewriterNonList {
12858 keyword: "defmonitor",
12859 got: SexpWitness::new(SexpShape::Int, "42"),
12860 };
12861 assert_eq!(err.position(), None);
12862 }
12863
12864 #[test]
12865 fn rewriter_non_list_got_carries_typed_witness_through_variant_slot() {
12866 // Pin the structural binding on `LispError::RewriterNonList.got`
12867 // — a regression that re-introduces a `String`-shaped got slot
12868 // (collapsing the typed witness back into a free-form literal at
12869 // the typed-EXIT boundary) fails-loudly here. After this lift the
12870 // variant's typed slot is the joint `SexpWitness` identity — the
12871 // SAME primitive the SEVEN typed-ENTRY-side `got` slots already
12872 // carry (`SpliceOutsideList`, `NonSymbolUnquoteTarget`,
12873 // `NonSymbolParam`, `DefmacroNonSymbolName`,
12874 // `DefmacroNonListParams`, `RestParamMissingName`,
12875 // `MissingHeadSymbol`). This is the EIGHTH consumer and the FIRST
12876 // on the typed-EXIT boundary; the typed-identity unification
12877 // contract is now closed across BOTH boundaries of the typed-IR
12878 // algebra. The Display projection through `SexpWitness::Display`
12879 // writes only the `display` field so the rendered `got {display}`
12880 // suffix is byte-for-byte identical to the legacy `got: String`
12881 // shape.
12882 let err = LispError::RewriterNonList {
12883 keyword: "defmonitor",
12884 got: SexpWitness::new(SexpShape::Int, "42"),
12885 };
12886 match &err {
12887 LispError::RewriterNonList { keyword, got } => {
12888 assert_eq!(*keyword, "defmonitor");
12889 assert_eq!(got.shape, SexpShape::Int);
12890 assert_eq!(got.display, "42");
12891 }
12892 other => panic!("expected RewriterNonList, got {other:?}"),
12893 }
12894 assert_eq!(
12895 format!("{err}"),
12896 "compile error in defmonitor: rewriter must return a list; got 42"
12897 );
12898 }
12899
12900 #[test]
12901 fn rewriter_non_list_got_distinguishes_int_from_keyword_at_variant_slot() {
12902 // Pin the typed-shape bifurcation at the variant slot's `got`
12903 // slot — `42` (int) and `:foo` (keyword) BOTH route to
12904 // `RewriterNonList` (the rewriter returned a non-list typed-exit
12905 // rejection), but the typed `got.shape` slot distinguishes them
12906 // structurally as `SexpShape::Int` vs. `SexpShape::Keyword`.
12907 // Sibling pin for the same structural-shape-bifurcation property
12908 // `splice_outside_list_got_distinguishes_symbol_from_list_at_variant_slot`
12909 // pins on the typed-ENTRY-side `SpliceOutsideList` variant — the
12910 // same posture applied to the typed-EXIT-side rejection variant.
12911 // A regression that erases the typed shape (e.g., reverts to
12912 // `got: String`) would lose this distinction — tooling that
12913 // wants to surface "your rewriter returned the int `42` where a
12914 // kwargs list was expected" vs. "your rewriter returned the
12915 // keyword `:foo` where a kwargs list was expected" would have to
12916 // substring-grep the `display` field, brittle.
12917 let err_int = LispError::RewriterNonList {
12918 keyword: "defmonitor",
12919 got: SexpWitness::new(SexpShape::Int, "42"),
12920 };
12921 let err_kw = LispError::RewriterNonList {
12922 keyword: "defmonitor",
12923 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
12924 };
12925 let (int_shape, kw_shape) = (
12926 match &err_int {
12927 LispError::RewriterNonList { got, .. } => got.shape,
12928 _ => unreachable!(),
12929 },
12930 match &err_kw {
12931 LispError::RewriterNonList { got, .. } => got.shape,
12932 _ => unreachable!(),
12933 },
12934 );
12935 assert_ne!(
12936 int_shape, kw_shape,
12937 "Int and Keyword witnesses must remain structurally distinct at the variant slot",
12938 );
12939 assert_eq!(int_shape, SexpShape::Int);
12940 assert_eq!(kw_shape, SexpShape::Keyword);
12941 }
12942
12943 #[test]
12944 fn rewriter_non_list_and_typed_entry_gates_share_one_witness_primitive() {
12945 // Pin that ALL EIGHT Sexp-display-source `got` slots in the
12946 // substrate carry the SAME typed `SexpWitness` primitive — the
12947 // closed set of "offending inner Sexp" identities is bound by
12948 // ONE typed primitive across EIGHT rejection surfaces spanning
12949 // BOTH boundaries of the typed-IR algebra: the typed-ENTRY side
12950 // (seven slots — the template-gate's `,X/,@X` pair, the
12951 // defmacro-syntax-gate's `parse_params` walker (BOTH
12952 // non-symbol-param AND post-`&rest`-non-symbol-follower rejection
12953 // points), BOTH of the defmacro-syntax-gate's outer
12954 // `macro_def_from` rejection points (name-symbol AND
12955 // param-list), AND the outer `compile_from_sexp` typed-entry
12956 // gate's non-symbol-head rejection point) AND the typed-EXIT
12957 // side (ONE slot — `rewrite_typed`'s `Sexp::List`-contract gate
12958 // for the rewriter's output). With this lift EVERY
12959 // `Sexp::Display`-source `got` slot in the substrate is
12960 // structurally unified end-to-end across BOTH typed boundaries.
12961 // The `Option`-wrap on `MissingHeadSymbol.got` and
12962 // `RestParamMissingName.got` is the bifurcation between "missing
12963 // entirely" and "present but malformed"; the typed witness
12964 // rides on the `Some` arm and is structurally identical to the
12965 // other six variants' got slots. A regression that diverges the
12966 // slot type on any one variant (e.g., re-collapses
12967 // `RewriterNonList.got` to `String` while leaving the others
12968 // typed) fails-loudly here because the assignment round-trips
12969 // the witness across all eight slot types. Sibling pin to
12970 // `missing_head_symbol_and_rest_param_gate_share_one_witness_primitive`
12971 // — extending the typed-identity unification contract from
12972 // seven slots (typed-ENTRY only) to eight slots (typed-ENTRY +
12973 // typed-EXIT), CLOSING the contract across BOTH boundaries of
12974 // the typed-IR algebra (THEORY.md §II.1 invariant 1 +
12975 // invariant 3).
12976 let same_witness = SexpWitness::new(SexpShape::Int, "42");
12977 let rewriter_non_list = LispError::RewriterNonList {
12978 keyword: "defmonitor",
12979 got: same_witness.clone(),
12980 };
12981 let missing_head = LispError::MissingHeadSymbol {
12982 keyword: "defmonitor",
12983 got: Some(same_witness.clone()),
12984 };
12985 let rest_param_missing_name = LispError::RestParamMissingName {
12986 rest_position: 0,
12987 got: Some(same_witness.clone()),
12988 };
12989 let defmacro_non_list_params = LispError::DefmacroNonListParams {
12990 head: MacroDefHead::Defmacro,
12991 got: same_witness.clone(),
12992 };
12993 let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
12994 head: MacroDefHead::Defmacro,
12995 got: same_witness.clone(),
12996 };
12997 let non_symbol_param = LispError::NonSymbolParam {
12998 position: 0,
12999 got: same_witness.clone(),
13000 };
13001 let non_symbol_target = LispError::NonSymbolUnquoteTarget {
13002 prefix: UnquoteForm::Unquote,
13003 got: same_witness.clone(),
13004 };
13005 let splice_outside = LispError::SpliceOutsideList {
13006 got: same_witness.clone(),
13007 };
13008 match (
13009 &rewriter_non_list,
13010 &missing_head,
13011 &rest_param_missing_name,
13012 &defmacro_non_list_params,
13013 &defmacro_non_symbol_name,
13014 &non_symbol_param,
13015 &non_symbol_target,
13016 &splice_outside,
13017 ) {
13018 (
13019 LispError::RewriterNonList { got: a, .. },
13020 LispError::MissingHeadSymbol { got: Some(b), .. },
13021 LispError::RestParamMissingName { got: Some(c), .. },
13022 LispError::DefmacroNonListParams { got: d, .. },
13023 LispError::DefmacroNonSymbolName { got: e, .. },
13024 LispError::NonSymbolParam { got: f, .. },
13025 LispError::NonSymbolUnquoteTarget { got: g, .. },
13026 LispError::SpliceOutsideList { got: h },
13027 ) => {
13028 assert_eq!(a.shape, b.shape);
13029 assert_eq!(b.shape, c.shape);
13030 assert_eq!(c.shape, d.shape);
13031 assert_eq!(d.shape, e.shape);
13032 assert_eq!(e.shape, f.shape);
13033 assert_eq!(f.shape, g.shape);
13034 assert_eq!(g.shape, h.shape);
13035 assert_eq!(a.display, b.display);
13036 assert_eq!(b.display, c.display);
13037 assert_eq!(c.display, d.display);
13038 assert_eq!(d.display, e.display);
13039 assert_eq!(e.display, f.display);
13040 assert_eq!(f.display, g.display);
13041 assert_eq!(g.display, h.display);
13042 assert_eq!(*a, same_witness);
13043 assert_eq!(*b, same_witness);
13044 assert_eq!(*c, same_witness);
13045 assert_eq!(*d, same_witness);
13046 assert_eq!(*e, same_witness);
13047 assert_eq!(*f, same_witness);
13048 assert_eq!(*g, same_witness);
13049 assert_eq!(*h, same_witness);
13050 }
13051 _ => unreachable!(),
13052 }
13053 }
13054
13055 // ── DomainSerialize: typed-exit `to_value` structural-variant lift ──
13056 //
13057 // `serialize_to_json_err::<T>` (the `to_value`-side gate shared
13058 // between `register::<T>`'s registry-dispatch closure and
13059 // `rewrite_typed::<T>`'s round-trip prelude) was promoted from the
13060 // `LispError::Compile`-shaped triple to the structural
13061 // `LispError::DomainSerialize { keyword, message }` variant. The
13062 // tests below pin: (a) Display matches the legacy `"compile error
13063 // in {keyword}: serialize: {message}"` shape byte-for-byte across
13064 // representative `message` renderings (serde_json's stock
13065 // diagnostic, hand-crafted message); (b) the legacy substring
13066 // `"serialize: "` and the legacy prefix `"compile error in
13067 // {keyword}:"` both survive the lift unchanged for substring-grep
13068 // consumers; (c) kebab-case keywords thread through unchanged.
13069 // The `position()` floor is pinned in the main
13070 // `position_is_none_for_non_positional_variants` block above.
13071
13072 #[test]
13073 fn domain_serialize_display_renders_legacy_shape_with_short_message() {
13074 // Hand-crafted `message` slot — the variant renders the legacy
13075 // `"compile error in {keyword}: serialize: {message}"` shape
13076 // byte-for-byte. Same wording as the pre-lift `Compile`-shaped
13077 // triple in `serialize_to_json_err`.
13078 let err = LispError::DomainSerialize {
13079 keyword: "defmonitor",
13080 message: "key must be a string".into(),
13081 };
13082 assert_eq!(
13083 format!("{err}"),
13084 "compile error in defmonitor: serialize: key must be a string"
13085 );
13086 }
13087
13088 #[test]
13089 fn domain_serialize_display_carries_serde_json_diagnostic_unchanged() {
13090 // Use a real `serde_json::Error` so the test exercises a
13091 // representative `{e}` shape (`"expected value at line L column
13092 // C"`) and pins that the variant's Display rendering threads
13093 // the underlying diagnostic through unchanged.
13094 let raw = serde_json::from_str::<i32>("not-a-number")
13095 .expect_err("parse must fail")
13096 .to_string();
13097 let err = LispError::DomainSerialize {
13098 keyword: "defmonitor",
13099 message: raw.clone(),
13100 };
13101 assert_eq!(
13102 format!("{err}"),
13103 format!("compile error in defmonitor: serialize: {raw}")
13104 );
13105 }
13106
13107 #[test]
13108 fn domain_serialize_display_carries_kebab_case_keyword_unchanged() {
13109 // Kebab-cased domain keywords (`defprocess-spec`,
13110 // `defalert-policy`) round-trip through the rendered
13111 // diagnostic's `compile error in {keyword}:` prefix unchanged.
13112 // A regression that camelCases the keyword fails-loudly here.
13113 let err = LispError::DomainSerialize {
13114 keyword: "defalert-policy",
13115 message: "expected struct".into(),
13116 };
13117 assert_eq!(
13118 format!("{err}"),
13119 "compile error in defalert-policy: serialize: expected struct"
13120 );
13121 }
13122
13123 #[test]
13124 fn domain_serialize_display_preserves_legacy_substring_for_message_grep() {
13125 // Pin the legacy substring — `"serialize: "` — as a separate
13126 // assertion so a regression that drifts the wording (e.g., to
13127 // "to_json failed", "json encode error") fails-loudly here.
13128 // The substring is what consumers downstream (`tatara-check`,
13129 // the REPL) substring-match on; the prefix matches the legacy
13130 // `Compile { form: T::KEYWORD.to_string(), message:
13131 // format!("serialize: {e}") }` byte-for-byte.
13132 let err = LispError::DomainSerialize {
13133 keyword: "defmonitor",
13134 message: "boom".into(),
13135 };
13136 let msg = format!("{err}");
13137 assert!(
13138 msg.contains("serialize: "),
13139 "expected legacy substring in message, got: {msg}"
13140 );
13141 assert!(
13142 msg.contains("compile error in defmonitor:"),
13143 "expected legacy form-label prefix in message, got: {msg}"
13144 );
13145 }
13146
13147 #[test]
13148 fn domain_serialize_display_empty_message_renders_bare_prefix() {
13149 // Edge case: an empty `message` slot renders as `"compile
13150 // error in {keyword}: serialize: "` — pin the trailing space
13151 // after the `serialize:` marker stays put. A regression that
13152 // strips trailing whitespace (e.g., via `.trim_end()`) or
13153 // drops the marker entirely fails-loudly here.
13154 let err = LispError::DomainSerialize {
13155 keyword: "defmonitor",
13156 message: String::new(),
13157 };
13158 assert_eq!(format!("{err}"), "compile error in defmonitor: serialize: ");
13159 }
13160
13161 // ── KwargDeserialize: typed-entry `from_value` structural-variant lift ──
13162 //
13163 // `deserialize_err(key, err)` and `deserialize_item_err(key, idx,
13164 // err)` (the `from_value`-side gate shared between
13165 // `extract_via_serde`, `extract_optional_via_serde`, and
13166 // `extract_vec_via_serde`) were promoted from the
13167 // `LispError::Compile`-shaped triple to the structural
13168 // `LispError::KwargDeserialize { path: KwargPath, message }`
13169 // variant — the `(key: String, idx: Option<usize>)` bifurcation
13170 // collapsed into the typed `KwargPath` enum's `Named` vs. `Item`
13171 // variant identity. The tests below pin: (a) Display matches the
13172 // legacy `"compile error in :{key}: deserialize: {message}"` shape
13173 // byte-for-byte for the scalar path (`path: KwargPath::Named`); (b)
13174 // Display matches the indexed `"compile error in :{key}[{idx}]:
13175 // deserialize: {message}"` shape byte-for-byte for the per-item
13176 // path (`path: KwargPath::Item`); (c) the legacy substring
13177 // `"deserialize: "` and the legacy prefix `"compile error in :"`
13178 // both survive the lift unchanged for substring-grep consumers;
13179 // (d) kebab-case keys thread through unchanged; (e) the typed
13180 // `path` slot carries `KwargPath` data DIRECTLY (not as a projection
13181 // through a helper), structurally bound via pattern-match on the
13182 // typed enum's variant identity. The `position()` floor is pinned
13183 // in the main `position_is_none_for_non_positional_variants` block
13184 // above.
13185
13186 #[test]
13187 fn kwarg_deserialize_display_scalar_path_renders_legacy_shape() {
13188 // `path: KwargPath::Named(_)` — scalar / `Option<T>` path. The
13189 // variant renders the legacy `"compile error in :{key}:
13190 // deserialize: {message}"` shape byte-for-byte. Same wording as
13191 // the pre-lift `Compile`-shaped triple in `deserialize_err`.
13192 let err = LispError::KwargDeserialize {
13193 path: KwargPath::named("level"),
13194 message: "unknown variant `NotASeverity`".into(),
13195 };
13196 assert_eq!(
13197 format!("{err}"),
13198 "compile error in :level: deserialize: unknown variant `NotASeverity`"
13199 );
13200 }
13201
13202 #[test]
13203 fn kwarg_deserialize_display_per_item_path_renders_indexed_shape() {
13204 // `path: KwargPath::Item { .. }` — per-item path. The variant
13205 // renders the legacy `"compile error in :{key}[{idx}]:
13206 // deserialize: {message}"` shape byte-for-byte. Same wording as
13207 // the pre-lift `Compile`-shaped triple in `deserialize_item_err`.
13208 let err = LispError::KwargDeserialize {
13209 path: KwargPath::item("steps", 1),
13210 message: "invalid type: integer `7`, expected a string".into(),
13211 };
13212 assert_eq!(
13213 format!("{err}"),
13214 "compile error in :steps[1]: deserialize: invalid type: integer `7`, expected a string"
13215 );
13216 }
13217
13218 #[test]
13219 fn kwarg_deserialize_display_carries_serde_json_diagnostic_unchanged() {
13220 // Use a real `serde_json::Error` so the test exercises a
13221 // representative `{e}` shape (`"expected value at line L
13222 // column C"`) and pins that the variant's Display rendering
13223 // threads the underlying diagnostic through unchanged.
13224 let raw = serde_json::from_str::<i32>("not-a-number")
13225 .expect_err("parse must fail")
13226 .to_string();
13227 let err = LispError::KwargDeserialize {
13228 path: KwargPath::named("count"),
13229 message: raw.clone(),
13230 };
13231 assert_eq!(
13232 format!("{err}"),
13233 format!("compile error in :count: deserialize: {raw}")
13234 );
13235 }
13236
13237 #[test]
13238 fn kwarg_deserialize_display_carries_kebab_case_key_unchanged() {
13239 // Kebab-cased kwarg names (`notify-ref`, `wait-minutes`,
13240 // `window-seconds`) round-trip through the rendered diagnostic's
13241 // `compile error in :{key}:` prefix unchanged. A regression that
13242 // camelCases the key fails-loudly here.
13243 let err = LispError::KwargDeserialize {
13244 path: KwargPath::named("notify-ref"),
13245 message: "missing field `notify-ref`".into(),
13246 };
13247 assert_eq!(
13248 format!("{err}"),
13249 "compile error in :notify-ref: deserialize: missing field `notify-ref`"
13250 );
13251 }
13252
13253 #[test]
13254 fn kwarg_deserialize_display_carries_kebab_case_key_with_index_unchanged() {
13255 // Kebab-cased keys round-trip through the indexed path too —
13256 // `:notify-refs[2]` not `:notifyRefs[2]`. Pinning both paths
13257 // means a regression in either site (scalar or per-item) fails-
13258 // loudly here.
13259 let err = LispError::KwargDeserialize {
13260 path: KwargPath::item("wait-minutes", 2),
13261 message: "expected u64".into(),
13262 };
13263 assert_eq!(
13264 format!("{err}"),
13265 "compile error in :wait-minutes[2]: deserialize: expected u64"
13266 );
13267 }
13268
13269 #[test]
13270 fn kwarg_deserialize_display_preserves_legacy_substring_for_message_grep() {
13271 // Pin the legacy substring — `"deserialize: "` — as a separate
13272 // assertion so a regression that drifts the wording (e.g., to
13273 // "from_json failed", "json decode error") fails-loudly here.
13274 // The substring is what consumers downstream (`tatara-check`,
13275 // the REPL) substring-match on; the prefix matches the legacy
13276 // `Compile { form: kwarg_form(key), message: format!("deserialize:
13277 // {e}") }` byte-for-byte. Both sub-modes (`KwargPath::Named`
13278 // AND `KwargPath::Item`) preserve the substring.
13279 let scalar = LispError::KwargDeserialize {
13280 path: KwargPath::named("level"),
13281 message: "boom".into(),
13282 };
13283 let scalar_msg = format!("{scalar}");
13284 assert!(
13285 scalar_msg.contains("deserialize: "),
13286 "expected legacy substring in scalar message, got: {scalar_msg}"
13287 );
13288 assert!(
13289 scalar_msg.contains("compile error in :level:"),
13290 "expected legacy form-label prefix in scalar message, got: {scalar_msg}"
13291 );
13292
13293 let item = LispError::KwargDeserialize {
13294 path: KwargPath::item("steps", 3),
13295 message: "boom".into(),
13296 };
13297 let item_msg = format!("{item}");
13298 assert!(
13299 item_msg.contains("deserialize: "),
13300 "expected legacy substring in item message, got: {item_msg}"
13301 );
13302 assert!(
13303 item_msg.contains("compile error in :steps[3]:"),
13304 "expected indexed form-label prefix in item message, got: {item_msg}"
13305 );
13306 }
13307
13308 #[test]
13309 fn kwarg_deserialize_display_zero_index_is_first_class() {
13310 // Edge case: `path: KwargPath::Item { idx: 0, .. }` must render
13311 // as `:steps[0]`, not `:steps` (which would collide with the
13312 // scalar path's `KwargPath::Named` rendering). Pin that the
13313 // bifurcation is by `KwargPath` variant identity, not by
13314 // `idx > 0`.
13315 let err = LispError::KwargDeserialize {
13316 path: KwargPath::item("steps", 0),
13317 message: "bad".into(),
13318 };
13319 assert_eq!(
13320 format!("{err}"),
13321 "compile error in :steps[0]: deserialize: bad"
13322 );
13323 }
13324
13325 #[test]
13326 fn kwarg_deserialize_path_named_threads_typed_kwarg_path_through_variant_slot() {
13327 // Structural pin: the scalar / `Option<T>` path's
13328 // `LispError::KwargDeserialize` carries the typed
13329 // `KwargPath::Named(key)` value DIRECTLY in its `path` slot,
13330 // not a `(key: String, idx: None)` pair. Authoring tools (REPL,
13331 // LSP, `tatara-check`) bind on the typed enum's variant
13332 // identity (`KwargPath::Named(_)`) rather than substring-
13333 // matching the rendered `form:` prefix, parallel to how
13334 // `TypeMismatch.form` is bound. A regression that re-bifurcates
13335 // the variant into a `(key, idx: Option<usize>)` pair fails the
13336 // structural assertion here (the slot would no longer be a
13337 // typed `KwargPath`).
13338 let err = LispError::KwargDeserialize {
13339 path: KwargPath::named("level"),
13340 message: "boom".into(),
13341 };
13342 let LispError::KwargDeserialize {
13343 ref path,
13344 ref message,
13345 } = err
13346 else {
13347 panic!("expected KwargDeserialize, got {err:?}");
13348 };
13349 assert_eq!(*path, KwargPath::Named("level".into()));
13350 assert_eq!(message, "boom");
13351 assert_eq!(
13352 format!("{err}"),
13353 "compile error in :level: deserialize: boom"
13354 );
13355 }
13356
13357 #[test]
13358 fn kwarg_deserialize_path_item_threads_typed_kwarg_path_through_variant_slot() {
13359 // Sibling structural pin to `…_path_named_…`: the per-item path
13360 // carries `KwargPath::Item { key, idx }` directly in its `path`
13361 // slot. The `(key, idx)` bifurcation lives inside the typed
13362 // enum's variant identity (`KwargPath::Named` vs.
13363 // `KwargPath::Item`), so the invalid sibling slot combination
13364 // `(key: "", idx: Some(_))` for a scalar / Optional path is
13365 // structurally unrepresentable in the variant's data shape.
13366 let err = LispError::KwargDeserialize {
13367 path: KwargPath::item("steps", 1),
13368 message: "bad".into(),
13369 };
13370 let LispError::KwargDeserialize {
13371 ref path,
13372 ref message,
13373 } = err
13374 else {
13375 panic!("expected KwargDeserialize, got {err:?}");
13376 };
13377 assert_eq!(
13378 *path,
13379 KwargPath::Item {
13380 key: "steps".into(),
13381 idx: 1
13382 }
13383 );
13384 assert_eq!(message, "bad");
13385 assert_eq!(
13386 format!("{err}"),
13387 "compile error in :steps[1]: deserialize: bad"
13388 );
13389 }
13390
13391 #[test]
13392 fn kwarg_deserialize_display_prefix_matches_kwarg_path_display() {
13393 // End-to-end pin: the `LispError::KwargDeserialize` variant's
13394 // Display rendering threads its typed `path: KwargPath` slot
13395 // through `KwargPath`'s `Display` impl directly (via the
13396 // `#[error("compile error in {path}: ...")]` annotation, no
13397 // intermediate helper). The full rendered diagnostic MUST be
13398 // anchored on the canonical `KwargPath`-projected prefix
13399 // across BOTH variants of `KwargPath` — a regression that
13400 // drifts either projection (e.g., re-introducing an inline
13401 // `format!` literal in a `#[error(..., fmt_fn(path))]` annotation
13402 // that diverges from `KwargPath`'s Display arm) fails-loudly
13403 // here.
13404 let scalar = LispError::KwargDeserialize {
13405 path: KwargPath::named("level"),
13406 message: "boom".into(),
13407 };
13408 assert_eq!(
13409 format!("{scalar}"),
13410 format!(
13411 "compile error in {}: deserialize: boom",
13412 KwargPath::named("level")
13413 )
13414 );
13415
13416 let item = LispError::KwargDeserialize {
13417 path: KwargPath::item("steps", 3),
13418 message: "boom".into(),
13419 };
13420 assert_eq!(
13421 format!("{item}"),
13422 format!(
13423 "compile error in {}: deserialize: boom",
13424 KwargPath::item("steps", 3)
13425 )
13426 );
13427 }
13428
13429 // ── CompilerSpecIo: disk-persistence structural-variant lift ────
13430 //
13431 // `compiler_spec_io_err` (the helper shared by all four
13432 // `realize_to_disk` / `load_from_disk` call sites in
13433 // `compiler_spec.rs`) was promoted from the `LispError::Compile`-
13434 // shaped triple to the structural `LispError::CompilerSpecIo {
13435 // stage, message }` variant — closing the LAST
13436 // `LispError::Compile { ... }` construction site in
13437 // `tatara-lisp/src/compiler_spec.rs`. The `stage` slot is the
13438 // typed closed-set `CompilerSpecIoStage` enum, so the
13439 // (operation, stage) pair is structurally constrained — only the
13440 // four reachable pairs (`realize_to_disk` × {serialize, write}
13441 // ⊎ `load_from_disk` × {read, deserialize}) are representable
13442 // in the variant.
13443 //
13444 // The tests below pin: (a) Display matches the legacy `"compile
13445 // error in {operation}: {stage}: {message}"` shape byte-for-byte
13446 // across all four stages; (b) the closed-set `operation()` /
13447 // `label()` projections; (c) the `CompilerSpecIoStage` enum is
13448 // Copy + Eq + Debug (matches the `MacroDefHead` posture); (d) the
13449 // legacy substring `"realize_to_disk"`, `"load_from_disk"`,
13450 // `"serialize: "`, `"write: "`, `"read: "`, `"deserialize: "`
13451 // survive the lift unchanged for substring-grep consumers. The
13452 // `position()` floor for both representative stages is pinned in
13453 // the main `position_is_none_for_non_positional_variants` block
13454 // above.
13455
13456 #[test]
13457 fn compiler_spec_io_stage_operation_projects_realize_for_serialize_and_write() {
13458 // Both `realize_to_disk` stages share the same `operation()`
13459 // projection. Pin the closed-set posture: the operation slot
13460 // of the legacy `Compile`-shaped triple is now a TYPED
13461 // projection from `CompilerSpecIoStage`, not an
13462 // independently-passed `&'static str` that could drift.
13463 assert_eq!(
13464 super::CompilerSpecIoStage::RealizeToDiskSerialize.operation(),
13465 "realize_to_disk"
13466 );
13467 assert_eq!(
13468 super::CompilerSpecIoStage::RealizeToDiskWrite.operation(),
13469 "realize_to_disk"
13470 );
13471 }
13472
13473 #[test]
13474 fn compiler_spec_io_stage_operation_projects_load_for_read_and_deserialize() {
13475 // Both `load_from_disk` stages share the same `operation()`
13476 // projection. Sibling to the realize-side assertion: pins the
13477 // bifurcation of the closed set by `operation()` is exhaustive
13478 // and exactly 2-way (`realize_to_disk` ⊎ `load_from_disk`).
13479 assert_eq!(
13480 super::CompilerSpecIoStage::LoadFromDiskRead.operation(),
13481 "load_from_disk"
13482 );
13483 assert_eq!(
13484 super::CompilerSpecIoStage::LoadFromDiskDeserialize.operation(),
13485 "load_from_disk"
13486 );
13487 }
13488
13489 #[test]
13490 fn compiler_spec_io_stage_label_projects_canonical_stage_strings() {
13491 // Each `CompilerSpecIoStage` projects to its canonical
13492 // `label()` — the `{stage}` slot of the legacy `"{stage}:
13493 // {error}"` message. Pin all four projections so a regression
13494 // that drifts ANY label (e.g., to "ser", "load", "decode",
13495 // "json-out") fails-loudly here. The four labels are the
13496 // surface that `tatara-check`'s diagnostic capture and the
13497 // REPL substring-grep on today.
13498 assert_eq!(
13499 super::CompilerSpecIoStage::RealizeToDiskSerialize.label(),
13500 "serialize"
13501 );
13502 assert_eq!(
13503 super::CompilerSpecIoStage::RealizeToDiskWrite.label(),
13504 "write"
13505 );
13506 assert_eq!(super::CompilerSpecIoStage::LoadFromDiskRead.label(), "read");
13507 assert_eq!(
13508 super::CompilerSpecIoStage::LoadFromDiskDeserialize.label(),
13509 "deserialize"
13510 );
13511 }
13512
13513 #[test]
13514 fn compiler_spec_io_stage_label_method_routes_through_typed_constants() {
13515 // PATH-UNIFORMITY: the inherent `Self::label(self)` method
13516 // MUST return the per-role `pub const` byte-for-byte for
13517 // each reachable variant. A regression that reverts ONE arm
13518 // to an inline `"serialize"` string literal (e.g. a merge-
13519 // conflict resolution that picked the pre-lift form)
13520 // silently reintroduces the ≥2 PRIME-DIRECTIVE trigger the
13521 // lift resolved — this test catches that by pinning the
13522 // arm's return value to the constant, so the two paths
13523 // (inline vs. typed constant) cannot both hold. Sibling
13524 // posture to
13525 // `kwarg_path_kind_label_method_routes_through_typed_constants`
13526 // on the `KwargPathKind` category-label algebra.
13527 assert_eq!(
13528 super::CompilerSpecIoStage::RealizeToDiskSerialize.label(),
13529 super::CompilerSpecIoStage::REALIZE_TO_DISK_SERIALIZE_LABEL,
13530 "CompilerSpecIoStage::RealizeToDiskSerialize.label() \
13531 drifted from CompilerSpecIoStage::\
13532 REALIZE_TO_DISK_SERIALIZE_LABEL — the match arm reverted \
13533 to an inline literal"
13534 );
13535 assert_eq!(
13536 super::CompilerSpecIoStage::RealizeToDiskWrite.label(),
13537 super::CompilerSpecIoStage::REALIZE_TO_DISK_WRITE_LABEL,
13538 "CompilerSpecIoStage::RealizeToDiskWrite.label() drifted \
13539 from CompilerSpecIoStage::REALIZE_TO_DISK_WRITE_LABEL — \
13540 the match arm reverted to an inline literal"
13541 );
13542 assert_eq!(
13543 super::CompilerSpecIoStage::LoadFromDiskRead.label(),
13544 super::CompilerSpecIoStage::LOAD_FROM_DISK_READ_LABEL,
13545 "CompilerSpecIoStage::LoadFromDiskRead.label() drifted \
13546 from CompilerSpecIoStage::LOAD_FROM_DISK_READ_LABEL — \
13547 the match arm reverted to an inline literal"
13548 );
13549 assert_eq!(
13550 super::CompilerSpecIoStage::LoadFromDiskDeserialize.label(),
13551 super::CompilerSpecIoStage::LOAD_FROM_DISK_DESERIALIZE_LABEL,
13552 "CompilerSpecIoStage::LoadFromDiskDeserialize.label() \
13553 drifted from CompilerSpecIoStage::\
13554 LOAD_FROM_DISK_DESERIALIZE_LABEL — the match arm reverted \
13555 to an inline literal"
13556 );
13557 }
13558
13559 #[test]
13560 fn compiler_spec_io_stage_labels_has_expected_cardinality() {
13561 // Cardinality contract: `Self::LABELS.len() == 4` — pinned
13562 // at the declaration site by rustc's forced-arity check on
13563 // `[&'static str; 4]`. This test surfaces the arity as a
13564 // fail-loud runtime pin so a future refactor that switches
13565 // the array type to `&[&'static str]` (dropping the
13566 // compile-time arity forcing) doesn't silently loosen the
13567 // closed-set discipline the family relies on. Sibling
13568 // posture to `kwarg_path_kind_labels_has_expected_cardinality`
13569 // on the `KwargPathKind::LABELS` family.
13570 assert_eq!(
13571 super::CompilerSpecIoStage::LABELS.len(),
13572 4,
13573 "CompilerSpecIoStage::LABELS cardinality drifted from 4 \
13574 — the disk-persistence stage-label closed set gained or \
13575 lost a variant without the ALL / LABELS pair being \
13576 updated in tandem"
13577 );
13578 }
13579
13580 #[test]
13581 fn compiler_spec_io_stage_labels_align_with_all_by_index() {
13582 // ALIGNMENT CONTRACT: `Self::LABELS[i] ==
13583 // Self::ALL[i].label()` element-wise. The two ALL arrays
13584 // (typed variants, canonical `&'static str` labels) share
13585 // ONE canonical declaration order — a regression that
13586 // reorders ONE array without reordering the other silently
13587 // misaligns every `zip(Self::ALL, Self::LABELS)` consumer
13588 // (LSP completion providers, metric-label emitters, coverage
13589 // reporters). Pinning by-index alignment here catches the
13590 // reorder at test time. Sibling posture to
13591 // `kwarg_path_kind_labels_align_with_all_by_index` on the
13592 // `KwargPathKind::LABELS` family.
13593 assert_eq!(
13594 super::CompilerSpecIoStage::LABELS.len(),
13595 super::CompilerSpecIoStage::ALL.len(),
13596 "CompilerSpecIoStage::LABELS and CompilerSpecIoStage::ALL \
13597 diverged in cardinality — the per-role constants and \
13598 enum variants must stay in lockstep"
13599 );
13600 for (i, stage) in super::CompilerSpecIoStage::ALL.iter().enumerate() {
13601 assert_eq!(
13602 super::CompilerSpecIoStage::LABELS[i],
13603 stage.label(),
13604 "CompilerSpecIoStage::LABELS[{i}] `{lb}` drifted from \
13605 CompilerSpecIoStage::ALL[{i}].label() `{via_variant}` \
13606 — the canonical declaration order of the two ALL \
13607 arrays must match element-wise",
13608 lb = super::CompilerSpecIoStage::LABELS[i],
13609 via_variant = stage.label(),
13610 );
13611 }
13612 }
13613
13614 #[test]
13615 fn compiler_spec_io_stage_labels_pairwise_distinct() {
13616 // PAIRWISE DISJOINTNESS: the four `&'static str` labels on
13617 // the disk-persistence stage-label algebra MUST differ so
13618 // any consumer that keys a hashmap / dispatch table on the
13619 // label bytes (`match label { "serialize" => ...,
13620 // "deserialize" => ..., ... }`) cannot route two variants
13621 // through the same arm. Family-wide sweep over LABELS ×
13622 // LABELS — supersedes any single per-pair assertion and
13623 // picks up new variants mechanically when the array grows.
13624 // Sibling posture to `kwarg_path_kind_labels_pairwise_distinct`
13625 // on the `KwargPathKind::LABELS` family.
13626 //
13627 // Note: unlike `KwargPathKind` / `ExpectedKwargShape`, the
13628 // parent enum keys `FromStr` on the compound
13629 // `"{operation}: {label}"` key rather than on `label` alone
13630 // (see `CompilerSpecIoStage::from_str`), so pairwise-distinct
13631 // labels are not a hard load-bearing invariant for the
13632 // FromStr sweep. But they ARE the load-bearing invariant for
13633 // every downstream consumer that keys on the label alone
13634 // (metric-label tables, LSP completion, `tatara-check` grep
13635 // budgets on the `{stage}:` prefix), and pinning the
13636 // property here catches a regression that collides two
13637 // variants on their singular label projection before those
13638 // consumer sites drift.
13639 for (i, a) in super::CompilerSpecIoStage::LABELS.iter().enumerate() {
13640 for (j, b) in super::CompilerSpecIoStage::LABELS.iter().enumerate() {
13641 if i == j {
13642 continue;
13643 }
13644 assert_ne!(
13645 a, b,
13646 "CompilerSpecIoStage::LABELS[{i}] `{a}` collides \
13647 with CompilerSpecIoStage::LABELS[{j}] `{b}` — \
13648 the singular label projection is no longer a \
13649 bijection with ALL, breaking every consumer that \
13650 keys on the label alone"
13651 );
13652 }
13653 }
13654 }
13655
13656 #[test]
13657 fn compiler_spec_io_stage_operation_method_routes_through_typed_constants() {
13658 // PATH-UNIFORMITY: the inherent `Self::operation(self)` method
13659 // MUST return the per-role `pub const` byte-for-byte for
13660 // each reachable variant. A regression that reverts ONE arm
13661 // to an inline `"realize_to_disk"` string literal (e.g. a
13662 // merge-conflict resolution that picked the pre-lift form)
13663 // silently reintroduces the ≥2 PRIME-DIRECTIVE trigger the
13664 // lift resolved — this test catches that by pinning each
13665 // arm's return value to the constant, so the two paths
13666 // (inline vs. typed constant) cannot both hold. Sibling
13667 // posture to
13668 // `compiler_spec_io_stage_label_method_routes_through_typed_constants`
13669 // on the paired stage-label algebra.
13670 assert_eq!(
13671 super::CompilerSpecIoStage::RealizeToDiskSerialize.operation(),
13672 super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
13673 "CompilerSpecIoStage::RealizeToDiskSerialize.operation() \
13674 drifted from CompilerSpecIoStage::\
13675 REALIZE_TO_DISK_OPERATION — the match arm reverted to an \
13676 inline literal"
13677 );
13678 assert_eq!(
13679 super::CompilerSpecIoStage::RealizeToDiskWrite.operation(),
13680 super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
13681 "CompilerSpecIoStage::RealizeToDiskWrite.operation() \
13682 drifted from CompilerSpecIoStage::\
13683 REALIZE_TO_DISK_OPERATION — the match arm reverted to an \
13684 inline literal"
13685 );
13686 assert_eq!(
13687 super::CompilerSpecIoStage::LoadFromDiskRead.operation(),
13688 super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
13689 "CompilerSpecIoStage::LoadFromDiskRead.operation() drifted \
13690 from CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION — the \
13691 match arm reverted to an inline literal"
13692 );
13693 assert_eq!(
13694 super::CompilerSpecIoStage::LoadFromDiskDeserialize.operation(),
13695 super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
13696 "CompilerSpecIoStage::LoadFromDiskDeserialize.operation() \
13697 drifted from CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION \
13698 — the match arm reverted to an inline literal"
13699 );
13700 }
13701
13702 #[test]
13703 fn compiler_spec_io_stage_operations_has_expected_cardinality() {
13704 // Cardinality contract: `Self::OPERATIONS.len() == 4` —
13705 // pinned at the declaration site by rustc's forced-arity
13706 // check on `[&'static str; 4]`. This test surfaces the arity
13707 // as a fail-loud runtime pin so a future refactor that
13708 // switches the array type to `&[&'static str]` (dropping the
13709 // compile-time arity forcing) doesn't silently loosen the
13710 // closed-set discipline the family relies on. Sibling posture
13711 // to `compiler_spec_io_stage_labels_has_expected_cardinality`
13712 // on the paired stage-label algebra.
13713 assert_eq!(
13714 super::CompilerSpecIoStage::OPERATIONS.len(),
13715 4,
13716 "CompilerSpecIoStage::OPERATIONS cardinality drifted from \
13717 4 — the disk-persistence operation-label closed set \
13718 gained or lost a variant without the ALL / OPERATIONS \
13719 pair being updated in tandem"
13720 );
13721 }
13722
13723 #[test]
13724 fn compiler_spec_io_stage_operations_align_with_all_by_index() {
13725 // ALIGNMENT CONTRACT: `Self::OPERATIONS[i] ==
13726 // Self::ALL[i].operation()` element-wise. The two ALL arrays
13727 // (typed variants, canonical `&'static str` operation labels)
13728 // share ONE canonical declaration order — a regression that
13729 // reorders ONE array without reordering the other silently
13730 // misaligns every `zip(Self::ALL, Self::OPERATIONS)` consumer
13731 // (LSP completion providers keyed on the operation slot,
13732 // metric-label emitters, coverage reporters that walk the
13733 // operation × stage compound-key surface). Pinning by-index
13734 // alignment here catches the reorder at test time. Sibling
13735 // posture to `compiler_spec_io_stage_labels_align_with_all_by_index`
13736 // on the paired stage-label algebra.
13737 assert_eq!(
13738 super::CompilerSpecIoStage::OPERATIONS.len(),
13739 super::CompilerSpecIoStage::ALL.len(),
13740 "CompilerSpecIoStage::OPERATIONS and \
13741 CompilerSpecIoStage::ALL diverged in cardinality — the \
13742 per-role constants and enum variants must stay in \
13743 lockstep"
13744 );
13745 for (i, stage) in super::CompilerSpecIoStage::ALL.iter().enumerate() {
13746 assert_eq!(
13747 super::CompilerSpecIoStage::OPERATIONS[i],
13748 stage.operation(),
13749 "CompilerSpecIoStage::OPERATIONS[{i}] `{op}` drifted \
13750 from CompilerSpecIoStage::ALL[{i}].operation() \
13751 `{via_variant}` — the canonical declaration order \
13752 of the two ALL arrays must match element-wise",
13753 op = super::CompilerSpecIoStage::OPERATIONS[i],
13754 via_variant = stage.operation(),
13755 );
13756 }
13757 }
13758
13759 #[test]
13760 fn compiler_spec_io_stage_operations_partition_all_two_ways() {
13761 // TWO-WAY PARTITION CONTRACT: unlike `Self::LABELS` — whose
13762 // four elements are pairwise distinct because each `label()`
13763 // projection is bijective with its variant — `Self::OPERATIONS`
13764 // contains DUPLICATES by construction. The operation
13765 // projection bifurcates the closed set into exactly two
13766 // disjoint groups of two: `{RealizeToDiskSerialize,
13767 // RealizeToDiskWrite}` share `"realize_to_disk"` and
13768 // `{LoadFromDiskRead, LoadFromDiskDeserialize}` share
13769 // `"load_from_disk"`. The 2-of-2-to-2 shape is load-bearing
13770 // for the compound-key `"{operation}: {label}"` surface —
13771 // if the operation projection ever grew bijective with
13772 // `ALL` the compound key would collapse to `label`-only and
13773 // the parse boundary would silently accept unreachable
13774 // cross-product pairs. Pin the exact multiplicities here so
13775 // a future refactor that (a) adds a distinct per-variant
13776 // operation, or (b) collapses the two operations into one
13777 // shared string, fails-loudly. Sibling posture to
13778 // `compiler_spec_io_stage_labels_pairwise_distinct` on the
13779 // paired stage-label algebra — where LABELS pins
13780 // distinctness, OPERATIONS pins the exact two-way partition
13781 // multiplicity.
13782 let realize_count = super::CompilerSpecIoStage::OPERATIONS
13783 .iter()
13784 .filter(|&&op| op == super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION)
13785 .count();
13786 let load_count = super::CompilerSpecIoStage::OPERATIONS
13787 .iter()
13788 .filter(|&&op| op == super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION)
13789 .count();
13790 assert_eq!(
13791 realize_count, 2,
13792 "CompilerSpecIoStage::OPERATIONS multiplicity of \
13793 REALIZE_TO_DISK_OPERATION drifted from 2 — the \
13794 realize-side bifurcation over {{serialize, write}} is \
13795 load-bearing for the compound-key surface"
13796 );
13797 assert_eq!(
13798 load_count, 2,
13799 "CompilerSpecIoStage::OPERATIONS multiplicity of \
13800 LOAD_FROM_DISK_OPERATION drifted from 2 — the load-side \
13801 bifurcation over {{read, deserialize}} is load-bearing \
13802 for the compound-key surface"
13803 );
13804 assert_eq!(
13805 realize_count + load_count,
13806 super::CompilerSpecIoStage::OPERATIONS.len(),
13807 "CompilerSpecIoStage::OPERATIONS gained an operation label \
13808 outside the two-way partition — the closed-set operation \
13809 axis is no longer a clean {{realize_to_disk ⊎ load_from_disk}} \
13810 bifurcation"
13811 );
13812 assert_ne!(
13813 super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
13814 super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
13815 "CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION collided \
13816 with CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION — the \
13817 two-way partition collapsed to one group, breaking every \
13818 compound-key `{{operation}}: {{label}}` round-trip"
13819 );
13820 }
13821
13822 #[test]
13823 fn compiler_spec_io_display_renders_legacy_shape_for_realize_serialize() {
13824 // `RealizeToDiskSerialize` — the `serde_json::to_string_pretty`
13825 // failure inside `realize_to_disk`. The variant renders the
13826 // legacy `"compile error in realize_to_disk: serialize:
13827 // {message}"` shape byte-for-byte — same wording as the
13828 // pre-lift `Compile { form: "realize_to_disk", message:
13829 // "serialize: {e}" }` triple.
13830 let err = LispError::CompilerSpecIo {
13831 stage: super::CompilerSpecIoStage::RealizeToDiskSerialize,
13832 message: "expected struct CompilerSpec".into(),
13833 };
13834 assert_eq!(
13835 format!("{err}"),
13836 "compile error in realize_to_disk: serialize: expected struct CompilerSpec"
13837 );
13838 }
13839
13840 #[test]
13841 fn compiler_spec_io_display_renders_legacy_shape_for_realize_write() {
13842 // `RealizeToDiskWrite` — the `std::fs::write` failure inside
13843 // `realize_to_disk`. Pin path-uniformity across the second
13844 // stage of the realize-side operation: same operation prefix,
13845 // distinct stage label (`write` vs `serialize`).
13846 let err = LispError::CompilerSpecIo {
13847 stage: super::CompilerSpecIoStage::RealizeToDiskWrite,
13848 message: "No such file or directory (os error 2)".into(),
13849 };
13850 assert_eq!(
13851 format!("{err}"),
13852 "compile error in realize_to_disk: write: No such file or directory (os error 2)"
13853 );
13854 }
13855
13856 #[test]
13857 fn compiler_spec_io_display_renders_legacy_shape_for_load_read() {
13858 // `LoadFromDiskRead` — the `std::fs::read_to_string` failure
13859 // inside `load_from_disk`. Pin path-uniformity across the
13860 // operation slot: `load_from_disk` vs `realize_to_disk` are
13861 // structurally distinct via the typed enum, both round-trip
13862 // through Display unchanged.
13863 let err = LispError::CompilerSpecIo {
13864 stage: super::CompilerSpecIoStage::LoadFromDiskRead,
13865 message: "No such file or directory (os error 2)".into(),
13866 };
13867 assert_eq!(
13868 format!("{err}"),
13869 "compile error in load_from_disk: read: No such file or directory (os error 2)"
13870 );
13871 }
13872
13873 #[test]
13874 fn compiler_spec_io_display_renders_legacy_shape_for_load_deserialize() {
13875 // `LoadFromDiskDeserialize` — the `serde_json::from_str`
13876 // failure inside `load_from_disk`. Pin path-uniformity across
13877 // the fourth and final reachable stage. Together with the
13878 // three sibling tests, this closes the structural-completeness
13879 // floor of the closed-set `CompilerSpecIoStage` × Display
13880 // matrix — all four reachable pairs are pinned.
13881 let err = LispError::CompilerSpecIo {
13882 stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
13883 message: "expected value at line 1 column 1".into(),
13884 };
13885 assert_eq!(
13886 format!("{err}"),
13887 "compile error in load_from_disk: deserialize: expected value at line 1 column 1"
13888 );
13889 }
13890
13891 #[test]
13892 fn compiler_spec_io_display_carries_serde_json_diagnostic_unchanged() {
13893 // Use a real `serde_json::Error` so the test exercises a
13894 // representative `{e}` shape and pins that the variant's
13895 // Display rendering threads the underlying diagnostic through
13896 // unchanged. Same posture as `domain_serialize_display_carries_
13897 // serde_json_diagnostic_unchanged` and
13898 // `kwarg_deserialize_display_carries_serde_json_diagnostic_
13899 // unchanged`.
13900 let raw = serde_json::from_str::<i32>("not-a-number")
13901 .expect_err("parse must fail")
13902 .to_string();
13903 let err = LispError::CompilerSpecIo {
13904 stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
13905 message: raw.clone(),
13906 };
13907 assert_eq!(
13908 format!("{err}"),
13909 format!("compile error in load_from_disk: deserialize: {raw}")
13910 );
13911 }
13912
13913 #[test]
13914 fn compiler_spec_io_display_preserves_legacy_substring_for_message_grep() {
13915 // Pin the legacy substring set — `"realize_to_disk"`,
13916 // `"load_from_disk"`, `"serialize: "`, `"write: "`,
13917 // `"read: "`, `"deserialize: "` — as a separate assertion so a
13918 // regression that drifts ANY of the six surface words (e.g.,
13919 // to "save", "load", "json-out", "json-in") fails-loudly here.
13920 // The substrings are what consumers downstream (`tatara-check`,
13921 // the REPL) substring-match on today; the prefix matches the
13922 // legacy `Compile { form: "{operation}", message: "{stage}:
13923 // {e}" }` byte-for-byte across all four reachable pairs.
13924 let realize_serialize = LispError::CompilerSpecIo {
13925 stage: super::CompilerSpecIoStage::RealizeToDiskSerialize,
13926 message: "boom".into(),
13927 };
13928 let msg = format!("{realize_serialize}");
13929 assert!(
13930 msg.contains("realize_to_disk"),
13931 "expected realize-side operation in message, got: {msg}"
13932 );
13933 assert!(
13934 msg.contains("serialize: "),
13935 "expected serialize-stage substring in message, got: {msg}"
13936 );
13937
13938 let realize_write = LispError::CompilerSpecIo {
13939 stage: super::CompilerSpecIoStage::RealizeToDiskWrite,
13940 message: "boom".into(),
13941 };
13942 let msg = format!("{realize_write}");
13943 assert!(
13944 msg.contains("write: "),
13945 "expected write-stage substring in message, got: {msg}"
13946 );
13947
13948 let load_read = LispError::CompilerSpecIo {
13949 stage: super::CompilerSpecIoStage::LoadFromDiskRead,
13950 message: "boom".into(),
13951 };
13952 let msg = format!("{load_read}");
13953 assert!(
13954 msg.contains("load_from_disk"),
13955 "expected load-side operation in message, got: {msg}"
13956 );
13957 assert!(
13958 msg.contains("read: "),
13959 "expected read-stage substring in message, got: {msg}"
13960 );
13961
13962 let load_deserialize = LispError::CompilerSpecIo {
13963 stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
13964 message: "boom".into(),
13965 };
13966 let msg = format!("{load_deserialize}");
13967 assert!(
13968 msg.contains("deserialize: "),
13969 "expected deserialize-stage substring in message, got: {msg}"
13970 );
13971 }
13972
13973 #[test]
13974 fn compiler_spec_io_display_empty_message_renders_bare_stage_marker() {
13975 // Edge case: an empty `message` slot renders as `"compile
13976 // error in {operation}: {stage}: "` — pin the trailing space
13977 // after the stage marker stays put across all four pairs. A
13978 // regression that strips trailing whitespace (e.g., via
13979 // `.trim_end()`) or drops the marker entirely fails-loudly here.
13980 // Sibling of `domain_serialize_display_empty_message_renders_
13981 // bare_prefix`.
13982 let err = LispError::CompilerSpecIo {
13983 stage: super::CompilerSpecIoStage::RealizeToDiskWrite,
13984 message: String::new(),
13985 };
13986 assert_eq!(
13987 format!("{err}"),
13988 "compile error in realize_to_disk: write: "
13989 );
13990 }
13991
13992 #[test]
13993 fn compiler_spec_io_stage_is_copy_and_partial_eq() {
13994 // Pin the closed-set posture: `CompilerSpecIoStage` derives
13995 // Copy + PartialEq + Eq + Debug so it composes ergonomically
13996 // in tests and in consumer pattern-matches (no clone-and-
13997 // own dance). Same posture as `MacroDefHead`. A regression
13998 // that drops Copy fails-loudly here (the let-binding would
13999 // move out instead of copy).
14000 let stage = super::CompilerSpecIoStage::LoadFromDiskRead;
14001 let copied = stage;
14002 assert_eq!(stage, copied);
14003 assert_eq!(stage, super::CompilerSpecIoStage::LoadFromDiskRead);
14004 assert_ne!(stage, super::CompilerSpecIoStage::RealizeToDiskWrite);
14005 }
14006
14007 #[test]
14008 fn compiler_spec_io_stage_all_is_unique_and_complete() {
14009 // Closed-set posture: `ALL` enumerates every reachable variant
14010 // EXACTLY ONCE — no duplicates, no omissions. The `[Self; 4]`
14011 // array literal in the declaration forces the arity at compile
14012 // time; this test catches the orthogonal failure modes — a
14013 // future variant added at the type without being added to ALL
14014 // (silently dropped from every consumer's sweep), or a typo
14015 // that duplicates an entry (silently double-counted). Same
14016 // truth-table pinning every sibling closed-set lift in the
14017 // workspace uses (ExpectedKwargShape::ALL, SexpShape::ALL,
14018 // MacroDefHead::ALL, UnquoteForm::ALL, …).
14019 //
14020 // The asserted compound keys are the canonical (operation,
14021 // label) pairs the disk-persistence surface emits — the four
14022 // entries are the reachable cells of the `operation × label`
14023 // cross-product, and the four-out-of-eight partiality is the
14024 // load-bearing thing: `realize_to_disk × {read, deserialize}`
14025 // and `load_from_disk × {serialize, write}` are conceivable
14026 // but unreachable, and `FromStr` enforces that asymmetry at
14027 // the parse boundary.
14028 assert_eq!(super::CompilerSpecIoStage::ALL.len(), 4);
14029 let mut sorted: Vec<String> = super::CompilerSpecIoStage::ALL
14030 .iter()
14031 .map(std::string::ToString::to_string)
14032 .collect();
14033 sorted.sort_unstable();
14034 let mut deduped = sorted.clone();
14035 deduped.dedup();
14036 assert_eq!(
14037 sorted, deduped,
14038 "CompilerSpecIoStage::ALL must not contain duplicates"
14039 );
14040 assert_eq!(
14041 sorted,
14042 vec![
14043 "load_from_disk: deserialize".to_string(),
14044 "load_from_disk: read".to_string(),
14045 "realize_to_disk: serialize".to_string(),
14046 "realize_to_disk: write".to_string(),
14047 ],
14048 "CompilerSpecIoStage::ALL must cover every reachable (operation, label) pair"
14049 );
14050 }
14051
14052 #[test]
14053 fn compiler_spec_io_stage_display_matches_diagnostic_prefix() {
14054 // Pin standalone Display to the canonical compound key — the
14055 // exact substring that lands between `"compile error in "` and
14056 // `": {message}"` inside the LispError::CompilerSpecIo
14057 // rendering. Consumers that extract the (operation, label)
14058 // prefix from a rendered diagnostic (LSP code-actions, REPL
14059 // replay) round-trip the captured substring through `FromStr`
14060 // back into the typed variant exactly. A regression that
14061 // drifts either side (Display's separator, the LispError's
14062 // `#[error(...)]` annotation, the projection methods) fails
14063 // loudly here because both renderings must agree.
14064 for stage in super::CompilerSpecIoStage::ALL {
14065 let standalone = stage.to_string();
14066 let full = format!(
14067 "{}",
14068 LispError::CompilerSpecIo {
14069 stage,
14070 message: "MSG".into()
14071 }
14072 );
14073 let prefix = full
14074 .strip_prefix("compile error in ")
14075 .expect("legacy rendering prefix must hold");
14076 let extracted = prefix
14077 .strip_suffix(": MSG")
14078 .expect("legacy rendering suffix must hold");
14079 assert_eq!(
14080 extracted, standalone,
14081 "extracted prefix `{extracted}` must equal standalone Display `{standalone}`"
14082 );
14083 }
14084 }
14085
14086 #[test]
14087 fn compiler_spec_io_stage_compound_key_round_trips_through_from_str() {
14088 // Bidirectional `Display` ↔ `FromStr` contract: for every
14089 // variant in ALL, `stage.to_string().parse() == Ok(stage)`. A
14090 // regression that drifts the (variant, compound-key) pairing
14091 // at the `Display` impl (typo, missing separator, swapped
14092 // projection order) OR at the `FromStr` decode body (off-by-
14093 // one, missing variant in the sweep) fails-loudly here. The
14094 // canonical-key site is singular (`Display` projects through
14095 // `operation()` and `label()`) so the round-trip is the only
14096 // way the typed surface and the rendered diagnostic literal
14097 // can drift apart — pinning it here means they cannot. Mirror
14098 // of `expected_kwarg_shape_label_round_trips_through_from_str`
14099 // and every sibling closed-set round-trip in the workspace —
14100 // the difference is the compound-key shape rather than a
14101 // single label.
14102 for stage in super::CompilerSpecIoStage::ALL {
14103 let key = stage.to_string();
14104 let parsed: super::CompilerSpecIoStage = key
14105 .parse()
14106 .expect("every ALL variant's compound key must round-trip through FromStr");
14107 assert_eq!(
14108 parsed, stage,
14109 "FromStr({key}) must round-trip to the same variant"
14110 );
14111 }
14112 }
14113
14114 #[test]
14115 fn compiler_spec_io_stage_from_str_rejects_partial_and_unreachable_keys() {
14116 // The compound-key shape is load-bearing: `FromStr` rejects
14117 // partial keys (one of the two projection slots alone),
14118 // separator-less inputs, AND the four conceivable-but-
14119 // unreachable cross-product pairs that the disk-persistence
14120 // surface does NOT emit. The partial-rejection turns the
14121 // call-site invariant ("only the four call sites in
14122 // `compiler_spec.rs` construct stages, each pairs the correct
14123 // operation with the correct stage") into a parse-boundary
14124 // invariant ("the four reachable pairs are structurally
14125 // distinct from the four unreachable ones").
14126 //
14127 // Without this guard a future LSP that captures the prefix
14128 // `"load_from_disk: write"` from a hand-crafted (corrupted)
14129 // log and replays it through `FromStr` would silently round-
14130 // trip an unreachable identity into the typed enum.
14131 for partial in [
14132 "serialize",
14133 "write",
14134 "read",
14135 "deserialize",
14136 "realize_to_disk",
14137 "load_from_disk",
14138 "",
14139 ] {
14140 partial.parse::<super::CompilerSpecIoStage>().expect_err(
14141 "partial key (one projection slot alone, or empty) must NOT decode to a variant",
14142 );
14143 }
14144 for unreachable in [
14145 "realize_to_disk: read",
14146 "realize_to_disk: deserialize",
14147 "load_from_disk: serialize",
14148 "load_from_disk: write",
14149 ] {
14150 unreachable
14151 .parse::<super::CompilerSpecIoStage>()
14152 .expect_err(
14153 "conceivable-but-unreachable (operation, label) pair must NOT decode to a variant",
14154 );
14155 }
14156 // Sanity: each reachable pair still decodes.
14157 assert_eq!(
14158 "realize_to_disk: serialize"
14159 .parse::<super::CompilerSpecIoStage>()
14160 .unwrap(),
14161 super::CompilerSpecIoStage::RealizeToDiskSerialize
14162 );
14163 assert_eq!(
14164 "load_from_disk: deserialize"
14165 .parse::<super::CompilerSpecIoStage>()
14166 .unwrap(),
14167 super::CompilerSpecIoStage::LoadFromDiskDeserialize
14168 );
14169 }
14170
14171 #[test]
14172 fn unknown_compiler_spec_io_stage_carries_offending_input_verbatim() {
14173 // Operator-facing diagnostic contract: the offending input
14174 // lands in the typed error verbatim — no normalization, no
14175 // case-folding, no truncation. Pin the exact `#[error(...)]`
14176 // rendering AND the typed `.0` field projection so a future
14177 // refactor that normalizes (e.g. `.to_lowercase()`) before
14178 // building the error or that drops the input fails-loudly
14179 // here. Symmetric to every sibling `Unknown*` carrier in the
14180 // workspace (`UnknownExpectedKwargShape`, `UnknownSexpShape`,
14181 // `UnknownMacroDefHead`, …).
14182 let err: super::UnknownCompilerSpecIoStage = "Realize_to_disk: serialize"
14183 .parse::<super::CompilerSpecIoStage>()
14184 .expect_err("capitalized operation must NOT decode — keys are byte-equal");
14185 assert_eq!(err.0, "Realize_to_disk: serialize");
14186 assert_eq!(
14187 format!("{err}"),
14188 "unknown compiler spec io stage: Realize_to_disk: serialize"
14189 );
14190
14191 let err: super::UnknownCompilerSpecIoStage = "load_from_disk: write"
14192 .parse::<super::CompilerSpecIoStage>()
14193 .expect_err("unreachable cross-product pair must NOT decode");
14194 assert_eq!(err.0, "load_from_disk: write");
14195 assert_eq!(
14196 format!("{err}"),
14197 "unknown compiler spec io stage: load_from_disk: write"
14198 );
14199
14200 let err: super::UnknownCompilerSpecIoStage = ""
14201 .parse::<super::CompilerSpecIoStage>()
14202 .expect_err("empty input must NOT decode to a CompilerSpecIoStage");
14203 assert_eq!(err.0, "");
14204 assert_eq!(format!("{err}"), "unknown compiler spec io stage: ");
14205 }
14206
14207 #[test]
14208 fn compiler_spec_io_stage_compound_key_separator_is_colon_space() {
14209 // Pin the canonical `": "` compound-key separator bytes
14210 // byte-for-byte on the typed algebra. A regression that
14211 // silently drops the ASCII space (`":"`), doubles it (`": "`),
14212 // or swaps to a different punctuation family (`" - "`, `" | "`)
14213 // fails-loudly here — every consumer that binds against the
14214 // constant picks up the drift at compile time, not at a
14215 // customer's rendered diagnostic. The exact byte sequence
14216 // matters: LSP substring extractors that key on `rsplit_once(":
14217 // ")` (matching the pre-lift inline literal) and the pre-lift
14218 // `Display` format string `"{}: {}"` both threaded the SAME
14219 // three bytes; post-lift they thread the SAME `pub const`.
14220 assert_eq!(
14221 super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR,
14222 ": ",
14223 "compound-key separator must be ASCII colon-space byte-for-byte",
14224 );
14225 assert_eq!(
14226 super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR.len(),
14227 2,
14228 "compound-key separator is exactly two bytes — no invisible whitespace drift",
14229 );
14230 assert_eq!(
14231 super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR.as_bytes(),
14232 b": ",
14233 "compound-key separator bytes are `:` then ` ` — pinned byte-array shape",
14234 );
14235 }
14236
14237 #[test]
14238 fn compiler_spec_io_stage_display_composes_operation_separator_label_verbatim() {
14239 // For every variant, the `Display` rendering equals the
14240 // triple-concatenation `operation() ++ COMPOUND_KEY_SEPARATOR
14241 // ++ label()` byte-for-byte. A regression that reintroduces
14242 // the pre-lift inline `": "` literal at the `write!` format
14243 // string, or swaps the projection order, or inserts extra
14244 // padding, fails loudly here — the composed reference walks
14245 // the SAME three typed source-of-truth axes the impl body
14246 // does. Sibling posture to
14247 // `compiler_spec_io_stage_operation_method_routes_through_typed_constants`
14248 // and `compiler_spec_io_stage_label_method_routes_through_typed_constants`
14249 // on the operation and label projection axes: the SAME
14250 // through-the-typed-constants routing contract now covers the
14251 // COMPOSITION axis too.
14252 for stage in super::CompilerSpecIoStage::ALL {
14253 let rendered = stage.to_string();
14254 let expected = {
14255 let mut out = String::with_capacity(
14256 stage.operation().len()
14257 + super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR.len()
14258 + stage.label().len(),
14259 );
14260 out.push_str(stage.operation());
14261 out.push_str(super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR);
14262 out.push_str(stage.label());
14263 out
14264 };
14265 assert_eq!(
14266 rendered, expected,
14267 "Display for {stage:?} must compose `operation` ++ COMPOUND_KEY_SEPARATOR ++ `label` verbatim",
14268 );
14269 }
14270 }
14271
14272 #[test]
14273 fn compiler_spec_io_stage_from_str_uses_typed_separator_bytes() {
14274 // Pin the parse boundary to the typed separator bytes: an
14275 // input that swaps the separator (colon-alone `":"`, space-
14276 // alone `" "`, wrong punctuation `" | "`) fails-loudly even
14277 // if the operation + label slot bytes are otherwise correct.
14278 // A regression that drops the ASCII space from the
14279 // `split_once` argument after a clippy `single_char_pattern`
14280 // suggestion would silently accept `"realize_to_disk:serialize"`
14281 // as a variant — this test rejects it at the parse boundary.
14282 for wrong_separator in [":", " ", " : ", " ", "|", " - ", "\t"] {
14283 let key = {
14284 let mut out = String::new();
14285 out.push_str(super::CompilerSpecIoStage::RealizeToDiskSerialize.operation());
14286 out.push_str(wrong_separator);
14287 out.push_str(super::CompilerSpecIoStage::RealizeToDiskSerialize.label());
14288 out
14289 };
14290 key.parse::<super::CompilerSpecIoStage>().expect_err(
14291 "compound key with wrong-separator bytes must NOT decode — the parse boundary keys on COMPOUND_KEY_SEPARATOR verbatim",
14292 );
14293 }
14294 // Sanity: constructing the input through the typed constant
14295 // still succeeds — the constant is the source of truth on
14296 // both the Display and the FromStr sides.
14297 let key = {
14298 let mut out = String::new();
14299 out.push_str(super::CompilerSpecIoStage::LoadFromDiskRead.operation());
14300 out.push_str(super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR);
14301 out.push_str(super::CompilerSpecIoStage::LoadFromDiskRead.label());
14302 out
14303 };
14304 assert_eq!(
14305 key.parse::<super::CompilerSpecIoStage>()
14306 .expect("compound key composed through COMPOUND_KEY_SEPARATOR must decode"),
14307 super::CompilerSpecIoStage::LoadFromDiskRead,
14308 );
14309 }
14310
14311 #[test]
14312 fn compiler_spec_io_stage_display_middle_bytes_equal_typed_separator() {
14313 // Extract the middle bytes of every variant's `Display`
14314 // rendering — the substring between the operation prefix and
14315 // the label suffix — and pin them to
14316 // `COMPOUND_KEY_SEPARATOR`. This closes the loop from the
14317 // OTHER direction: the two projection-axis tests
14318 // (`operation_method_routes_through_typed_constants`,
14319 // `label_method_routes_through_typed_constants`) pin what
14320 // the operation and label slots ARE at rendering time; this
14321 // test pins what everything BETWEEN them is. Together the
14322 // three tests span every byte of every variant's rendered
14323 // compound key by threading each byte through some typed
14324 // source-of-truth constant on `CompilerSpecIoStage` —
14325 // matching the same span posture `Self::OPERATIONS +
14326 // Self::LABELS` establishes at the (variant → slot bytes)
14327 // aggregate-array level.
14328 for stage in super::CompilerSpecIoStage::ALL {
14329 let rendered = stage.to_string();
14330 let after_op = rendered
14331 .strip_prefix(stage.operation())
14332 .expect("Display must start with operation bytes");
14333 let middle = after_op
14334 .strip_suffix(stage.label())
14335 .expect("Display must end with label bytes");
14336 assert_eq!(
14337 middle,
14338 super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR,
14339 "the substring between operation and label in Display for {stage:?} must be COMPOUND_KEY_SEPARATOR verbatim",
14340 );
14341 }
14342 }
14343
14344 // ── TemplateInvariantKind + TemplateInvariant variant ───────────
14345 //
14346 // Closed-set posture for the bytecode-runtime invariant surface in
14347 // `macro_expand.rs::apply_compiled`. The index payload of the Subst /
14348 // Splice gates lives INSIDE the variants (`SubstBadIndex(usize)` /
14349 // `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
14350 // kind with an op-index" (e.g. `EndListEmptyStack` carrying a
14351 // `usize`) is structurally unrepresentable. Display matches the
14352 // legacy `Compile`-shaped diagnostic byte-for-byte through the
14353 // `TemplateInvariantKind::message()` projection so authoring-tool
14354 // substring greps see no drift across the lift.
14355
14356 #[test]
14357 fn template_invariant_kind_message_for_subst_bad_idx() {
14358 // `SubstBadIndex(idx)` projects to the canonical
14359 // `"compiled template referenced bad param index {idx}"`
14360 // shape — byte-for-byte equivalent to the pre-lift inline
14361 // `format!()` at the Subst gate.
14362 assert_eq!(
14363 super::TemplateInvariantKind::SubstBadIndex(99).message(),
14364 "compiled template referenced bad param index 99"
14365 );
14366 assert_eq!(
14367 super::TemplateInvariantKind::SubstBadIndex(0).message(),
14368 "compiled template referenced bad param index 0"
14369 );
14370 }
14371
14372 #[test]
14373 fn template_invariant_kind_message_for_splice_bad_idx() {
14374 // `SpliceBadIndex(idx)` projects to the canonical
14375 // `"compiled template referenced bad splice index {idx}"`
14376 // shape — byte-for-byte equivalent to the pre-lift inline
14377 // `format!()` at the Splice gate. Distinct word (`splice` vs
14378 // `param`) keeps the two gates legible in diagnostic output.
14379 assert_eq!(
14380 super::TemplateInvariantKind::SpliceBadIndex(42).message(),
14381 "compiled template referenced bad splice index 42"
14382 );
14383 }
14384
14385 #[test]
14386 fn template_invariant_kind_message_for_endlist_empty_stack() {
14387 // `EndListEmptyStack` projects to the canonical static-string
14388 // shape — no dynamic payload, no `format!()` overhead. The
14389 // pre-lift inline `&'static str` literal at the EndList gate
14390 // is preserved verbatim.
14391 assert_eq!(
14392 super::TemplateInvariantKind::EndListEmptyStack.message(),
14393 "compiled template: EndList with empty stack"
14394 );
14395 }
14396
14397 #[test]
14398 fn template_invariant_kind_message_for_final_no_value() {
14399 // `FinalNoValue` projects to the canonical static-string
14400 // shape for the post-loop final-pop gate. Preserves the
14401 // pre-lift inline `&'static str` literal verbatim.
14402 assert_eq!(
14403 super::TemplateInvariantKind::FinalNoValue.message(),
14404 "compiled template produced no value"
14405 );
14406 }
14407
14408 #[test]
14409 fn template_invariant_kind_static_message_constants_pin_canonical_bytes() {
14410 // Pin each per-role `pub const *_MESSAGE: &'static str` at its
14411 // exact canonical byte string — the source-of-truth constants
14412 // the `message` projection routes the STATIC 2-of-4 subset
14413 // arms through. A rename here (say, if a future extension
14414 // ports the byte prefix from `"compiled template"` to
14415 // `"compiled macro template"`) must land at ONE `pub const`
14416 // per role rather than at TWO sites (the per-role const AND
14417 // an inline arm literal). Sibling posture to
14418 // `macro_def_head_keyword_constants_pin_canonical_bytes` /
14419 // `unquote_form_label_constants_pin_canonical_bytes` /
14420 // `expected_kwarg_shape_label_constants_pin_canonical_bytes`
14421 // on the peer closed-set surfaces.
14422 assert_eq!(
14423 super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE,
14424 "compiled template: EndList with empty stack",
14425 "END_LIST_EMPTY_STACK_MESSAGE must pin the exact mid-loop stack-gate diagnostic bytes",
14426 );
14427 assert_eq!(
14428 super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE,
14429 "compiled template produced no value",
14430 "FINAL_NO_VALUE_MESSAGE must pin the exact post-loop final-pop diagnostic bytes",
14431 );
14432 }
14433
14434 #[test]
14435 fn template_invariant_kind_static_messages_pin_two_arm_forced_arity_array() {
14436 // Pin `STATIC_MESSAGES: [&'static str; 2]` as the family-wide
14437 // forced-arity ALL array over the STATIC 2-of-4 subset
14438 // carving. The array literal's arity IS the closed-set-size
14439 // proof — rustc's `[&'static str; 2]` check on the const's
14440 // type fails compilation if a future refactor adds a third
14441 // static arm without extending the array's length in
14442 // lockstep. Cardinality pin at the runtime layer is a
14443 // fail-loud belt-and-braces peer to the compile-time
14444 // `[_; 2]` check: a hypothetical port to `&[&'static str]`
14445 // (a slice, losing the arity discipline) fails here rather
14446 // than silently loosening the closed-set discipline.
14447 assert_eq!(
14448 super::TemplateInvariantKind::STATIC_MESSAGES.len(),
14449 2,
14450 "STATIC_MESSAGES must have exactly two entries — the 2-of-4 static subset carving",
14451 );
14452 assert_eq!(
14453 super::TemplateInvariantKind::STATIC_MESSAGES,
14454 [
14455 super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE,
14456 super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE,
14457 ],
14458 "STATIC_MESSAGES must compose from the two per-role message constants in declaration order",
14459 );
14460 }
14461
14462 #[test]
14463 fn template_invariant_kind_message_static_projects_static_subset_to_typed_option() {
14464 // Pin `message_static(self) -> Option<&'static str>`'s per-arm
14465 // truth table: the two STATIC arms project to `Some(bytes)`
14466 // routed through the per-role `pub const`s; the two dynamic
14467 // arms (regardless of payload) project to `None`. Payload
14468 // invariance on the dynamic arms is load-bearing: a
14469 // regression that surfaces the payload's `usize` bytes on
14470 // the `Option<&'static str>` axis (say, by lifting the
14471 // format prefix to a per-role static-str) would silently
14472 // widen the static subset — this test pins the current
14473 // 2-of-4 carving.
14474 assert_eq!(
14475 super::TemplateInvariantKind::EndListEmptyStack.message_static(),
14476 Some(super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE),
14477 );
14478 assert_eq!(
14479 super::TemplateInvariantKind::FinalNoValue.message_static(),
14480 Some(super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE),
14481 );
14482 // Dynamic arms project to None regardless of payload value —
14483 // the `usize` bytes format into `message()`'s dynamic
14484 // fallthrough closure and cannot bind to a `&'static str`
14485 // on this axis.
14486 for idx in [0_usize, 1, 42, 99, usize::MAX] {
14487 assert_eq!(
14488 super::TemplateInvariantKind::SubstBadIndex(idx).message_static(),
14489 None,
14490 "SubstBadIndex({idx}) must project to None — the `usize` payload cannot bind to a &'static str",
14491 );
14492 assert_eq!(
14493 super::TemplateInvariantKind::SpliceBadIndex(idx).message_static(),
14494 None,
14495 "SpliceBadIndex({idx}) must project to None — the `usize` payload cannot bind to a &'static str",
14496 );
14497 }
14498 }
14499
14500 #[test]
14501 fn template_invariant_kind_static_messages_align_with_message_static_by_index() {
14502 // Pin the ALIGNMENT contract:
14503 // `Self::STATIC_MESSAGES[0] == Self::EndListEmptyStack.message_static().unwrap()`
14504 // and
14505 // `Self::STATIC_MESSAGES[1] == Self::FinalNoValue.message_static().unwrap()`
14506 // element-wise. A regression that swaps the array's ordering
14507 // against the projection method's arm order (say, a future
14508 // refactor that reorders the enum variants without
14509 // reordering the array) fails loudly here. Sibling posture
14510 // to `kwarg_path_kind_labels_align_with_all_by_index`,
14511 // `expected_kwarg_shape_labels_align_with_all_by_index`,
14512 // `macro_def_head_keywords_align_with_all_by_index` on the
14513 // peer closed-set surfaces.
14514 assert_eq!(
14515 super::TemplateInvariantKind::STATIC_MESSAGES[0],
14516 super::TemplateInvariantKind::EndListEmptyStack
14517 .message_static()
14518 .unwrap(),
14519 );
14520 assert_eq!(
14521 super::TemplateInvariantKind::STATIC_MESSAGES[1],
14522 super::TemplateInvariantKind::FinalNoValue
14523 .message_static()
14524 .unwrap(),
14525 );
14526 }
14527
14528 #[test]
14529 fn template_invariant_kind_message_delegates_static_arms_through_message_static() {
14530 // Pin the `message()` composition contract: for every arm on
14531 // the STATIC 2-of-4 subset,
14532 // `variant.message() == variant.message_static().unwrap().to_string()`
14533 // — the projection method routes the STATIC arms through
14534 // `message_static()`, and any regression that re-inlines the
14535 // per-role `.into()` at `message()` (undoing the composition)
14536 // still passes the per-variant byte pins but fails THIS
14537 // composition assertion. Sibling posture to a hypothetical
14538 // `OptionalParamMalformedReason::label_delegates_static_arms_through_label_static`
14539 // on the peer 3-of-4 static carving of the sibling optional-
14540 // section-malformed four-arm closed set.
14541 for (variant, static_bytes) in [
14542 (
14543 super::TemplateInvariantKind::EndListEmptyStack,
14544 super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE,
14545 ),
14546 (
14547 super::TemplateInvariantKind::FinalNoValue,
14548 super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE,
14549 ),
14550 ] {
14551 assert_eq!(
14552 variant.message(),
14553 static_bytes,
14554 "message() for {variant:?} must equal its message_static() bytes — routed through the typed subset projection",
14555 );
14556 }
14557 // Dynamic arms retain the format!() behavior — the composition
14558 // routes them through the fallthrough closure. Pin that the
14559 // outer `map_or_else` did not accidentally swallow the
14560 // payload's `usize` bytes.
14561 assert_eq!(
14562 super::TemplateInvariantKind::SubstBadIndex(7).message(),
14563 "compiled template referenced bad param index 7",
14564 );
14565 assert_eq!(
14566 super::TemplateInvariantKind::SpliceBadIndex(11).message(),
14567 "compiled template referenced bad splice index 11",
14568 );
14569 }
14570
14571 #[test]
14572 fn template_invariant_kind_dynamic_descriptor_constants_pin_canonical_bytes() {
14573 // Per-role `pub const &'static str` truth pins on the DYNAMIC
14574 // 2-of-4 subset carving of the four-arm closed set. Pins the
14575 // canonical descriptor bytes byte-for-byte so a regression on
14576 // any per-role const (a typo like `"parms"` for
14577 // `SUBST_BAD_INDEX_DESCRIPTOR`, a widening drift like
14578 // `"splice-target"` for `SPLICE_BAD_INDEX_DESCRIPTOR`) fails
14579 // loudly here rather than silently drifting the rendered
14580 // diagnostic bytes downstream (through `message()` /
14581 // `LispError::Display`). Peer to the STATIC 2-of-4 subset's
14582 // `template_invariant_kind_static_message_constants_pin_canonical_bytes`.
14583 assert_eq!(
14584 super::TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR,
14585 "param",
14586 );
14587 assert_eq!(
14588 super::TemplateInvariantKind::SPLICE_BAD_INDEX_DESCRIPTOR,
14589 "splice",
14590 );
14591 }
14592
14593 #[test]
14594 fn template_invariant_kind_dynamic_descriptors_pin_two_arm_forced_arity_array() {
14595 // Pin the ARITY + COMPOSITION contract of the DYNAMIC 2-of-4
14596 // subset ALL array. A regression that grows the array
14597 // without adding a per-role const + `descriptor_dynamic` arm
14598 // fails compilation (forced-arity `[&'static str; 2]`); a
14599 // regression that reorders the array against the projection
14600 // method's arm order fails
14601 // `template_invariant_kind_dynamic_descriptors_align_with_descriptor_dynamic_by_index`
14602 // in lockstep. Cardinality pin here is a fail-loud belt-and-
14603 // braces peer to the compile-time `[_; 2]` check.
14604 assert_eq!(
14605 super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS.len(),
14606 2,
14607 "DYNAMIC_DESCRIPTORS must have exactly two entries — the 2-of-4 dynamic subset carving",
14608 );
14609 assert_eq!(
14610 super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS,
14611 [
14612 super::TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR,
14613 super::TemplateInvariantKind::SPLICE_BAD_INDEX_DESCRIPTOR,
14614 ],
14615 "DYNAMIC_DESCRIPTORS must compose from the two per-role descriptor constants in declaration order",
14616 );
14617 }
14618
14619 #[test]
14620 fn template_invariant_kind_descriptor_dynamic_projects_dynamic_subset_to_typed_option() {
14621 // Pin `descriptor_dynamic(self) -> Option<&'static str>`'s
14622 // per-arm truth table: the two DYNAMIC arms project to
14623 // `Some(descriptor)` routed through the per-role `pub const`s
14624 // regardless of `usize` payload value; the two STATIC arms
14625 // project to `None`. Payload invariance on the DYNAMIC arms is
14626 // load-bearing: a regression that surfaces the payload's
14627 // `usize` bytes on the descriptor axis (say, by folding the
14628 // idx into a Format-typed descriptor slot) would silently
14629 // widen the DYNAMIC descriptor from a per-arm noun to a
14630 // per-callsite string — this test pins the current per-arm
14631 // per-noun carving. Sibling posture to
14632 // `template_invariant_kind_message_static_projects_static_subset_to_typed_option`.
14633 for idx in [0_usize, 1, 42, 99, usize::MAX] {
14634 assert_eq!(
14635 super::TemplateInvariantKind::SubstBadIndex(idx).descriptor_dynamic(),
14636 Some(super::TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR),
14637 "SubstBadIndex({idx}) must project to Some(SUBST_BAD_INDEX_DESCRIPTOR) — descriptor is per-arm, not per-idx",
14638 );
14639 assert_eq!(
14640 super::TemplateInvariantKind::SpliceBadIndex(idx).descriptor_dynamic(),
14641 Some(super::TemplateInvariantKind::SPLICE_BAD_INDEX_DESCRIPTOR),
14642 "SpliceBadIndex({idx}) must project to Some(SPLICE_BAD_INDEX_DESCRIPTOR) — descriptor is per-arm, not per-idx",
14643 );
14644 }
14645 // STATIC arms project to None on the descriptor axis — their
14646 // full bytes bind on the message_static() axis, and they carry
14647 // no per-role descriptor slot to project.
14648 assert_eq!(
14649 super::TemplateInvariantKind::EndListEmptyStack.descriptor_dynamic(),
14650 None,
14651 );
14652 assert_eq!(
14653 super::TemplateInvariantKind::FinalNoValue.descriptor_dynamic(),
14654 None,
14655 );
14656 }
14657
14658 #[test]
14659 fn template_invariant_kind_dynamic_descriptors_align_with_descriptor_dynamic_by_index() {
14660 // Pin the ALIGNMENT contract:
14661 // `Self::DYNAMIC_DESCRIPTORS[0] == Self::SubstBadIndex(_).descriptor_dynamic().unwrap()`
14662 // and
14663 // `Self::DYNAMIC_DESCRIPTORS[1] == Self::SpliceBadIndex(_).descriptor_dynamic().unwrap()`
14664 // element-wise. A regression that swaps the array's ordering
14665 // against the projection method's arm order fails loudly
14666 // here. Sibling posture to
14667 // `template_invariant_kind_static_messages_align_with_message_static_by_index`.
14668 assert_eq!(
14669 super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS[0],
14670 super::TemplateInvariantKind::SubstBadIndex(0)
14671 .descriptor_dynamic()
14672 .unwrap(),
14673 );
14674 assert_eq!(
14675 super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS[1],
14676 super::TemplateInvariantKind::SpliceBadIndex(0)
14677 .descriptor_dynamic()
14678 .unwrap(),
14679 );
14680 }
14681
14682 #[test]
14683 fn template_invariant_kind_message_dynamic_projects_dynamic_subset_to_typed_option() {
14684 // Pin `message_dynamic(self) -> Option<String>`'s per-arm
14685 // truth table: the two DYNAMIC arms project to
14686 // `Some(rendered)` composing the shared
14687 // `"compiled template referenced bad {descriptor} index {idx}"`
14688 // template with their per-arm descriptor const + payload; the
14689 // two STATIC arms project to `None`. The DYNAMIC-subset bytes
14690 // are byte-for-byte equivalent to the pre-lift inline
14691 // `format!(...)` arms — authoring-tool substring greps see no
14692 // drift across the lift.
14693 for idx in [0_usize, 1, 42, 99] {
14694 assert_eq!(
14695 super::TemplateInvariantKind::SubstBadIndex(idx).message_dynamic(),
14696 Some(format!("compiled template referenced bad param index {idx}")),
14697 "SubstBadIndex({idx}).message_dynamic() must render the shared template with the `param` descriptor and idx payload",
14698 );
14699 assert_eq!(
14700 super::TemplateInvariantKind::SpliceBadIndex(idx).message_dynamic(),
14701 Some(format!("compiled template referenced bad splice index {idx}")),
14702 "SpliceBadIndex({idx}).message_dynamic() must render the shared template with the `splice` descriptor and idx payload",
14703 );
14704 }
14705 // STATIC arms project to None on the message_dynamic axis —
14706 // their full bytes bind on the message_static() axis and carry
14707 // no `usize` payload to format into the shared template.
14708 assert_eq!(
14709 super::TemplateInvariantKind::EndListEmptyStack.message_dynamic(),
14710 None,
14711 );
14712 assert_eq!(
14713 super::TemplateInvariantKind::FinalNoValue.message_dynamic(),
14714 None,
14715 );
14716 }
14717
14718 #[test]
14719 fn template_invariant_kind_static_dynamic_projections_partition_closed_set() {
14720 // Pin the PARTITION contract: for every legal variant, EXACTLY
14721 // ONE of `message_static(self)` and `descriptor_dynamic(self)`
14722 // returns `Some`. Together the two typed subset projections
14723 // partition the four-arm closed set into its STATIC ⊕ DYNAMIC
14724 // halves at zero allocation cost. The partition is what makes
14725 // `message()`'s `.expect(...)` structurally unreachable —
14726 // rustc's exhaustive-match checks on both projections keep the
14727 // partition sound; this test pins the runtime witness. A
14728 // regression that widens EITHER subset (say, a lift that
14729 // projects `SubstBadIndex` into a per-role STATIC message)
14730 // fails loudly here because both projections would return
14731 // `Some` for the same variant.
14732 for variant in [
14733 super::TemplateInvariantKind::EndListEmptyStack,
14734 super::TemplateInvariantKind::FinalNoValue,
14735 super::TemplateInvariantKind::SubstBadIndex(0),
14736 super::TemplateInvariantKind::SubstBadIndex(usize::MAX),
14737 super::TemplateInvariantKind::SpliceBadIndex(0),
14738 super::TemplateInvariantKind::SpliceBadIndex(usize::MAX),
14739 ] {
14740 let static_hit = variant.message_static().is_some();
14741 let dynamic_hit = variant.descriptor_dynamic().is_some();
14742 assert!(
14743 static_hit ^ dynamic_hit,
14744 "message_static/descriptor_dynamic must partition the closed set — \
14745 exactly one projection returns Some for {variant:?}, got static_hit={static_hit} dynamic_hit={dynamic_hit}",
14746 );
14747 }
14748 }
14749
14750 #[test]
14751 fn template_invariant_kind_message_delegates_dynamic_arms_through_message_dynamic() {
14752 // Pin the `message()` composition contract on the DYNAMIC side:
14753 // for every arm on the DYNAMIC 2-of-4 subset,
14754 // `variant.message() == variant.message_dynamic().unwrap()`
14755 // — the projection method routes the DYNAMIC arms through
14756 // `message_dynamic()` (which itself routes through
14757 // `descriptor_dynamic()`), and any regression that re-inlines
14758 // the per-arm `format!(...)` at `message()` (undoing the
14759 // composition) still passes the per-variant byte pins but
14760 // fails THIS composition assertion. Peer to
14761 // `template_invariant_kind_message_delegates_static_arms_through_message_static`.
14762 for variant in [
14763 super::TemplateInvariantKind::SubstBadIndex(0),
14764 super::TemplateInvariantKind::SubstBadIndex(99),
14765 super::TemplateInvariantKind::SubstBadIndex(usize::MAX),
14766 super::TemplateInvariantKind::SpliceBadIndex(0),
14767 super::TemplateInvariantKind::SpliceBadIndex(42),
14768 super::TemplateInvariantKind::SpliceBadIndex(usize::MAX),
14769 ] {
14770 assert_eq!(
14771 variant.message(),
14772 variant.message_dynamic().unwrap(),
14773 "message() for {variant:?} must equal its message_dynamic() bytes — routed through the typed subset projection",
14774 );
14775 }
14776 }
14777
14778 #[test]
14779 fn template_invariant_display_renders_legacy_compile_shape_for_subst_bad_idx() {
14780 // End-to-end through the `LispError` Display impl — pins the
14781 // rendered diagnostic byte-for-byte: `"compile error in
14782 // {macro_name}: compiled template referenced bad param index
14783 // {idx}"`. Authoring tools that substring-grep the rendered
14784 // diagnostic (`tatara-check`, REPL substring-greps) see no
14785 // drift across the lift from the pre-lift `Compile { form:
14786 // macro_name, message: format!(...) }` shape.
14787 let err = LispError::TemplateInvariant {
14788 macro_name: "test-macro".into(),
14789 kind: super::TemplateInvariantKind::SubstBadIndex(99),
14790 };
14791 assert_eq!(
14792 format!("{err}"),
14793 "compile error in test-macro: compiled template referenced bad param index 99"
14794 );
14795 }
14796
14797 #[test]
14798 fn template_invariant_display_renders_legacy_compile_shape_for_splice_bad_idx() {
14799 // Sibling Display test for the Splice gate. Pins the message
14800 // byte-for-byte: `"compile error in call-macro: compiled
14801 // template referenced bad splice index 42"`.
14802 let err = LispError::TemplateInvariant {
14803 macro_name: "call-macro".into(),
14804 kind: super::TemplateInvariantKind::SpliceBadIndex(42),
14805 };
14806 assert_eq!(
14807 format!("{err}"),
14808 "compile error in call-macro: compiled template referenced bad splice index 42"
14809 );
14810 }
14811
14812 #[test]
14813 fn template_invariant_display_renders_legacy_compile_shape_for_endlist() {
14814 // Sibling Display test for the EndList gate. Pins the static-
14815 // message byte-for-byte: `"compile error in wrap: compiled
14816 // template: EndList with empty stack"`. Even though this gate
14817 // is currently guarded by `last_mut().unwrap()` and not
14818 // reachable through any single CompiledTemplate, the structural
14819 // variant carries the canonical message verbatim — defensive
14820 // against future changes to the stack discipline.
14821 let err = LispError::TemplateInvariant {
14822 macro_name: "wrap".into(),
14823 kind: super::TemplateInvariantKind::EndListEmptyStack,
14824 };
14825 assert_eq!(
14826 format!("{err}"),
14827 "compile error in wrap: compiled template: EndList with empty stack"
14828 );
14829 }
14830
14831 #[test]
14832 fn template_invariant_display_renders_legacy_compile_shape_for_final_no_value() {
14833 // Sibling Display test for the final-no-value gate. Pins the
14834 // static-message byte-for-byte: `"compile error in id: compiled
14835 // template produced no value"`. Closes the structural-
14836 // completeness floor of the closed-set `TemplateInvariantKind`
14837 // × Display matrix — all four reachable kinds are pinned.
14838 let err = LispError::TemplateInvariant {
14839 macro_name: "id".into(),
14840 kind: super::TemplateInvariantKind::FinalNoValue,
14841 };
14842 assert_eq!(
14843 format!("{err}"),
14844 "compile error in id: compiled template produced no value"
14845 );
14846 }
14847
14848 #[test]
14849 fn template_invariant_display_preserves_legacy_substring_for_message_grep() {
14850 // Pin the legacy substring set — `"compiled template"`,
14851 // `"bad param index"`, `"bad splice index"`, `"EndList with
14852 // empty stack"`, `"produced no value"` — as a separate
14853 // assertion so a regression that drifts ANY of the four
14854 // surface words (e.g., to "invalid", "missing", "no result")
14855 // fails-loudly here. The substrings are what consumers
14856 // downstream (`tatara-check`, REPL) substring-match on today.
14857 let subst = LispError::TemplateInvariant {
14858 macro_name: "m".into(),
14859 kind: super::TemplateInvariantKind::SubstBadIndex(0),
14860 };
14861 let msg = format!("{subst}");
14862 assert!(
14863 msg.contains("compiled template"),
14864 "expected `compiled template` prefix, got: {msg}"
14865 );
14866 assert!(
14867 msg.contains("bad param index"),
14868 "expected `bad param index` substring, got: {msg}"
14869 );
14870
14871 let splice = LispError::TemplateInvariant {
14872 macro_name: "m".into(),
14873 kind: super::TemplateInvariantKind::SpliceBadIndex(0),
14874 };
14875 let msg = format!("{splice}");
14876 assert!(
14877 msg.contains("bad splice index"),
14878 "expected `bad splice index` substring, got: {msg}"
14879 );
14880
14881 let endlist = LispError::TemplateInvariant {
14882 macro_name: "m".into(),
14883 kind: super::TemplateInvariantKind::EndListEmptyStack,
14884 };
14885 let msg = format!("{endlist}");
14886 assert!(
14887 msg.contains("EndList with empty stack"),
14888 "expected `EndList with empty stack` substring, got: {msg}"
14889 );
14890
14891 let final_nv = LispError::TemplateInvariant {
14892 macro_name: "m".into(),
14893 kind: super::TemplateInvariantKind::FinalNoValue,
14894 };
14895 let msg = format!("{final_nv}");
14896 assert!(
14897 msg.contains("produced no value"),
14898 "expected `produced no value` substring, got: {msg}"
14899 );
14900 }
14901
14902 #[test]
14903 fn template_invariant_kind_is_copy_and_partial_eq() {
14904 // Pin the closed-set posture: `TemplateInvariantKind` derives
14905 // Copy + PartialEq + Eq + Debug so it composes ergonomically
14906 // in tests and in consumer pattern-matches (no clone-and-own
14907 // dance). Same posture as `CompilerSpecIoStage` and
14908 // `MacroDefHead`. A regression that drops Copy fails-loudly
14909 // here (the let-binding would move out instead of copy).
14910 let kind = super::TemplateInvariantKind::SubstBadIndex(7);
14911 let copied = kind;
14912 assert_eq!(kind, copied);
14913 assert_eq!(kind, super::TemplateInvariantKind::SubstBadIndex(7));
14914 assert_ne!(kind, super::TemplateInvariantKind::SubstBadIndex(8));
14915 assert_ne!(kind, super::TemplateInvariantKind::SpliceBadIndex(7));
14916 assert_ne!(kind, super::TemplateInvariantKind::EndListEmptyStack);
14917 }
14918
14919 #[test]
14920 fn template_invariant_kind_index_payload_is_structurally_scoped_to_index_carrying_variants() {
14921 // The closed-set invariant: only `SubstBadIndex` and
14922 // `SpliceBadIndex` carry a `usize` payload; `EndListEmptyStack`
14923 // and `FinalNoValue` are bare. This is enforced by the variant
14924 // shape itself — there is no way to construct
14925 // `EndListEmptyStack(7)` because the variant has no fields.
14926 // This test pins the structural shape: the four reachable
14927 // failure modes split 2+2 into "index-carrying" and "bare".
14928 // A regression that adds a payload to the bare variants (or
14929 // strips it from the index-carrying ones) fails to compile,
14930 // making this test redundant — but the test documents the
14931 // shape for readers walking the closed set.
14932 match super::TemplateInvariantKind::SubstBadIndex(5) {
14933 super::TemplateInvariantKind::SubstBadIndex(idx) => assert_eq!(idx, 5),
14934 other => panic!("expected SubstBadIndex, got {other:?}"),
14935 }
14936 match super::TemplateInvariantKind::EndListEmptyStack {
14937 super::TemplateInvariantKind::EndListEmptyStack => {}
14938 other => panic!("expected EndListEmptyStack, got {other:?}"),
14939 }
14940 }
14941
14942 // --- MacroDefHead typed-slot lift (the closed-set promotion) ---
14943 //
14944 // The next eight tests pin the typed-slot promotion that closes
14945 // the three-times rule across the `LispError::Defmacro*` family.
14946 // Before this lift the three variants' `head` slot was
14947 // `&'static str`, projected from a `MacroDefHead` via
14948 // `head.keyword()` at the helper boundary; consumers had to
14949 // substring-compare against three string literals to recognize
14950 // a head identity. After the lift the slot IS the typed enum,
14951 // so authoring tools (REPL, LSP, `tatara-check`) pattern-match
14952 // on `MacroDefHead::Defmacro` etc. directly — same posture as
14953 // `CompilerSpecIoStage` for `LispError::CompilerSpecIo` and
14954 // `TemplateInvariantKind` for `LispError::TemplateInvariant`.
14955
14956 #[test]
14957 fn defmacro_arity_head_slot_is_macro_def_head_not_static_str() {
14958 // Pin that the `head` slot of `LispError::DefmacroArity` is
14959 // `MacroDefHead` (the typed closed-set enum), not `&'static
14960 // str`. A regression that reverts the slot to `&'static str`
14961 // breaks the typed binding here at compile time; a
14962 // construction with a stringly-typed head would fail to
14963 // construct. This test is the structural-completeness pin
14964 // for the typed-slot promotion, parallel to how
14965 // `compiler_spec_io_carries_typed_stage_field` (if it
14966 // existed) would pin `LispError::CompilerSpecIo.stage`.
14967 let err = LispError::DefmacroArity {
14968 head: MacroDefHead::Defmacro,
14969 arity: 1,
14970 };
14971 match err {
14972 LispError::DefmacroArity { head, arity } => {
14973 assert_eq!(head, MacroDefHead::Defmacro);
14974 assert_eq!(arity, 1);
14975 }
14976 other => panic!("expected DefmacroArity, got {other:?}"),
14977 }
14978 }
14979
14980 #[test]
14981 fn defmacro_non_symbol_name_head_slot_is_macro_def_head_not_static_str() {
14982 // Sibling pin of `defmacro_arity_head_slot_is_macro_def_head_not_static_str`
14983 // for the `LispError::DefmacroNonSymbolName` variant. The
14984 // `head` slot carries `MacroDefHead` directly so consumers
14985 // bind on variant identity (`MacroDefHead::DefpointTemplate`)
14986 // instead of substring-matching the rendered diagnostic.
14987 let err = LispError::DefmacroNonSymbolName {
14988 head: MacroDefHead::DefpointTemplate,
14989 got: SexpWitness::new(SexpShape::Int, "5"),
14990 };
14991 match err {
14992 LispError::DefmacroNonSymbolName { head, got } => {
14993 assert_eq!(head, MacroDefHead::DefpointTemplate);
14994 assert_eq!(got.shape, SexpShape::Int);
14995 assert_eq!(got.display, "5");
14996 }
14997 other => panic!("expected DefmacroNonSymbolName, got {other:?}"),
14998 }
14999 }
15000
15001 #[test]
15002 fn defmacro_non_list_params_head_slot_is_macro_def_head_not_static_str() {
15003 // Sibling pin of `defmacro_arity_head_slot_is_macro_def_head_not_static_str`
15004 // for the `LispError::DefmacroNonListParams` variant. The
15005 // `head` slot carries `MacroDefHead` directly so consumers
15006 // pattern-match on `MacroDefHead::Defcheck` etc. for the
15007 // workspace-coherence authoring surface's third head
15008 // keyword.
15009 let err = LispError::DefmacroNonListParams {
15010 head: MacroDefHead::Defcheck,
15011 got: SexpWitness::new(SexpShape::Int, "7"),
15012 };
15013 match err {
15014 LispError::DefmacroNonListParams { head, got } => {
15015 assert_eq!(head, MacroDefHead::Defcheck);
15016 assert_eq!(got.shape, SexpShape::Int);
15017 assert_eq!(got.display, "7");
15018 }
15019 other => panic!("expected DefmacroNonListParams, got {other:?}"),
15020 }
15021 }
15022
15023 #[test]
15024 fn macro_def_head_display_renders_canonical_keyword_for_each_variant() {
15025 // Pin `MacroDefHead`'s Display impl — it must project through
15026 // `keyword()` so the `#[error(...)]` annotation on each
15027 // `LispError::Defmacro*` variant renders the canonical
15028 // `&'static str` literal byte-for-byte. The Display
15029 // bidirection is `MacroDefHead → &'static str`; the inverse
15030 // (`&str → Option<MacroDefHead>`) lives in `from_keyword`.
15031 // Together the two close the bidirectional projection on the
15032 // closed set.
15033 assert_eq!(format!("{}", MacroDefHead::Defmacro), "defmacro");
15034 assert_eq!(
15035 format!("{}", MacroDefHead::DefpointTemplate),
15036 "defpoint-template"
15037 );
15038 assert_eq!(format!("{}", MacroDefHead::Defcheck), "defcheck");
15039 }
15040
15041 // ── `MacroDefHead::{DEFMACRO_KEYWORD, DEFPOINT_TEMPLATE_KEYWORD,
15042 // DEFCHECK_KEYWORD, KEYWORDS}` — the typed per-role head-keyword
15043 // algebra ─────────────────────────────────────────────────────
15044 //
15045 // Sibling posture to
15046 // `crate::macro_expand::MacroParams::{REST_MARKER,
15047 // OPTIONAL_MARKER, LAMBDA_LIST_KEYWORDS}` — the per-role canonical
15048 // `&'static str` marker + closed-set `[&'static str; N]` array
15049 // pattern applied to the `MacroDefHead` closed set. Pre-lift the
15050 // three canonical keyword literals lived at TWO structural sites
15051 // — the `Self::keyword` match arm AND
15052 // `macro_def_head_display_renders_canonical_keyword_for_each_variant`'s
15053 // per-variant `assert_eq!` truth-table — plus at consumer
15054 // test-site literals in `crate::ast` / `crate::compile`. Post-lift
15055 // each (variant, canonical `&'static str`) pairing binds at ONE
15056 // `pub const` on the typed [`MacroDefHead`] algebra the
15057 // [`Self::keyword`] arm routes through.
15058
15059 #[test]
15060 fn macro_def_head_defmacro_keyword_projects_canonical_defmacro_bytes() {
15061 // Pins the exact `"defmacro"` bytes at the typed constant.
15062 // A regression that drifts the constant (e.g. typo
15063 // `"def-macro"` with a hyphen, `"defMacro"` in camelCase, or
15064 // an accidental trailing whitespace) fails-loudly here.
15065 // This is the single site the substrate's canonical-Lisp
15066 // macro-definition head keyword resolves to; every downstream
15067 // consumer (Display, FromStr, tests, LSP completion, metric
15068 // labels) routes through this constant.
15069 assert_eq!(
15070 MacroDefHead::DEFMACRO_KEYWORD,
15071 "defmacro",
15072 "MacroDefHead::DEFMACRO_KEYWORD drifted from the substrate- \
15073 canonical CL-style macro-definition head keyword `\"defmacro\"`"
15074 );
15075 }
15076
15077 #[test]
15078 fn macro_def_head_defpoint_template_keyword_projects_canonical_defpoint_template_bytes() {
15079 // Pins the exact `"defpoint-template"` bytes at the typed
15080 // constant. The hyphen (NOT an underscore, NOT a camel-case
15081 // capital) is load-bearing: it matches the caixa Aplicacao
15082 // authoring surface's convention where multi-word Lisp-style
15083 // identifiers use kebab-case rather than snake_case.
15084 assert_eq!(
15085 MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD,
15086 "defpoint-template",
15087 "MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD drifted from the substrate- \
15088 canonical K8s-as-processes authoring-surface head keyword \
15089 `\"defpoint-template\"`"
15090 );
15091 }
15092
15093 #[test]
15094 fn macro_def_head_defcheck_keyword_projects_canonical_defcheck_bytes() {
15095 // Pins the exact `"defcheck"` bytes at the typed constant.
15096 // The `tatara-reconciler/checks.lisp` workspace-coherence
15097 // driver reads this exact keyword to classify `(defcheck …)`
15098 // forms; any drift here silently breaks the check runner.
15099 assert_eq!(
15100 MacroDefHead::DEFCHECK_KEYWORD,
15101 "defcheck",
15102 "MacroDefHead::DEFCHECK_KEYWORD drifted from the substrate- \
15103 canonical workspace-coherence authoring-surface head keyword \
15104 `\"defcheck\"`"
15105 );
15106 }
15107
15108 #[test]
15109 fn macro_def_head_keyword_method_routes_through_typed_constants() {
15110 // PATH-UNIFORMITY: the inherent `Self::keyword(self)` method
15111 // MUST return the per-role `pub const` byte-for-byte for each
15112 // reachable variant. A regression that reverts ONE arm to an
15113 // inline `"defmacro"` string literal (e.g. a merge-conflict
15114 // resolution that picked the pre-lift form) silently
15115 // reintroduces the ≥2 PRIME-DIRECTIVE trigger the lift
15116 // resolved — this test catches that by pinning the arm's
15117 // return value to the constant, so the two paths (inline vs.
15118 // typed constant) cannot both hold.
15119 assert_eq!(
15120 MacroDefHead::Defmacro.keyword(),
15121 MacroDefHead::DEFMACRO_KEYWORD,
15122 "MacroDefHead::Defmacro.keyword() drifted from \
15123 MacroDefHead::DEFMACRO_KEYWORD — the match arm reverted to \
15124 an inline literal"
15125 );
15126 assert_eq!(
15127 MacroDefHead::DefpointTemplate.keyword(),
15128 MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD,
15129 "MacroDefHead::DefpointTemplate.keyword() drifted from \
15130 MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD — the match arm \
15131 reverted to an inline literal"
15132 );
15133 assert_eq!(
15134 MacroDefHead::Defcheck.keyword(),
15135 MacroDefHead::DEFCHECK_KEYWORD,
15136 "MacroDefHead::Defcheck.keyword() drifted from \
15137 MacroDefHead::DEFCHECK_KEYWORD — the match arm reverted to \
15138 an inline literal"
15139 );
15140 }
15141
15142 #[test]
15143 fn macro_def_head_keywords_has_expected_cardinality() {
15144 // Cardinality contract: `Self::KEYWORDS.len() == 3` — pinned
15145 // at the declaration site by rustc's forced-arity check on
15146 // `[&'static str; 3]`. This test surfaces the arity as a
15147 // fail-loud runtime pin so a future refactor that switches
15148 // the array type to `&[&'static str]` (dropping the compile-
15149 // time arity forcing) doesn't silently loosen the closed-set
15150 // discipline the family relies on. Sibling posture to
15151 // `macro_params_lambda_list_keywords_has_expected_cardinality`
15152 // on the `MacroParams::LAMBDA_LIST_KEYWORDS` family.
15153 assert_eq!(
15154 MacroDefHead::KEYWORDS.len(),
15155 3,
15156 "MacroDefHead::KEYWORDS cardinality drifted from 3 — the \
15157 head-keyword closed set gained or lost a variant without \
15158 the ALL / KEYWORDS pair being updated in tandem"
15159 );
15160 }
15161
15162 #[test]
15163 fn macro_def_head_keywords_align_with_all_by_index() {
15164 // ALIGNMENT CONTRACT: `Self::KEYWORDS[i] ==
15165 // Self::ALL[i].keyword()` element-wise. The two ALL arrays
15166 // (typed variants, canonical `&'static str` keywords) share
15167 // ONE canonical declaration order — a regression that
15168 // reorders ONE array without reordering the other silently
15169 // misaligns every `zip(Self::ALL, Self::KEYWORDS)` consumer
15170 // (LSP completion providers, metric-label emitters, coverage
15171 // reporters). Pinning by-index alignment here catches the
15172 // reorder at test time.
15173 assert_eq!(
15174 MacroDefHead::KEYWORDS.len(),
15175 MacroDefHead::ALL.len(),
15176 "MacroDefHead::KEYWORDS and MacroDefHead::ALL diverged in \
15177 cardinality — the per-role constants and enum variants must \
15178 stay in lockstep"
15179 );
15180 for (i, head) in MacroDefHead::ALL.iter().enumerate() {
15181 assert_eq!(
15182 MacroDefHead::KEYWORDS[i],
15183 head.keyword(),
15184 "MacroDefHead::KEYWORDS[{i}] `{kw}` drifted from \
15185 MacroDefHead::ALL[{i}].keyword() `{via_variant}` — the \
15186 canonical declaration order of the two ALL arrays must \
15187 match element-wise",
15188 kw = MacroDefHead::KEYWORDS[i],
15189 via_variant = head.keyword(),
15190 );
15191 }
15192 }
15193
15194 #[test]
15195 fn macro_def_head_keywords_pairwise_distinct() {
15196 // PAIRWISE DISJOINTNESS: the three `&'static str` keywords on
15197 // the head-keyword algebra MUST differ so the derive-generated
15198 // FromStr's linear sweep cannot route two variants through
15199 // the same arm. Family-wide sweep over KEYWORDS × KEYWORDS —
15200 // supersedes any single per-pair assertion and picks up new
15201 // variants mechanically when the array grows. Sibling posture
15202 // to `macro_params_lambda_list_keywords_pairwise_distinct` on
15203 // the CL lambda-list-keyword family.
15204 for (i, a) in MacroDefHead::KEYWORDS.iter().enumerate() {
15205 for (j, b) in MacroDefHead::KEYWORDS.iter().enumerate() {
15206 if i == j {
15207 continue;
15208 }
15209 assert_ne!(
15210 a, b,
15211 "MacroDefHead::KEYWORDS[{i}] `{a}` collides with \
15212 MacroDefHead::KEYWORDS[{j}] `{b}` — the derive- \
15213 generated FromStr sweep would route two variants \
15214 through the same arm"
15215 );
15216 }
15217 }
15218 }
15219
15220 #[test]
15221 fn macro_def_head_keywords_all_round_trip_through_from_str() {
15222 // Family-wide bidirection: every entry of `Self::KEYWORDS`
15223 // MUST parse back to some `MacroDefHead` variant AND that
15224 // variant's `keyword()` MUST equal the original entry.
15225 // Sibling posture to `macro_def_head_keyword_round_trips_through_from_str`
15226 // (which sweeps over `Self::ALL`); this test sweeps over the
15227 // per-role `&'static str` constants directly, catching a
15228 // regression where one of the three constants drifts from
15229 // the corresponding `Self::keyword` arm (e.g. `DEFCHECK_KEYWORD`
15230 // set to `"defvalidation"` while `Self::Defcheck.keyword()`
15231 // still returns `Self::DEFCHECK_KEYWORD`, which would parse
15232 // to `MacroDefHead::Defcheck` correctly but break every
15233 // downstream consumer that pinned the literal `"defcheck"`
15234 // bytes).
15235 for kw in MacroDefHead::KEYWORDS {
15236 let parsed: MacroDefHead = kw.parse().unwrap_or_else(|_| {
15237 panic!(
15238 "MacroDefHead::KEYWORDS entry `{kw}` failed to parse — \
15239 the entry drifted from Self::keyword"
15240 )
15241 });
15242 assert_eq!(
15243 parsed.keyword(),
15244 kw,
15245 "MacroDefHead::KEYWORDS entry `{kw}` parses to a variant \
15246 whose keyword() `{}` does not match — the constant and \
15247 the match arm drifted apart",
15248 parsed.keyword(),
15249 );
15250 }
15251 }
15252
15253 #[test]
15254 fn defmacro_arity_display_renders_legacy_prefix_via_macro_def_head_display() {
15255 // End-to-end through `LispError`'s Display: the typed `head:
15256 // MacroDefHead` slot projects to the canonical `&'static
15257 // str` literal at render time via `MacroDefHead`'s Display
15258 // impl, so the rendered diagnostic is byte-for-byte
15259 // identical to the pre-lift `head: &'static str` shape.
15260 // Authoring tools that substring-match on `"compile error
15261 // in defmacro:"` see no drift across the typed-slot
15262 // promotion.
15263 let err = LispError::DefmacroArity {
15264 head: MacroDefHead::Defmacro,
15265 arity: 2,
15266 };
15267 assert_eq!(
15268 format!("{err}"),
15269 "compile error in defmacro: (defmacro name (params) body) required \
15270 (got 2 elements, need 4)"
15271 );
15272 }
15273
15274 #[test]
15275 fn defmacro_non_symbol_name_display_renders_via_macro_def_head_display_for_defpoint_template() {
15276 // Sibling end-to-end test for the `defpoint-template` head:
15277 // pins that the typed-slot promotion preserves the
15278 // K8s-as-processes authoring surface's diagnostic shape
15279 // byte-for-byte. A regression that drifts `MacroDefHead`'s
15280 // Display impl (e.g. returns `"DefpointTemplate"` instead of
15281 // `"defpoint-template"`) fails-loudly here.
15282 let err = LispError::DefmacroNonSymbolName {
15283 head: MacroDefHead::DefpointTemplate,
15284 got: SexpWitness::new(SexpShape::Keyword, ":foo"),
15285 };
15286 assert_eq!(
15287 format!("{err}"),
15288 "compile error in defpoint-template: expected name symbol, got :foo"
15289 );
15290 }
15291
15292 #[test]
15293 fn defmacro_non_list_params_display_renders_via_macro_def_head_display_for_defcheck() {
15294 // Sibling end-to-end test for the `defcheck` head: pins that
15295 // the typed-slot promotion preserves the workspace-coherence
15296 // authoring surface's diagnostic shape byte-for-byte.
15297 let err = LispError::DefmacroNonListParams {
15298 head: MacroDefHead::Defcheck,
15299 got: SexpWitness::new(SexpShape::Symbol, "x"),
15300 };
15301 assert_eq!(
15302 format!("{err}"),
15303 "compile error in defcheck: expected param list, got x"
15304 );
15305 }
15306
15307 #[test]
15308 fn macro_def_head_is_copy_and_partial_eq_for_pattern_match_ergonomics() {
15309 // Pin that `MacroDefHead` derives `Copy + PartialEq + Eq +
15310 // Debug + Clone` — the posture every closed-set typed enum
15311 // in this module shares (`CompilerSpecIoStage`,
15312 // `TemplateInvariantKind`). Copy lets consumers pattern-match
15313 // on the variant without explicit cloning; `PartialEq + Eq`
15314 // makes `assert_eq!` and `matches!` ergonomic; `Debug` makes
15315 // the `other => panic!("got {other:?}")` shape ergonomic at
15316 // assertion sites. A regression that drops any of these
15317 // derives breaks compilation here.
15318 let h = MacroDefHead::Defmacro;
15319 let h_copy: MacroDefHead = h; // Copy
15320 assert_eq!(h, h_copy); // PartialEq
15321 assert!(matches!(h, MacroDefHead::Defmacro)); // exhaustive match
15322 let _: String = format!("{h:?}"); // Debug
15323 }
15324
15325 // --- UnquoteForm typed-slot lift (the closed-set promotion) ---
15326 //
15327 // The next six tests pin the typed-slot promotion that closes the
15328 // three-times rule across `LispError::UnboundTemplateVar` /
15329 // `LispError::NonSymbolUnquoteTarget` — the two template-marker
15330 // rejection variants for the no-evaluator template language.
15331 // Before this lift each variant's `prefix` slot was `&'static
15332 // str`, set from the literal `","` / `",@"` at four (Unbound) +
15333 // four (NonSymbol) call sites; consumers had to substring-compare
15334 // against two string literals to recognize the marker identity.
15335 // After the lift the slot IS the typed `UnquoteForm` enum, so
15336 // authoring tools (REPL, LSP, `tatara-check`) pattern-match on
15337 // `UnquoteForm::Splice` etc. directly — same posture as
15338 // `MacroDefHead` for `LispError::Defmacro*`, `CompilerSpecIoStage`
15339 // for `LispError::CompilerSpecIo`, `TemplateInvariantKind` for
15340 // `LispError::TemplateInvariant`.
15341
15342 #[test]
15343 fn unbound_template_var_prefix_slot_is_unquote_form_not_static_str() {
15344 // Pin that the `prefix` slot of `LispError::UnboundTemplateVar`
15345 // is `UnquoteForm` (the typed closed-set enum), not `&'static
15346 // str`. A regression that reverts the slot to `&'static str`
15347 // breaks the typed binding here at compile time; a
15348 // construction with a stringly-typed prefix would fail to
15349 // construct. This test is the structural-completeness pin
15350 // for the typed-slot promotion, parallel to how
15351 // `defmacro_arity_head_slot_is_macro_def_head_not_static_str`
15352 // pins `LispError::DefmacroArity.head`.
15353 let err = LispError::UnboundTemplateVar {
15354 prefix: UnquoteForm::Unquote,
15355 name: "xs".into(),
15356 hint: None,
15357 };
15358 match err {
15359 LispError::UnboundTemplateVar { prefix, name, hint } => {
15360 assert_eq!(prefix, UnquoteForm::Unquote);
15361 assert_eq!(name, "xs");
15362 assert_eq!(hint, None);
15363 }
15364 other => panic!("expected UnboundTemplateVar, got {other:?}"),
15365 }
15366 }
15367
15368 #[test]
15369 fn non_symbol_unquote_target_prefix_slot_is_unquote_form_not_static_str() {
15370 // Sibling pin for `LispError::NonSymbolUnquoteTarget`. The
15371 // `prefix` slot carries `UnquoteForm` directly so consumers
15372 // bind on variant identity (`UnquoteForm::Splice`) instead
15373 // of substring-matching the rendered diagnostic. Together
15374 // with `unbound_template_var_prefix_slot_is_unquote_form_not_static_str`,
15375 // the two pin the typed-slot promotion across ALL template-
15376 // marker rejection variants — the two reachable rejection
15377 // identities (`UnboundTemplateVar`, `NonSymbolUnquoteTarget`)
15378 // share ONE typed marker identity.
15379 let err = LispError::NonSymbolUnquoteTarget {
15380 prefix: UnquoteForm::Splice,
15381 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
15382 };
15383 match err {
15384 LispError::NonSymbolUnquoteTarget { prefix, got } => {
15385 assert_eq!(prefix, UnquoteForm::Splice);
15386 assert_eq!(got.shape, SexpShape::List);
15387 assert_eq!(got.display, "(list 1 2)");
15388 }
15389 other => panic!("expected NonSymbolUnquoteTarget, got {other:?}"),
15390 }
15391 }
15392
15393 #[test]
15394 fn unquote_form_marker_projects_canonical_literal_for_each_variant() {
15395 // Pin `UnquoteForm::marker()` — it must project each variant
15396 // to the canonical `&'static str` literal byte-for-byte. The
15397 // projection feeds the `#[error(...)]` annotation on
15398 // `LispError::UnboundTemplateVar` /
15399 // `LispError::NonSymbolUnquoteTarget` via the Display impl,
15400 // and the `unbound_hint_suffix` helper's `prefix.marker()`
15401 // call site. A regression that drifts the literal (e.g.,
15402 // returns `"un"` instead of `","`) fails-loudly here.
15403 assert_eq!(UnquoteForm::Unquote.marker(), ",");
15404 assert_eq!(UnquoteForm::Splice.marker(), ",@");
15405 }
15406
15407 #[test]
15408 fn unquote_form_display_renders_canonical_marker_for_each_variant() {
15409 // Pin `UnquoteForm`'s Display impl — it must project through
15410 // `marker()` so the `#[error(...)]` annotation on each
15411 // affected `LispError::*` variant renders the canonical
15412 // `&'static str` literal byte-for-byte. Same posture as
15413 // `MacroDefHead`'s Display impl (which projects through
15414 // `keyword()`).
15415 assert_eq!(format!("{}", UnquoteForm::Unquote), ",");
15416 assert_eq!(format!("{}", UnquoteForm::Splice), ",@");
15417 }
15418
15419 #[test]
15420 fn unbound_template_var_display_renders_canonical_marker_for_each_variant() {
15421 // End-to-end through `LispError`'s Display: the typed `prefix:
15422 // UnquoteForm` slot projects to the canonical `&'static str`
15423 // literal at render time via `UnquoteForm`'s Display impl,
15424 // so the rendered diagnostic is byte-for-byte identical to
15425 // the pre-lift `prefix: &'static str` shape. Authoring tools
15426 // that substring-match on `,xs` / `,@xs` see no drift across
15427 // the typed-slot promotion. Paired with
15428 // `non_symbol_unquote_target_display_renders_canonical_marker_for_each_variant`
15429 // to pin the lift end-to-end on BOTH affected variants.
15430 let unquote = LispError::UnboundTemplateVar {
15431 prefix: UnquoteForm::Unquote,
15432 name: "xs".into(),
15433 hint: None,
15434 };
15435 assert_eq!(format!("{unquote}"), "compile error in ,xs: unbound");
15436
15437 let splice = LispError::UnboundTemplateVar {
15438 prefix: UnquoteForm::Splice,
15439 name: "argz".into(),
15440 hint: Some("args".into()),
15441 };
15442 assert_eq!(
15443 format!("{splice}"),
15444 "compile error in ,@argz: unbound; did you mean ,@args?"
15445 );
15446 }
15447
15448 #[test]
15449 fn non_symbol_unquote_target_display_renders_canonical_marker_for_each_variant() {
15450 // Sibling end-to-end pin for `LispError::NonSymbolUnquoteTarget`.
15451 // Pins that the typed-slot promotion preserves the
15452 // template-marker diagnostic shape byte-for-byte for BOTH
15453 // variants of `UnquoteForm`.
15454 let unquote = LispError::NonSymbolUnquoteTarget {
15455 prefix: UnquoteForm::Unquote,
15456 got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
15457 };
15458 assert_eq!(
15459 format!("{unquote}"),
15460 "compile error in ,: expected symbol, got (list 1 2)"
15461 );
15462
15463 let splice = LispError::NonSymbolUnquoteTarget {
15464 prefix: UnquoteForm::Splice,
15465 got: SexpWitness::new(SexpShape::Int, "5"),
15466 };
15467 assert_eq!(
15468 format!("{splice}"),
15469 "compile error in ,@: expected symbol, got 5"
15470 );
15471 }
15472
15473 #[test]
15474 fn unquote_form_is_copy_and_partial_eq_for_pattern_match_ergonomics() {
15475 // Pin that `UnquoteForm` derives `Copy + PartialEq + Eq +
15476 // Debug + Clone` — the posture every closed-set typed enum
15477 // in this module shares (`MacroDefHead`, `CompilerSpecIoStage`,
15478 // `TemplateInvariantKind`). Copy lets consumers pattern-match
15479 // on the variant without explicit cloning; `PartialEq + Eq`
15480 // makes `assert_eq!` and `matches!` ergonomic; `Debug` makes
15481 // the `other => panic!("got {other:?}")` shape ergonomic at
15482 // assertion sites. A regression that drops any of these
15483 // derives breaks compilation here.
15484 let f = UnquoteForm::Splice;
15485 let f_copy: UnquoteForm = f; // Copy
15486 assert_eq!(f, f_copy); // PartialEq
15487 assert!(matches!(f, UnquoteForm::Splice)); // exhaustive match
15488 assert_ne!(f, UnquoteForm::Unquote); // Eq/Ne
15489 let _: String = format!("{f:?}"); // Debug
15490 }
15491
15492 #[test]
15493 fn kwarg_path_named_display_renders_legacy_colon_key_literal() {
15494 // `KwargPath::Named(":<key>")` Display must render the literal
15495 // `:<key>` byte-for-byte equivalent to the pre-lift
15496 // `format!(":{key}")` inline literal in `kwarg_form`. The
15497 // canonical literal lives in ONE place (the Display impl), so
15498 // a regression that drifts the prefix (e.g., to `key:` or
15499 // `<key>:`) fails-loudly here AND breaks every
15500 // `LispError::TypeMismatch.form` consumer that depends on the
15501 // `:<key>` shape (the substrate's hot path).
15502 assert_eq!(format!("{}", KwargPath::named("threshold")), ":threshold");
15503 }
15504
15505 #[test]
15506 fn kwarg_path_item_display_renders_legacy_colon_key_bracket_idx_literal() {
15507 // `KwargPath::Item { key, idx }` Display must render
15508 // `:<key>[<idx>]` byte-for-byte equivalent to the pre-lift
15509 // `format!(":{key}[{idx}]")` inline literal in `kwarg_item_form`.
15510 // The bracketed-index suffix is what `extract_string_list`'s
15511 // per-item failure path emits; a regression that drifts the
15512 // bracket-shape (e.g., to `:steps.1` or `:steps#1`) breaks every
15513 // LSP underline that depends on the bracket shape.
15514 assert_eq!(format!("{}", KwargPath::item("steps", 1)), ":steps[1]");
15515 }
15516
15517 #[test]
15518 fn kwarg_path_slot_display_renders_legacy_kwargs_bracket_idx_literal() {
15519 // `KwargPath::Slot(idx)` Display must render `kwargs[<idx>]`
15520 // byte-for-byte equivalent to the pre-lift
15521 // `format!("kwargs[{idx}]")` inline literal in `kwargs_pos_form`.
15522 // The `kwargs` prefix (no leading colon) is what `parse_kwargs`'s
15523 // "this-position-must-be-a-keyword" gate emits when the slot
15524 // failed BEFORE a key was known; the slot shape is structurally
15525 // distinct from the named-kwarg shape (`:<key>` vs `kwargs[i]`)
15526 // so consumers can bifurcate on path identity.
15527 assert_eq!(format!("{}", KwargPath::Slot(0)), "kwargs[0]");
15528 }
15529
15530 #[test]
15531 fn kwarg_path_named_carries_kebab_case_keys_unchanged() {
15532 // Kebab-cased kwarg names (`:notify-ref`, `:window-seconds`)
15533 // round-trip through the Display unchanged — the path shape
15534 // doesn't transform the key, it just wraps it in the `:<…>`
15535 // prefix. Pinning this contract means a regression that
15536 // camelCases or lowercases the key in the rendered prefix
15537 // fails-loudly here.
15538 assert_eq!(format!("{}", KwargPath::named("notify-ref")), ":notify-ref");
15539 assert_eq!(
15540 format!("{}", KwargPath::item("window-seconds", 3)),
15541 ":window-seconds[3]"
15542 );
15543 }
15544
15545 #[test]
15546 fn kwarg_path_is_clone_and_partial_eq_for_pattern_match_ergonomics() {
15547 // `KwargPath` derives Clone + Debug + PartialEq + Eq so that
15548 // pattern-matching call sites (REPL diagnostic capture,
15549 // `tatara-check`'s failure clustering, an LSP that surfaces
15550 // "your `:steps[3]` failed" with structural binding) compare
15551 // by reference cheaply AND inhabit the same kind of test
15552 // assertion shape as `MacroDefHead`, `UnquoteForm`,
15553 // `CompilerSpecIoStage`, and `TemplateInvariantKind`. `Copy`
15554 // is intentionally NOT derived because `String` is not `Copy`
15555 // — the owned key payload is the load-bearing property of the
15556 // typed-slot promotion onto `LispError::TypeMismatch.form`. A
15557 // regression that drops any of the retained derives or that
15558 // re-adds `Copy` breaks compilation here.
15559 let p = KwargPath::item("steps", 2);
15560 let p_clone = p.clone(); // Clone
15561 assert_eq!(p, p_clone); // PartialEq
15562 assert!(matches!(p, KwargPath::Item { idx: 2, .. })); // exhaustive match
15563 assert_ne!(p, KwargPath::Slot(2)); // Eq/Ne — Item and Slot are distinct path identities
15564 let _: String = format!("{p:?}"); // Debug
15565 }
15566
15567 #[test]
15568 fn kwarg_path_named_and_slot_have_distinct_display_shapes() {
15569 // The bifurcation between named-kwarg failures (`:<key>`) and
15570 // pre-key slot failures (`kwargs[<idx>]`) is structural — same
15571 // failure surface (kwargs gate), different path identity. Pin
15572 // the structural-distinctness: even at the rendered-string
15573 // level the two shapes don't collide. Two consumers depend on
15574 // this:
15575 // 1. `tatara-check`'s diagnostic capture, which groups by
15576 // path prefix — a slot failure must NOT be confused with
15577 // a `:kwargs`-keyed named-kwarg failure.
15578 // 2. An LSP's structural binding — the `KwargPath::Slot`
15579 // identity says "we don't know which kwarg yet"; the
15580 // `KwargPath::Named` identity says "we know the kwarg
15581 // identifier and it's this".
15582 let named = format!("{}", KwargPath::named("kwargs"));
15583 let slot = format!("{}", KwargPath::Slot(0));
15584 assert_eq!(named, ":kwargs");
15585 assert_eq!(slot, "kwargs[0]");
15586 assert_ne!(named, slot);
15587 }
15588
15589 #[test]
15590 fn type_mismatch_form_carries_typed_kwarg_path_named_through_variant_slot() {
15591 // After the typed-slot promotion, `LispError::TypeMismatch.form`
15592 // is `KwargPath` — owned, structurally bound to the closed-set
15593 // typed enum. Consumers (REPL, LSP, `tatara-check`) pattern-match
15594 // on the variant identity `KwargPath::Named(_)` directly rather
15595 // than substring-matching a rendered prefix. Pin the structural
15596 // binding AND the Display projection so the byte-for-byte
15597 // rendering contract is anchored from both angles. A regression
15598 // that re-introduces a String-shaped form (collapsing the typed
15599 // enum back into a free-form label) fails-loudly here.
15600 let err = LispError::TypeMismatch {
15601 form: KwargPath::named("threshold"),
15602 expected: ExpectedKwargShape::Number,
15603 got: SexpShape::String,
15604 };
15605 match &err {
15606 LispError::TypeMismatch { form, .. } => {
15607 assert_eq!(*form, KwargPath::Named("threshold".into()));
15608 }
15609 other => panic!("expected TypeMismatch, got {other:?}"),
15610 }
15611 assert_eq!(
15612 format!("{err}"),
15613 "compile error in :threshold: expected number, got string"
15614 );
15615 }
15616
15617 #[test]
15618 fn type_mismatch_form_carries_typed_kwarg_path_item_through_variant_slot() {
15619 // Sibling pin to `…_named_…` for the per-item path. The
15620 // `KwargPath::Item { key, idx }` shape names the offending kwarg
15621 // AND the failing item index in one structural variant; the
15622 // bracketed `:<key>[<idx>]` rendering is unchanged.
15623 let err = LispError::TypeMismatch {
15624 form: KwargPath::item("steps", 3),
15625 expected: ExpectedKwargShape::String,
15626 got: SexpShape::Int,
15627 };
15628 match &err {
15629 LispError::TypeMismatch { form, .. } => {
15630 assert_eq!(
15631 *form,
15632 KwargPath::Item {
15633 key: "steps".into(),
15634 idx: 3
15635 }
15636 );
15637 }
15638 other => panic!("expected TypeMismatch, got {other:?}"),
15639 }
15640 assert_eq!(
15641 format!("{err}"),
15642 "compile error in :steps[3]: expected string, got int"
15643 );
15644 }
15645
15646 #[test]
15647 fn type_mismatch_form_carries_typed_kwarg_path_slot_through_variant_slot() {
15648 // Sibling pin to `…_named_…` for the pre-key slot path. The
15649 // `KwargPath::Slot(idx)` shape names the offending position
15650 // without binding a key — it's the
15651 // "this-position-must-be-a-keyword" gate firing before any
15652 // identifier is known. The rendered `kwargs[<idx>]` shape
15653 // (no leading colon) bifurcates structurally from
15654 // `KwargPath::Named`'s `:<key>` shape.
15655 let err = LispError::TypeMismatch {
15656 form: KwargPath::Slot(2),
15657 expected: ExpectedKwargShape::Keyword,
15658 got: SexpShape::String,
15659 };
15660 match &err {
15661 LispError::TypeMismatch { form, .. } => {
15662 assert_eq!(*form, KwargPath::Slot(2));
15663 }
15664 other => panic!("expected TypeMismatch, got {other:?}"),
15665 }
15666 assert_eq!(
15667 format!("{err}"),
15668 "compile error in kwargs[2]: expected keyword, got string"
15669 );
15670 }
15671
15672 // ── KwargPathKind closed-set lift ───────────────────────────────────
15673 //
15674 // Sibling discriminator view of `KwargPath` — the payload-stripped
15675 // closed set of path-shape CATEGORIES (`Named` / `Item` / `Slot`).
15676 // Same shape every sibling payload-carrying closed-set enum in the
15677 // workspace pairs with (`AutoTerminate` / `AutoTerminateKind`,
15678 // `TerminateReason` / `TerminateReasonKind`, `SelectStrategy` /
15679 // `SelectStrategyKind`, `ChannelVariant` / `ChannelKind`). Consumers
15680 // that bucket type-mismatch failures by path-shape category (metrics
15681 // labels `path_kind=named` etc., a future tatara-check failure
15682 // histogram, an LSP that switches UI before drilling into the
15683 // bracket suffix) project through `KwargPath::kind` instead of
15684 // pattern-matching the full payload and discarding it at every site.
15685
15686 #[test]
15687 fn kwarg_path_kind_all_is_unique_and_complete() {
15688 // Closed-set posture: `ALL` enumerates every reachable variant
15689 // EXACTLY ONCE — no duplicates, no omissions. The `[Self; 3]`
15690 // array literal in the declaration forces the arity at compile
15691 // time; this test catches the orthogonal failure modes — a
15692 // future variant added at the type without being added to ALL
15693 // (silently dropped from every consumer's sweep), or a typo
15694 // that duplicates an entry (silently double-counted). Same
15695 // truth-table pinning every sibling closed-set lift in the
15696 // workspace uses (ExpectedKwargShape::ALL, SexpShape::ALL,
15697 // MacroDefHead::ALL, CompilerSpecIoStage::ALL,
15698 // AutoTerminateKind::ALL, SelectStrategyKind::ALL, …).
15699 //
15700 // The `iter+map+collect+sort_unstable` quadruple this test
15701 // inlined pre-lift now binds at `<KwargPathKind as
15702 // ClosedSet>::sorted_labels()` — the canonical-ordered
15703 // candidate-list projection on the trait. Distinctness of the
15704 // sorted result is covered by
15705 // `assert_closed_set_well_formed::<KwargPathKind>()`.
15706 assert_eq!(KwargPathKind::ALL.len(), 3);
15707 assert_eq!(
15708 <KwargPathKind as tatara_closed_set::ClosedSet>::sorted_labels(),
15709 vec!["item", "named", "slot"],
15710 "KwargPathKind::ALL must cover every reachable path-shape category"
15711 );
15712 }
15713
15714 #[test]
15715 fn kwarg_path_kind_label_round_trips_through_from_str() {
15716 // Bidirectional `label` ↔ `FromStr` contract: for every variant
15717 // in ALL, `kind.label().parse() == Ok(kind)`. A regression that
15718 // drifts the (variant, literal) pairing at ONE arm of `label`
15719 // (typo, capitalization drift) OR at the `FromStr` decode body
15720 // (off-by-one, missing variant in the sweep) fails-loudly here.
15721 // The canonical-literal site is singular (`label`) so the
15722 // round-trip is the only way the typed surface and the rendered
15723 // category literal can drift apart — pinning it here means they
15724 // cannot. Mirror of `expected_kwarg_shape_label_round_trips_…`
15725 // and every sibling closed-set round-trip in the workspace.
15726 for kind in KwargPathKind::ALL {
15727 let parsed: KwargPathKind = kind
15728 .label()
15729 .parse()
15730 .expect("every ALL variant's label must round-trip through FromStr");
15731 assert_eq!(
15732 parsed,
15733 kind,
15734 "FromStr({}) must round-trip to the same variant",
15735 kind.label()
15736 );
15737 }
15738 }
15739
15740 #[test]
15741 fn kwarg_path_kind_display_matches_label_for_every_variant() {
15742 // Display delegates to `label` — pin the byte-for-byte equality
15743 // for every variant so a future Display impl that diverges from
15744 // the canonical projection (e.g., re-adds a prefix like
15745 // `"kind=named"`) fails-loudly here. Same posture as
15746 // `expected_kwarg_shape_display_matches_label_for_every_variant`.
15747 for kind in KwargPathKind::ALL {
15748 assert_eq!(format!("{kind}"), kind.label());
15749 }
15750 }
15751
15752 #[test]
15753 fn unknown_kwarg_path_kind_carries_offending_input_verbatim() {
15754 // Operator-facing diagnostic contract: the offending input lands
15755 // in the typed error verbatim — no normalization, no case-folding,
15756 // no truncation. Pin the exact `#[error(...)]` rendering AND the
15757 // typed `.0` field projection so a future refactor that
15758 // normalizes (e.g. `.to_lowercase()`) before building the error
15759 // or that drops the input fails-loudly here. Symmetric to every
15760 // sibling `Unknown*` carrier in the workspace.
15761 let err: UnknownKwargPathKind = "Named".parse::<KwargPathKind>().expect_err(
15762 "capitalized `Named` must NOT decode — labels are byte-equal case-sensitive",
15763 );
15764 assert_eq!(err.0, "Named");
15765 assert_eq!(format!("{err}"), "unknown kwarg path kind: Named");
15766
15767 // A kwargs-path RENDERING (`:foo`) is NOT a kind label — the
15768 // CATEGORY axis is orthogonal to the IDENTITY axis; FromStr must
15769 // reject rendered identities, not silently coerce them.
15770 let err: UnknownKwargPathKind = ":foo"
15771 .parse::<KwargPathKind>()
15772 .expect_err("`:foo` is a KwargPath rendering, not a KwargPathKind label");
15773 assert_eq!(err.0, ":foo");
15774 assert_eq!(format!("{err}"), "unknown kwarg path kind: :foo");
15775
15776 let err: UnknownKwargPathKind = ""
15777 .parse::<KwargPathKind>()
15778 .expect_err("empty input must NOT decode to a KwargPathKind");
15779 assert_eq!(err.0, "");
15780 assert_eq!(format!("{err}"), "unknown kwarg path kind: ");
15781 }
15782
15783 #[test]
15784 fn kwarg_path_kind_projects_each_variant_to_canonical_kind() {
15785 // Load-bearing discriminator contract: `KwargPath::kind()` strips
15786 // the payload and projects to the canonical `KwargPathKind`
15787 // variant for each `KwargPath` shape. A regression that swaps
15788 // arms (e.g., `Named` → `KwargPathKind::Slot`) fails-loudly here
15789 // AND breaks every consumer that buckets by category. Symmetric
15790 // to `AutoTerminate::kind` and `TerminateReason::kind` in
15791 // tatara-process.
15792 assert_eq!(KwargPath::named("threshold").kind(), KwargPathKind::Named);
15793 assert_eq!(KwargPath::item("steps", 3).kind(), KwargPathKind::Item);
15794 assert_eq!(KwargPath::Slot(2).kind(), KwargPathKind::Slot);
15795 }
15796
15797 #[test]
15798 fn kwarg_path_kind_is_copy_and_hash_for_metrics_label_ergonomics() {
15799 // `KwargPathKind` derives Clone + Copy + Debug + PartialEq + Eq
15800 // + Hash so consumers that use the kind as a metrics-label key
15801 // (failure-cluster histogram keyed by `path_kind`), a HashMap
15802 // key (per-category counter), or a Copy-able discriminator in a
15803 // hot loop (kind projection in a kwarg-gate batching loop) reach
15804 // for the type without `.clone()` overhead. `String`-carrying
15805 // `KwargPath` is intentionally NOT `Copy`; `KwargPathKind` IS —
15806 // the split is the whole point of the discriminator view. A
15807 // regression that drops Copy or Hash breaks compilation here.
15808 let k = KwargPathKind::Named;
15809 let k_copy = k; // Copy
15810 assert_eq!(k, k_copy);
15811 let _: String = format!("{k:?}"); // Debug
15812 let mut s: std::collections::HashSet<KwargPathKind> = std::collections::HashSet::new();
15813 s.insert(KwargPathKind::Named);
15814 s.insert(KwargPathKind::Item);
15815 s.insert(KwargPathKind::Slot);
15816 s.insert(KwargPathKind::Named); // duplicate insert is a no-op (Hash + Eq)
15817 assert_eq!(s.len(), 3);
15818 }
15819
15820 #[test]
15821 fn kwarg_path_kind_label_does_not_overlap_kwarg_path_display_renderings() {
15822 // Cross-axis guard: the CATEGORY labels (`"named"` / `"item"` /
15823 // `"slot"`) are intentionally disjoint from the IDENTITY
15824 // renderings (`":<key>"` / `":<key>[<idx>]"` / `"kwargs[<idx>]"`)
15825 // so a consumer that confuses the two surfaces (e.g., parses
15826 // `kind.label()` as a `KwargPath` rendering, or parses a rendered
15827 // path as a kind label) fails-loudly through `FromStr` rejection
15828 // in both directions. Pin the non-overlap so a future label
15829 // rename that drifts into the rendering vocabulary (e.g.,
15830 // renaming `Slot`'s label to `"kwargs"`) fails-loudly here.
15831 for kind in KwargPathKind::ALL {
15832 let label = kind.label();
15833 assert!(
15834 !label.starts_with(':'),
15835 "kind label {label:?} must not start with `:` (would collide with KwargPath::Named/Item rendering)"
15836 );
15837 assert!(
15838 !label.contains('['),
15839 "kind label {label:?} must not contain `[` (would collide with KwargPath::Item/Slot rendering)"
15840 );
15841 }
15842 }
15843
15844 // ── `KwargPathKind::{NAMED_LABEL, ITEM_LABEL, SLOT_LABEL, LABELS}`
15845 // — the typed per-role category-label algebra ─────────────────────
15846 //
15847 // Sibling posture to `MacroDefHead::{DEFMACRO_KEYWORD,
15848 // DEFPOINT_TEMPLATE_KEYWORD, DEFCHECK_KEYWORD, KEYWORDS}` — the
15849 // per-role canonical `&'static str` marker + closed-set
15850 // `[&'static str; N]` array pattern applied to the
15851 // `KwargPathKind` closed set. Pre-lift the three canonical
15852 // category labels lived at TWO structural sites — the
15853 // `Self::label` match arm AND the sibling truth-table tests
15854 // (`kwarg_path_kind_all_is_unique_and_complete` sorted-labels
15855 // pin, `kwarg_path_kind_label_does_not_overlap_kwarg_path_display_renderings`
15856 // disjointness sweep). Post-lift each (variant, canonical
15857 // `&'static str`) pairing binds at ONE `pub const` on the typed
15858 // `KwargPathKind` algebra the `Self::label` arm routes through.
15859
15860 #[test]
15861 fn kwarg_path_kind_named_label_projects_canonical_named_bytes() {
15862 // Pins the exact `"named"` bytes at the typed constant. A
15863 // regression that drifts the constant (e.g. typo `"name"`,
15864 // `"Named"` in PascalCase, or an accidental trailing
15865 // whitespace) fails-loudly here. This is the single site
15866 // the substrate's canonical typed-atom-extractor failure
15867 // category resolves to; every downstream consumer (Display,
15868 // FromStr, tests, LSP completion, metric labels) routes
15869 // through this constant.
15870 assert_eq!(
15871 KwargPathKind::NAMED_LABEL,
15872 "named",
15873 "KwargPathKind::NAMED_LABEL drifted from the substrate-\
15874 canonical typed-atom-extractor failure category `\"named\"`"
15875 );
15876 }
15877
15878 #[test]
15879 fn kwarg_path_kind_item_label_projects_canonical_item_bytes() {
15880 // Pins the exact `"item"` bytes at the typed constant. The
15881 // `extract_string_list` per-item gate emits this exact
15882 // category; any drift here silently breaks every metric or
15883 // diagnostic consumer that partitions failures by category.
15884 assert_eq!(
15885 KwargPathKind::ITEM_LABEL,
15886 "item",
15887 "KwargPathKind::ITEM_LABEL drifted from the substrate-\
15888 canonical list-item failure category `\"item\"`"
15889 );
15890 }
15891
15892 #[test]
15893 fn kwarg_path_kind_slot_label_projects_canonical_slot_bytes() {
15894 // Pins the exact `"slot"` bytes at the typed constant. The
15895 // `parse_kwargs` slot-must-be-a-keyword gate emits this
15896 // exact category; any drift here silently breaks every
15897 // metric or diagnostic consumer that partitions failures by
15898 // category.
15899 assert_eq!(
15900 KwargPathKind::SLOT_LABEL,
15901 "slot",
15902 "KwargPathKind::SLOT_LABEL drifted from the substrate-\
15903 canonical kwargs-slice-slot failure category `\"slot\"`"
15904 );
15905 }
15906
15907 #[test]
15908 fn kwarg_path_kind_label_method_routes_through_typed_constants() {
15909 // PATH-UNIFORMITY: the inherent `Self::label(self)` method
15910 // MUST return the per-role `pub const` byte-for-byte for
15911 // each reachable variant. A regression that reverts ONE arm
15912 // to an inline `"named"` string literal (e.g. a merge-
15913 // conflict resolution that picked the pre-lift form)
15914 // silently reintroduces the ≥2 PRIME-DIRECTIVE trigger the
15915 // lift resolved — this test catches that by pinning the
15916 // arm's return value to the constant, so the two paths
15917 // (inline vs. typed constant) cannot both hold. Sibling
15918 // posture to `macro_def_head_keyword_method_routes_through_typed_constants`
15919 // on the `MacroDefHead` head-keyword algebra.
15920 assert_eq!(
15921 KwargPathKind::Named.label(),
15922 KwargPathKind::NAMED_LABEL,
15923 "KwargPathKind::Named.label() drifted from \
15924 KwargPathKind::NAMED_LABEL — the match arm reverted to \
15925 an inline literal"
15926 );
15927 assert_eq!(
15928 KwargPathKind::Item.label(),
15929 KwargPathKind::ITEM_LABEL,
15930 "KwargPathKind::Item.label() drifted from \
15931 KwargPathKind::ITEM_LABEL — the match arm reverted to \
15932 an inline literal"
15933 );
15934 assert_eq!(
15935 KwargPathKind::Slot.label(),
15936 KwargPathKind::SLOT_LABEL,
15937 "KwargPathKind::Slot.label() drifted from \
15938 KwargPathKind::SLOT_LABEL — the match arm reverted to \
15939 an inline literal"
15940 );
15941 }
15942
15943 #[test]
15944 fn kwarg_path_kind_labels_has_expected_cardinality() {
15945 // Cardinality contract: `Self::LABELS.len() == 3` — pinned
15946 // at the declaration site by rustc's forced-arity check on
15947 // `[&'static str; 3]`. This test surfaces the arity as a
15948 // fail-loud runtime pin so a future refactor that switches
15949 // the array type to `&[&'static str]` (dropping the
15950 // compile-time arity forcing) doesn't silently loosen the
15951 // closed-set discipline the family relies on. Sibling
15952 // posture to `macro_def_head_keywords_has_expected_cardinality`
15953 // on the `MacroDefHead::KEYWORDS` family.
15954 assert_eq!(
15955 KwargPathKind::LABELS.len(),
15956 3,
15957 "KwargPathKind::LABELS cardinality drifted from 3 — the \
15958 category-label closed set gained or lost a variant \
15959 without the ALL / LABELS pair being updated in tandem"
15960 );
15961 }
15962
15963 #[test]
15964 fn kwarg_path_kind_labels_align_with_all_by_index() {
15965 // ALIGNMENT CONTRACT: `Self::LABELS[i] ==
15966 // Self::ALL[i].label()` element-wise. The two ALL arrays
15967 // (typed variants, canonical `&'static str` labels) share
15968 // ONE canonical declaration order — a regression that
15969 // reorders ONE array without reordering the other silently
15970 // misaligns every `zip(Self::ALL, Self::LABELS)` consumer
15971 // (LSP completion providers, metric-label emitters, coverage
15972 // reporters). Pinning by-index alignment here catches the
15973 // reorder at test time. Sibling posture to
15974 // `macro_def_head_keywords_align_with_all_by_index` on the
15975 // `MacroDefHead::KEYWORDS` family.
15976 assert_eq!(
15977 KwargPathKind::LABELS.len(),
15978 KwargPathKind::ALL.len(),
15979 "KwargPathKind::LABELS and KwargPathKind::ALL diverged in \
15980 cardinality — the per-role constants and enum variants \
15981 must stay in lockstep"
15982 );
15983 for (i, kind) in KwargPathKind::ALL.iter().enumerate() {
15984 assert_eq!(
15985 KwargPathKind::LABELS[i],
15986 kind.label(),
15987 "KwargPathKind::LABELS[{i}] `{lb}` drifted from \
15988 KwargPathKind::ALL[{i}].label() `{via_variant}` — \
15989 the canonical declaration order of the two ALL \
15990 arrays must match element-wise",
15991 lb = KwargPathKind::LABELS[i],
15992 via_variant = kind.label(),
15993 );
15994 }
15995 }
15996
15997 #[test]
15998 fn kwarg_path_kind_labels_pairwise_distinct() {
15999 // PAIRWISE DISJOINTNESS: the three `&'static str` labels on
16000 // the category-label algebra MUST differ so the derive-
16001 // generated FromStr's linear sweep cannot route two variants
16002 // through the same arm. Family-wide sweep over LABELS ×
16003 // LABELS — supersedes any single per-pair assertion and
16004 // picks up new variants mechanically when the array grows.
16005 // Sibling posture to `macro_def_head_keywords_pairwise_distinct`
16006 // on the `MacroDefHead::KEYWORDS` family.
16007 for (i, a) in KwargPathKind::LABELS.iter().enumerate() {
16008 for (j, b) in KwargPathKind::LABELS.iter().enumerate() {
16009 if i == j {
16010 continue;
16011 }
16012 assert_ne!(
16013 a, b,
16014 "KwargPathKind::LABELS[{i}] `{a}` collides with \
16015 KwargPathKind::LABELS[{j}] `{b}` — the derive-\
16016 generated FromStr sweep would route two variants \
16017 through the same arm"
16018 );
16019 }
16020 }
16021 }
16022
16023 #[test]
16024 fn kwarg_path_kind_labels_all_round_trip_through_from_str() {
16025 // Family-wide bidirection: every entry of `Self::LABELS`
16026 // MUST parse back to some `KwargPathKind` variant AND that
16027 // variant's `label()` MUST equal the original entry.
16028 // Sibling posture to `macro_def_head_keywords_all_round_trip_through_from_str`
16029 // (which sweeps over `Self::ALL`); this test sweeps over the
16030 // per-role `&'static str` constants directly, catching a
16031 // regression where one of the three constants drifts from
16032 // the corresponding `Self::label` arm (e.g. `SLOT_LABEL` set
16033 // to `"slice"` while `Self::Slot.label()` still returns
16034 // `Self::SLOT_LABEL`, which would parse to
16035 // `KwargPathKind::Slot` correctly but break every downstream
16036 // consumer that pinned the literal `"slot"` bytes).
16037 for lb in KwargPathKind::LABELS {
16038 let parsed: KwargPathKind = lb.parse().unwrap_or_else(|_| {
16039 panic!(
16040 "KwargPathKind::LABELS entry `{lb}` failed to parse \
16041 — the entry drifted from Self::label"
16042 )
16043 });
16044 assert_eq!(
16045 parsed.label(),
16046 lb,
16047 "KwargPathKind::LABELS entry `{lb}` parses to a variant \
16048 whose label() `{}` does not match — the constant and \
16049 the match arm drifted apart",
16050 parsed.label(),
16051 );
16052 }
16053 }
16054
16055 // ── ExpectedKwargShape closed-set lift ──────────────────────────────
16056 //
16057 // The `LispError::TypeMismatch.expected` slot was promoted from
16058 // `&'static str` to the typed closed-set `ExpectedKwargShape` enum.
16059 // The seven reachable expected-shape labels — `Keyword` /
16060 // `String` / `Int` / `Number` / `Bool` / `List` / `ListOfStrings`
16061 // — are now encoded as variant identities so authoring tools (REPL,
16062 // LSP, `tatara-check`) bind on `ExpectedKwargShape::Number` etc.
16063 // directly rather than substring-matching `expected == "number"`.
16064 // Same posture as `KwargPath`, `MacroDefHead`, `UnquoteForm`,
16065 // `CompilerSpecIoStage`, and `TemplateInvariantKind`.
16066
16067 #[test]
16068 fn label_renders_canonical_string_for_every_variant() {
16069 // Pin every variant's canonical `&'static str` projection — a
16070 // regression that drifts any label (typo in `"strin"` for
16071 // `"string"`, swap of `"int"` ↔ `"number"`) fails-loudly here.
16072 // The seven labels are byte-for-byte identical to the pre-lift
16073 // `&'static str` literals scattered across `domain.rs` so
16074 // existing `format!("{err}").contains("expected string")`
16075 // / `expected int` / `expected number` / etc. assertions in
16076 // consumer crates pass unchanged across the lift.
16077 assert_eq!(ExpectedKwargShape::Keyword.label(), "keyword");
16078 assert_eq!(ExpectedKwargShape::String.label(), "string");
16079 assert_eq!(ExpectedKwargShape::Int.label(), "int");
16080 assert_eq!(ExpectedKwargShape::Number.label(), "number");
16081 assert_eq!(ExpectedKwargShape::Bool.label(), "bool");
16082 assert_eq!(ExpectedKwargShape::List.label(), "list");
16083 assert_eq!(ExpectedKwargShape::ListOfStrings.label(), "list of strings");
16084 }
16085
16086 #[test]
16087 fn display_matches_label_for_every_variant() {
16088 // Pin Display-equals-label: the `#[error("... expected
16089 // {expected}, ...")]` annotation on `LispError::TypeMismatch`
16090 // projects through Display, and Display delegates to `label()`.
16091 // A regression that introduces a Display impl that deviates from
16092 // `label()` (e.g. capitalizing one variant) would drift the
16093 // diagnostic surface; this test pins the contract.
16094 assert_eq!(format!("{}", ExpectedKwargShape::Keyword), "keyword");
16095 assert_eq!(format!("{}", ExpectedKwargShape::String), "string");
16096 assert_eq!(format!("{}", ExpectedKwargShape::Int), "int");
16097 assert_eq!(format!("{}", ExpectedKwargShape::Number), "number");
16098 assert_eq!(format!("{}", ExpectedKwargShape::Bool), "bool");
16099 assert_eq!(format!("{}", ExpectedKwargShape::List), "list");
16100 assert_eq!(
16101 format!("{}", ExpectedKwargShape::ListOfStrings),
16102 "list of strings"
16103 );
16104 }
16105
16106 #[test]
16107 fn type_mismatch_expected_carries_typed_shape_through_variant_slot() {
16108 // After the typed-slot promotion, `LispError::TypeMismatch.expected`
16109 // is `ExpectedKwargShape` — the closed-set typed enum.
16110 // Consumers (REPL, LSP, `tatara-check`) pattern-match on the
16111 // variant identity `ExpectedKwargShape::Number` directly rather
16112 // than substring-matching a rendered `"expected number"` prefix.
16113 // Pin the structural binding AND the Display projection so the
16114 // byte-for-byte rendering contract is anchored from both
16115 // angles. A regression that re-introduces a `&'static str`-
16116 // shaped expected slot (collapsing the typed enum back into a
16117 // free-form label) fails-loudly here.
16118 let err = LispError::TypeMismatch {
16119 form: KwargPath::named("threshold"),
16120 expected: ExpectedKwargShape::Number,
16121 got: SexpShape::String,
16122 };
16123 match &err {
16124 LispError::TypeMismatch { expected, .. } => {
16125 assert_eq!(*expected, ExpectedKwargShape::Number);
16126 }
16127 other => panic!("expected TypeMismatch, got {other:?}"),
16128 }
16129 assert_eq!(
16130 format!("{err}"),
16131 "compile error in :threshold: expected number, got string"
16132 );
16133 }
16134
16135 #[test]
16136 fn type_mismatch_expected_list_of_strings_bifurcates_from_list() {
16137 // The `extract_string_list` outer-shape gate emits
16138 // `ExpectedKwargShape::ListOfStrings` (`"list of strings"`),
16139 // bifurcating structurally from `extract_vec_via_serde`'s
16140 // outer-shape gate which emits `ExpectedKwargShape::List`
16141 // (`"list"`). Two related-but-distinct gates, two distinct
16142 // variant identities; the typed enum makes that bifurcation
16143 // load-bearing. A regression that collapses them into one
16144 // variant (e.g. `ExpectedKwargShape::AnyList`) would drift the
16145 // diagnostic message; this test pins both shapes.
16146 let list_of_strings = LispError::TypeMismatch {
16147 form: KwargPath::named("tags"),
16148 expected: ExpectedKwargShape::ListOfStrings,
16149 got: SexpShape::String,
16150 };
16151 let list = LispError::TypeMismatch {
16152 form: KwargPath::named("steps"),
16153 expected: ExpectedKwargShape::List,
16154 got: SexpShape::String,
16155 };
16156 assert_eq!(
16157 format!("{list_of_strings}"),
16158 "compile error in :tags: expected list of strings, got string"
16159 );
16160 assert_eq!(
16161 format!("{list}"),
16162 "compile error in :steps: expected list, got string"
16163 );
16164 match (&list_of_strings, &list) {
16165 (
16166 LispError::TypeMismatch { expected: a, .. },
16167 LispError::TypeMismatch { expected: b, .. },
16168 ) => {
16169 assert_ne!(a, b);
16170 assert_eq!(*a, ExpectedKwargShape::ListOfStrings);
16171 assert_eq!(*b, ExpectedKwargShape::List);
16172 }
16173 _ => panic!("both must be TypeMismatch"),
16174 }
16175 }
16176
16177 #[test]
16178 fn expected_kwarg_shape_all_is_unique_and_complete() {
16179 // Closed-set posture: `ALL` enumerates every reachable variant
16180 // EXACTLY ONCE — no duplicates, no omissions. The `[Self; 7]`
16181 // array literal in the declaration forces the arity at compile
16182 // time; this test catches the orthogonal failure modes — a
16183 // future variant added at the type without being added to ALL
16184 // (silently dropped from every consumer's sweep), or a typo
16185 // that duplicates an entry (silently double-counted). Same
16186 // truth-table pinning every sibling closed-set lift in the
16187 // workspace uses (SexpShape::ALL, MacroDefHead::ALL,
16188 // UnquoteForm::ALL, RequestorKind::ALL, ReceiptKind::ALL,
16189 // ConditionKind::ALL, ProcessPhase::ALL, ChannelKind::ALL, …).
16190 //
16191 // The `iter+map+collect+sort_unstable` quadruple this test
16192 // inlined pre-lift now binds at `<ExpectedKwargShape as
16193 // ClosedSet>::sorted_labels()` — the canonical-ordered
16194 // candidate-list projection on the trait. Distinctness of the
16195 // sorted result is covered by
16196 // `assert_closed_set_well_formed::<ExpectedKwargShape>()`.
16197 assert_eq!(ExpectedKwargShape::ALL.len(), 7);
16198 assert_eq!(
16199 <ExpectedKwargShape as tatara_closed_set::ClosedSet>::sorted_labels(),
16200 vec![
16201 "bool",
16202 "int",
16203 "keyword",
16204 "list",
16205 "list of strings",
16206 "number",
16207 "string",
16208 ],
16209 "ExpectedKwargShape::ALL must cover every reachable expected-shape label"
16210 );
16211 }
16212
16213 #[test]
16214 fn expected_kwarg_shape_label_round_trips_through_from_str() {
16215 // Bidirectional `label` ↔ `FromStr` contract: for every
16216 // variant in ALL, `shape.label().parse() == Ok(shape)`. A
16217 // regression that drifts the (variant, literal) pairing at
16218 // ONE arm of `label` (typo, capitalization drift) OR at the
16219 // `FromStr` decode body (off-by-one, missing variant in the
16220 // sweep) fails-loudly here. The canonical-literal site is
16221 // singular (`label`) so the round-trip is the only way the
16222 // typed surface and the rendered diagnostic literal can drift
16223 // apart — pinning it here means they cannot. Mirror of
16224 // `sexp_shape_label_round_trips_through_from_str` and every
16225 // sibling closed-set round-trip in the workspace.
16226 for shape in ExpectedKwargShape::ALL {
16227 let parsed: ExpectedKwargShape = shape
16228 .label()
16229 .parse()
16230 .expect("every ALL variant's label must round-trip through FromStr");
16231 assert_eq!(
16232 parsed,
16233 shape,
16234 "FromStr({}) must round-trip to the same variant",
16235 shape.label()
16236 );
16237 }
16238 }
16239
16240 #[test]
16241 fn unknown_expected_kwarg_shape_carries_offending_input_verbatim() {
16242 // Operator-facing diagnostic contract: the offending input
16243 // lands in the typed error verbatim — no normalization, no
16244 // case-folding, no truncation. Pin the exact `#[error(...)]`
16245 // rendering AND the typed `.0` field projection so a future
16246 // refactor that normalizes (e.g. `.to_lowercase()`) before
16247 // building the error or that drops the input fails-loudly
16248 // here. Symmetric to every sibling `Unknown*` carrier in the
16249 // workspace.
16250 let err: UnknownExpectedKwargShape = "Number".parse::<ExpectedKwargShape>().expect_err(
16251 "capitalized `Number` must NOT decode — labels are byte-equal case-sensitive",
16252 );
16253 assert_eq!(err.0, "Number");
16254 assert_eq!(format!("{err}"), "unknown expected kwarg shape: Number");
16255
16256 let err: UnknownExpectedKwargShape = "float"
16257 .parse::<ExpectedKwargShape>()
16258 .expect_err("`float` is SexpShape's vocabulary, not ExpectedKwargShape's");
16259 assert_eq!(err.0, "float");
16260 assert_eq!(format!("{err}"), "unknown expected kwarg shape: float");
16261
16262 let err: UnknownExpectedKwargShape = ""
16263 .parse::<ExpectedKwargShape>()
16264 .expect_err("empty input must NOT decode to an ExpectedKwargShape");
16265 assert_eq!(err.0, "");
16266 assert_eq!(format!("{err}"), "unknown expected kwarg shape: ");
16267 }
16268
16269 #[test]
16270 fn expected_kwarg_shape_from_str_accepts_only_canonical_labels() {
16271 // Cross-axis guard: `SexpShape::label()`'s vocabulary overlaps
16272 // with `ExpectedKwargShape::label()` on five of seven entries
16273 // (`keyword` / `string` / `int` / `bool` / `list`) and DOES
16274 // NOT overlap on the structural-only `nil` / `symbol` /
16275 // `float` / `quote` / `quasiquote` / `unquote` / `unquote-splice`
16276 // entries — those name Sexp identities the typed-entry kwarg
16277 // gate cannot `expect`. The overlap is intentional — both
16278 // axes are projections of the same `Sexp` algebra at typed-
16279 // entry gates — but the non-overlap is the load-bearing part:
16280 // a `FromStr` that silently accepts `"float"` as an
16281 // `ExpectedKwargShape` would corrupt the typed identity. Pin
16282 // BOTH directions: the overlap decodes successfully (and to
16283 // the matching `ExpectedKwargShape` variant), the non-overlap
16284 // rejects. Symmetric to `sexp_shape_from_str_accepts_only_
16285 // canonical_labels` from the other axis.
16286 assert_eq!(
16287 "keyword".parse::<ExpectedKwargShape>().unwrap(),
16288 ExpectedKwargShape::Keyword
16289 );
16290 assert_eq!(
16291 "string".parse::<ExpectedKwargShape>().unwrap(),
16292 ExpectedKwargShape::String
16293 );
16294 assert_eq!(
16295 "int".parse::<ExpectedKwargShape>().unwrap(),
16296 ExpectedKwargShape::Int
16297 );
16298 assert_eq!(
16299 "bool".parse::<ExpectedKwargShape>().unwrap(),
16300 ExpectedKwargShape::Bool
16301 );
16302 assert_eq!(
16303 "list".parse::<ExpectedKwargShape>().unwrap(),
16304 ExpectedKwargShape::List
16305 );
16306 // Non-overlap: SexpShape-only labels reject through FromStr.
16307 for sexp_only in ["nil", "symbol", "float", "quote", "quasiquote", "unquote"] {
16308 sexp_only.parse::<ExpectedKwargShape>().unwrap_err();
16309 }
16310 // ExpectedKwargShape-only labels: `number` and `list of strings`
16311 // decode here but reject through SexpShape::FromStr — the
16312 // non-overlap axis is symmetric.
16313 assert_eq!(
16314 "number".parse::<ExpectedKwargShape>().unwrap(),
16315 ExpectedKwargShape::Number
16316 );
16317 assert_eq!(
16318 "list of strings".parse::<ExpectedKwargShape>().unwrap(),
16319 ExpectedKwargShape::ListOfStrings
16320 );
16321 "number".parse::<SexpShape>().unwrap_err();
16322 "list of strings".parse::<SexpShape>().unwrap_err();
16323 }
16324
16325 // ── ExpectedKwargShape per-role `pub const` + LABELS ALL array lift ─
16326 //
16327 // Sibling posture to `KwargPathKind::{NAMED_LABEL, ITEM_LABEL,
16328 // SLOT_LABEL, LABELS}` on the kwarg-path category algebra —
16329 // per-role canonical `&'static str` marker + closed-set
16330 // `[&'static str; N]` array pattern applied to the
16331 // `ExpectedKwargShape` closed set. Pre-lift the seven canonical
16332 // expected-shape labels lived at TWO structural sites — the
16333 // `Self::label` match arms AND the sibling truth-table tests
16334 // (`label_renders_canonical_string_for_every_variant`,
16335 // `expected_kwarg_shape_all_is_unique_and_complete` sorted-labels
16336 // pin). Post-lift each (variant, canonical `&'static str`)
16337 // pairing binds at ONE `pub const` on the typed algebra the
16338 // `Self::label` arm routes through.
16339
16340 #[test]
16341 fn expected_kwarg_shape_keyword_label_projects_canonical_keyword_bytes() {
16342 // Pins the exact `"keyword"` bytes at the typed constant. The
16343 // `parse_kwargs` slot-must-be-a-keyword gate emits this exact
16344 // expected-shape; any drift here silently breaks every metric
16345 // or diagnostic consumer that partitions failures by expected-
16346 // shape. Sibling posture to
16347 // `kwarg_path_kind_named_label_projects_canonical_named_bytes`.
16348 assert_eq!(
16349 ExpectedKwargShape::KEYWORD_LABEL,
16350 "keyword",
16351 "ExpectedKwargShape::KEYWORD_LABEL drifted from the \
16352 substrate-canonical slot-must-be-a-keyword gate label \
16353 `\"keyword\"`"
16354 );
16355 }
16356
16357 #[test]
16358 fn expected_kwarg_shape_string_label_projects_canonical_string_bytes() {
16359 // Pins the exact `"string"` bytes at the typed constant.
16360 // `extract_string` / `extract_optional_string` / the
16361 // `extract_string_list` per-item gate all emit this exact
16362 // expected-shape; any drift here silently breaks every
16363 // consumer. Byte-equal to `SexpShape::STRING_LABEL` (future
16364 // lift) — the cross-axis overlap on the same `Sexp` algebra
16365 // is load-bearing.
16366 assert_eq!(
16367 ExpectedKwargShape::STRING_LABEL,
16368 "string",
16369 "ExpectedKwargShape::STRING_LABEL drifted from the \
16370 substrate-canonical extract-string gate label \
16371 `\"string\"`"
16372 );
16373 }
16374
16375 #[test]
16376 fn expected_kwarg_shape_int_label_projects_canonical_int_bytes() {
16377 // Pins the exact `"int"` bytes at the typed constant.
16378 // `extract_int` / `extract_optional_int` emit this exact
16379 // expected-shape; the sibling `NUMBER_LABEL` names the wider
16380 // numeric-union the `extract_float` extractor emits.
16381 assert_eq!(
16382 ExpectedKwargShape::INT_LABEL,
16383 "int",
16384 "ExpectedKwargShape::INT_LABEL drifted from the substrate-\
16385 canonical extract-int gate label `\"int\"`"
16386 );
16387 }
16388
16389 #[test]
16390 fn expected_kwarg_shape_number_label_projects_canonical_number_bytes() {
16391 // Pins the exact `"number"` bytes at the typed constant.
16392 // `extract_float` / `extract_optional_float` emit this exact
16393 // expected-shape; the naming (not `"float"`) is load-bearing
16394 // — `extract_float` accepts BOTH `Sexp::Atom(Float(_))` and
16395 // `Sexp::Atom(Int(_))` via `Sexp::as_float`, so the label
16396 // names the union rather than the narrower Float element-
16397 // type. A regression to `"float"` here would silently narrow
16398 // the diagnostic's semantics without changing the extractor's
16399 // acceptance set.
16400 assert_eq!(
16401 ExpectedKwargShape::NUMBER_LABEL,
16402 "number",
16403 "ExpectedKwargShape::NUMBER_LABEL drifted from the \
16404 substrate-canonical extract-float gate label \
16405 `\"number\"`"
16406 );
16407 }
16408
16409 #[test]
16410 fn expected_kwarg_shape_bool_label_projects_canonical_bool_bytes() {
16411 // Pins the exact `"bool"` bytes at the typed constant.
16412 // `extract_bool` / `extract_optional_bool` emit this exact
16413 // expected-shape.
16414 assert_eq!(
16415 ExpectedKwargShape::BOOL_LABEL,
16416 "bool",
16417 "ExpectedKwargShape::BOOL_LABEL drifted from the substrate-\
16418 canonical extract-bool gate label `\"bool\"`"
16419 );
16420 }
16421
16422 #[test]
16423 fn expected_kwarg_shape_list_label_projects_canonical_list_bytes() {
16424 // Pins the exact `"list"` bytes at the typed constant.
16425 // `extract_vec_via_serde`'s outer-shape gate emits this exact
16426 // expected-shape; the sibling `LIST_OF_STRINGS_LABEL` names
16427 // the element-typed variant `extract_string_list`'s outer
16428 // gate emits.
16429 assert_eq!(
16430 ExpectedKwargShape::LIST_LABEL,
16431 "list",
16432 "ExpectedKwargShape::LIST_LABEL drifted from the substrate-\
16433 canonical extract-vec-via-serde gate label `\"list\"`"
16434 );
16435 }
16436
16437 #[test]
16438 fn expected_kwarg_shape_list_of_strings_label_projects_canonical_bytes() {
16439 // Pins the exact `"list of strings"` bytes at the typed
16440 // constant. `extract_string_list`'s outer-shape gate emits
16441 // this exact expected-shape; the naming (not the narrower
16442 // `"list"`) is load-bearing — it names the element-type so
16443 // the diagnostic reads `expected list of strings, got
16444 // string` instead of the ambiguous `expected list, got
16445 // string`. A regression to `"list"` here would silently
16446 // collapse the two structurally distinct extractor gates
16447 // into one indistinguishable diagnostic.
16448 assert_eq!(
16449 ExpectedKwargShape::LIST_OF_STRINGS_LABEL,
16450 "list of strings",
16451 "ExpectedKwargShape::LIST_OF_STRINGS_LABEL drifted from \
16452 the substrate-canonical extract-string-list outer-shape \
16453 gate label `\"list of strings\"`"
16454 );
16455 }
16456
16457 #[test]
16458 fn expected_kwarg_shape_label_method_routes_through_typed_constants() {
16459 // PATH-UNIFORMITY: the inherent `Self::label(self)` method
16460 // MUST return the per-role `pub const` byte-for-byte for
16461 // each reachable variant. A regression that reverts ONE arm
16462 // to an inline `"number"` / `"list of strings"` string
16463 // literal (e.g. a merge-conflict resolution that picked the
16464 // pre-lift form) silently reintroduces the ≥2 PRIME-DIRECTIVE
16465 // trigger the lift resolved — this test catches that by
16466 // pinning the arm's return value to the constant, so the two
16467 // paths (inline vs. typed constant) cannot both hold.
16468 // Sibling posture to
16469 // `kwarg_path_kind_label_method_routes_through_typed_constants`.
16470 assert_eq!(
16471 ExpectedKwargShape::Keyword.label(),
16472 ExpectedKwargShape::KEYWORD_LABEL,
16473 "ExpectedKwargShape::Keyword.label() drifted from \
16474 ExpectedKwargShape::KEYWORD_LABEL — the match arm \
16475 reverted to an inline literal"
16476 );
16477 assert_eq!(
16478 ExpectedKwargShape::String.label(),
16479 ExpectedKwargShape::STRING_LABEL,
16480 "ExpectedKwargShape::String.label() drifted from \
16481 ExpectedKwargShape::STRING_LABEL — the match arm \
16482 reverted to an inline literal"
16483 );
16484 assert_eq!(
16485 ExpectedKwargShape::Int.label(),
16486 ExpectedKwargShape::INT_LABEL,
16487 "ExpectedKwargShape::Int.label() drifted from \
16488 ExpectedKwargShape::INT_LABEL — the match arm reverted \
16489 to an inline literal"
16490 );
16491 assert_eq!(
16492 ExpectedKwargShape::Number.label(),
16493 ExpectedKwargShape::NUMBER_LABEL,
16494 "ExpectedKwargShape::Number.label() drifted from \
16495 ExpectedKwargShape::NUMBER_LABEL — the match arm \
16496 reverted to an inline literal"
16497 );
16498 assert_eq!(
16499 ExpectedKwargShape::Bool.label(),
16500 ExpectedKwargShape::BOOL_LABEL,
16501 "ExpectedKwargShape::Bool.label() drifted from \
16502 ExpectedKwargShape::BOOL_LABEL — the match arm reverted \
16503 to an inline literal"
16504 );
16505 assert_eq!(
16506 ExpectedKwargShape::List.label(),
16507 ExpectedKwargShape::LIST_LABEL,
16508 "ExpectedKwargShape::List.label() drifted from \
16509 ExpectedKwargShape::LIST_LABEL — the match arm reverted \
16510 to an inline literal"
16511 );
16512 assert_eq!(
16513 ExpectedKwargShape::ListOfStrings.label(),
16514 ExpectedKwargShape::LIST_OF_STRINGS_LABEL,
16515 "ExpectedKwargShape::ListOfStrings.label() drifted from \
16516 ExpectedKwargShape::LIST_OF_STRINGS_LABEL — the match \
16517 arm reverted to an inline literal"
16518 );
16519 }
16520
16521 #[test]
16522 fn expected_kwarg_shape_labels_has_expected_cardinality() {
16523 // Cardinality contract: `Self::LABELS.len() == 7` — pinned at
16524 // the declaration site by rustc's forced-arity check on
16525 // `[&'static str; 7]`. This test surfaces the arity as a
16526 // fail-loud runtime pin so a future refactor that switches
16527 // the array type to `&[&'static str]` (dropping the compile-
16528 // time arity forcing) doesn't silently loosen the closed-set
16529 // discipline the family relies on. Sibling posture to
16530 // `kwarg_path_kind_labels_has_expected_cardinality` on the
16531 // `KwargPathKind::LABELS` family.
16532 assert_eq!(
16533 ExpectedKwargShape::LABELS.len(),
16534 7,
16535 "ExpectedKwargShape::LABELS cardinality drifted from 7 \
16536 — the expected-shape closed set gained or lost a variant \
16537 without the ALL / LABELS pair being updated in tandem"
16538 );
16539 }
16540
16541 #[test]
16542 fn expected_kwarg_shape_labels_align_with_all_by_index() {
16543 // ALIGNMENT CONTRACT: `Self::LABELS[i] ==
16544 // Self::ALL[i].label()` element-wise. The two ALL arrays
16545 // (typed variants, canonical `&'static str` labels) share
16546 // ONE canonical declaration order — a regression that
16547 // reorders ONE array without reordering the other silently
16548 // misaligns every `zip(Self::ALL, Self::LABELS)` consumer
16549 // (LSP completion providers, metric-label emitters, coverage
16550 // reporters). Pinning by-index alignment here catches the
16551 // reorder at test time. Sibling posture to
16552 // `kwarg_path_kind_labels_align_with_all_by_index`.
16553 assert_eq!(
16554 ExpectedKwargShape::LABELS.len(),
16555 ExpectedKwargShape::ALL.len(),
16556 "ExpectedKwargShape::LABELS and ExpectedKwargShape::ALL \
16557 diverged in cardinality — the per-role constants and \
16558 enum variants must stay in lockstep"
16559 );
16560 for (i, shape) in ExpectedKwargShape::ALL.iter().enumerate() {
16561 assert_eq!(
16562 ExpectedKwargShape::LABELS[i],
16563 shape.label(),
16564 "ExpectedKwargShape::LABELS[{i}] `{lb}` drifted from \
16565 ExpectedKwargShape::ALL[{i}].label() `{via_variant}` \
16566 — the canonical declaration order of the two ALL \
16567 arrays must match element-wise",
16568 lb = ExpectedKwargShape::LABELS[i],
16569 via_variant = shape.label(),
16570 );
16571 }
16572 }
16573
16574 #[test]
16575 fn expected_kwarg_shape_labels_pairwise_distinct() {
16576 // PAIRWISE DISJOINTNESS: the seven `&'static str` labels on
16577 // the expected-shape algebra MUST differ so the derive-
16578 // generated FromStr's linear sweep cannot route two variants
16579 // through the same arm. Family-wide sweep over LABELS ×
16580 // LABELS — supersedes any single per-pair assertion and picks
16581 // up new variants mechanically when the array grows. Sibling
16582 // posture to `kwarg_path_kind_labels_pairwise_distinct`.
16583 for (i, a) in ExpectedKwargShape::LABELS.iter().enumerate() {
16584 for (j, b) in ExpectedKwargShape::LABELS.iter().enumerate() {
16585 if i == j {
16586 continue;
16587 }
16588 assert_ne!(
16589 a, b,
16590 "ExpectedKwargShape::LABELS[{i}] `{a}` collides \
16591 with ExpectedKwargShape::LABELS[{j}] `{b}` — the \
16592 derive-generated FromStr sweep would route two \
16593 variants through the same arm"
16594 );
16595 }
16596 }
16597 }
16598
16599 #[test]
16600 fn expected_kwarg_shape_labels_all_round_trip_through_from_str() {
16601 // Family-wide bidirection: every entry of `Self::LABELS` MUST
16602 // parse back to some `ExpectedKwargShape` variant AND that
16603 // variant's `label()` MUST equal the original entry. Sibling
16604 // posture to
16605 // `kwarg_path_kind_labels_all_round_trip_through_from_str`
16606 // (sweeps over `Self::ALL`); this test sweeps over the per-
16607 // role `&'static str` constants directly, catching a
16608 // regression where one of the seven constants drifts from
16609 // the corresponding `Self::label` arm (e.g.
16610 // `LIST_OF_STRINGS_LABEL` set to `"list-of-strings"` while
16611 // `Self::ListOfStrings.label()` still returns
16612 // `Self::LIST_OF_STRINGS_LABEL`, which would parse to
16613 // `ExpectedKwargShape::ListOfStrings` correctly but break
16614 // every downstream consumer that pinned the literal `"list of
16615 // strings"` bytes).
16616 for lb in ExpectedKwargShape::LABELS {
16617 let parsed: ExpectedKwargShape = lb.parse().unwrap_or_else(|_| {
16618 panic!(
16619 "ExpectedKwargShape::LABELS entry `{lb}` failed to \
16620 parse — the entry drifted from Self::label"
16621 )
16622 });
16623 assert_eq!(
16624 parsed.label(),
16625 lb,
16626 "ExpectedKwargShape::LABELS entry `{lb}` parses to a \
16627 variant whose label() `{}` does not match — the \
16628 constant and the match arm drifted apart",
16629 parsed.label(),
16630 );
16631 }
16632 }
16633
16634 #[test]
16635 fn expected_kwarg_shape_labels_cover_sorted_closed_set() {
16636 // Family-wide sweep: `Self::LABELS`, projected through sort,
16637 // MUST equal the derive-generated `sorted_labels()` on the
16638 // `ClosedSet` trait. Catches a regression where a new
16639 // variant is added to `Self::ALL` + `Self::label` (so
16640 // `sorted_labels()` grows) but the maintainer forgets to
16641 // extend the parallel per-role `pub const` + `Self::LABELS`
16642 // pair. Distinct from `_align_with_all_by_index` (which pins
16643 // ORDER preservation) and `_pairwise_distinct` (which pins
16644 // WITHIN-array uniqueness): this pin cross-checks the LABELS
16645 // array against the derive-generated candidate-list on the
16646 // trait, so a partial extension where LABELS grows but ALL
16647 // doesn't (or vice-versa) surfaces at both this pin AND
16648 // `_align_with_all_by_index`.
16649 let mut sorted_labels: Vec<&'static str> = ExpectedKwargShape::LABELS.to_vec();
16650 sorted_labels.sort_unstable();
16651 assert_eq!(
16652 sorted_labels,
16653 <ExpectedKwargShape as tatara_closed_set::ClosedSet>::sorted_labels(),
16654 "ExpectedKwargShape::LABELS diverged from the derive-\
16655 generated `sorted_labels()` — a variant was added or \
16656 removed from one array without the other tracking"
16657 );
16658 }
16659
16660 // ── SexpShape closed-set lift ───────────────────────────────────────
16661 //
16662 // The `LispError::TypeMismatch.got` and
16663 // `LispError::NamedFormNonSymbolName.got` slots were promoted from
16664 // `&'static str` to the typed closed-set `SexpShape` enum. The
16665 // twelve reachable Sexp outermost shapes — `Nil` / `Symbol` /
16666 // `Keyword` / `String` / `Int` / `Float` / `Bool` / `List` /
16667 // `Quote` / `Quasiquote` / `Unquote` / `UnquoteSplice` — are now
16668 // encoded as variant identities so authoring tools (REPL, LSP,
16669 // `tatara-check`) bind on `SexpShape::Int` etc. directly rather
16670 // than substring-matching `got == "int"`. Same posture as
16671 // `KwargPath`, `ExpectedKwargShape`, `MacroDefHead`, `UnquoteForm`,
16672 // `CompilerSpecIoStage`, and `TemplateInvariantKind`. After this
16673 // lift the `TypeMismatch` variant is fully closed-set typed in
16674 // ALL THREE of its slots — no `&'static str` projection remains
16675 // at any helper boundary.
16676
16677 #[test]
16678 fn sexp_shape_label_renders_canonical_string_for_every_variant() {
16679 // Pin every variant's canonical `&'static str` projection — a
16680 // regression that drifts any label (typo in `"strin"` for
16681 // `"string"`, swap of `"int"` ↔ `"float"`, capitalization
16682 // drift `"Quote"` for `"quote"`) fails-loudly here. The twelve
16683 // labels are byte-for-byte identical to the pre-lift
16684 // `sexp_type_name` projection so existing
16685 // `format!("{err}").contains("got int")` /
16686 // `got string` / `got list` / etc. assertions in consumer
16687 // crates pass unchanged across the lift.
16688 assert_eq!(SexpShape::Nil.label(), "nil");
16689 assert_eq!(SexpShape::Symbol.label(), "symbol");
16690 assert_eq!(SexpShape::Keyword.label(), "keyword");
16691 assert_eq!(SexpShape::String.label(), "string");
16692 assert_eq!(SexpShape::Int.label(), "int");
16693 assert_eq!(SexpShape::Float.label(), "float");
16694 assert_eq!(SexpShape::Bool.label(), "bool");
16695 assert_eq!(SexpShape::List.label(), "list");
16696 assert_eq!(SexpShape::Quote.label(), "quote");
16697 assert_eq!(SexpShape::Quasiquote.label(), "quasiquote");
16698 assert_eq!(SexpShape::Unquote.label(), "unquote");
16699 assert_eq!(SexpShape::UnquoteSplice.label(), "unquote-splice");
16700 }
16701
16702 #[test]
16703 fn sexp_shape_display_matches_label_for_every_variant() {
16704 // Pin Display-equals-label: the `#[error("... got {got}")]`
16705 // annotations on `LispError::TypeMismatch` and
16706 // `LispError::NamedFormNonSymbolName` project through Display,
16707 // and Display delegates to `label()`. A regression that
16708 // introduces a Display impl that deviates from `label()`
16709 // (e.g. capitalizing one variant) would drift the diagnostic
16710 // surface; this test pins the contract.
16711 assert_eq!(format!("{}", SexpShape::Nil), "nil");
16712 assert_eq!(format!("{}", SexpShape::Symbol), "symbol");
16713 assert_eq!(format!("{}", SexpShape::Keyword), "keyword");
16714 assert_eq!(format!("{}", SexpShape::String), "string");
16715 assert_eq!(format!("{}", SexpShape::Int), "int");
16716 assert_eq!(format!("{}", SexpShape::Float), "float");
16717 assert_eq!(format!("{}", SexpShape::Bool), "bool");
16718 assert_eq!(format!("{}", SexpShape::List), "list");
16719 assert_eq!(format!("{}", SexpShape::Quote), "quote");
16720 assert_eq!(format!("{}", SexpShape::Quasiquote), "quasiquote");
16721 assert_eq!(format!("{}", SexpShape::Unquote), "unquote");
16722 assert_eq!(format!("{}", SexpShape::UnquoteSplice), "unquote-splice");
16723 }
16724
16725 #[test]
16726 fn sexp_shape_label_routes_through_typed_per_variant_constants() {
16727 // PATH-UNIFORMITY: `SexpShape::label(v)` MUST equal the per-role
16728 // `pub const V_LABEL` for every `v`. Pre-lift the twelve arms
16729 // held inline `&'static str` literals; post-lift each arm binds
16730 // to its per-role constant. A regression that reverts ANY arm
16731 // to an inline literal (e.g. `Self::Symbol => "symbol"`) still
16732 // renders identically at runtime but silently disagrees with
16733 // the typed source of truth — this pin closes that drift.
16734 assert_eq!(SexpShape::Nil.label(), SexpShape::NIL_LABEL);
16735 assert_eq!(SexpShape::Symbol.label(), SexpShape::SYMBOL_LABEL);
16736 assert_eq!(SexpShape::Keyword.label(), SexpShape::KEYWORD_LABEL);
16737 assert_eq!(SexpShape::String.label(), SexpShape::STRING_LABEL);
16738 assert_eq!(SexpShape::Int.label(), SexpShape::INT_LABEL);
16739 assert_eq!(SexpShape::Float.label(), SexpShape::FLOAT_LABEL);
16740 assert_eq!(SexpShape::Bool.label(), SexpShape::BOOL_LABEL);
16741 assert_eq!(SexpShape::List.label(), SexpShape::LIST_LABEL);
16742 assert_eq!(SexpShape::Quote.label(), SexpShape::QUOTE_LABEL);
16743 assert_eq!(SexpShape::Quasiquote.label(), SexpShape::QUASIQUOTE_LABEL);
16744 assert_eq!(SexpShape::Unquote.label(), SexpShape::UNQUOTE_LABEL);
16745 assert_eq!(
16746 SexpShape::UnquoteSplice.label(),
16747 SexpShape::UNQUOTE_SPLICE_LABEL
16748 );
16749 }
16750
16751 #[test]
16752 fn sexp_shape_labels_has_expected_cardinality() {
16753 // Cardinality contract: `Self::LABELS.len() == 12` — the closed
16754 // set of Sexp outermost shapes has EXACTLY twelve members. A
16755 // regression that loosens the type to `&[&'static str]` fails
16756 // HERE, as does a refactor that adds a thirteenth variant to
16757 // ALL without adding its label to LABELS (rustc's forced-arity
16758 // check on `[&'static str; 12]` fails compilation).
16759 assert_eq!(
16760 SexpShape::LABELS.len(),
16761 12,
16762 "SexpShape::LABELS cardinality drifted from 12 — the twelve \
16763 reachable Sexp outermost shapes were extended without \
16764 extending the LABELS array in lockstep"
16765 );
16766 assert_eq!(SexpShape::LABELS.len(), SexpShape::ALL.len(),);
16767 }
16768
16769 #[test]
16770 fn sexp_shape_labels_align_with_all_by_index() {
16771 // ALIGNMENT CONTRACT: `Self::LABELS[i] == Self::ALL[i].label()`
16772 // element-wise. Binds the closed-set outer algebra's ordering
16773 // to the per-role LABEL constant ordering so any
16774 // `zip(SexpShape::ALL, SexpShape::LABELS)` consumer (LSP
16775 // completion, `tatara-check` coverage sweep, Sekiban metric-
16776 // label emitter) reads a coherent (variant, label) pair off
16777 // ONE forced-arity array pair rather than through a per-
16778 // consumer paired-iteration convention.
16779 for (i, v) in SexpShape::ALL.iter().enumerate() {
16780 assert_eq!(
16781 SexpShape::LABELS[i],
16782 v.label(),
16783 "SexpShape::LABELS[{i}] `{kw}` drifted from \
16784 SexpShape::ALL[{i}].label() `{via_variant}` — the \
16785 canonical ALL ordering and the LABELS ordering must \
16786 match element-wise",
16787 kw = SexpShape::LABELS[i],
16788 via_variant = v.label(),
16789 );
16790 }
16791 }
16792
16793 #[test]
16794 fn sexp_shape_labels_pairwise_distinct() {
16795 // Pairwise disjointness contract: every distinct pair of rows
16796 // in `Self::LABELS` MUST be byte-distinct. A collision would
16797 // silently degrade a `TypeMismatch { got: SexpShape::Int, ... }`
16798 // and a `TypeMismatch { got: SexpShape::Float, ... }` to
16799 // identically-rendered diagnostics, defeating the closed-set
16800 // typed-slot promotion. Twelve labels × twelve rows sweeps
16801 // `LABELS × LABELS` — supersedes any single collision pin.
16802 for (i, a) in SexpShape::LABELS.iter().enumerate() {
16803 for (j, b) in SexpShape::LABELS.iter().enumerate() {
16804 if i == j {
16805 continue;
16806 }
16807 assert_ne!(
16808 a, b,
16809 "SexpShape::LABELS[{i}] `{a}` collides with \
16810 SexpShape::LABELS[{j}] `{b}` — the diagnostic \
16811 label surface must be pairwise byte-distinct across \
16812 the twelve reachable Sexp outermost shapes"
16813 );
16814 }
16815 }
16816 }
16817
16818 #[test]
16819 fn sexp_shape_atom_carving_labels_align_with_expected_kwarg_shape_labels() {
16820 // CROSS-AXIS IDENTITY: the SexpShape's atomic-payload carving
16821 // ({Symbol, Keyword, String, Int, Bool, List}) shares FIVE
16822 // label-bytes byte-for-byte with ExpectedKwargShape's
16823 // corresponding per-role constants — the ExpectedKwargShape
16824 // docstrings on `KEYWORD_LABEL`, `STRING_LABEL`, `INT_LABEL`,
16825 // `BOOL_LABEL`, `LIST_LABEL` already anticipate this identity
16826 // ("matches `SexpShape::X_LABEL` byte-for-byte"). Post-lift the
16827 // identity is a compile-time cross-const composition pin
16828 // rather than a per-consumer byte-string convention.
16829 //
16830 // The overlap is intentional and load-bearing: consumer
16831 // diagnostic strings like `"expected keyword, got keyword"`
16832 // reuse the SAME byte-string on both sides of the got/expected
16833 // gate, so a spelling migration on ONE axis without the OTHER
16834 // would introduce a silent diagnostic asymmetry. Pinned here.
16835 assert_eq!(SexpShape::KEYWORD_LABEL, ExpectedKwargShape::KEYWORD_LABEL);
16836 assert_eq!(SexpShape::STRING_LABEL, ExpectedKwargShape::STRING_LABEL);
16837 assert_eq!(SexpShape::INT_LABEL, ExpectedKwargShape::INT_LABEL);
16838 assert_eq!(SexpShape::BOOL_LABEL, ExpectedKwargShape::BOOL_LABEL);
16839 assert_eq!(SexpShape::LIST_LABEL, ExpectedKwargShape::LIST_LABEL);
16840 // Non-overlapping bytes on the sibling axes: SexpShape::Float
16841 // ("float") is the narrower element-type; ExpectedKwargShape's
16842 // NUMBER_LABEL ("number") is the wider numeric-union label the
16843 // `extract_float` gate emits. The pair is intentionally
16844 // distinct — pin it so a naive rename doesn't collapse them.
16845 assert_ne!(SexpShape::FLOAT_LABEL, ExpectedKwargShape::NUMBER_LABEL);
16846 }
16847
16848 #[test]
16849 fn expected_kwarg_shape_per_role_labels_alias_sexp_shape_per_role_labels_via_static_ptr() {
16850 // ALIAS-CHAIN ROUTING CONTRACT: the FIVE per-role
16851 // `pub const ExpectedKwargShape::X_LABEL` constants that share
16852 // vocabulary with a matching `SexpShape` carving arm MUST route
16853 // through the parent-superset's `pub const SexpShape::X_LABEL`
16854 // as a compile-time typed alias
16855 // (`pub const ExpectedKwargShape::X_LABEL: &'static str =
16856 // SexpShape::X_LABEL`) rather than as two independent inline
16857 // string literals whose bytes happen to agree — the byte-
16858 // equality contract is anchored one test up at
16859 // `sexp_shape_atom_carving_labels_align_with_expected_kwarg_shape_labels`;
16860 // THIS pin catches a regression that re-inlines ONE per-role
16861 // ExpectedKwargShape constant to a fresh literal even when the
16862 // rendered bytes still agree (because a distinct inline
16863 // literal lives at a different `&'static str` address inside
16864 // the rodata segment, while a `pub const` aliased through
16865 // `SexpShape::X_LABEL` MUST point at the SAME rodata address
16866 // as the parent superset's constant).
16867 //
16868 // Sibling-shape pin to the four prior-run alias-chain routing
16869 // pins on the UnquoteForm ⊂ QuoteForm 2-of-4 subset carving
16870 // (`unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`,
16871 // `unquote_form_label_routes_through_to_quote_form_label_via_composition`,
16872 // `unquote_form_iac_forge_tag_routes_through_to_quote_form_iac_forge_tag_via_composition`,
16873 // `unquote_form_hash_discriminator_routes_through_to_quote_form_hash_discriminator_via_composition`)
16874 // — those pin the substitution-subset composition through the
16875 // superset via runtime pointer-equality on a value-carrying
16876 // method; THIS pin pins the ExpectedKwargShape ⊂ SexpShape
16877 // 5-of-7 subset carving's alias-chain through the parent-
16878 // superset via compile-time `pub const` identity on a
16879 // vocabulary-carrying constant.
16880 //
16881 // The TWO non-overlapping ExpectedKwargShape arms
16882 // (`NUMBER_LABEL` and `LIST_OF_STRINGS_LABEL`) stay OUTSIDE
16883 // this pin's sweep — those are direct-literal constants with
16884 // no [`SexpShape`] peer to alias through.
16885 //
16886 // Theory anchor: THEORY.md §II.1 invariant 5 (composition
16887 // preserves proofs) — the alias-chain composition law
16888 // `ExpectedKwargShape::X_LABEL == SexpShape::X_LABEL` on the
16889 // 5-of-7 subset carving is a rustc-time typed identity, not a
16890 // runtime byte-equality convention.
16891 assert!(
16892 std::ptr::eq(
16893 ExpectedKwargShape::KEYWORD_LABEL.as_ptr(),
16894 SexpShape::KEYWORD_LABEL.as_ptr(),
16895 ),
16896 "ExpectedKwargShape::KEYWORD_LABEL and SexpShape::KEYWORD_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
16897 );
16898 assert!(
16899 std::ptr::eq(
16900 ExpectedKwargShape::STRING_LABEL.as_ptr(),
16901 SexpShape::STRING_LABEL.as_ptr(),
16902 ),
16903 "ExpectedKwargShape::STRING_LABEL and SexpShape::STRING_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
16904 );
16905 assert!(
16906 std::ptr::eq(
16907 ExpectedKwargShape::INT_LABEL.as_ptr(),
16908 SexpShape::INT_LABEL.as_ptr(),
16909 ),
16910 "ExpectedKwargShape::INT_LABEL and SexpShape::INT_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
16911 );
16912 assert!(
16913 std::ptr::eq(
16914 ExpectedKwargShape::BOOL_LABEL.as_ptr(),
16915 SexpShape::BOOL_LABEL.as_ptr(),
16916 ),
16917 "ExpectedKwargShape::BOOL_LABEL and SexpShape::BOOL_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
16918 );
16919 assert!(
16920 std::ptr::eq(
16921 ExpectedKwargShape::LIST_LABEL.as_ptr(),
16922 SexpShape::LIST_LABEL.as_ptr(),
16923 ),
16924 "ExpectedKwargShape::LIST_LABEL and SexpShape::LIST_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
16925 );
16926 }
16927
16928 #[test]
16929 fn expected_kwarg_shape_residual_labels_lie_outside_sexp_shape_label_vocabulary() {
16930 // RESIDUAL-CARVING DISJOINTNESS CONTRACT: the TWO
16931 // ExpectedKwargShape arms that lie OUTSIDE the 5-of-7 aliased
16932 // subset carving (`NUMBER_LABEL`, `LIST_OF_STRINGS_LABEL`)
16933 // MUST carry vocabulary distinct from EVERY per-role
16934 // `SexpShape::X_LABEL` constant — otherwise the 5-of-7
16935 // carving would fail its subset-and-residual invariant (the
16936 // subset would carry MORE than 5 aliasable arms, or the
16937 // residual would carry FEWER than 2 direct-literal arms). The
16938 // pre-existing pin
16939 // `sexp_shape_atom_carving_labels_align_with_expected_kwarg_shape_labels`
16940 // anchors ONE of the two disjointness edges (SexpShape::FLOAT
16941 // vs ExpectedKwargShape::NUMBER); THIS pin anchors the
16942 // whole-family disjointness of both residual arms against
16943 // every SexpShape label at once.
16944 //
16945 // A regression that adds `SexpShape::Number` as a new variant
16946 // (renaming Float to Number) would fail HERE by making
16947 // `ExpectedKwargShape::NUMBER_LABEL` collide with the newly
16948 // added `SexpShape::NUMBER_LABEL`; the 5-of-7 carving MUST be
16949 // re-derived to 6-of-7 in that case (aliasing NUMBER_LABEL
16950 // through the new SexpShape arm). The pin surfaces the
16951 // structural-partition-change requirement at the disjointness
16952 // gate rather than through a silent literal-duplication.
16953 for &shape in SexpShape::ALL.iter() {
16954 let via_shape = SexpShape::label(shape);
16955 assert_ne!(
16956 ExpectedKwargShape::NUMBER_LABEL,
16957 via_shape,
16958 "ExpectedKwargShape::NUMBER_LABEL `\"number\"` collides with SexpShape::{shape:?}.label() `{via_shape}` — the 5-of-7 aliased subset carving's residual arm {{NUMBER}} MUST lie outside every SexpShape label; a collision means the carving needs to be re-derived to a 6-of-7 subset aliased through the newly-overlapping SexpShape arm",
16959 );
16960 assert_ne!(
16961 ExpectedKwargShape::LIST_OF_STRINGS_LABEL,
16962 via_shape,
16963 "ExpectedKwargShape::LIST_OF_STRINGS_LABEL `\"list of strings\"` collides with SexpShape::{shape:?}.label() `{via_shape}` — the 5-of-7 aliased subset carving's residual arm {{LIST_OF_STRINGS}} MUST lie outside every SexpShape label",
16964 );
16965 }
16966 }
16967
16968 #[test]
16969 fn sexp_shape_quote_carving_labels_align_with_quote_form_iac_forge_tags() {
16970 // CROSS-AXIS IDENTITY: SexpShape's quote-family carving
16971 // ({Quote, Quasiquote, Unquote}) shares THREE label-bytes
16972 // byte-for-byte with QuoteForm's corresponding iac-forge tag
16973 // constants. Post-lift the identity is a compile-time cross-
16974 // const composition pin rather than a per-consumer convention.
16975 //
16976 // The UnquoteSplice variant is the ONE intentional exception:
16977 // SexpShape::UNQUOTE_SPLICE_LABEL is `"unquote-splice"` (short
16978 // form used in `LispError::TypeMismatch` diagnostic Display)
16979 // while QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG is
16980 // `"unquote-splicing"` (long form used for cross-crate byte-
16981 // identical interoperability with the iac-forge canonical
16982 // form). The asymmetry is documented in the per-role constant's
16983 // docstring and pinned in
16984 // `sexp_shape_unquote_splice_label_differs_from_quote_form_iac_forge_tag`.
16985 assert_eq!(
16986 SexpShape::QUOTE_LABEL,
16987 crate::ast::QuoteForm::QUOTE_IAC_FORGE_TAG
16988 );
16989 assert_eq!(
16990 SexpShape::QUASIQUOTE_LABEL,
16991 crate::ast::QuoteForm::QUASIQUOTE_IAC_FORGE_TAG
16992 );
16993 assert_eq!(
16994 SexpShape::UNQUOTE_LABEL,
16995 crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG
16996 );
16997 }
16998
16999 #[test]
17000 fn sexp_shape_unquote_splice_label_differs_from_quote_form_iac_forge_tag() {
17001 // ONE intentional cross-axis asymmetry: SexpShape's diagnostic
17002 // label surface uses the SHORTER `"unquote-splice"` bytes while
17003 // QuoteForm's iac-forge canonical-form tag surface uses the
17004 // LONGER `"unquote-splicing"` bytes. Pin the asymmetry so a
17005 // naive rename on either surface doesn't collapse the two
17006 // sibling label vocabularies onto ONE spelling.
17007 assert_ne!(
17008 SexpShape::UNQUOTE_SPLICE_LABEL,
17009 crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG,
17010 "SexpShape::UNQUOTE_SPLICE_LABEL and \
17011 QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG collapsed to ONE \
17012 spelling — the two sibling label surfaces must render \
17013 distinct bytes so the UnquoteSplice diagnostic renders \
17014 the short `\"unquote-splice\"` bytes while iac-forge \
17015 canonical-form emits the long `\"unquote-splicing\"` bytes"
17016 );
17017 assert_eq!(SexpShape::UNQUOTE_SPLICE_LABEL, "unquote-splice");
17018 assert_eq!(
17019 crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG,
17020 "unquote-splicing"
17021 );
17022 }
17023
17024 #[test]
17025 fn type_mismatch_got_carries_typed_shape_through_variant_slot() {
17026 // After the typed-slot promotion, `LispError::TypeMismatch.got`
17027 // is `SexpShape` — the closed-set typed enum. Consumers
17028 // (REPL, LSP, `tatara-check`) pattern-match on the variant
17029 // identity `SexpShape::Int` directly rather than
17030 // substring-matching a rendered `"got int"` prefix. Pin the
17031 // structural binding AND the Display projection so the
17032 // byte-for-byte rendering contract is anchored from both
17033 // angles. A regression that re-introduces a `&'static str`-
17034 // shaped `got` slot (collapsing the typed enum back into a
17035 // free-form label) fails-loudly here.
17036 let err = LispError::TypeMismatch {
17037 form: KwargPath::named("threshold"),
17038 expected: ExpectedKwargShape::Number,
17039 got: SexpShape::String,
17040 };
17041 match &err {
17042 LispError::TypeMismatch { got, .. } => {
17043 assert_eq!(*got, SexpShape::String);
17044 }
17045 other => panic!("expected TypeMismatch, got {other:?}"),
17046 }
17047 assert_eq!(
17048 format!("{err}"),
17049 "compile error in :threshold: expected number, got string"
17050 );
17051 }
17052
17053 #[test]
17054 fn named_form_non_symbol_name_got_carries_typed_shape_through_variant_slot() {
17055 // Sibling pin to `type_mismatch_got_…` on the second `got`
17056 // slot that flows from `sexp_shape`. Both
17057 // `LispError::TypeMismatch.got` and
17058 // `LispError::NamedFormNonSymbolName.got` are typed
17059 // `SexpShape` now — one helper (`crate::domain::sexp_shape`)
17060 // is the single projection source, and rustc-enforces
17061 // matching at every projection site. A regression that
17062 // bifurcates the two slots (e.g. typed `SexpShape` on one,
17063 // `&'static str` on the other) fails-loudly here.
17064 let err = LispError::NamedFormNonSymbolName {
17065 keyword: "defpoint",
17066 got: SexpShape::List,
17067 };
17068 match &err {
17069 LispError::NamedFormNonSymbolName { got, .. } => {
17070 assert_eq!(*got, SexpShape::List);
17071 }
17072 other => panic!("expected NamedFormNonSymbolName, got {other:?}"),
17073 }
17074 assert_eq!(
17075 format!("{err}"),
17076 "compile error in defpoint: positional NAME must be a symbol or string (got list)"
17077 );
17078 }
17079
17080 #[test]
17081 fn sexp_shape_all_is_unique_and_complete() {
17082 // Closed-set posture: `ALL` enumerates every reachable variant
17083 // EXACTLY ONCE — no duplicates, no omissions. The `[Self; 12]`
17084 // array literal in the declaration forces the arity at compile
17085 // time; this test catches the orthogonal failure modes — a
17086 // future variant added at the type without being added to ALL
17087 // (silently dropped from every consumer's sweep), or a typo
17088 // that duplicates an entry (silently double-counted). Same
17089 // truth-table pinning every sibling closed-set lift in the
17090 // workspace uses (RequestorKind::ALL, ReceiptKind::ALL,
17091 // ConditionKind::ALL, ProcessPhase::ALL, ChannelKind::ALL, …).
17092 //
17093 // The `iter+map+collect+sort_unstable` quadruple this test
17094 // inlined pre-lift now binds at `<SexpShape as
17095 // ClosedSet>::sorted_labels()` — the canonical-ordered
17096 // candidate-list projection on the trait. Distinctness of the
17097 // sorted result is covered by
17098 // `assert_closed_set_well_formed::<SexpShape>()`.
17099 assert_eq!(SexpShape::ALL.len(), 12);
17100 assert_eq!(
17101 <SexpShape as tatara_closed_set::ClosedSet>::sorted_labels(),
17102 vec![
17103 "bool",
17104 "float",
17105 "int",
17106 "keyword",
17107 "list",
17108 "nil",
17109 "quasiquote",
17110 "quote",
17111 "string",
17112 "symbol",
17113 "unquote",
17114 "unquote-splice",
17115 ],
17116 "SexpShape::ALL must cover every reachable Sexp outermost shape"
17117 );
17118 }
17119
17120 #[test]
17121 fn sexp_shape_label_round_trips_through_from_str() {
17122 // Bidirectional `label` ↔ `FromStr` contract: for every
17123 // variant in ALL, `shape.label().parse() == Ok(shape)`. A
17124 // regression that drifts the (variant, literal) pairing at
17125 // ONE arm of `label` (typo, capitalization drift) OR at the
17126 // `FromStr` decode body (off-by-one, missing variant in the
17127 // sweep) fails-loudly here. The canonical-literal site is
17128 // singular (`label`) so the round-trip is the only way the
17129 // typed surface and the rendered diagnostic literal can
17130 // drift apart — pinning it here means they cannot.
17131 for shape in SexpShape::ALL {
17132 let parsed: SexpShape = shape
17133 .label()
17134 .parse()
17135 .expect("every ALL variant's label must round-trip through FromStr");
17136 assert_eq!(
17137 parsed,
17138 shape,
17139 "FromStr({}) must round-trip to the same variant",
17140 shape.label()
17141 );
17142 }
17143 }
17144
17145 #[test]
17146 fn unknown_sexp_shape_carries_offending_input_verbatim() {
17147 // Operator-facing diagnostic contract: the offending input
17148 // lands in the typed error verbatim — no normalization, no
17149 // case-folding, no truncation. Pin the exact `#[error(...)]`
17150 // rendering AND the typed `.0` field projection so a future
17151 // refactor that normalizes (e.g. `.to_lowercase()`) before
17152 // building the error or that drops the input fails-loudly
17153 // here. Symmetric to every sibling `Unknown*` carrier in the
17154 // workspace.
17155 let err: UnknownSexpShape = "Symbol".parse::<SexpShape>().expect_err(
17156 "capitalized `Symbol` must NOT decode — labels are byte-equal case-sensitive",
17157 );
17158 assert_eq!(err.0, "Symbol");
17159 assert_eq!(format!("{err}"), "unknown sexp shape: Symbol");
17160
17161 let err: UnknownSexpShape = "number"
17162 .parse::<SexpShape>()
17163 .expect_err("`number` is ExpectedKwargShape's vocabulary, not SexpShape's");
17164 assert_eq!(err.0, "number");
17165 assert_eq!(format!("{err}"), "unknown sexp shape: number");
17166
17167 let err: UnknownSexpShape = ""
17168 .parse::<SexpShape>()
17169 .expect_err("empty input must NOT decode to a SexpShape");
17170 assert_eq!(err.0, "");
17171 assert_eq!(format!("{err}"), "unknown sexp shape: ");
17172 }
17173
17174 #[test]
17175 fn sexp_shape_from_str_accepts_only_canonical_labels() {
17176 // Cross-axis guard: `ExpectedKwargShape::label()`'s vocabulary
17177 // overlaps with `SexpShape::label()` on five of seven entries
17178 // (`keyword` / `string` / `int` / `bool` / `list`) and DOES
17179 // NOT overlap on two (`number` / `list of strings`). The
17180 // overlap is intentional — both axes are projections of the
17181 // same `Sexp` algebra at typed-entry gates — but the
17182 // non-overlap is the load-bearing part: a `FromStr` that
17183 // silently accepts `"number"` as a `SexpShape` would corrupt
17184 // the typed identity. Pin BOTH directions: the overlap
17185 // decodes successfully (and to the matching `SexpShape`
17186 // variant), the non-overlap rejects.
17187 assert_eq!("keyword".parse::<SexpShape>().unwrap(), SexpShape::Keyword);
17188 assert_eq!("string".parse::<SexpShape>().unwrap(), SexpShape::String);
17189 assert_eq!("int".parse::<SexpShape>().unwrap(), SexpShape::Int);
17190 assert_eq!("bool".parse::<SexpShape>().unwrap(), SexpShape::Bool);
17191 assert_eq!("list".parse::<SexpShape>().unwrap(), SexpShape::List);
17192
17193 assert!("number".parse::<SexpShape>().is_err());
17194 assert!("list of strings".parse::<SexpShape>().is_err());
17195
17196 // Sanity: every UnquoteForm marker literal (`,` / `,@` / etc.)
17197 // is also NOT a SexpShape label — the marker projection lives
17198 // on a different axis (the rendered punctuation) than the
17199 // shape label (the structural identity).
17200 assert!(",".parse::<SexpShape>().is_err());
17201 assert!(",@".parse::<SexpShape>().is_err());
17202 }
17203
17204 #[test]
17205 fn sexp_shape_int_bifurcates_from_float_through_variant_slot() {
17206 // `Int` and `Float` are distinct typed variants — a regression
17207 // that collapses them into a single `Number` variant (which
17208 // would drop the bifurcation that `Sexp::Atom(Int(_))` and
17209 // `Sexp::Atom(Float(_))` already carry at the AST layer) is
17210 // caught here. The two render distinct rendered labels and
17211 // hold distinct variant identities.
17212 let int_err = LispError::TypeMismatch {
17213 form: KwargPath::named("count"),
17214 expected: ExpectedKwargShape::String,
17215 got: SexpShape::Int,
17216 };
17217 let float_err = LispError::TypeMismatch {
17218 form: KwargPath::named("ratio"),
17219 expected: ExpectedKwargShape::String,
17220 got: SexpShape::Float,
17221 };
17222 assert_eq!(
17223 format!("{int_err}"),
17224 "compile error in :count: expected string, got int"
17225 );
17226 assert_eq!(
17227 format!("{float_err}"),
17228 "compile error in :ratio: expected string, got float"
17229 );
17230 match (&int_err, &float_err) {
17231 (LispError::TypeMismatch { got: a, .. }, LispError::TypeMismatch { got: b, .. }) => {
17232 assert_ne!(a, b);
17233 assert_eq!(*a, SexpShape::Int);
17234 assert_eq!(*b, SexpShape::Float);
17235 }
17236 _ => panic!("both must be TypeMismatch"),
17237 }
17238 }
17239
17240 // ── SexpShape ↔ AtomKind / QuoteForm: typed-shape lattice inverses ──
17241 //
17242 // The forward embed projections [`crate::ast::AtomKind::sexp_shape`]
17243 // (6→12) and [`crate::ast::QuoteForm::sexp_shape`] (4→12) have
17244 // existed for prior runs (commits 121bb60 + b15-ish); their dual
17245 // projections [`SexpShape::as_atom_kind`] (12→6, partial) and
17246 // [`SexpShape::as_quote_form`] (12→4, partial) close the
17247 // embed/project section on the typed-shape lattice. The
17248 // composition laws below pin the (embed, project) pair is an
17249 // `Iso(AtomKind, AtomShape ⊂ SexpShape)` AND
17250 // `Iso(QuoteForm, QuoteShape ⊂ SexpShape)` — every typed marker
17251 // round-trips through the embed, every shape pre-image recovers
17252 // the typed marker. Pre-lift the typed-shape lattice's two
17253 // forward embeds had no dual projection naming the inverse 6-of-
17254 // 12 + 4-of-12 carvings; post-lift the carvings live at ONE site
17255 // each on the [`SexpShape`] algebra so a regression that drifts
17256 // the inverse from the embed surfaces at the round-trip pin
17257 // instead of at every speculative LSP / REPL / `tatara-check` /
17258 // metrics consumer's per-carving inline `match`.
17259
17260 #[test]
17261 fn as_atom_kind_projects_each_atom_shape_to_canonical_atom_kind_and_rejects_non_atom_shapes() {
17262 // Per-variant truth-table sweep across every `SexpShape::ALL`
17263 // entry — pins each variant's canonical mapping (atomic-payload
17264 // arms project to the matching `AtomKind`; `Nil` / `List` /
17265 // every quote-family arm project to `None`) byte-for-byte so a
17266 // regression that drifts ONE arm (e.g. swaps `Symbol →
17267 // AtomKind::Keyword`, drops `Bool`'s arm to `None`, accepts
17268 // `List` as `Some(AtomKind::Str)`) fails loudly. The full
17269 // `SexpShape::ALL` sweep doubles as exhaustiveness — adding a
17270 // hypothetical thirteenth variant (e.g. `Vector` for `#(...)`)
17271 // forces the test author to extend BOTH this sweep AND the
17272 // typed `match` body, with rustc enforcing the match arm's
17273 // presence and this sweep enforcing the (variant, projected
17274 // mapping) pairing's canonical-form.
17275 use crate::ast::AtomKind;
17276 for shape in SexpShape::ALL {
17277 let projected = shape.as_atom_kind();
17278 let expected = match shape {
17279 SexpShape::Symbol => Some(AtomKind::Symbol),
17280 SexpShape::Keyword => Some(AtomKind::Keyword),
17281 SexpShape::String => Some(AtomKind::Str),
17282 SexpShape::Int => Some(AtomKind::Int),
17283 SexpShape::Float => Some(AtomKind::Float),
17284 SexpShape::Bool => Some(AtomKind::Bool),
17285 SexpShape::Nil
17286 | SexpShape::List
17287 | SexpShape::Quote
17288 | SexpShape::Quasiquote
17289 | SexpShape::Unquote
17290 | SexpShape::UnquoteSplice => None,
17291 };
17292 assert_eq!(
17293 projected, expected,
17294 "SexpShape::{shape:?}.as_atom_kind() drifted from canonical mapping"
17295 );
17296 }
17297 }
17298
17299 #[test]
17300 fn atom_kind_sexp_shape_round_trips_through_as_atom_kind() {
17301 // The embed/project section law on the atomic carving:
17302 // `AtomKind::sexp_shape(k).as_atom_kind() == Some(k)` for every
17303 // `k: AtomKind::ALL`. Pinning the round-trip for every variant
17304 // in the closed set proves the (embed, project) pair is an
17305 // `Iso(AtomKind, AtomShape ⊂ SexpShape)` — the section is total
17306 // on `AtomKind`'s carving. A regression that drifts EITHER
17307 // direction (an `AtomKind::sexp_shape` arm that mis-maps OR a
17308 // `SexpShape::as_atom_kind` arm that mis-inverts) fails here
17309 // without depending on any per-consumer call site. Same posture
17310 // as `unquote_form_marker_routes_through_to_quote_form_prefix_
17311 // via_composition`'s round-trip on the 2-of-4 quote-family
17312 // subset.
17313 use crate::ast::AtomKind;
17314 for kind in AtomKind::ALL {
17315 let shape = kind.sexp_shape();
17316 let recovered = shape.as_atom_kind();
17317 assert_eq!(
17318 recovered,
17319 Some(kind),
17320 "AtomKind::{kind:?} did NOT round-trip — sexp_shape().as_atom_kind() must recover the typed marker"
17321 );
17322 }
17323 }
17324
17325 #[test]
17326 fn as_quote_form_projects_each_quote_shape_to_canonical_quote_form_and_rejects_non_quote_shapes(
17327 ) {
17328 // Per-variant truth-table sweep across every `SexpShape::ALL`
17329 // entry — pins each variant's canonical mapping (quote-family
17330 // arms project to the matching `QuoteForm`; `Nil` / `List` /
17331 // every atomic-payload arm project to `None`) byte-for-byte.
17332 // Sibling sweep to
17333 // `as_atom_kind_projects_each_atom_shape_to_canonical_atom_kind_and_rejects_non_atom_shapes`
17334 // on the quote-family axis.
17335 use crate::ast::QuoteForm;
17336 for shape in SexpShape::ALL {
17337 let projected = shape.as_quote_form();
17338 let expected = match shape {
17339 SexpShape::Quote => Some(QuoteForm::Quote),
17340 SexpShape::Quasiquote => Some(QuoteForm::Quasiquote),
17341 SexpShape::Unquote => Some(QuoteForm::Unquote),
17342 SexpShape::UnquoteSplice => Some(QuoteForm::UnquoteSplice),
17343 SexpShape::Nil
17344 | SexpShape::List
17345 | SexpShape::Symbol
17346 | SexpShape::Keyword
17347 | SexpShape::String
17348 | SexpShape::Int
17349 | SexpShape::Float
17350 | SexpShape::Bool => None,
17351 };
17352 assert_eq!(
17353 projected, expected,
17354 "SexpShape::{shape:?}.as_quote_form() drifted from canonical mapping"
17355 );
17356 }
17357 }
17358
17359 #[test]
17360 fn quote_form_sexp_shape_round_trips_through_as_quote_form() {
17361 // The embed/project section law on the quote-family carving:
17362 // `QuoteForm::sexp_shape(qf).as_quote_form() == Some(qf)` for
17363 // every `qf: QuoteForm::ALL`. Proves the (embed, project) pair
17364 // is an `Iso(QuoteForm, QuoteShape ⊂ SexpShape)` — the section
17365 // is total on `QuoteForm`'s carving. Sibling round-trip to
17366 // `atom_kind_sexp_shape_round_trips_through_as_atom_kind` on
17367 // the quote-family axis.
17368 use crate::ast::QuoteForm;
17369 for qf in QuoteForm::ALL {
17370 let shape = qf.sexp_shape();
17371 let recovered = shape.as_quote_form();
17372 assert_eq!(
17373 recovered,
17374 Some(qf),
17375 "QuoteForm::{qf:?} did NOT round-trip — sexp_shape().as_quote_form() must recover the typed marker"
17376 );
17377 }
17378 }
17379
17380 #[test]
17381 fn as_atom_kind_and_as_quote_form_partition_carvable_sexp_shape_variants() {
17382 // Disjointness invariant: for every variant in
17383 // `SexpShape::ALL`, AT MOST ONE of `as_atom_kind()` and
17384 // `as_quote_form()` returns `Some` — the typed-shape lattice's
17385 // two named carvings partition the carve-able SexpShape
17386 // variants. The two non-carved variants (`Nil`, `List`) project
17387 // to `None` through BOTH projections — the kernel of both
17388 // partial inverses. A regression that drifts the partition
17389 // (e.g. accepts `SexpShape::List` as an atom kind, or as a
17390 // quote form) fails here. Sibling to
17391 // `as_atom_kind_projects_each_atom_shape_to_canonical_atom_kind_and_rejects_non_atom_shapes`
17392 // and
17393 // `as_quote_form_projects_each_quote_shape_to_canonical_quote_form_and_rejects_non_quote_shapes`
17394 // — those pin the per-axis canonical mapping; this pins the
17395 // joint disjointness across both axes. The residual-side of the
17396 // partition (the two `Nil`/`List` variants) is now itself a
17397 // typed carving `StructuralKind` — pinned by
17398 // `sexp_shape_partition_is_total_across_atom_quote_structural_carvings`.
17399 for shape in SexpShape::ALL {
17400 let atom = shape.as_atom_kind().is_some();
17401 let quote = shape.as_quote_form().is_some();
17402 assert!(
17403 !(atom && quote),
17404 "SexpShape::{shape:?} projects as BOTH an atom kind AND a quote form — typed-shape carvings must be disjoint"
17405 );
17406 // Cross-axis closure: the only variants that project as
17407 // NEITHER are the structural-residual shapes (`Nil` and
17408 // `List`). Every other variant must be in exactly ONE of
17409 // the two named carvings — the substrate's typed-shape
17410 // lattice's structural completeness pin. Post-lift the
17411 // residual-side identity binds to the typed
17412 // `as_structural_kind()` projection rather than a runtime
17413 // `matches!(shape, SexpShape::Nil | SexpShape::List)`
17414 // predicate; the invariant lives at ONE closed-set match
17415 // on the outer algebra.
17416 let carved = atom || quote;
17417 let expected_carved = shape.as_structural_kind().is_none();
17418 assert_eq!(
17419 carved, expected_carved,
17420 "SexpShape::{shape:?} must be carved iff it does not project through as_structural_kind"
17421 );
17422 }
17423 }
17424
17425 #[test]
17426 fn sexp_shape_partition_is_total_across_atom_quote_structural_carvings() {
17427 // Partition-total invariant across ALL THREE typed-shape
17428 // carvings — the (typed theorem, previously test assertion)
17429 // this lift makes structural. For every variant in
17430 // `SexpShape::ALL`, EXACTLY ONE of `as_atom_kind()`,
17431 // `as_quote_form()`, `as_structural_kind()` returns `Some(_)`.
17432 // Pre-lift the residual side lived at ONE inline
17433 // `!matches!(shape, SexpShape::Nil | SexpShape::List)`
17434 // assertion inside the carving-disjointness pin above (the
17435 // runtime witness the substrate maintained by test
17436 // discipline); post-lift the residual is a TYPED CARVING
17437 // binding at ONE typed-algebra method, and the partition-
17438 // total invariant across the three carvings becomes a TYPED
17439 // THEOREM the joint sweep here surfaces byte-for-byte.
17440 //
17441 // Cardinalities: 6 + 4 + 2 = 12 (atomic-payload + quote-family
17442 // + structural-residual = every SexpShape). A regression that
17443 // drifts the partition-membership of any variant across any
17444 // of the three carvings surfaces here.
17445 let mut atom_count = 0;
17446 let mut quote_count = 0;
17447 let mut structural_count = 0;
17448 for shape in SexpShape::ALL {
17449 let atom = shape.as_atom_kind().is_some();
17450 let quote = shape.as_quote_form().is_some();
17451 let structural = shape.as_structural_kind().is_some();
17452 let membership = usize::from(atom) + usize::from(quote) + usize::from(structural);
17453 assert_eq!(
17454 membership, 1,
17455 "SexpShape::{shape:?} lands in {membership} carvings — partition must land it in EXACTLY ONE of (as_atom_kind, as_quote_form, as_structural_kind)",
17456 );
17457 atom_count += usize::from(atom);
17458 quote_count += usize::from(quote);
17459 structural_count += usize::from(structural);
17460 }
17461 assert_eq!(
17462 atom_count, 6,
17463 "atomic-payload carving must cover EXACTLY 6 of SexpShape::ALL",
17464 );
17465 assert_eq!(
17466 quote_count, 4,
17467 "quote-family carving must cover EXACTLY 4 of SexpShape::ALL",
17468 );
17469 assert_eq!(
17470 structural_count, 2,
17471 "structural-residual carving must cover EXACTLY 2 of SexpShape::ALL",
17472 );
17473 assert_eq!(
17474 atom_count + quote_count + structural_count,
17475 SexpShape::ALL.len(),
17476 "the three carvings must partition SexpShape::ALL totally (6 + 4 + 2 = 12)",
17477 );
17478 }
17479
17480 #[test]
17481 fn as_atom_kind_composes_with_sexp_shape_via_atom_kind_label_round_trip() {
17482 // Cross-projection composition law: for every atom kind, the
17483 // diagnostic label round-trips through both directions of the
17484 // embed/project pair AND `AtomKind::label`:
17485 // `AtomKind::ALL[i].label() == AtomKind::ALL[i].sexp_shape()
17486 // .as_atom_kind().expect("...").label()`. This pin proves the
17487 // typed-shape lattice's two carvings are not only structural
17488 // inverses but ALSO label-coherent — a regression that drifts
17489 // the (AtomKind variant, SexpShape variant) pairing while
17490 // preserving the projection's structural inverseness (e.g. a
17491 // future refactor that renames the variants in lockstep but
17492 // leaves the per-variant labels stale) surfaces here. The
17493 // label-coherence binds the diagnostic surface to the typed
17494 // algebra at BOTH layers.
17495 use crate::ast::AtomKind;
17496 for kind in AtomKind::ALL {
17497 let via_round_trip = kind
17498 .sexp_shape()
17499 .as_atom_kind()
17500 .expect("every AtomKind round-trips through the embed/project pair")
17501 .label();
17502 assert_eq!(
17503 via_round_trip,
17504 kind.label(),
17505 "AtomKind::{kind:?}.label() drifted from sexp_shape().as_atom_kind().label() — embed/project must preserve label coherence"
17506 );
17507 }
17508 }
17509
17510 #[test]
17511 fn as_quote_form_composes_with_sexp_shape_via_quote_form_prefix_round_trip() {
17512 // Cross-projection composition law (quote-family sibling of
17513 // `as_atom_kind_composes_with_sexp_shape_via_atom_kind_label_round_trip`):
17514 // for every `qf: QuoteForm::ALL`,
17515 // `qf.prefix() == qf.sexp_shape().as_quote_form().expect("...").prefix()`.
17516 // Pins the (QuoteForm variant, SexpShape variant) pairing
17517 // round-trips through the embed/project pair AND preserves
17518 // each variant's canonical homoiconic-prefix punctuation
17519 // (`"'"` / `` "`" `` / `","` / `",@"`) — a regression that
17520 // drifts the round-trip OR drifts the prefix surfaces here.
17521 use crate::ast::QuoteForm;
17522 for qf in QuoteForm::ALL {
17523 let via_round_trip = qf
17524 .sexp_shape()
17525 .as_quote_form()
17526 .expect("every QuoteForm round-trips through the embed/project pair")
17527 .prefix();
17528 assert_eq!(
17529 via_round_trip,
17530 qf.prefix(),
17531 "QuoteForm::{qf:?}.prefix() drifted from sexp_shape().as_quote_form().prefix() — embed/project must preserve prefix coherence"
17532 );
17533 }
17534 }
17535
17536 // ── UnquoteForm: ALL closure + FromStr round-trip ──────────────────
17537 //
17538 // `UnquoteForm` (the two template-marker syntactic forms `,` and
17539 // `,@`) joins the substrate's closed-set algebra family —
17540 // `SexpShape::ALL` + `FromStr`, `AtomKind::ALL` + `FromStr`,
17541 // `RequestorKind::ALL` + `FromStr`, etc. — by lifting the canonical
17542 // `&'static str` marker literal vocabulary onto ONE site
17543 // (`Self::marker` keyed by `Self::ALL`) the operator-facing decode
17544 // path inverts. Pre-lift the punctuation vocabulary lived ONLY
17545 // in `marker()`'s match arms; post-lift the SAME vocabulary
17546 // round-trips through `FromStr` keyed on the closed set, so the
17547 // typed surface and the rendered diagnostic literal cannot drift.
17548 // Same posture as `sexp_shape_label_round_trips_through_from_str`
17549 // / `atom_kind_label_round_trips_through_from_str`.
17550
17551 #[test]
17552 fn unquote_form_all_is_unique_and_complete() {
17553 // Closed-set posture: `ALL` enumerates every reachable variant
17554 // EXACTLY ONCE — no duplicates, no omissions. The `[Self; 2]`
17555 // array literal in the declaration forces the arity at compile
17556 // time; this test catches the orthogonal failure modes — a
17557 // future variant added at the type without being added to ALL
17558 // (silently dropped from every consumer's sweep), or a typo
17559 // that duplicates an entry (silently double-counted). Same
17560 // truth-table pinning every sibling closed-set lift in the
17561 // workspace uses (SexpShape::ALL, AtomKind::ALL,
17562 // RequestorKind::ALL, ReceiptKind::ALL, ConditionKind::ALL, …).
17563 //
17564 // The `iter+map+collect+sort_unstable` quadruple this test
17565 // inlined pre-lift now binds at `<UnquoteForm as
17566 // ClosedSet>::sorted_labels()` — the canonical-ordered
17567 // candidate-list projection on the trait. Distinctness of the
17568 // sorted result is covered by
17569 // `assert_closed_set_well_formed::<UnquoteForm>()`.
17570 assert_eq!(UnquoteForm::ALL.len(), 2);
17571 assert_eq!(
17572 <UnquoteForm as tatara_closed_set::ClosedSet>::sorted_labels(),
17573 vec![",", ",@"],
17574 "UnquoteForm::ALL must cover both template-marker syntactic forms"
17575 );
17576 }
17577
17578 #[test]
17579 fn unquote_form_marker_round_trips_through_from_str() {
17580 // Bidirectional `marker` ↔ `FromStr` contract: for every
17581 // variant in ALL, `form.marker().parse() == Ok(form)`. A
17582 // regression that drifts the (variant, literal) pairing at
17583 // ONE arm of `marker` (typo, `,,` instead of `,`, `, @` with
17584 // a stray space) OR at the `FromStr` decode body (off-by-one,
17585 // missing variant in the sweep) fails-loudly here. The
17586 // canonical-literal site is singular (`marker`) so the
17587 // round-trip is the only way the typed surface and the
17588 // rendered diagnostic literal can drift apart — pinning it
17589 // here means they cannot.
17590 for form in UnquoteForm::ALL {
17591 let parsed: UnquoteForm = form
17592 .marker()
17593 .parse()
17594 .expect("every ALL variant's marker must round-trip through FromStr");
17595 assert_eq!(
17596 parsed,
17597 form,
17598 "FromStr({}) must round-trip to the same variant",
17599 form.marker()
17600 );
17601 }
17602 }
17603
17604 #[test]
17605 fn unknown_unquote_form_carries_offending_input_verbatim() {
17606 // Operator-facing diagnostic contract: the offending input
17607 // lands in the typed error verbatim — no normalization, no
17608 // truncation, no whitespace coercion. Pin the exact
17609 // `#[error(...)]` rendering AND the typed `.0` field
17610 // projection so a future refactor that normalizes (e.g.
17611 // `.trim()`) before building the error or that drops the
17612 // input fails-loudly here. Symmetric to every sibling
17613 // `Unknown*` carrier in the workspace
17614 // ([`UnknownSexpShape`], [`crate::ast::UnknownAtomKind`],
17615 // `tatara_process::allocation::UnknownRequestorKind`, …).
17616 let err: UnknownUnquoteForm = ",,"
17617 .parse::<UnquoteForm>()
17618 .expect_err("doubled comma `,,` is not a canonical template marker");
17619 assert_eq!(err.0, ",,");
17620 assert_eq!(format!("{err}"), "unknown unquote form: ,,");
17621
17622 let err: UnknownUnquoteForm = ",@@"
17623 .parse::<UnquoteForm>()
17624 .expect_err("doubled-at `,@@` is not a canonical template marker");
17625 assert_eq!(err.0, ",@@");
17626 assert_eq!(format!("{err}"), "unknown unquote form: ,@@");
17627
17628 let err: UnknownUnquoteForm = ""
17629 .parse::<UnquoteForm>()
17630 .expect_err("empty input must NOT decode to an UnquoteForm");
17631 assert_eq!(err.0, "");
17632 assert_eq!(format!("{err}"), "unknown unquote form: ");
17633 }
17634
17635 #[test]
17636 fn unquote_form_from_str_rejects_sexp_shape_labels_on_template_marker_axis() {
17637 // Cross-axis guard: [`SexpShape`] projects the SAME two
17638 // `Sexp::Unquote` / `Sexp::UnquoteSplice` constructors as
17639 // [`UnquoteForm`] does, but onto a DIFFERENT vocabulary —
17640 // `"unquote"` / `"unquote-splice"` (structural-identity labels)
17641 // vs `","` / `",@"` (punctuation markers). The two axes share
17642 // the same closed-set cardinality (2) but their vocabularies
17643 // are intentionally disjoint. A `FromStr` that silently
17644 // accepted `"unquote"` as an `UnquoteForm` would corrupt the
17645 // typed identity at the diagnostic boundary. Pin BOTH
17646 // directions: the SAME punctuation labels (`,` / `,@`) decode
17647 // through [`UnquoteForm`] but NOT through [`SexpShape`]; the
17648 // SAME structural labels (`"unquote"` / `"unquote-splice"`)
17649 // decode through [`SexpShape`] but NOT through [`UnquoteForm`].
17650 // Anchors the cross-axis disjointness from BOTH sides so a
17651 // regression that conflates the two axes' vocabularies fails
17652 // here.
17653 assert_eq!(",".parse::<UnquoteForm>().unwrap(), UnquoteForm::Unquote);
17654 assert_eq!(",@".parse::<UnquoteForm>().unwrap(), UnquoteForm::Splice);
17655
17656 // The structural-identity labels project the SAME variants on
17657 // the SexpShape axis but are NOT canonical UnquoteForm markers.
17658 assert!("unquote".parse::<UnquoteForm>().is_err());
17659 assert!("unquote-splice".parse::<UnquoteForm>().is_err());
17660
17661 // Sibling homoiconic-prefix-wrapper markers (`'` for quote,
17662 // `` ` `` for quasiquote) belong to the WIDER QuoteForm
17663 // superset on the SAME punctuation axis — they MUST reject
17664 // here because UnquoteForm carves the 2-of-4 template-
17665 // substitution subset of QuoteForm's 4-prefix closed set.
17666 assert!("'".parse::<UnquoteForm>().is_err());
17667 assert!("`".parse::<UnquoteForm>().is_err());
17668
17669 // Whitespace-padded markers are NOT canonical — the
17670 // round-trip must be exact byte-for-byte against `marker()`.
17671 assert!(" ,".parse::<UnquoteForm>().is_err());
17672 assert!(", ".parse::<UnquoteForm>().is_err());
17673 assert!(", @".parse::<UnquoteForm>().is_err());
17674 }
17675
17676 #[test]
17677 fn unquote_form_to_quote_form_round_trips_through_as_unquote_form() {
17678 // Pin the 2-of-4 subset → superset projection as a typed
17679 // section of [`crate::ast::QuoteForm::as_unquote_form`]: for
17680 // every `uf: UnquoteForm`, `uf.to_quote_form().as_unquote_form()
17681 // == Some(uf)`. Closes the (UnquoteForm, QuoteForm) pairing as
17682 // a round-trip identity on the typed algebra — pre-lift the
17683 // pairing only lived in the existing
17684 // `unquote_form_marker_subset_decodes_through_quote_form_from_str`
17685 // cross-axis test (which round-tripped through the rendered
17686 // marker string + FromStr); post-lift the pairing rides the
17687 // typed projection directly so a future regression that drifts
17688 // the (UnquoteForm variant, QuoteForm variant) pairing
17689 // (e.g., a future arm that maps `UnquoteForm::Splice →
17690 // QuoteForm::Unquote`) fails this assertion without depending
17691 // on the FromStr decoder sitting between the two.
17692 use crate::ast::QuoteForm;
17693 for uf in UnquoteForm::ALL {
17694 assert_eq!(
17695 uf.to_quote_form().as_unquote_form(),
17696 Some(uf),
17697 "UnquoteForm::{uf:?} → QuoteForm via to_quote_form does not invert through QuoteForm::as_unquote_form — the 2-of-4 subset projection is no longer a section",
17698 );
17699 }
17700
17701 // Per-arm pinning of the canonical mapping (the byte-for-byte
17702 // pairing the composition `marker == to_quote_form().prefix()`
17703 // depends on).
17704 assert_eq!(UnquoteForm::Unquote.to_quote_form(), QuoteForm::Unquote);
17705 assert_eq!(
17706 UnquoteForm::Splice.to_quote_form(),
17707 QuoteForm::UnquoteSplice
17708 );
17709 }
17710
17711 #[test]
17712 fn unquote_form_marker_routes_through_to_quote_form_prefix_via_composition() {
17713 // Post-lift composition pin: for every `uf: UnquoteForm`,
17714 // `uf.marker()` and `uf.to_quote_form().prefix()` agree on
17715 // BOTH axes — (a) byte equality (the rendered diagnostic
17716 // literal cannot drift between the two projections); (b)
17717 // pointer equality (the canonical `&'static str` literal lives
17718 // at ONE site — `QuoteForm::prefix`'s Unquote/UnquoteSplice
17719 // arms in `ast.rs` — and `UnquoteForm::marker` routes through
17720 // that ONE address via the typed composition).
17721 //
17722 // The pointer-equality axis is load-bearing: a regression that
17723 // re-inlines the literals at `UnquoteForm::marker` as a
17724 // parallel match-table fails the pointer pin even when the
17725 // rendered bytes still agree (the inline-literal-table copy
17726 // lives at a different `&'static str` address — rustc may
17727 // de-duplicate identical literals within a single Rust
17728 // compilation unit, but the contract this test pins is
17729 // routing-through-the-canonical-site, not deduplication-by-the-
17730 // optimizer; a future build flag that disables literal dedup
17731 // would unmask the regression that the bytes-only assertion
17732 // misses). Sibling-shape pin to commit 1db697f's
17733 // `atom_kind_label_routes_through_sexp_shape_label_via_sexp_shape_projection`
17734 // — both pin the subset's projection through the superset's
17735 // canonical site via pointer-equality, the structural invariant
17736 // the subset-to-superset composition was lifted to make
17737 // load-bearing on the type system rather than on per-callsite
17738 // discipline.
17739 for uf in UnquoteForm::ALL {
17740 let from_marker = uf.marker();
17741 let from_composition = uf.to_quote_form().prefix();
17742 assert_eq!(
17743 from_marker, from_composition,
17744 "UnquoteForm::{uf:?}.marker() bytes drifted from .to_quote_form().prefix() bytes — the subset's diagnostic vocabulary is no longer derived from the superset's canonical site",
17745 );
17746 assert!(
17747 std::ptr::eq(from_marker.as_ptr(), from_composition.as_ptr()),
17748 "UnquoteForm::{uf:?}.marker() and .to_quote_form().prefix() disagree on `&'static str` address — pointer drift means the lift composes through a parallel literal table rather than routing into the canonical QuoteForm::prefix site",
17749 );
17750 }
17751 }
17752
17753 #[test]
17754 fn unquote_form_per_role_markers_alias_quote_form_per_role_prefixes_byte_for_byte() {
17755 // ALIAS CONTRACT: pin both per-role `pub const UnquoteForm::*_MARKER`
17756 // aliases equal the corresponding `pub const QuoteForm::*_PREFIX`
17757 // byte-for-byte — so the UnquoteForm ⊂ QuoteForm marker-vocabulary
17758 // containment routes through the typed
17759 // `pub const UnquoteForm::V_MARKER: &'static str = QuoteForm::V_PREFIX`
17760 // alias chain rather than through two independent literal-discipline
17761 // sites. A regression that renames the QuoteForm side without
17762 // updating the UnquoteForm alias pointing at it fails-loudly here
17763 // with the exact axis identified (UNQUOTE / SPLICE); a regression
17764 // that re-inlines the UnquoteForm constant to a fresh literal still
17765 // passes this pin but loses the alias-chain typing (which is what
17766 // `unquote_form_marker_arms_route_through_per_role_markers_for_every_variant`
17767 // + `unquote_form_markers_align_with_all_by_index` catch in
17768 // combination).
17769 //
17770 // Sibling posture to
17771 // `atom_kind_per_role_labels_alias_sexp_shape_per_role_labels_byte_for_byte`
17772 // (commit fc126b8): both pin the invariant that a typed-subset
17773 // enum's per-role bytes are structurally derived from the parent
17774 // superset's per-role bytes via a `pub const = Parent::CONST`
17775 // alias rather than through parallel literal tables. The subset
17776 // there is AtomKind ⊂ SexpShape (6-of-12 on the atomic-payload
17777 // axis); the subset here is UnquoteForm ⊂ QuoteForm (2-of-4 on
17778 // the template-substitution axis) — both are load-bearing
17779 // structural carvings whose per-role vocabularies MUST agree
17780 // byte-for-byte with their parent's.
17781 use crate::ast::QuoteForm;
17782 assert_eq!(UnquoteForm::UNQUOTE_MARKER, QuoteForm::UNQUOTE_PREFIX);
17783 assert_eq!(UnquoteForm::SPLICE_MARKER, QuoteForm::UNQUOTE_SPLICE_PREFIX);
17784 }
17785
17786 #[test]
17787 fn unquote_form_marker_arms_route_through_per_role_markers_for_every_variant() {
17788 // PATH-UNIFORMITY: `UnquoteForm::V.marker()` MUST equal the
17789 // per-role `pub const UnquoteForm::V_MARKER` for every
17790 // `v: UnquoteForm`. Pre-lift the two template-marker bytes
17791 // were reachable through `UnquoteForm::marker` (the composition
17792 // `self.to_quote_form().prefix()` — routing into
17793 // `QuoteForm::UNQUOTE_PREFIX` / `UNQUOTE_SPLICE_PREFIX`) OR
17794 // through direct `QuoteForm::UNQUOTE_PREFIX` reach-across;
17795 // post-lift each variant's canonical bytes are reachable
17796 // through the per-role `UnquoteForm::*_MARKER` alias too. Pin
17797 // the byte-equality between the runtime projection and the
17798 // compile-time alias so a regression that renames the alias
17799 // without updating the arm (or vice versa) fails-loudly at
17800 // the exact axis.
17801 //
17802 // Sibling-shape pin to
17803 // `atom_kind_label_arms_route_through_per_role_labels_for_every_variant`
17804 // one algebra layer over — the AtomKind subset algebra's per-
17805 // role aliases are pinned against `AtomKind::label`'s
17806 // composition-routed arms there; this pin binds the
17807 // UnquoteForm subset algebra's per-role aliases against
17808 // `UnquoteForm::marker`'s composition-routed arms so the two
17809 // template-marker bytes project through ONE aliased typed
17810 // source of truth per role rather than through per-consumer
17811 // inline literals.
17812 assert_eq!(UnquoteForm::Unquote.marker(), UnquoteForm::UNQUOTE_MARKER);
17813 assert_eq!(UnquoteForm::Splice.marker(), UnquoteForm::SPLICE_MARKER);
17814 }
17815
17816 #[test]
17817 fn unquote_form_markers_has_expected_cardinality() {
17818 // Cardinality pin: `MARKERS.len() == 2` matches `ALL.len()` so
17819 // a refactor that loosens the type to `&'static [&'static str]`
17820 // fails HERE (the `[_; 2]` slot cannot be sliced silently),
17821 // and a variant added to `ALL` without a matching `MARKERS`
17822 // row fails the pair-arity gate at the array literal itself
17823 // before this test even runs. The pin doubles as an operator-
17824 // visible mark of the family's cardinality across the
17825 // substrate — two template markers, matching the 2-of-4
17826 // carving of the parent `QuoteForm::PREFIXES` (the
17827 // substitution-subset of the four canonical homoiconic prefix
17828 // bytes).
17829 assert_eq!(UnquoteForm::MARKERS.len(), 2);
17830 assert_eq!(UnquoteForm::MARKERS.len(), UnquoteForm::ALL.len());
17831 }
17832
17833 #[test]
17834 fn unquote_form_markers_align_with_all_by_index() {
17835 // ALIGNMENT PIN: sweep `MARKERS[i] == ALL[i].marker()` so any
17836 // `zip(ALL, MARKERS)` consumer reads a coherent (variant,
17837 // marker) pair off ONE forced-arity array pair. The
17838 // declaration-order pin makes a family-wide consumer that
17839 // walks the ALL / MARKERS pair in lockstep (an LSP completion
17840 // bar keyed on `UnquoteForm::MARKERS`, a Sekiban metric
17841 // emitter labeling
17842 // `tatara_lisp_unbound_template_var_total{prefix}` by the
17843 // per-index marker) read one canonical (variant, bytes) pair
17844 // per slot rather than routing through per-consumer paired-
17845 // iteration. A regression that reorders MARKERS without also
17846 // reordering ALL (or vice versa) fails-loudly at the exact
17847 // index that drifted.
17848 assert_eq!(UnquoteForm::MARKERS.len(), UnquoteForm::ALL.len());
17849 for (i, form) in UnquoteForm::ALL.iter().enumerate() {
17850 assert_eq!(
17851 UnquoteForm::MARKERS[i],
17852 form.marker(),
17853 "UnquoteForm::MARKERS[{i}] `{mkr}` drifted from \
17854 UnquoteForm::ALL[{i}].marker() `{via_variant}` — the \
17855 canonical ALL ordering and the MARKERS ordering must \
17856 match element-wise",
17857 mkr = UnquoteForm::MARKERS[i],
17858 via_variant = form.marker(),
17859 );
17860 }
17861 }
17862
17863 #[test]
17864 fn unquote_form_markers_pairwise_distinct() {
17865 // 2x2 pairwise sweep so a collision between the two markers
17866 // (which would silently degrade two distinct template-
17867 // substitution markers to the SAME diagnostic bytes and
17868 // violate the closed-set FromStr round-trip) fails-loudly at
17869 // the exact pair. Distinctness is already enforced
17870 // structurally by `assert_closed_set_well_formed::<UnquoteForm>()`
17871 // (clause 3), so this pin is a secondary guard focused on the
17872 // per-role `pub const` surface directly rather than the
17873 // runtime projection through the trait's default `labels()`.
17874 for (i, a) in UnquoteForm::MARKERS.iter().enumerate() {
17875 for (j, b) in UnquoteForm::MARKERS.iter().enumerate() {
17876 if i == j {
17877 continue;
17878 }
17879 assert_ne!(
17880 a, b,
17881 "UnquoteForm::MARKERS[{i}] ({a:?}) collides with \
17882 UnquoteForm::MARKERS[{j}] ({b:?}) — two distinct \
17883 template markers cannot share diagnostic bytes",
17884 );
17885 }
17886 }
17887 }
17888
17889 #[test]
17890 fn unquote_form_unquote_lead_aliases_quote_form_unquote_lead_byte_for_byte() {
17891 // ALIAS CONTRACT: pin the sole per-role
17892 // `pub const UnquoteForm::UNQUOTE_LEAD` alias equals
17893 // `pub const QuoteForm::UNQUOTE_LEAD` byte-for-byte — so the
17894 // UnquoteForm ⊂ QuoteForm reader-lead-char vocabulary
17895 // containment routes through the typed
17896 // `pub const UnquoteForm::UNQUOTE_LEAD: char =
17897 // QuoteForm::UNQUOTE_LEAD` alias chain rather than through an
17898 // independent literal-discipline site. A regression that
17899 // renames the QuoteForm side (a hypothetical repartitioning
17900 // that swaps the substitution-family lead byte from `,` to a
17901 // different char) without updating the UnquoteForm alias
17902 // pointing at it fails-loudly here; a regression that re-
17903 // inlines the UnquoteForm constant to a fresh `','` literal
17904 // still passes this pin but loses the alias-chain typing
17905 // (which is what the routing pin below catches in
17906 // combination).
17907 //
17908 // Sibling posture to
17909 // `unquote_form_per_role_markers_alias_quote_form_per_role_prefixes_byte_for_byte`
17910 // (commit 3640f76 — reader-punctuation-prefix axis),
17911 // `unquote_form_per_role_iac_forge_tags_alias_quote_form_per_role_iac_forge_tags_byte_for_byte`
17912 // (commit 6acab84 — iac-forge canonical-form axis),
17913 // `unquote_form_per_role_labels_alias_quote_form_per_role_labels_byte_for_byte`
17914 // (commit da68af5 — diagnostic-label axis), and
17915 // `unquote_form_per_role_hash_discriminators_alias_quote_form_per_role_hash_discriminators_byte_for_byte`
17916 // (commit eca4730 — outer-`Sexp` cache-key axis) — this
17917 // FIFTH alias-contract pin closes the QUINTUPLE of per-role
17918 // `pub const` axes on the UnquoteForm subset carving.
17919 assert_eq!(
17920 UnquoteForm::UNQUOTE_LEAD,
17921 crate::ast::QuoteForm::UNQUOTE_LEAD,
17922 "UnquoteForm::UNQUOTE_LEAD drifted from QuoteForm::UNQUOTE_LEAD — the alias chain broke",
17923 );
17924 }
17925
17926 #[test]
17927 fn unquote_form_unquote_lead_pins_canonical_reader_lead_char() {
17928 // Byte-for-byte pin of the canonical
17929 // (UnquoteForm, reader-lead char) mapping at the per-role
17930 // `pub const` axis: `UnquoteForm::UNQUOTE_LEAD == ','` — the
17931 // exact char the reader's outer tokenizer dispatch selects
17932 // template-substitution-family entry on. This char is
17933 // load-bearing: the reader's outer tokenizer pre-check
17934 // (`QuoteForm::from_lead_char(ch)`) routes into the
17935 // substitution-family branch iff `ch == UNQUOTE_LEAD`, and
17936 // the `,`-vs-`,@` disambiguation lives at the reader's
17937 // peek-then-consume `@` second-char step (promoting the
17938 // default `Unquote` return into a `Splice` on `@`). A
17939 // regression that drifts this const from `,` silently
17940 // fractures the reader's outer dispatch AND the
17941 // `PROMOTIONS[0].1 == UNQUOTE_LEAD.next_char_promoter_source`
17942 // structural pairing that the promotion table binds to.
17943 // Sibling of
17944 // `unquote_form_per_role_hash_discriminators_pin_legacy_cache_key_bytes`
17945 // (which pins the outer-`Sexp` cache-key bytes at the
17946 // per-role `pub const` axis) — this test pins the reader-
17947 // lead char on the same axis one production vocabulary over.
17948 assert_eq!(
17949 UnquoteForm::UNQUOTE_LEAD,
17950 ',',
17951 "UnquoteForm::UNQUOTE_LEAD drifted from legacy reader-lead char ','",
17952 );
17953 }
17954
17955 #[test]
17956 fn unquote_form_unquote_lead_routes_through_to_quote_form_lead_char_via_composition() {
17957 // Post-lift composition pin: for every `uf: UnquoteForm`,
17958 // `UnquoteForm::UNQUOTE_LEAD` and
17959 // `uf.to_quote_form().lead_char()` agree byte-for-byte — the
17960 // (subset marker, reader-lead char) pairing rides through the
17961 // superset's canonical `QuoteForm::lead_char` closed-set
17962 // match rather than through a parallel two-arm inline table
17963 // on the subset. A regression that re-inlines the two arms
17964 // as a parallel match-table (e.g. a future edit that spells
17965 // `Self::Unquote => ',' / Self::Splice => ','` directly at
17966 // `UnquoteForm::lead_char` instead of routing through
17967 // `self.to_quote_form().lead_char()`) is caught here — both
17968 // subset variants project to the SAME parent-superset lead
17969 // byte at
17970 // [`crate::ast::QuoteForm::lead_char`]'s
17971 // `Self::Unquote | Self::UnquoteSplice => Self::UNQUOTE_LEAD`
17972 // MERGED arm, and this composition pin binds that merge on
17973 // the subset carving. Sibling-shape pin to the four prior-
17974 // lift composition pins on `UnquoteForm`:
17975 // `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`,
17976 // `unquote_form_iac_forge_tag_routes_through_to_quote_form_iac_forge_tag_via_composition`,
17977 // `unquote_form_label_routes_through_to_quote_form_label_via_composition`,
17978 // and
17979 // `unquote_form_hash_discriminator_routes_through_to_quote_form_hash_discriminator_via_composition`
17980 // — all five pin the subset's projection through the
17981 // superset's canonical site via the same typed composition
17982 // posture. The FIFTH composition-through-`to_quote_form`
17983 // axis on the substitution-subset closed set is now load-
17984 // bearing on the type system.
17985 for uf in UnquoteForm::ALL {
17986 let via_composition = uf.to_quote_form().lead_char();
17987 assert_eq!(
17988 UnquoteForm::UNQUOTE_LEAD, via_composition,
17989 "UnquoteForm::{uf:?} projects through .to_quote_form().lead_char() = `{via_composition}` — but UnquoteForm::UNQUOTE_LEAD = `{}` — the SHAPE-ASYMMETRIC-COLLAPSE identity (both subset arms → one lead byte) broke",
17990 UnquoteForm::UNQUOTE_LEAD,
17991 );
17992 }
17993 }
17994
17995 #[test]
17996 fn unquote_form_leads_has_expected_shape_asymmetric_cardinality() {
17997 // Cardinality pin: `LEADS.len() == 1` — the SHAPE-ASYMMETRIC-
17998 // COLLAPSE cardinality that IS the collapse witness on the
17999 // 2-of-4 subset carving. UnquoteForm has 2 arms
18000 // (`Self::ALL.len() == 2`), but both arms share the same
18001 // reader-lead byte `,`, so the distinct-lead-byte closed-set
18002 // ALL array collapses to `[char; 1]`. The cardinality gap
18003 // `Self::ALL.len() (== 2) - Self::LEADS.len() (== 1) == 1`
18004 // IS the shared-lead-char invariant carried at the type-
18005 // system level.
18006 //
18007 // Peer to `unquote_form_markers_has_expected_cardinality`
18008 // (which pins `MARKERS.len() == 2 == ALL.len()` — the
18009 // arm-per-arm axis has NO collapse), this test pins the
18010 // OPPOSITE `LEADS.len() == 1 < ALL.len()` invariant — the
18011 // distinct-lead-set axis DOES collapse. The two axes together
18012 // fully characterize the reader-vocabulary shape on the
18013 // UnquoteForm closed set.
18014 //
18015 // Peer to `quote_form_leads_has_expected_shape_asymmetric_cardinality`
18016 // (parent 4-of-4 carving pins `QuoteForm::LEADS.len() == 3`
18017 // for 4 arms — the same shared-lead-byte collapse identity
18018 // one algebra level up).
18019 assert_eq!(
18020 UnquoteForm::LEADS.len(),
18021 1,
18022 "UnquoteForm::LEADS cardinality drifted from 1 — the shared-lead-char collapse invariant on the 2-of-4 substitution-subset carving is broken",
18023 );
18024 assert!(
18025 UnquoteForm::LEADS.len() < UnquoteForm::ALL.len(),
18026 "UnquoteForm::LEADS.len() ({leads}) MUST be strictly less than UnquoteForm::ALL.len() ({all}) — the SHAPE-ASYMMETRIC-COLLAPSE identity requires a strict cardinality gap",
18027 leads = UnquoteForm::LEADS.len(),
18028 all = UnquoteForm::ALL.len(),
18029 );
18030 }
18031
18032 #[test]
18033 fn unquote_form_leads_covers_all_variants_via_to_quote_form_lead_char() {
18034 // COVERAGE pin: every UnquoteForm variant's reader-lead byte
18035 // (projected through the superset composition
18036 // `self.to_quote_form().lead_char()`) MUST appear in
18037 // `UnquoteForm::LEADS`. Combined with the cardinality pin
18038 // above (`LEADS.len() == 1`), this pins that `LEADS` is
18039 // EXACTLY the image of `ALL` under the composition — no
18040 // superfluous entries, no missing coverage. A regression
18041 // that adds a spurious char to `LEADS` (e.g. a future edit
18042 // that includes both `,` and `@` in the array under the
18043 // mistaken belief that `,@` is a separate lead byte) fails
18044 // the cardinality pin above; a regression that DROPS the
18045 // sole entry from `LEADS` fails this coverage pin at every
18046 // variant.
18047 for uf in UnquoteForm::ALL {
18048 let lead = uf.to_quote_form().lead_char();
18049 assert!(
18050 UnquoteForm::LEADS.contains(&lead),
18051 "UnquoteForm::{uf:?} projects through .to_quote_form().lead_char() = `{lead}` — but UnquoteForm::LEADS ({leads:?}) does NOT contain `{lead}`; the distinct-lead-byte ALL array must cover every variant's lead byte",
18052 leads = UnquoteForm::LEADS,
18053 );
18054 }
18055 }
18056
18057 #[test]
18058 fn unquote_form_leads_is_subset_of_quote_form_leads() {
18059 // CONTAINMENT pin: `UnquoteForm::LEADS` is a subset of
18060 // `QuoteForm::LEADS` — the substitution-subset carving's
18061 // reader-lead-byte set sits inside the parent QuoteForm
18062 // superset's reader-lead-byte set by the UnquoteForm ⊂
18063 // QuoteForm inclusion. The sole entry `UnquoteForm::LEADS[0]
18064 // == ','` must equal `QuoteForm::UNQUOTE_LEAD`, which IS the
18065 // third entry `QuoteForm::LEADS[2]` (Quote/Quasiquote/Unquote
18066 // in declaration order per `QuoteForm::LEADS`). A regression
18067 // that drifts `UnquoteForm::UNQUOTE_LEAD` out of alignment
18068 // with the parent (e.g. a future edit that inlines a fresh
18069 // `';'` literal into the UnquoteForm alias but leaves
18070 // QuoteForm's canonical byte at `','`) fails-loudly here.
18071 // Sibling of
18072 // `unquote_form_hash_discriminator_partitions_disjointly_from_non_substitution_carvings`
18073 // (which pins the cache-key axis's partition-membership on
18074 // the same subset carving) — this test pins the reader-lead-
18075 // byte axis's subset-membership one production vocabulary
18076 // over.
18077 use crate::ast::QuoteForm;
18078 for lead in UnquoteForm::LEADS {
18079 assert!(
18080 QuoteForm::LEADS.contains(&lead),
18081 "UnquoteForm::LEADS entry `{lead}` MUST appear in QuoteForm::LEADS ({parent:?}) — the UnquoteForm ⊂ QuoteForm subset-inclusion breaks otherwise",
18082 parent = QuoteForm::LEADS,
18083 );
18084 }
18085 }
18086
18087 #[test]
18088 fn unquote_form_leads_matches_to_quote_form_lead_char_image_exactly() {
18089 // EXACT-IMAGE pin: `UnquoteForm::LEADS` as an unordered set
18090 // equals the image of `UnquoteForm::ALL` under
18091 // `.to_quote_form().lead_char()`. Combined with the
18092 // cardinality pin (`LEADS.len() == 1`) and the coverage pin
18093 // (every variant's lead appears in LEADS), this ties LEADS
18094 // to the composition's image at the SET level — a regression
18095 // that reorders entries, adds duplicates, or drops entries
18096 // fails at exactly one of the three pins.
18097 use std::collections::HashSet;
18098 let leads_set: HashSet<char> = UnquoteForm::LEADS.iter().copied().collect();
18099 let image_set: HashSet<char> = UnquoteForm::ALL
18100 .iter()
18101 .map(|uf| uf.to_quote_form().lead_char())
18102 .collect();
18103 assert_eq!(
18104 leads_set, image_set,
18105 "UnquoteForm::LEADS ({leads_set:?}) as a set MUST equal the image of UnquoteForm::ALL under .to_quote_form().lead_char() ({image_set:?}) — the distinct-lead-byte closed-set ALL array drifted from its composition source",
18106 );
18107 }
18108
18109 #[test]
18110 fn unquote_form_per_role_iac_forge_tags_alias_quote_form_per_role_iac_forge_tags_byte_for_byte()
18111 {
18112 // ALIAS CONTRACT: pin both per-role
18113 // `pub const UnquoteForm::*_IAC_FORGE_TAG` aliases equal the
18114 // corresponding `pub const QuoteForm::*_IAC_FORGE_TAG`
18115 // byte-for-byte — so the UnquoteForm ⊂ QuoteForm iac-forge-
18116 // canonical-form vocabulary containment routes through the
18117 // typed
18118 // `pub const UnquoteForm::V_IAC_FORGE_TAG: &'static str = QuoteForm::V_IAC_FORGE_TAG`
18119 // alias chain rather than through two independent literal-
18120 // discipline sites. A regression that renames the QuoteForm
18121 // side (e.g. a Common-Lisp-standard rename of `"unquote-
18122 // splicing"` on the iac-forge canonical-form surface) without
18123 // updating the UnquoteForm alias pointing at it fails-loudly
18124 // here with the exact axis identified (UNQUOTE / SPLICE); a
18125 // regression that re-inlines the UnquoteForm constant to a
18126 // fresh literal still passes this pin but loses the alias-
18127 // chain typing (which is what
18128 // `unquote_form_iac_forge_tag_arms_route_through_per_role_iac_forge_tags_for_every_variant`
18129 // + `unquote_form_iac_forge_tags_align_with_all_by_index`
18130 // catch in combination).
18131 //
18132 // Sibling posture to
18133 // `unquote_form_per_role_markers_alias_quote_form_per_role_prefixes_byte_for_byte`
18134 // (commit 3640f76): both pin the invariant that the
18135 // UnquoteForm subset algebra's per-role bytes are structurally
18136 // derived from the parent QuoteForm superset's per-role bytes
18137 // via a `pub const = Parent::CONST` alias rather than through
18138 // parallel literal tables — the axis there is the reader-
18139 // punctuation vocabulary; the axis here is the iac-forge
18140 // canonical-form vocabulary. Both axes span the two production
18141 // byte-vocabularies the parent [`QuoteForm`] closed set
18142 // carries, and both bind through the same aliased typed source
18143 // of truth on this UnquoteForm subset.
18144 use crate::ast::QuoteForm;
18145 assert_eq!(
18146 UnquoteForm::UNQUOTE_IAC_FORGE_TAG,
18147 QuoteForm::UNQUOTE_IAC_FORGE_TAG,
18148 );
18149 assert_eq!(
18150 UnquoteForm::SPLICE_IAC_FORGE_TAG,
18151 QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG,
18152 );
18153 }
18154
18155 #[test]
18156 fn unquote_form_iac_forge_tag_arms_route_through_per_role_iac_forge_tags_for_every_variant() {
18157 // PATH-UNIFORMITY: `UnquoteForm::V.iac_forge_tag()` MUST equal
18158 // the per-role `pub const UnquoteForm::V_IAC_FORGE_TAG` for
18159 // every `v: UnquoteForm`. Pre-lift the two iac-forge canonical-
18160 // form tag bytes were reachable through
18161 // `UnquoteForm::iac_forge_tag` (the composition
18162 // `self.to_quote_form().iac_forge_tag()` — routing into
18163 // `QuoteForm::UNQUOTE_IAC_FORGE_TAG` /
18164 // `UNQUOTE_SPLICE_IAC_FORGE_TAG`) OR through direct
18165 // `QuoteForm::UNQUOTE_IAC_FORGE_TAG` reach-across; post-lift
18166 // each variant's canonical bytes are reachable through the
18167 // per-role `UnquoteForm::*_IAC_FORGE_TAG` alias too. Pin the
18168 // byte-equality between the runtime projection and the
18169 // compile-time alias so a regression that renames the alias
18170 // without updating the arm (or vice versa) fails-loudly at the
18171 // exact axis.
18172 //
18173 // Sibling-shape pin to
18174 // `unquote_form_marker_arms_route_through_per_role_markers_for_every_variant`
18175 // one vocabulary over on the SAME subset algebra — the reader-
18176 // punctuation axis is pinned there; the iac-forge canonical-
18177 // form axis is pinned here. Together the two pins close the
18178 // subset algebra's `pub const *_MARKER` + `pub const
18179 // *_IAC_FORGE_TAG` surfaces against the runtime projections
18180 // that feed the two production byte-vocabularies.
18181 assert_eq!(
18182 UnquoteForm::Unquote.iac_forge_tag(),
18183 UnquoteForm::UNQUOTE_IAC_FORGE_TAG,
18184 );
18185 assert_eq!(
18186 UnquoteForm::Splice.iac_forge_tag(),
18187 UnquoteForm::SPLICE_IAC_FORGE_TAG,
18188 );
18189 }
18190
18191 #[test]
18192 fn unquote_form_iac_forge_tags_has_expected_cardinality() {
18193 // Cardinality pin: `IAC_FORGE_TAGS.len() == 2` matches
18194 // `ALL.len()` so a refactor that loosens the type to
18195 // `&'static [&'static str]` fails HERE (the `[_; 2]` slot
18196 // cannot be sliced silently), and a variant added to `ALL`
18197 // without a matching `IAC_FORGE_TAGS` row fails the pair-arity
18198 // gate at the array literal itself before this test even runs.
18199 // The pin doubles as an operator-visible mark of the family's
18200 // cardinality across the substrate — two iac-forge tags,
18201 // matching the 2-of-4 carving of the parent
18202 // `QuoteForm::IAC_FORGE_TAGS` (the substitution-subset of the
18203 // four canonical homoiconic canonical-form tag strings).
18204 assert_eq!(UnquoteForm::IAC_FORGE_TAGS.len(), 2);
18205 assert_eq!(UnquoteForm::IAC_FORGE_TAGS.len(), UnquoteForm::ALL.len(),);
18206 }
18207
18208 #[test]
18209 fn unquote_form_iac_forge_tags_align_with_all_by_index() {
18210 // ALIGNMENT PIN: sweep `IAC_FORGE_TAGS[i] ==
18211 // ALL[i].iac_forge_tag()` so any `zip(ALL, IAC_FORGE_TAGS)`
18212 // consumer reads a coherent (variant, iac-forge tag) pair off
18213 // ONE forced-arity array pair. The declaration-order pin
18214 // makes a family-wide consumer that walks the
18215 // ALL / IAC_FORGE_TAGS pair in lockstep (an LSP completion
18216 // bar keyed on `UnquoteForm::IAC_FORGE_TAGS`, a Sekiban
18217 // audit-trail metric labeling
18218 // `tatara_lisp_iac_forge_tag_total{tag}` by the per-index
18219 // iac-forge tag) read one canonical (variant, bytes) pair per
18220 // slot rather than routing through per-consumer paired-
18221 // iteration. A regression that reorders IAC_FORGE_TAGS without
18222 // also reordering ALL (or vice versa) fails-loudly at the
18223 // exact index that drifted.
18224 assert_eq!(UnquoteForm::IAC_FORGE_TAGS.len(), UnquoteForm::ALL.len(),);
18225 for (i, form) in UnquoteForm::ALL.iter().enumerate() {
18226 assert_eq!(
18227 UnquoteForm::IAC_FORGE_TAGS[i],
18228 form.iac_forge_tag(),
18229 "UnquoteForm::IAC_FORGE_TAGS[{i}] `{tag}` drifted \
18230 from UnquoteForm::ALL[{i}].iac_forge_tag() \
18231 `{via_variant}` — the canonical ALL ordering and \
18232 the IAC_FORGE_TAGS ordering must match element-wise",
18233 tag = UnquoteForm::IAC_FORGE_TAGS[i],
18234 via_variant = form.iac_forge_tag(),
18235 );
18236 }
18237 }
18238
18239 #[test]
18240 fn unquote_form_iac_forge_tags_pairwise_distinct() {
18241 // 2x2 pairwise sweep so a collision between the two iac-forge
18242 // canonical-form tags (which would silently degrade two
18243 // distinct template-substitution markers to the SAME cross-
18244 // crate canonical-form bytes and mis-hash the substrate's
18245 // BLAKE3 attestation intent-hash pillar) fails-loudly at the
18246 // exact pair. Sibling-shape pin to
18247 // `unquote_form_markers_pairwise_distinct` one vocabulary over
18248 // on the SAME subset algebra — the reader-punctuation axis is
18249 // pinned there; the iac-forge canonical-form axis is pinned
18250 // here.
18251 for (i, a) in UnquoteForm::IAC_FORGE_TAGS.iter().enumerate() {
18252 for (j, b) in UnquoteForm::IAC_FORGE_TAGS.iter().enumerate() {
18253 if i == j {
18254 continue;
18255 }
18256 assert_ne!(
18257 a, b,
18258 "UnquoteForm::IAC_FORGE_TAGS[{i}] ({a:?}) \
18259 collides with UnquoteForm::IAC_FORGE_TAGS[{j}] \
18260 ({b:?}) — two distinct template markers cannot \
18261 share cross-crate canonical-form bytes",
18262 );
18263 }
18264 }
18265 }
18266
18267 #[test]
18268 fn unquote_form_label_routes_through_to_quote_form_label_via_composition() {
18269 // Post-lift composition pin: for every `uf: UnquoteForm`,
18270 // `uf.label()` and `uf.to_quote_form().label()` agree on BOTH
18271 // axes — (a) byte equality (the rendered diagnostic literal
18272 // cannot drift between the two projections); (b) pointer
18273 // equality (the canonical `&'static str` literal lives at ONE
18274 // site — `SexpShape::UNQUOTE_LABEL` /
18275 // `SexpShape::UNQUOTE_SPLICE_LABEL` — reached through
18276 // `QuoteForm::label`'s composition through
18277 // `sexp_shape().label()`, and `UnquoteForm::label` routes
18278 // through that ONE address via the two-hop typed composition
18279 // `to_quote_form().label()`).
18280 //
18281 // The pointer-equality axis is load-bearing: a regression that
18282 // re-inlines the literals at `UnquoteForm::label` as a
18283 // parallel match-table fails the pointer pin even when the
18284 // rendered bytes still agree (the inline-literal-table copy
18285 // lives at a different `&'static str` address). Sibling-shape
18286 // pin to
18287 // `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`
18288 // and
18289 // `unquote_form_iac_forge_tag_routes_through_to_quote_form_iac_forge_tag_via_composition`
18290 // — this closes the THIRD composition-through-`to_quote_form`
18291 // per-role-bytes projection on the subset algebra pinned
18292 // against the superset's canonical site.
18293 for uf in UnquoteForm::ALL {
18294 let from_label = uf.label();
18295 let from_composition = uf.to_quote_form().label();
18296 assert_eq!(
18297 from_label, from_composition,
18298 "UnquoteForm::{uf:?}.label() bytes drifted from .to_quote_form().label() bytes — the subset's diagnostic vocabulary is no longer derived from the superset's canonical site",
18299 );
18300 assert!(
18301 std::ptr::eq(from_label.as_ptr(), from_composition.as_ptr()),
18302 "UnquoteForm::{uf:?}.label() and .to_quote_form().label() disagree on `&'static str` address — pointer drift means the lift composes through a parallel literal table rather than routing into the canonical QuoteForm::label / SexpShape::*_LABEL site",
18303 );
18304 }
18305 }
18306
18307 #[test]
18308 fn unquote_form_per_role_labels_alias_quote_form_per_role_labels_byte_for_byte() {
18309 // ALIAS CONTRACT: pin both per-role
18310 // `pub const UnquoteForm::*_LABEL` aliases equal the
18311 // corresponding `pub const QuoteForm::*_LABEL` byte-for-byte
18312 // — so the UnquoteForm ⊂ QuoteForm diagnostic-label
18313 // vocabulary containment routes through the typed
18314 // `pub const UnquoteForm::V_LABEL: &'static str = QuoteForm::V_LABEL`
18315 // alias chain (which itself aliases the ultimate canonical
18316 // site at `SexpShape::*_LABEL`) rather than through two
18317 // independent literal-discipline sites. A regression that
18318 // renames the QuoteForm side without updating the UnquoteForm
18319 // alias pointing at it fails-loudly here with the exact axis
18320 // identified (UNQUOTE / SPLICE); a regression that re-inlines
18321 // the UnquoteForm constant to a fresh literal still passes
18322 // this pin but loses the alias-chain typing (which is what
18323 // `unquote_form_label_arms_route_through_per_role_labels_for_every_variant`
18324 // + `unquote_form_labels_align_with_all_by_index` catch in
18325 // combination).
18326 //
18327 // Sibling posture to
18328 // `unquote_form_per_role_markers_alias_quote_form_per_role_prefixes_byte_for_byte`
18329 // (commit 3640f76) and
18330 // `unquote_form_per_role_iac_forge_tags_alias_quote_form_per_role_iac_forge_tags_byte_for_byte`
18331 // (commit 6acab84): all three pin the invariant that the
18332 // UnquoteForm subset algebra's per-role bytes are structurally
18333 // derived from the parent QuoteForm superset's per-role bytes
18334 // via a `pub const = Parent::CONST` alias rather than through
18335 // parallel literal tables — the axis there is the reader-
18336 // punctuation vocabulary and the iac-forge canonical-form
18337 // vocabulary; the axis here is the substrate diagnostic-label
18338 // vocabulary. All three axes span the THREE production byte-
18339 // vocabularies the parent [`QuoteForm`] closed set carries,
18340 // and all three bind through the same aliased typed source of
18341 // truth on this UnquoteForm subset.
18342 use crate::ast::QuoteForm;
18343 assert_eq!(UnquoteForm::UNQUOTE_LABEL, QuoteForm::UNQUOTE_LABEL);
18344 assert_eq!(UnquoteForm::SPLICE_LABEL, QuoteForm::UNQUOTE_SPLICE_LABEL);
18345 }
18346
18347 #[test]
18348 fn unquote_form_label_arms_route_through_per_role_labels_for_every_variant() {
18349 // PATH-UNIFORMITY: `UnquoteForm::V.label()` MUST equal the
18350 // per-role `pub const UnquoteForm::V_LABEL` for every
18351 // `v: UnquoteForm`. Pre-lift the two diagnostic-label bytes
18352 // were reachable through the two-step composition
18353 // `uf.to_quote_form().label()` OR through direct
18354 // `QuoteForm::UNQUOTE_LABEL` reach-across; post-lift each
18355 // variant's canonical bytes are reachable through the per-
18356 // role `UnquoteForm::*_LABEL` alias AND through the inherent
18357 // `label()` method too. Pin the byte-equality between the
18358 // runtime projection and the compile-time alias so a
18359 // regression that renames the alias without updating the arm
18360 // (or vice versa) fails-loudly at the exact axis.
18361 //
18362 // Sibling-shape pin to
18363 // `unquote_form_marker_arms_route_through_per_role_markers_for_every_variant`
18364 // and
18365 // `unquote_form_iac_forge_tag_arms_route_through_per_role_iac_forge_tags_for_every_variant`
18366 // one vocabulary over on the SAME subset algebra — together
18367 // the three pins close the subset algebra's
18368 // `pub const *_MARKER` + `pub const *_IAC_FORGE_TAG` +
18369 // `pub const *_LABEL` surfaces against the runtime
18370 // projections that feed the three production byte-
18371 // vocabularies.
18372 assert_eq!(UnquoteForm::Unquote.label(), UnquoteForm::UNQUOTE_LABEL);
18373 assert_eq!(UnquoteForm::Splice.label(), UnquoteForm::SPLICE_LABEL);
18374 }
18375
18376 #[test]
18377 fn unquote_form_labels_has_expected_cardinality() {
18378 // Cardinality pin: `LABELS.len() == 2` matches `ALL.len()` so
18379 // a refactor that loosens the type to `&'static [&'static str]`
18380 // fails HERE (the `[_; 2]` slot cannot be sliced silently),
18381 // and a variant added to `ALL` without a matching `LABELS`
18382 // row fails the pair-arity gate at the array literal itself
18383 // before this test even runs.
18384 assert_eq!(UnquoteForm::LABELS.len(), 2);
18385 assert_eq!(UnquoteForm::LABELS.len(), UnquoteForm::ALL.len());
18386 }
18387
18388 #[test]
18389 fn unquote_form_labels_align_with_all_by_index() {
18390 // ALIGNMENT PIN: sweep `LABELS[i] == ALL[i].label()` so any
18391 // `zip(ALL, LABELS)` consumer reads a coherent (variant,
18392 // label) pair off ONE forced-arity array pair. A regression
18393 // that reorders LABELS without also reordering ALL (or vice
18394 // versa) fails-loudly at the exact index that drifted.
18395 assert_eq!(UnquoteForm::LABELS.len(), UnquoteForm::ALL.len());
18396 for (i, form) in UnquoteForm::ALL.iter().enumerate() {
18397 assert_eq!(
18398 UnquoteForm::LABELS[i],
18399 form.label(),
18400 "UnquoteForm::LABELS[{i}] `{lbl}` drifted from \
18401 UnquoteForm::ALL[{i}].label() `{via_variant}` — the \
18402 canonical ALL ordering and the LABELS ordering must \
18403 match element-wise",
18404 lbl = UnquoteForm::LABELS[i],
18405 via_variant = form.label(),
18406 );
18407 }
18408 }
18409
18410 #[test]
18411 fn unquote_form_labels_pairwise_distinct() {
18412 // 2x2 pairwise sweep so a collision between the two
18413 // diagnostic labels (which would silently degrade two distinct
18414 // template-substitution markers to the SAME operator-facing
18415 // rendered vocabulary) fails-loudly at the exact pair.
18416 // Sibling-shape pin to `unquote_form_markers_pairwise_distinct`
18417 // and `unquote_form_iac_forge_tags_pairwise_distinct` one
18418 // vocabulary over on the SAME subset algebra.
18419 for (i, a) in UnquoteForm::LABELS.iter().enumerate() {
18420 for (j, b) in UnquoteForm::LABELS.iter().enumerate() {
18421 if i == j {
18422 continue;
18423 }
18424 assert_ne!(
18425 a, b,
18426 "UnquoteForm::LABELS[{i}] ({a:?}) collides with \
18427 UnquoteForm::LABELS[{j}] ({b:?}) — two distinct \
18428 template markers cannot share diagnostic bytes",
18429 );
18430 }
18431 }
18432 }
18433
18434 #[test]
18435 fn unquote_form_label_emits_canonical_sexp_shape_label_for_every_marker() {
18436 // Per-arm truth-table pin of the canonical (UnquoteForm
18437 // variant, diagnostic label) mapping: `UnquoteForm::Unquote →
18438 // "unquote"` and `UnquoteForm::Splice → "unquote-splice"`
18439 // byte-for-byte. The `-splice` short form on `Splice` is
18440 // load-bearing: the substrate diagnostic surface renders
18441 // `unquote-splice`, distinct from the iac-forge canonical
18442 // form's `unquote-splicing` — a regression that homogenizes
18443 // the two axes surfaces here on the diagnostic-surface side
18444 // (`unquote_form_iac_forge_tag_diverges_from_sexp_shape_label_at_splice_arm`
18445 // surfaces it on the iac-forge-surface side). Sibling of
18446 // `unquote_form_iac_forge_tag_emits_canonical_cl_tag_for_every_marker`
18447 // on the iac-forge axis rather than the diagnostic axis.
18448 assert_eq!(
18449 UnquoteForm::Unquote.label(),
18450 "unquote",
18451 "UnquoteForm::Unquote.label() drifted from canonical substrate 'unquote' diagnostic",
18452 );
18453 assert_eq!(
18454 UnquoteForm::Splice.label(),
18455 "unquote-splice",
18456 "UnquoteForm::Splice.label() drifted from canonical substrate 'unquote-splice' \
18457 diagnostic — the short form is load-bearing (distinct from iac-forge 'unquote-splicing')",
18458 );
18459 }
18460
18461 #[test]
18462 fn unquote_form_label_diverges_from_iac_forge_tag_at_splice_arm() {
18463 // BOUNDARY-DISTINCT CONTRACT (subset peer): the diagnostic
18464 // label for `UnquoteForm::Splice` is `"unquote-splice"` (short
18465 // substrate form), distinct from `UnquoteForm::Splice.iac_forge_tag()`
18466 // which renders the longer `"unquote-splicing"` (Common-Lisp
18467 // canonical form). Pins the divergence on the substitution-
18468 // subset carving. On the non-`Splice` arm the two projections
18469 // MUST agree (both spell `"unquote"` at the substitution-
18470 // subset's `Unquote` arm). Peer of
18471 // `unquote_form_iac_forge_tag_diverges_from_sexp_shape_label_at_splice_arm`
18472 // one composition path over — that pin routes through
18473 // `sexp_shape().label()`; this pin routes through the direct
18474 // subset `label()` inherent (which composes through
18475 // `to_quote_form().label()`).
18476 assert_eq!(
18477 UnquoteForm::Unquote.label(),
18478 UnquoteForm::Unquote.iac_forge_tag(),
18479 "UnquoteForm::Unquote.label() and .iac_forge_tag() must agree on 'unquote' — the two axes only diverge at the Splice arm",
18480 );
18481 assert_ne!(
18482 UnquoteForm::Splice.label(),
18483 UnquoteForm::Splice.iac_forge_tag(),
18484 "UnquoteForm::Splice.label() and .iac_forge_tag() must diverge — substrate renders 'unquote-splice', iac-forge canonical form is 'unquote-splicing'",
18485 );
18486 }
18487
18488 #[test]
18489 fn unquote_form_wrap_routes_through_to_quote_form_wrap_via_composition() {
18490 // Post-lift composition pin: for every `uf: UnquoteForm` and
18491 // every representative `inner: Sexp`, `uf.wrap(inner.clone())
18492 // == uf.to_quote_form().wrap(inner)` byte-for-byte. The
18493 // (UnquoteForm marker, `Sexp::*` tuple-variant constructor)
18494 // pairing on the substitution-subset closed set is derived
18495 // structurally from the superset's canonical
18496 // [`crate::ast::QuoteForm::wrap`] closed-set match rather than
18497 // from a parallel two-arm inline table on this subset. A
18498 // regression that re-inlines the two arms as a parallel
18499 // match-table (a future edit that spells `Self::Unquote =>
18500 // Sexp::Unquote(Box::new(inner))` / `Self::Splice =>
18501 // Sexp::UnquoteSplice(Box::new(inner))` directly at
18502 // `UnquoteForm::wrap` instead of routing through
18503 // `self.to_quote_form().wrap(inner)`) still passes the round-
18504 // trip and canonical-tuple-variant sweeps below but fails
18505 // THIS composition pin — the subset's construct-family
18506 // vocabulary is no longer derived from the superset's
18507 // canonical site. Sibling-shape pin to commit 250c001's
18508 // `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`:
18509 // both pin the subset's projection through the superset's
18510 // canonical site via structural equality, the invariant the
18511 // subset-to-superset composition was lifted to make load-
18512 // bearing on the type system rather than on per-callsite
18513 // discipline.
18514 use crate::ast::Sexp;
18515 let inners = [
18516 Sexp::Nil,
18517 Sexp::symbol("x"),
18518 Sexp::keyword("k"),
18519 Sexp::string("s"),
18520 Sexp::int(42),
18521 Sexp::float(1.5),
18522 Sexp::boolean(true),
18523 Sexp::List(vec![Sexp::symbol("f"), Sexp::int(1), Sexp::int(2)]),
18524 ];
18525 for uf in UnquoteForm::ALL {
18526 for inner in &inners {
18527 let via_wrap = uf.wrap(inner.clone());
18528 let via_composition = uf.to_quote_form().wrap(inner.clone());
18529 assert_eq!(
18530 via_wrap, via_composition,
18531 "UnquoteForm::{uf:?}.wrap(inner) drifted from .to_quote_form().wrap(inner) — the subset's construct vocabulary is no longer derived from the superset's canonical site",
18532 );
18533 }
18534 }
18535 }
18536
18537 #[test]
18538 fn unquote_form_wrap_emits_canonical_tuple_variant_for_every_marker() {
18539 // Byte-for-byte tuple-variant emission pin: for every
18540 // `uf: UnquoteForm` and every representative `inner: Sexp`,
18541 // `uf.wrap(inner)` produces the canonical `Sexp::Unquote(
18542 // Box::new(inner))` / `Sexp::UnquoteSplice(Box::new(inner))`
18543 // shape byte-for-byte. Pins the (subset marker, `Sexp::*`
18544 // tuple-variant constructor) pairing at the observable
18545 // wrapper-shape boundary — a regression that maps
18546 // `UnquoteForm::Unquote → Sexp::UnquoteSplice` (a marker/
18547 // constructor swap that still routes through the superset's
18548 // `wrap`) surfaces here because the composition through
18549 // `to_quote_form()` picks up the swap at the subset-to-
18550 // superset projection. Sibling of the outer `Sexp` construct
18551 // family's
18552 // `sexp_quote_family_constructors_emit_canonical_tuple_variant_for_every_marker`
18553 // (commit 38f076b) — same posture on the subset closed set.
18554 use crate::ast::Sexp;
18555 let inners = [
18556 Sexp::Nil,
18557 Sexp::symbol("x"),
18558 Sexp::keyword("k"),
18559 Sexp::string("s"),
18560 Sexp::int(-7),
18561 Sexp::float(2.0),
18562 Sexp::boolean(false),
18563 Sexp::List(vec![Sexp::symbol("f"), Sexp::int(1)]),
18564 ];
18565 for inner in &inners {
18566 assert_eq!(
18567 UnquoteForm::Unquote.wrap(inner.clone()),
18568 Sexp::Unquote(Box::new(inner.clone())),
18569 "UnquoteForm::Unquote.wrap({inner:?}) drifted from Sexp::Unquote(Box::new({inner:?})) canonical tuple-variant shape",
18570 );
18571 assert_eq!(
18572 UnquoteForm::Splice.wrap(inner.clone()),
18573 Sexp::UnquoteSplice(Box::new(inner.clone())),
18574 "UnquoteForm::Splice.wrap({inner:?}) drifted from Sexp::UnquoteSplice(Box::new({inner:?})) canonical tuple-variant shape",
18575 );
18576 }
18577 }
18578
18579 #[test]
18580 fn unquote_form_wrap_round_trips_through_sexp_as_unquote() {
18581 // Section-for-retraction pin: `uf.wrap(inner).as_unquote() ==
18582 // Some((uf, &inner))` for every `uf: UnquoteForm` and every
18583 // representative `inner: Sexp`. Closes the (construct,
18584 // project) algebra dual on the closed-set `UnquoteForm`
18585 // algebra — the substitution-subset peer of the closed-set
18586 // superset algebra's already-closed
18587 // ([`crate::ast::QuoteForm::wrap`],
18588 // [`crate::ast::Sexp::as_quote_form`]) dual. The typed
18589 // constructor + typed projection pair form an `Iso(inner,
18590 // Sexp::X_variant(inner))` on the subset closed set. A future
18591 // arm added to `UnquoteForm` extends [`UnquoteForm::ALL`] +
18592 // [`UnquoteForm::to_quote_form`] + this sweep in lockstep —
18593 // rustc-enforced through the closed-set exhaustiveness across
18594 // the `UnquoteForm` match. Sibling of the outer `Sexp`
18595 // construct family's
18596 // `sexp_quote_family_constructors_round_trip_through_as_quote_form`
18597 // (commit 38f076b) — same posture on the subset closed set,
18598 // routed through `Sexp::as_unquote` rather than
18599 // `Sexp::as_quote_form`.
18600 use crate::ast::Sexp;
18601 let inners = [
18602 Sexp::Nil,
18603 Sexp::symbol("x"),
18604 Sexp::keyword("k"),
18605 Sexp::string("s"),
18606 Sexp::int(1),
18607 Sexp::List(vec![Sexp::symbol("f")]),
18608 ];
18609 for uf in UnquoteForm::ALL {
18610 for inner in &inners {
18611 let wrapped = uf.wrap(inner.clone());
18612 assert_eq!(
18613 wrapped.as_unquote(),
18614 Some((uf, inner)),
18615 "UnquoteForm::{uf:?}.wrap({inner:?}).as_unquote() failed to round-trip — the (construct, project) pair on the subset algebra is not a section-for-retraction of Sexp::as_unquote",
18616 );
18617 }
18618 }
18619 }
18620
18621 #[test]
18622 fn unquote_form_wrap_composes_with_shape_via_to_quote_form_sexp_shape() {
18623 // Outer-shape composition pin: for every `uf: UnquoteForm`
18624 // and every representative `inner: Sexp`, `uf.wrap(inner)
18625 // .shape() == uf.to_quote_form().sexp_shape()` — the
18626 // (subset marker, outer [`crate::error::SexpShape`]) pairing
18627 // binds through the SAME closed-set composition
18628 // (`to_quote_form` → `QuoteForm::sexp_shape`) that
18629 // [`crate::ast::QuoteForm::wrap`]'s outer-shape composition
18630 // rides on. A regression that drifts ONE construct arm's
18631 // outer-shape from the superset's `sexp_shape` while
18632 // preserving the tuple-variant emission surfaces here
18633 // alongside the round-trip pin. Sibling of the outer `Sexp`
18634 // construct family's
18635 // `sexp_quote_family_constructors_compose_with_shape_via_quote_form_sexp_shape`
18636 // (commit 38f076b) — same posture on the subset closed set.
18637 use crate::ast::Sexp;
18638 let inners = [
18639 Sexp::Nil,
18640 Sexp::symbol("x"),
18641 Sexp::int(42),
18642 Sexp::List(vec![Sexp::symbol("f")]),
18643 ];
18644 for uf in UnquoteForm::ALL {
18645 for inner in &inners {
18646 assert_eq!(
18647 uf.wrap(inner.clone()).shape(),
18648 uf.to_quote_form().sexp_shape(),
18649 "UnquoteForm::{uf:?}.wrap({inner:?}).shape() drifted from .to_quote_form().sexp_shape() — the (subset marker, outer SexpShape) pairing is no longer derived from the superset's canonical composition",
18650 );
18651 }
18652 }
18653 }
18654
18655 #[test]
18656 fn unquote_form_sexp_shape_routes_through_to_quote_form_sexp_shape_via_composition() {
18657 // Post-lift composition pin: for every `uf: UnquoteForm`,
18658 // `uf.sexp_shape()` and `uf.to_quote_form().sexp_shape()`
18659 // agree byte-for-byte — the (subset marker, outer SexpShape)
18660 // pairing rides through the superset's canonical
18661 // `QuoteForm::sexp_shape` closed-set match rather than through
18662 // a parallel two-arm inline table on the subset. A regression
18663 // that re-inlines the two arms as a parallel match-table (e.g.
18664 // a future edit that spells `Self::Unquote => SexpShape::Unquote`
18665 // / `Self::Splice => SexpShape::UnquoteSplice` directly at
18666 // `UnquoteForm::sexp_shape` instead of routing through
18667 // `self.to_quote_form().sexp_shape()`) still passes the round-
18668 // trip sweep below but fails THIS composition pin — the
18669 // subset's outer-shape vocabulary is no longer derived from
18670 // the superset's canonical site. Sibling-shape pin to commit
18671 // 250c001's `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`
18672 // and commit 92daace's `unquote_form_wrap_routes_through_to_quote_form_wrap_via_composition`:
18673 // all three pin the subset's projection through the superset's
18674 // canonical site via the same typed composition posture, the
18675 // invariant the subset-to-superset composition was lifted to
18676 // make load-bearing on the type system rather than on
18677 // per-callsite discipline.
18678 for uf in UnquoteForm::ALL {
18679 let from_shape = uf.sexp_shape();
18680 let from_composition = uf.to_quote_form().sexp_shape();
18681 assert_eq!(
18682 from_shape, from_composition,
18683 "UnquoteForm::{uf:?}.sexp_shape() drifted from .to_quote_form().sexp_shape() — the subset's outer-shape vocabulary is no longer derived from the superset's canonical site",
18684 );
18685 }
18686 }
18687
18688 #[test]
18689 fn unquote_form_sexp_shape_emits_canonical_shape_for_every_marker() {
18690 // Per-arm truth-table pin of the canonical (UnquoteForm variant,
18691 // SexpShape variant) mapping: `UnquoteForm::Unquote →
18692 // SexpShape::Unquote` and `UnquoteForm::Splice →
18693 // SexpShape::UnquoteSplice` byte-for-byte. A regression that
18694 // swaps the two arms (a marker/shape swap that still routes
18695 // through the superset's `sexp_shape`) surfaces here because
18696 // the composition through `to_quote_form()` picks up the swap
18697 // at the subset-to-superset projection. Sibling of the outer
18698 // `Sexp` construct family's
18699 // `sexp_quote_family_constructors_emit_canonical_tuple_variant_for_every_marker`
18700 // (commit 38f076b) on the outer-shape axis rather than the
18701 // tuple-variant axis.
18702 assert_eq!(
18703 UnquoteForm::Unquote.sexp_shape(),
18704 SexpShape::Unquote,
18705 "UnquoteForm::Unquote.sexp_shape() drifted from SexpShape::Unquote",
18706 );
18707 assert_eq!(
18708 UnquoteForm::Splice.sexp_shape(),
18709 SexpShape::UnquoteSplice,
18710 "UnquoteForm::Splice.sexp_shape() drifted from SexpShape::UnquoteSplice",
18711 );
18712 }
18713
18714 #[test]
18715 fn unquote_form_iac_forge_tag_routes_through_to_quote_form_iac_forge_tag_via_composition() {
18716 // Post-lift composition pin: for every `uf: UnquoteForm`,
18717 // `uf.iac_forge_tag()` and `uf.to_quote_form().iac_forge_tag()`
18718 // agree byte-for-byte — the (subset marker, canonical
18719 // iac-forge tag) pairing rides through the superset's
18720 // canonical `QuoteForm::iac_forge_tag` closed-set match rather
18721 // than through a parallel two-arm inline table on the subset.
18722 // A regression that re-inlines the two arms as a parallel
18723 // match-table (e.g. a future edit that spells `Self::Unquote
18724 // => "unquote"` / `Self::Splice => "unquote-splicing"` directly
18725 // at `UnquoteForm::iac_forge_tag` instead of routing through
18726 // `self.to_quote_form().iac_forge_tag()`) still passes the
18727 // per-arm truth-table pin below but fails THIS composition
18728 // pin — the subset's canonical interop vocabulary is no
18729 // longer derived from the superset's canonical site. Sibling-
18730 // shape pin to commit 250c001's
18731 // `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`,
18732 // commit 92daace's
18733 // `unquote_form_wrap_routes_through_to_quote_form_wrap_via_composition`,
18734 // and this run's sibling
18735 // `unquote_form_sexp_shape_routes_through_to_quote_form_sexp_shape_via_composition`
18736 // — all four pin the subset's projection through the
18737 // superset's canonical site via the same typed composition
18738 // posture, the invariant the subset-to-superset composition
18739 // was lifted to make load-bearing on the type system rather
18740 // than on per-callsite discipline.
18741 for uf in UnquoteForm::ALL {
18742 let from_tag = uf.iac_forge_tag();
18743 let from_composition = uf.to_quote_form().iac_forge_tag();
18744 assert_eq!(
18745 from_tag, from_composition,
18746 "UnquoteForm::{uf:?}.iac_forge_tag() drifted from .to_quote_form().iac_forge_tag() — the subset's canonical interop vocabulary is no longer derived from the superset's canonical site",
18747 );
18748 }
18749 }
18750
18751 #[test]
18752 fn unquote_form_iac_forge_tag_emits_canonical_cl_tag_for_every_marker() {
18753 // Per-arm truth-table pin of the canonical (UnquoteForm variant,
18754 // iac-forge tag) mapping: `UnquoteForm::Unquote → "unquote"`
18755 // and `UnquoteForm::Splice → "unquote-splicing"` byte-for-byte.
18756 // The `-splicing` suffix on `Splice` is load-bearing: the
18757 // Common-Lisp-canonical form REQUIRES `(unquote-splicing x)`
18758 // rather than `(unquote-splice x)`, and every downstream
18759 // BLAKE3 attestation key + render-cache shape hashes on the
18760 // exact tag spelling. A regression that swaps the two arms
18761 // (a marker/tag swap that still routes through the superset's
18762 // `iac_forge_tag`) surfaces here because the composition
18763 // through `to_quote_form()` picks up the swap at the subset-
18764 // to-superset projection. A regression that drops the
18765 // `-splicing` suffix and consolidates the tag with the
18766 // substrate's diagnostic label (`unquote-splice`) also
18767 // surfaces here. Sibling of the outer `Sexp` construct
18768 // family's canonical tuple-variant pins on the interop-tag
18769 // axis rather than the tuple-variant axis.
18770 assert_eq!(
18771 UnquoteForm::Unquote.iac_forge_tag(),
18772 "unquote",
18773 "UnquoteForm::Unquote.iac_forge_tag() drifted from canonical CL 'unquote' tag",
18774 );
18775 assert_eq!(
18776 UnquoteForm::Splice.iac_forge_tag(),
18777 "unquote-splicing",
18778 "UnquoteForm::Splice.iac_forge_tag() drifted from canonical CL 'unquote-splicing' tag \
18779 — the '-splicing' suffix is load-bearing for iac-forge canonical-form round-trip",
18780 );
18781 }
18782
18783 #[test]
18784 fn unquote_form_iac_forge_tag_diverges_from_sexp_shape_label_at_splice_arm() {
18785 // BOUNDARY-DISTINCT CONTRACT (subset peer): the iac-forge
18786 // canonical tag for `UnquoteForm::Splice` is
18787 // `"unquote-splicing"` (CL canonical form), distinct from
18788 // `SexpShape::label` for the corresponding subset shape
18789 // `SexpShape::UnquoteSplice`, which renders the shorter
18790 // `"unquote-splice"` (substrate diagnostic surface). Pins the
18791 // divergence on the substitution-subset carving in lockstep
18792 // with the superset-side pin
18793 // `quote_form_iac_forge_tag_diverges_from_sexp_shape_label_for_unquote_splice`
18794 // in `ast.rs`. On the non-`Splice` arm the two projections
18795 // MUST agree (both spell `"unquote"` at the substitution-
18796 // subset's `Unquote` arm) — pin that path-uniformity too so
18797 // a regression that homogenizes both arms surfaces here.
18798 // A future "consolidation" PR that renames one side would
18799 // silently break either the iac-forge canonical-form round-
18800 // trip OR the operator-facing diagnostic surface at the
18801 // template-substitution rejection sites.
18802 assert_eq!(
18803 UnquoteForm::Unquote.iac_forge_tag(),
18804 SexpShape::Unquote.label(),
18805 "the Unquote arm's iac-forge tag AND diagnostic label MUST agree — \
18806 both spell 'unquote' by CL + substrate convention alike",
18807 );
18808 assert_eq!(
18809 UnquoteForm::Splice.iac_forge_tag(),
18810 "unquote-splicing",
18811 "UnquoteForm::Splice.iac_forge_tag() drifted from canonical CL 'unquote-splicing' tag",
18812 );
18813 assert_eq!(
18814 SexpShape::UnquoteSplice.label(),
18815 "unquote-splice",
18816 "SexpShape::UnquoteSplice.label() drifted from substrate diagnostic 'unquote-splice' label",
18817 );
18818 assert_ne!(
18819 UnquoteForm::Splice.iac_forge_tag(),
18820 SexpShape::UnquoteSplice.label(),
18821 "the two projections MUST disagree at Splice — the CL canonical form \
18822 requires '-splicing' while the substrate's diagnostic label uses the \
18823 shorter '-splice'; consolidating them would break either side",
18824 );
18825 }
18826
18827 #[test]
18828 fn unquote_form_iac_forge_tag_agrees_with_outer_sexp_iac_forge_arm_when_iac_forge_feature_gate_is_on(
18829 ) {
18830 // Cross-boundary intent pin: for every `uf: UnquoteForm` and a
18831 // representative `inner: Sexp`, the tag the outer `From<&Sexp>
18832 // for iac_forge::SExpr` interop arm emits when it decomposes
18833 // the wrapper via `expect_quote_form()` and projects through
18834 // `QuoteForm::iac_forge_tag()` agrees byte-for-byte with the
18835 // direct `UnquoteForm::iac_forge_tag()` projection on the
18836 // marker the outer projection would have recovered. Pinned
18837 // through the composition `qf.iac_forge_tag() ==
18838 // qf.as_unquote_form().map(UnquoteForm::iac_forge_tag)
18839 // .unwrap_or_else(|| qf.iac_forge_tag())` — the (QuoteForm,
18840 // UnquoteForm) tag agreement on the substitution-subset
18841 // carving is exactly the invariant a future refactor that
18842 // routes the outer interop arm through THIS subset method
18843 // (on Unquote/UnquoteSplice) instead of the superset method
18844 // (on all four) can rely on to stay byte-identical.
18845 //
18846 // Feature-gated because the iac-forge feature is off by
18847 // default in the isolated tatara-lisp build (the actual
18848 // `From<&Sexp> for iac_forge::SExpr` impl and its tests live
18849 // behind the same gate); this test pins the invariant that
18850 // WOULD bind the cross-boundary agreement WITHOUT depending
18851 // on the feature-gated impl compiling — the composition law
18852 // holds on the typed algebra regardless.
18853 for uf in UnquoteForm::ALL {
18854 let via_subset = uf.iac_forge_tag();
18855 let via_superset = uf.to_quote_form().iac_forge_tag();
18856 assert_eq!(
18857 via_subset, via_superset,
18858 "UnquoteForm::{uf:?}.iac_forge_tag() drifted from .to_quote_form().iac_forge_tag() — \
18859 the outer interop arm's byte-identity on the substitution-subset carving is broken",
18860 );
18861 }
18862 }
18863
18864 #[test]
18865 fn unquote_form_iac_forge_tag_specializes_to_matching_arm_of_quote_form_iac_forge_tag() {
18866 // PER-VARIANT RESTRICTION LAW pin: the subset's tag projection
18867 // agrees with the superset's tag projection on the
18868 // substitution-subset carving arm-for-arm. `UnquoteForm::
18869 // Unquote.iac_forge_tag() == crate::ast::QuoteForm::Unquote
18870 // .iac_forge_tag()`, and `UnquoteForm::Splice.iac_forge_tag()
18871 // == crate::ast::QuoteForm::UnquoteSplice.iac_forge_tag()`.
18872 // The composition through `to_quote_form()` binds this
18873 // agreement to the SAME closed-set match arm on the
18874 // superset's `iac_forge_tag` per subset variant — a regression
18875 // that drifts `to_quote_form`'s arm mapping (e.g. a future
18876 // edit that pairs `UnquoteForm::Splice` with
18877 // `QuoteForm::Unquote` on the subset → superset projection)
18878 // fails BOTH the arm-check here AND the composition pin above,
18879 // with distinct arm-anchored failure signatures. Sibling of
18880 // commit 3e92457's `sexp_shape` per-variant restriction pins
18881 // and commit 92daace's `wrap` per-variant restriction pins
18882 // on the interop-tag axis.
18883 use crate::ast::QuoteForm;
18884 assert_eq!(
18885 UnquoteForm::Unquote.iac_forge_tag(),
18886 QuoteForm::Unquote.iac_forge_tag(),
18887 "UnquoteForm::Unquote.iac_forge_tag() drifted from QuoteForm::Unquote.iac_forge_tag()",
18888 );
18889 assert_eq!(
18890 UnquoteForm::Splice.iac_forge_tag(),
18891 QuoteForm::UnquoteSplice.iac_forge_tag(),
18892 "UnquoteForm::Splice.iac_forge_tag() drifted from QuoteForm::UnquoteSplice.iac_forge_tag()",
18893 );
18894 }
18895
18896 #[test]
18897 fn unquote_form_hash_discriminator_routes_through_to_quote_form_hash_discriminator_via_composition(
18898 ) {
18899 // Post-lift composition pin: for every `uf: UnquoteForm`,
18900 // `uf.hash_discriminator()` and
18901 // `uf.to_quote_form().hash_discriminator()` agree byte-for-byte
18902 // — the (subset marker, outer-Sexp cache-key byte) pairing
18903 // rides through the superset's canonical
18904 // `QuoteForm::hash_discriminator` closed-set match rather than
18905 // through a parallel two-arm inline table on the subset. A
18906 // regression that re-inlines the two arms as a parallel
18907 // match-table (e.g. a future edit that spells `Self::Unquote
18908 // => 5` / `Self::Splice => 6` directly at
18909 // `UnquoteForm::hash_discriminator` instead of routing through
18910 // `self.to_quote_form().hash_discriminator()`) still passes
18911 // the per-arm truth-table pin below but fails THIS composition
18912 // pin — the subset's cache-key vocabulary is no longer derived
18913 // from the superset's canonical site. Sibling-shape pin to the
18914 // four prior-lift composition pins on `UnquoteForm`:
18915 // `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`,
18916 // `unquote_form_wrap_routes_through_to_quote_form_wrap_via_composition`,
18917 // `unquote_form_sexp_shape_routes_through_to_quote_form_sexp_shape_via_composition`,
18918 // and
18919 // `unquote_form_iac_forge_tag_routes_through_to_quote_form_iac_forge_tag_via_composition`
18920 // — all five pin the subset's projection through the
18921 // superset's canonical site via the same typed composition
18922 // posture. The FIFTH composition-through-`to_quote_form` axis
18923 // on the substitution-subset closed set is now load-bearing
18924 // on the type system rather than on per-callsite discipline.
18925 for uf in UnquoteForm::ALL {
18926 let from_disc = uf.hash_discriminator();
18927 let from_composition = uf.to_quote_form().hash_discriminator();
18928 assert_eq!(
18929 from_disc, from_composition,
18930 "UnquoteForm::{uf:?}.hash_discriminator() drifted from .to_quote_form().hash_discriminator() — the subset's cache-key vocabulary is no longer derived from the superset's canonical site",
18931 );
18932 }
18933 }
18934
18935 #[test]
18936 fn unquote_form_hash_discriminator_pins_legacy_cache_key_bytes() {
18937 // Per-arm truth-table pin of the canonical (UnquoteForm variant,
18938 // outer-Sexp cache-key byte) mapping: `UnquoteForm::Unquote → 5`
18939 // and `UnquoteForm::Splice → 6` byte-for-byte. These bytes are
18940 // load-bearing: the substrate's `Hash for Sexp` body composes
18941 // `QuoteForm::hash_discriminator` at the Unquote/UnquoteSplice
18942 // arms and the macro-expansion cache keys on those bytes;
18943 // changing either arm silently invalidates every cached
18944 // expansion across the substrate AND risks collision with the
18945 // reserved bytes the non-substitution carvings' arms use
18946 // (`{0, 2}` for `StructuralKind`, `1` for the outer `Atom`
18947 // marker, `{3, 4}` for the non-substitution quote-family arms
18948 // `Quote`/`Quasiquote`). Sibling of
18949 // `quote_form_hash_discriminator_pins_legacy_cache_key_bytes`
18950 // (4-arm superset pin),
18951 // `structural_kind_hash_discriminator_pins_legacy_cache_key_bytes`
18952 // (2-arm structural-residual pin),
18953 // `atom_kind_hash_discriminator_pins_legacy_atom_cache_key_bytes`
18954 // (6-arm atomic-payload pin), and
18955 // `sexp_shape_hash_discriminator_pins_legacy_outer_cache_key_bytes`
18956 // (12-arm shape-level pin) — this 2-arm subset pin joins the
18957 // family the closed-set typed algebra composes on for outer-
18958 // Sexp cache-key partition coherence.
18959 assert_eq!(
18960 UnquoteForm::Unquote.hash_discriminator(),
18961 5,
18962 "UnquoteForm::Unquote.hash_discriminator() drifted from legacy cache-key byte 5",
18963 );
18964 assert_eq!(
18965 UnquoteForm::Splice.hash_discriminator(),
18966 6,
18967 "UnquoteForm::Splice.hash_discriminator() drifted from legacy cache-key byte 6",
18968 );
18969 }
18970
18971 #[test]
18972 fn unquote_form_hash_discriminator_specializes_to_matching_arm_of_quote_form_hash_discriminator(
18973 ) {
18974 // PER-VARIANT RESTRICTION LAW pin: the subset's discriminator
18975 // projection agrees with the superset's discriminator
18976 // projection on the substitution-subset carving arm-for-arm.
18977 // `UnquoteForm::Unquote.hash_discriminator() ==
18978 // QuoteForm::Unquote.hash_discriminator()`, and
18979 // `UnquoteForm::Splice.hash_discriminator() ==
18980 // QuoteForm::UnquoteSplice.hash_discriminator()`. The
18981 // composition through `to_quote_form()` binds this agreement
18982 // to the SAME closed-set match arm on the superset's
18983 // `hash_discriminator` per subset variant — a regression that
18984 // drifts `to_quote_form`'s arm mapping (e.g. a future edit
18985 // that pairs `UnquoteForm::Splice` with `QuoteForm::Unquote`
18986 // on the subset → superset projection) fails BOTH the
18987 // arm-check here AND the composition pin above, with distinct
18988 // arm-anchored failure signatures. Sibling of the four prior
18989 // per-variant restriction pins on `UnquoteForm`: `marker`
18990 // (through `QuoteForm::prefix`), `wrap` (through
18991 // `QuoteForm::wrap`), `sexp_shape` (through
18992 // `QuoteForm::sexp_shape`), and `iac_forge_tag` (through
18993 // `QuoteForm::iac_forge_tag`) — the cache-key axis closes the
18994 // per-variant restriction family on the substitution subset.
18995 use crate::ast::QuoteForm;
18996 assert_eq!(
18997 UnquoteForm::Unquote.hash_discriminator(),
18998 QuoteForm::Unquote.hash_discriminator(),
18999 "UnquoteForm::Unquote.hash_discriminator() drifted from QuoteForm::Unquote.hash_discriminator()",
19000 );
19001 assert_eq!(
19002 UnquoteForm::Splice.hash_discriminator(),
19003 QuoteForm::UnquoteSplice.hash_discriminator(),
19004 "UnquoteForm::Splice.hash_discriminator() drifted from QuoteForm::UnquoteSplice.hash_discriminator()",
19005 );
19006 }
19007
19008 #[test]
19009 fn unquote_form_hash_discriminator_partitions_disjointly_from_non_substitution_carvings() {
19010 // DISJOINTNESS CONTRACT pin: the substitution-subset
19011 // discriminator image `{5, 6}` MUST be disjoint from every
19012 // other outer-Sexp carving's discriminator image — otherwise
19013 // the substrate's `Hash for Sexp` cache-key partition
19014 // silently collides and macro-expansion cache hits leak
19015 // across carvings. The reserved bytes are:
19016 // * `StructuralKind::hash_discriminator` image: `{0, 2}`
19017 // (Nil=0, List=2)
19018 // * outer `Atom` carve marker: `{1}` (single-arm literal
19019 // inside `Hash for Sexp`)
19020 // * non-substitution `QuoteForm` arms
19021 // (Quote/Quasiquote): `{3, 4}` (subset image of
19022 // `QuoteForm::hash_discriminator` restricted to the
19023 // non-`UnquoteForm` carving)
19024 // Sibling of the joint outer-Sexp discriminator-partition
19025 // pin at the shape-level algebra
19026 // (`sexp_shape_hash_discriminator_partitions_by_three_way_carving_disjointly`)
19027 // — this test pins the FOURTH carving's partition membership
19028 // (the 2-of-4 substitution-subset carving of `QuoteForm`)
19029 // rather than the three top-level carvings of `SexpShape`.
19030 use crate::ast::QuoteForm;
19031 use std::collections::HashSet;
19032
19033 let subset_image: HashSet<u8> = UnquoteForm::ALL
19034 .iter()
19035 .map(|uf| uf.hash_discriminator())
19036 .collect();
19037 assert_eq!(
19038 subset_image,
19039 HashSet::from([5, 6]),
19040 "UnquoteForm::ALL swept through hash_discriminator MUST cover exactly {{5, 6}} — the substitution-subset cache-key partition",
19041 );
19042
19043 let structural_image: HashSet<u8> = StructuralKind::ALL
19044 .iter()
19045 .map(|sk| sk.hash_discriminator())
19046 .collect();
19047 assert!(
19048 subset_image.is_disjoint(&structural_image),
19049 "UnquoteForm hash_discriminator image {subset_image:?} MUST be disjoint from StructuralKind image {structural_image:?} — a cache-key collision would leak macro-expansion hits across the substitution-subset and structural-residual carvings",
19050 );
19051
19052 let atom_outer_byte: HashSet<u8> = HashSet::from([1]);
19053 assert!(
19054 subset_image.is_disjoint(&atom_outer_byte),
19055 "UnquoteForm hash_discriminator image {subset_image:?} MUST be disjoint from the outer Atom carve marker {{1}} — a cache-key collision would leak macro-expansion hits across the substitution-subset and atomic-payload carvings",
19056 );
19057
19058 let non_substitution_quote_image: HashSet<u8> = [QuoteForm::Quote, QuoteForm::Quasiquote]
19059 .iter()
19060 .map(|qf| qf.hash_discriminator())
19061 .collect();
19062 assert!(
19063 subset_image.is_disjoint(&non_substitution_quote_image),
19064 "UnquoteForm hash_discriminator image {subset_image:?} MUST be disjoint from the non-substitution QuoteForm arms' image {non_substitution_quote_image:?} — a cache-key collision would leak macro-expansion hits across the substitution-subset and non-substitution quote-family carvings",
19065 );
19066 }
19067
19068 #[test]
19069 fn unquote_form_per_role_hash_discriminators_alias_quote_form_per_role_hash_discriminators_byte_for_byte(
19070 ) {
19071 // ALIAS CONTRACT: pin both per-role
19072 // `pub(crate) const UnquoteForm::*_HASH_DISCRIMINATOR`
19073 // aliases equal the corresponding
19074 // `pub(crate) const QuoteForm::*_HASH_DISCRIMINATOR`
19075 // byte-for-byte — so the UnquoteForm ⊂ QuoteForm outer-`Sexp`
19076 // cache-key-byte vocabulary containment routes through the
19077 // typed `pub(crate) const UnquoteForm::V_HASH_DISCRIMINATOR:
19078 // u8 = QuoteForm::V_HASH_DISCRIMINATOR` alias chain rather
19079 // than through two independent literal-discipline sites. A
19080 // regression that renames the QuoteForm side without updating
19081 // the UnquoteForm alias pointing at it fails-loudly here with
19082 // the exact axis identified (UNQUOTE / SPLICE); a regression
19083 // that re-inlines the UnquoteForm constant to a fresh literal
19084 // still passes this pin but loses the alias-chain typing
19085 // (which is what
19086 // `unquote_form_hash_discriminators_align_with_all_by_index`
19087 // catches in combination).
19088 //
19089 // Sibling posture to
19090 // `unquote_form_per_role_markers_alias_quote_form_per_role_prefixes_byte_for_byte`
19091 // (commit 3640f76),
19092 // `unquote_form_per_role_iac_forge_tags_alias_quote_form_per_role_iac_forge_tags_byte_for_byte`
19093 // (commit 6acab84), and
19094 // `unquote_form_per_role_labels_alias_quote_form_per_role_labels_byte_for_byte`
19095 // (commit da68af5): all four pin the invariant that the
19096 // UnquoteForm subset algebra's per-role bytes are structurally
19097 // derived from the parent QuoteForm superset's per-role bytes
19098 // via a `pub const = Parent::CONST` alias rather than through
19099 // parallel literal tables — the axis there is the three
19100 // `&'static str` production vocabularies (reader punctuation,
19101 // iac-forge canonical-form, diagnostic label); the axis here
19102 // is the `u8` outer-`Sexp` cache-key vocabulary. All four axes
19103 // span the FOUR production byte-vocabularies the parent
19104 // [`QuoteForm`] closed set carries per role, and all four
19105 // bind through the same aliased typed source of truth on
19106 // this UnquoteForm subset.
19107 use crate::ast::QuoteForm;
19108 assert_eq!(
19109 UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR,
19110 QuoteForm::UNQUOTE_HASH_DISCRIMINATOR
19111 );
19112 assert_eq!(
19113 UnquoteForm::SPLICE_HASH_DISCRIMINATOR,
19114 QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR
19115 );
19116 }
19117
19118 #[test]
19119 fn unquote_form_hash_discriminator_arms_route_through_per_role_hash_discriminators_for_every_variant(
19120 ) {
19121 // PATH-UNIFORMITY: `UnquoteForm::V.hash_discriminator()` MUST
19122 // equal the per-role `pub(crate) const
19123 // UnquoteForm::V_HASH_DISCRIMINATOR` for every
19124 // `v: UnquoteForm`. Pre-lift the two cache-key bytes were
19125 // reachable through the two-step composition
19126 // `uf.to_quote_form().hash_discriminator()` OR through direct
19127 // `QuoteForm::UNQUOTE_HASH_DISCRIMINATOR` reach-across; post-
19128 // lift each variant's canonical bytes are reachable through
19129 // the per-role `UnquoteForm::*_HASH_DISCRIMINATOR` alias AND
19130 // through the inherent `hash_discriminator()` method too. Pin
19131 // the byte-equality between the runtime projection and the
19132 // compile-time alias so a regression that renames the alias
19133 // without updating the arm (or vice versa) fails-loudly at
19134 // the exact axis.
19135 //
19136 // Sibling-shape pin to
19137 // `unquote_form_marker_arms_route_through_per_role_markers_for_every_variant`,
19138 // `unquote_form_iac_forge_tag_arms_route_through_per_role_iac_forge_tags_for_every_variant`,
19139 // and
19140 // `unquote_form_label_arms_route_through_per_role_labels_for_every_variant`
19141 // one vocabulary over on the SAME subset algebra — together
19142 // the four pins close the subset algebra's
19143 // `pub const *_MARKER` + `pub const *_IAC_FORGE_TAG` +
19144 // `pub const *_LABEL` + `pub(crate) const *_HASH_DISCRIMINATOR`
19145 // surfaces against the runtime projections that feed the four
19146 // production byte-vocabularies.
19147 assert_eq!(
19148 UnquoteForm::Unquote.hash_discriminator(),
19149 UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR
19150 );
19151 assert_eq!(
19152 UnquoteForm::Splice.hash_discriminator(),
19153 UnquoteForm::SPLICE_HASH_DISCRIMINATOR
19154 );
19155 }
19156
19157 #[test]
19158 fn unquote_form_hash_discriminators_has_expected_cardinality() {
19159 // Cardinality pin: `HASH_DISCRIMINATORS.len() == 2` matches
19160 // `ALL.len()` so a refactor that loosens the type to `&[u8]`
19161 // fails HERE (the `[_; 2]` slot cannot be sliced silently),
19162 // and a variant added to `ALL` without a matching
19163 // `HASH_DISCRIMINATORS` row fails the pair-arity gate at the
19164 // array literal itself before this test even runs. Sibling
19165 // posture to `unquote_form_markers_has_expected_cardinality`,
19166 // `unquote_form_iac_forge_tags_has_expected_cardinality`, and
19167 // `unquote_form_labels_has_expected_cardinality` one
19168 // vocabulary over on the SAME subset algebra.
19169 assert_eq!(UnquoteForm::HASH_DISCRIMINATORS.len(), 2);
19170 assert_eq!(
19171 UnquoteForm::HASH_DISCRIMINATORS.len(),
19172 UnquoteForm::ALL.len()
19173 );
19174 }
19175
19176 #[test]
19177 fn unquote_form_hash_discriminators_align_with_all_by_index() {
19178 // ALIGNMENT PIN: sweep `HASH_DISCRIMINATORS[i] ==
19179 // ALL[i].hash_discriminator()` so any `zip(ALL,
19180 // HASH_DISCRIMINATORS)` consumer reads a coherent (variant,
19181 // cache-key byte) pair off ONE forced-arity array pair. A
19182 // regression that reorders HASH_DISCRIMINATORS without also
19183 // reordering ALL (or vice versa) fails-loudly at the exact
19184 // index that drifted. Sibling of
19185 // `unquote_form_markers_align_with_all_by_index`,
19186 // `unquote_form_iac_forge_tags_align_with_all_by_index`, and
19187 // `unquote_form_labels_align_with_all_by_index` — same
19188 // alignment contract on the fourth byte-vocabulary axis.
19189 assert_eq!(
19190 UnquoteForm::HASH_DISCRIMINATORS.len(),
19191 UnquoteForm::ALL.len()
19192 );
19193 for (i, form) in UnquoteForm::ALL.iter().enumerate() {
19194 assert_eq!(
19195 UnquoteForm::HASH_DISCRIMINATORS[i],
19196 form.hash_discriminator(),
19197 "UnquoteForm::HASH_DISCRIMINATORS[{i}] `{byte}` drifted \
19198 from UnquoteForm::ALL[{i}].hash_discriminator() \
19199 `{via_variant}` — the canonical ALL ordering and the \
19200 HASH_DISCRIMINATORS ordering must match element-wise",
19201 byte = UnquoteForm::HASH_DISCRIMINATORS[i],
19202 via_variant = form.hash_discriminator(),
19203 );
19204 }
19205 }
19206
19207 #[test]
19208 fn unquote_form_hash_discriminators_pairwise_distinct() {
19209 // 2x2 pairwise sweep so a collision between the two cache-
19210 // key bytes (which would silently degrade two distinct
19211 // template-substitution markers to the SAME macro-expansion
19212 // cache key) fails-loudly at the exact pair. Sibling-shape
19213 // pin to `unquote_form_markers_pairwise_distinct`,
19214 // `unquote_form_iac_forge_tags_pairwise_distinct`, and
19215 // `unquote_form_labels_pairwise_distinct` one vocabulary
19216 // over on the SAME subset algebra.
19217 for (i, a) in UnquoteForm::HASH_DISCRIMINATORS.iter().enumerate() {
19218 for (j, b) in UnquoteForm::HASH_DISCRIMINATORS.iter().enumerate() {
19219 if i == j {
19220 continue;
19221 }
19222 assert_ne!(
19223 a, b,
19224 "UnquoteForm::HASH_DISCRIMINATORS[{i}] ({a}) collides \
19225 with UnquoteForm::HASH_DISCRIMINATORS[{j}] ({b}) — \
19226 two distinct template markers cannot share cache-key \
19227 bytes",
19228 );
19229 }
19230 }
19231 }
19232
19233 #[test]
19234 fn unquote_form_hash_discriminators_pin_legacy_cache_key_bytes() {
19235 // Per-arm truth-table pin of each per-role
19236 // `pub(crate) const UnquoteForm::*_HASH_DISCRIMINATOR` at
19237 // its exact canonical `u8` byte: `UNQUOTE_HASH_DISCRIMINATOR
19238 // == 5` and `SPLICE_HASH_DISCRIMINATOR == 6`. Pins each
19239 // per-role `pub(crate) const` value itself (as opposed to
19240 // pinning the `hash_discriminator()` projection method's
19241 // returns, which is
19242 // `unquote_form_hash_discriminator_pins_legacy_cache_key_bytes`'s
19243 // job) so a regression that drifts the alias source on the
19244 // parent [`QuoteForm`] side without updating this subset's
19245 // aliases (which would require ALSO fixing this test) fails
19246 // here BEFORE the composition-through-`to_quote_form()`
19247 // routing masks the drift. Together the two pins close the
19248 // (per-role const, projection method) pairing on the
19249 // substitution-subset cache-key axis. Sibling posture to
19250 // `structural_kind_hash_discriminators_pin_legacy_cache_key_bytes`
19251 // (2-arm structural-residual),
19252 // `quote_form_hash_discriminators_pin_legacy_cache_key_bytes`
19253 // (4-arm quote-family),
19254 // `atom_kind_hash_discriminators_pin_legacy_atom_cache_key_bytes`
19255 // (6-arm atomic-payload nested inner), and
19256 // `sexp_shape_hash_discriminators_pin_legacy_outer_cache_key_bytes`
19257 // (12-arm outer shape) — this 2-arm subset pin joins the
19258 // family the closed-set typed algebra composes on for outer-
19259 // Sexp cache-key partition coherence.
19260 assert_eq!(
19261 UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR,
19262 5,
19263 "UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR drifted from legacy cache-key byte 5",
19264 );
19265 assert_eq!(
19266 UnquoteForm::SPLICE_HASH_DISCRIMINATOR,
19267 6,
19268 "UnquoteForm::SPLICE_HASH_DISCRIMINATOR drifted from legacy cache-key byte 6",
19269 );
19270 }
19271
19272 #[test]
19273 fn unquote_form_hash_discriminators_align_with_superset_by_projection() {
19274 // CROSS-ALGEBRA COMPOSITION contract: for every `i in 0..2`,
19275 // `UnquoteForm::HASH_DISCRIMINATORS[i]` equals the byte the
19276 // parent superset's projection returns for
19277 // `UnquoteForm::ALL[i].to_quote_form()` — the outer-subset
19278 // peer alias equals the superset's canonical byte at rustc-
19279 // time through the `const` initializer AND at test time
19280 // through the projection composition. Sibling posture to
19281 // `sexp_shape_hash_discriminators_align_with_sub_carvings_by_projection`
19282 // (12-arm SexpShape × 3 sub-carvings composition) — this
19283 // pin closes the same composition contract at the 2-of-4
19284 // subset carving level.
19285 use crate::ast::QuoteForm;
19286 for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
19287 let subset_byte = UnquoteForm::HASH_DISCRIMINATORS[i];
19288 let superset_byte = uf.to_quote_form().hash_discriminator();
19289 assert_eq!(
19290 subset_byte, superset_byte,
19291 "UnquoteForm::HASH_DISCRIMINATORS[{i}] ({subset_byte}) \
19292 drifted from ALL[{i}].to_quote_form().hash_discriminator() \
19293 ({superset_byte}) — the subset-through-superset composition \
19294 no longer agrees with the aliased `pub(crate) const` array",
19295 );
19296 }
19297 // Additionally pin that the subset array is exactly the
19298 // Unquote/UnquoteSplice slice of the superset's
19299 // HASH_DISCRIMINATORS. QuoteForm::ALL declaration order is
19300 // `[Quote, Quasiquote, Unquote, UnquoteSplice]` (indices 2/3
19301 // are the substitution subset) — the two subset bytes are
19302 // exactly the `[2..4]` slice of QuoteForm::HASH_DISCRIMINATORS.
19303 assert_eq!(
19304 UnquoteForm::HASH_DISCRIMINATORS[0],
19305 QuoteForm::HASH_DISCRIMINATORS[2]
19306 );
19307 assert_eq!(
19308 UnquoteForm::HASH_DISCRIMINATORS[1],
19309 QuoteForm::HASH_DISCRIMINATORS[3]
19310 );
19311 }
19312
19313 #[test]
19314 fn unquote_form_promotions_has_expected_cardinality() {
19315 // CARDINALITY CONTRACT: `UnquoteForm::PROMOTIONS.len() == 1` —
19316 // pin the single-arm collapse structurally so a future
19317 // refactor that switches the array type to `&[(Self, char,
19318 // Self)]` or extends the triple set doesn't silently loosen
19319 // the closed-set discipline. Sibling posture to
19320 // `quote_form_promotions_has_expected_cardinality` on the
19321 // parent four-arm superset — that pin binds the parent's
19322 // singleton collapse; this pin binds the same collapse at the
19323 // substitution-subset level.
19324 assert_eq!(UnquoteForm::PROMOTIONS.len(), 1);
19325 }
19326
19327 #[test]
19328 fn unquote_form_promotions_pin_legacy_substitution_subset_splice_promotion_triple() {
19329 // TRIPLE-IDENTITY CONTRACT: pin the singleton entry of
19330 // `UnquoteForm::PROMOTIONS` at its exact canonical triple
19331 // `(Self::Unquote, '@', Self::Splice)` — the reader's ONE
19332 // `,` + `@` → `,@` promotion at the substitution-subset
19333 // level. A regression that drifts the triple (e.g. flips the
19334 // head column to `Self::Splice`, drifts the discriminator to
19335 // a different byte, or promotes to a phantom variant) fails
19336 // loudly here at rustc-time equality rather than silently at
19337 // reader tokenizer drift where `,@xs` sequences degrade to
19338 // the wrong substitution-subset marker. Sibling posture to
19339 // `quote_form_promotions_pin_legacy_splice_promotion_triple`
19340 // on the parent four-arm superset — the triple's shape is
19341 // byte-identical up to the substitution-subset variant
19342 // renaming (`QuoteForm::Unquote` ↔ `UnquoteForm::Unquote`,
19343 // `QuoteForm::UnquoteSplice` ↔ `UnquoteForm::Splice`, with
19344 // the middle discriminator column aliased directly at the
19345 // constant site).
19346 let (head, disc, promoted) = UnquoteForm::PROMOTIONS[0];
19347 assert_eq!(head, UnquoteForm::Unquote);
19348 assert_eq!(disc, crate::ast::QuoteForm::SPLICE_DISCRIMINATOR);
19349 assert_eq!(disc, '@');
19350 assert_eq!(promoted, UnquoteForm::Splice);
19351 }
19352
19353 #[test]
19354 fn unquote_form_promotions_align_with_quote_form_promotions_by_projection() {
19355 // CROSS-ALGEBRA COMPOSITION contract: for every `i in 0..1`,
19356 // `(UnquoteForm::PROMOTIONS[i].0.to_quote_form(),
19357 // UnquoteForm::PROMOTIONS[i].1,
19358 // UnquoteForm::PROMOTIONS[i].2.to_quote_form())`
19359 // equals `QuoteForm::PROMOTIONS[0]` byte-for-byte — the
19360 // substitution-subset promotion triple embeds into the parent
19361 // superset's promotion triple through the pre-existing 2-of-4
19362 // subset → superset projection `Self::to_quote_form` on the
19363 // head + promoted endpoints AND identity on the middle
19364 // discriminator `char` column. Sibling posture to
19365 // `unquote_form_hash_discriminators_align_with_superset_by_projection`
19366 // (2-arm subset × 4-arm superset composition on the outer-
19367 // `Sexp` cache-key axis) — this pin closes the same
19368 // composition contract on the (head, disc, promoted) triple
19369 // axis at the substitution-subset carving level.
19370 use crate::ast::QuoteForm;
19371 for i in 0..UnquoteForm::PROMOTIONS.len() {
19372 let (subset_head, subset_disc, subset_promoted) = UnquoteForm::PROMOTIONS[i];
19373 let projected_head = subset_head.to_quote_form();
19374 let projected_promoted = subset_promoted.to_quote_form();
19375 let superset_triple = QuoteForm::PROMOTIONS[i];
19376 assert_eq!(
19377 (projected_head, subset_disc, projected_promoted),
19378 superset_triple,
19379 "UnquoteForm::PROMOTIONS[{i}] projection through \
19380 `to_quote_form()` drifted from \
19381 QuoteForm::PROMOTIONS[{i}]",
19382 );
19383 }
19384 }
19385
19386 #[test]
19387 fn unquote_form_promotions_head_column_is_unquote_and_promoted_column_is_splice() {
19388 // CARVING PARTITION CONTRACT (substitution-subset level): on
19389 // the two-variant substitution-subset lattice, the promotion
19390 // triple's head column is the ONLY variant with a single-
19391 // char rendered marker (`Self::Unquote`, marker `,`) and the
19392 // promoted column is the ONLY variant with a two-char
19393 // rendered marker (`Self::Splice`, marker `,@`). Pin the
19394 // (head, promoted) endpoint identity across the closed
19395 // `UnquoteForm::ALL` set so a regression that flips the
19396 // endpoints or promotes to a phantom variant fails-loudly
19397 // here. The two variants of `UnquoteForm` split cleanly along
19398 // the promotion axis: single-char-marker `Unquote` is the
19399 // head, two-char-marker `Splice` is the promoted target.
19400 for (head, _, promoted) in UnquoteForm::PROMOTIONS {
19401 assert_eq!(
19402 head,
19403 UnquoteForm::Unquote,
19404 "the substitution-subset promotion head must be the \
19405 single-char-marker variant `UnquoteForm::Unquote`",
19406 );
19407 assert_eq!(
19408 promoted,
19409 UnquoteForm::Splice,
19410 "the substitution-subset promotion target must be \
19411 the two-char-marker variant `UnquoteForm::Splice`",
19412 );
19413 assert_eq!(
19414 head.marker(),
19415 crate::ast::QuoteForm::UNQUOTE_PREFIX,
19416 "the substitution-subset promotion head's marker \
19417 must alias `QuoteForm::UNQUOTE_PREFIX`",
19418 );
19419 assert_eq!(
19420 promoted.marker(),
19421 crate::ast::QuoteForm::UNQUOTE_SPLICE_PREFIX,
19422 "the substitution-subset promotion target's marker \
19423 must alias `QuoteForm::UNQUOTE_SPLICE_PREFIX`",
19424 );
19425 }
19426 }
19427
19428 #[test]
19429 fn unquote_form_promotions_compose_marker_from_head_marker_and_discriminator() {
19430 // RENDERED-MARKER COMPOSITION LAW (subset-level): for every
19431 // `(head, disc, promoted)` in `UnquoteForm::PROMOTIONS`,
19432 // `format!("{}{}", head.marker(), disc) == promoted.marker()`.
19433 // The subset's rendered-marker composition (head-marker byte
19434 // + discriminator byte = promoted-marker two-byte string)
19435 // agrees byte-for-byte with the promoted variant's rendered
19436 // marker — the reader's peek-then-consume arm's rendered
19437 // marker identity closes the read↔write duality across the
19438 // substitution-subset promotion algebra. Sibling posture to
19439 // `quote_form_promotions_compose_prefix_from_source_prefix_and_discriminator_for_every_entry`
19440 // on the parent four-arm superset — that pin binds the
19441 // parent superset's rendered-prefix composition; this pin
19442 // binds the substitution-subset's rendered-marker
19443 // composition on the SAME single promotion triple.
19444 for (head, disc, promoted) in UnquoteForm::PROMOTIONS {
19445 let composed = format!("{}{}", head.marker(), disc);
19446 assert_eq!(
19447 composed,
19448 promoted.marker(),
19449 "UnquoteForm::PROMOTIONS `({head:?}, {disc:?}, {promoted:?})` \
19450 marker composition drifted — expected \
19451 `head.marker() + disc` to equal `promoted.marker()`",
19452 );
19453 }
19454 }
19455
19456 #[test]
19457 fn unquote_form_sexp_shape_round_trips_through_as_unquote_form() {
19458 // The embed/project section law on the substitution-subset
19459 // carving: `UnquoteForm::sexp_shape(uf).as_unquote_form() ==
19460 // Some(uf)` for every `uf: UnquoteForm::ALL`. Proves the
19461 // (embed, project) pair is an `Iso(UnquoteForm, UnquoteShape ⊂
19462 // SexpShape)` — the section is total on `UnquoteForm`'s carving.
19463 // Sibling round-trip to
19464 // `atom_kind_sexp_shape_round_trips_through_as_atom_kind`
19465 // (on the 6-of-12 atomic-payload carving) and
19466 // `quote_form_sexp_shape_round_trips_through_as_quote_form`
19467 // (on the 4-of-12 quote-family carving). Closes the third and
19468 // final closed-set carving of `SexpShape` (the 2-of-12
19469 // substitution-subset carving) as a round-trip identity on
19470 // the typed algebra — pre-lift the pairing only lived in
19471 // the two-step composition `uf.to_quote_form().sexp_shape()
19472 // .as_quote_form().and_then(QuoteForm::as_unquote_form)` at
19473 // callsite; post-lift the pairing rides the typed projection
19474 // directly so a future regression that drifts the
19475 // (UnquoteForm variant, SexpShape variant) pairing fails
19476 // this assertion without depending on the composition
19477 // chain sitting between the two.
19478 for uf in UnquoteForm::ALL {
19479 let shape = uf.sexp_shape();
19480 let recovered = shape.as_unquote_form();
19481 assert_eq!(
19482 recovered,
19483 Some(uf),
19484 "UnquoteForm::{uf:?} did NOT round-trip — sexp_shape().as_unquote_form() must recover the typed marker",
19485 );
19486 }
19487 }
19488
19489 #[test]
19490 fn unquote_form_per_role_shapes_alias_quote_form_per_role_shapes_byte_for_byte() {
19491 // ALIAS-CHAIN CONTRACT: each per-role `pub const *_SHAPE` on
19492 // `UnquoteForm` MUST bind byte-for-byte to the corresponding
19493 // per-role SHAPE on the parent [`crate::ast::QuoteForm`]
19494 // superset — the substitution-subset per-role embed targets
19495 // live at ONE substrate primitive per arm (aliased through
19496 // QuoteForm's canonical per-role SHAPE constants) rather than
19497 // at inline `SexpShape::X` literals OR a parallel two-arm
19498 // match table on this subset. Sibling posture to
19499 // `unquote_form_per_role_labels_alias_quote_form_per_role_labels_byte_for_byte`
19500 // on the diagnostic-label axis of the SAME closed set and to
19501 // `unquote_form_per_role_hash_discriminators_alias_quote_form_per_role_hash_discriminators_byte_for_byte`
19502 // on the outer-Sexp cache-key byte axis — every per-role
19503 // sub-vocabulary axis on this substitution subset now closes
19504 // through the parent's per-role canonical site.
19505 assert_eq!(
19506 UnquoteForm::UNQUOTE_SHAPE,
19507 crate::ast::QuoteForm::UNQUOTE_SHAPE,
19508 );
19509 assert_eq!(
19510 UnquoteForm::SPLICE_SHAPE,
19511 crate::ast::QuoteForm::UNQUOTE_SPLICE_SHAPE,
19512 );
19513 }
19514
19515 #[test]
19516 fn unquote_form_per_role_shapes_pin_canonical_sexp_shape_variants() {
19517 // CANONICAL-SEXP-SHAPE CONTRACT: each per-role `pub const
19518 // *_SHAPE` on `UnquoteForm` MUST project to the canonical
19519 // [`SexpShape`] variant — `UNQUOTE_SHAPE = SexpShape::Unquote`,
19520 // `SPLICE_SHAPE = SexpShape::UnquoteSplice`. A regression that
19521 // silently renumbers QuoteForm's per-role SHAPE at the parent
19522 // superset (which this subset aliases) fails-loudly here at
19523 // the byte-equality level — sibling posture to
19524 // `quote_form_per_role_shapes_pin_canonical_sexp_shape_variants`
19525 // on the parent's 4-of-4 quote-family sweep.
19526 assert_eq!(UnquoteForm::UNQUOTE_SHAPE, SexpShape::Unquote);
19527 assert_eq!(UnquoteForm::SPLICE_SHAPE, SexpShape::UnquoteSplice);
19528 }
19529
19530 #[test]
19531 fn unquote_form_shapes_has_expected_cardinality() {
19532 // CARDINALITY CONTRACT: `UnquoteForm::SHAPES.len() == 2 ==
19533 // UnquoteForm::ALL.len()`. Arity is forced at the declaration
19534 // site by rustc's `[SexpShape; 2]` check; this runtime pin
19535 // fails-loud so a future refactor that switches the array
19536 // type to `&[SexpShape]` doesn't silently loosen the closed-
19537 // set discipline. Sibling posture to
19538 // `unquote_form_hash_discriminators_has_expected_cardinality`
19539 // on the cache-key byte axis of the SAME closed set.
19540 assert_eq!(
19541 UnquoteForm::SHAPES.len(),
19542 2,
19543 "UnquoteForm::SHAPES cardinality drifted from the two-arm substitution-subset algebra",
19544 );
19545 assert_eq!(
19546 UnquoteForm::SHAPES.len(),
19547 UnquoteForm::ALL.len(),
19548 "UnquoteForm::SHAPES cardinality drifted from UnquoteForm::ALL — every family-wide array on this algebra must stay ALL-length in lockstep",
19549 );
19550 }
19551
19552 #[test]
19553 fn unquote_form_shapes_align_with_all_by_index() {
19554 // ALIGNMENT CONTRACT: for every `i` in `0..UnquoteForm::ALL.len()`,
19555 // `UnquoteForm::SHAPES[i] == UnquoteForm::ALL[i].sexp_shape()`
19556 // — the family-wide array is a projection of the two-arm
19557 // `sexp_shape` method sweep in declaration order. A regression
19558 // that reorders ONE array without reordering the other fails-
19559 // loudly here. Sibling posture to
19560 // `unquote_form_hash_discriminators_align_with_all_by_index`
19561 // on the cache-key byte axis of the SAME closed set.
19562 for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
19563 let via_variant = uf.sexp_shape();
19564 let via_array = UnquoteForm::SHAPES[i];
19565 assert_eq!(
19566 via_variant, via_array,
19567 "UnquoteForm::SHAPES[{i}] `{via_array:?}` diverged from UnquoteForm::ALL[{i}].sexp_shape() `{via_variant:?}`",
19568 );
19569 }
19570 }
19571
19572 #[test]
19573 fn unquote_form_shapes_align_with_all_by_index_through_as_unquote_form() {
19574 // ROUND-TRIP CONTRACT: for every `i` in `0..UnquoteForm::ALL.len()`,
19575 // `UnquoteForm::SHAPES[i].as_unquote_form() ==
19576 // Some(UnquoteForm::ALL[i])` — closes the (embed, project)
19577 // section of the `Iso(UnquoteForm, UnquoteShape ⊂ SexpShape)`
19578 // as a family-wide array-indexed law rather than as a per-
19579 // variant assertion sweep (which the pre-existing
19580 // `unquote_form_sexp_shape_round_trips_through_as_unquote_form`
19581 // pins per-variant). Post-lift the round-trip identity closes
19582 // at BOTH the per-variant projection and the family-wide array
19583 // indexing. Sibling posture to
19584 // `quote_form_shapes_align_with_all_by_index_through_as_quote_form`
19585 // on the parent's 4-of-4 sweep.
19586 for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
19587 let shape = UnquoteForm::SHAPES[i];
19588 assert_eq!(
19589 shape.as_unquote_form(),
19590 Some(*uf),
19591 "UnquoteForm::SHAPES[{i}] `{shape:?}` did NOT round-trip through SexpShape::as_unquote_form — the (embed, project) family-wide law broke",
19592 );
19593 }
19594 }
19595
19596 #[test]
19597 fn unquote_form_shapes_pairwise_distinct() {
19598 // INJECTIVITY CONTRACT: the two entries of `UnquoteForm::SHAPES`
19599 // MUST be pairwise distinct — the UnquoteForm ⊂ SexpShape
19600 // 2-of-12 composed carving is injective (each subset variant
19601 // embeds into a UNIQUE outer SexpShape variant). A regression
19602 // that drifts one of the two aliases into the other (e.g. a
19603 // typo that spells `SPLICE_SHAPE = QuoteForm::UNQUOTE_SHAPE`)
19604 // fails-loudly here at the array level rather than as a silent
19605 // round-trip collision at consumer sites. Sibling posture to
19606 // `quote_form_shapes_pairwise_distinct` on the parent's 4-of-4
19607 // sweep and to `unquote_form_hash_discriminators_pairwise_distinct`
19608 // on the cache-key byte axis of the SAME closed set.
19609 assert_ne!(
19610 UnquoteForm::SHAPES[0],
19611 UnquoteForm::SHAPES[1],
19612 "UnquoteForm::SHAPES[0] `{a:?}` collides with UnquoteForm::SHAPES[1] `{b:?}` — the two-arm carving must be injective",
19613 a = UnquoteForm::SHAPES[0],
19614 b = UnquoteForm::SHAPES[1],
19615 );
19616 }
19617
19618 #[test]
19619 fn unquote_form_shapes_align_with_superset_by_projection() {
19620 // CROSS-ALGEBRA COMPOSITION CONTRACT: for every `i`,
19621 // `UnquoteForm::SHAPES[i]` equals the byte the SUPERSET
19622 // projection returns for the corresponding QuoteForm variant
19623 // — the substitution-subset per-role embed target equals the
19624 // parent quote-family superset's canonical SHAPE at rustc-time
19625 // through the `const` alias AND at test time through the
19626 // projection composition. Pin the composition-identity across
19627 // the closed subset so a regression that drifts EITHER the
19628 // subset alias OR the superset canonical site fails-loudly
19629 // here. Sibling posture to
19630 // `unquote_form_hash_discriminators_align_with_superset_by_projection`
19631 // on the cache-key byte axis of the SAME closed set — that
19632 // pin binds the subset cache-key byte to the parent's
19633 // canonical byte; this pin binds the subset embed target to
19634 // the parent's canonical SHAPE.
19635 for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
19636 let via_subset = UnquoteForm::SHAPES[i];
19637 let via_superset = uf.to_quote_form().sexp_shape();
19638 assert_eq!(
19639 via_subset, via_superset,
19640 "UnquoteForm::SHAPES[{i}] `{via_subset:?}` diverged from UnquoteForm::ALL[{i}].to_quote_form().sexp_shape() `{via_superset:?}` — the composition law across the subset carving broke",
19641 );
19642 }
19643 }
19644
19645 #[test]
19646 fn as_unquote_form_projects_each_unquote_shape_to_canonical_unquote_form_and_rejects_non_unquote_shapes(
19647 ) {
19648 // Per-variant truth-table sweep across every `SexpShape::ALL`
19649 // entry — pins each variant's canonical mapping (the two
19650 // substitution-subset shapes project to the matching
19651 // `UnquoteForm`; every other shape — `Quote`, `Quasiquote`,
19652 // `Nil`, `List`, every atomic-payload arm — projects to
19653 // `None`) byte-for-byte. Sibling sweep to
19654 // `as_quote_form_projects_each_quote_shape_to_canonical_quote_form_and_rejects_non_quote_shapes`
19655 // and
19656 // `as_atom_kind_projects_each_atom_shape_to_canonical_atom_kind_and_rejects_non_atom_shapes`
19657 // on the substitution-subset carving axis.
19658 for shape in SexpShape::ALL {
19659 let projected = shape.as_unquote_form();
19660 let expected = match shape {
19661 SexpShape::Unquote => Some(UnquoteForm::Unquote),
19662 SexpShape::UnquoteSplice => Some(UnquoteForm::Splice),
19663 SexpShape::Nil
19664 | SexpShape::List
19665 | SexpShape::Quote
19666 | SexpShape::Quasiquote
19667 | SexpShape::Symbol
19668 | SexpShape::Keyword
19669 | SexpShape::String
19670 | SexpShape::Int
19671 | SexpShape::Float
19672 | SexpShape::Bool => None,
19673 };
19674 assert_eq!(
19675 projected, expected,
19676 "SexpShape::{shape:?}.as_unquote_form() drifted from canonical mapping",
19677 );
19678 }
19679 }
19680
19681 #[test]
19682 fn as_unquote_form_routes_through_as_quote_form_and_quote_form_as_unquote_form_via_composition()
19683 {
19684 // Post-lift composition pin: for every `shape: SexpShape`,
19685 // `shape.as_unquote_form()` and `shape.as_quote_form()
19686 // .and_then(QuoteForm::as_unquote_form)` agree byte-for-byte
19687 // — the 2-of-12 substitution-subset carving rides through
19688 // the composition of the 4-of-12 quote-family carving
19689 // (`SexpShape::as_quote_form`) and the 2-of-4 template-
19690 // substitution subset gate on the quote-family superset
19691 // (`QuoteForm::as_unquote_form`), rather than through a
19692 // parallel twelve-arm inline match table here. A regression
19693 // that re-inlines the twelve arms as a parallel match-table
19694 // still passes the round-trip and truth-table sweeps but
19695 // fails THIS composition pin — the (outer shape, subset
19696 // marker) pairing is no longer derived from the two existing
19697 // closed-set matches the substrate already owns. Sibling
19698 // posture to `unquote_form_sexp_shape_routes_through_to_quote_form_sexp_shape_via_composition`
19699 // on the retraction direction of the (embed, project) dual.
19700 for shape in SexpShape::ALL {
19701 let from_projection = shape.as_unquote_form();
19702 let from_composition = shape
19703 .as_quote_form()
19704 .and_then(crate::ast::QuoteForm::as_unquote_form);
19705 assert_eq!(
19706 from_projection, from_composition,
19707 "SexpShape::{shape:?}.as_unquote_form() drifted from .as_quote_form().and_then(QuoteForm::as_unquote_form) — the retraction is no longer derived from the composition through the quote-family carving",
19708 );
19709 }
19710 }
19711
19712 #[test]
19713 fn as_unquote_form_composes_with_marker_via_unquote_form_marker_round_trip() {
19714 // Cross-projection composition law (substitution-subset sibling
19715 // of `as_quote_form_composes_with_sexp_shape_via_quote_form_prefix_round_trip`
19716 // and `as_atom_kind_composes_with_sexp_shape_via_atom_kind_label_round_trip`):
19717 // for every `uf: UnquoteForm::ALL`, `uf.marker() ==
19718 // uf.sexp_shape().as_unquote_form().expect("...").marker()`.
19719 // Pins the (UnquoteForm variant, SexpShape variant) pairing
19720 // round-trips through the embed/project pair AND preserves
19721 // each variant's canonical template-substitution punctuation
19722 // (`","` / `",@"`) — a regression that drifts the round-trip
19723 // OR drifts the marker surfaces here. The label-coherence
19724 // binds the diagnostic surface to the typed algebra at BOTH
19725 // layers, matching the posture the atomic-payload and quote-
19726 // family cross-projections already carry.
19727 for uf in UnquoteForm::ALL {
19728 let via_round_trip = uf
19729 .sexp_shape()
19730 .as_unquote_form()
19731 .expect("every UnquoteForm round-trips through the embed/project pair")
19732 .marker();
19733 assert_eq!(
19734 via_round_trip,
19735 uf.marker(),
19736 "UnquoteForm::{uf:?}.marker() drifted from sexp_shape().as_unquote_form().marker() — embed/project must preserve marker coherence",
19737 );
19738 }
19739 }
19740
19741 // --- MacroDefHead closed-set FromStr + UnknownMacroDefHead lift ---
19742 //
19743 // Same posture as `unquote_form_*` / `sexp_shape_*` /
19744 // `atom_kind_*` / `quote_form_*`: pin the four contract laws of the
19745 // closed-set (ALL, projection, FromStr, Unknown*) quadruple so the
19746 // typed surface and the rendered diagnostic literal cannot drift,
19747 // and the open-by-design `from_keyword` face matches the
19748 // typed-error `FromStr` face byte-for-byte (the same closed-set
19749 // sweep, different rejection polarities).
19750
19751 #[test]
19752 fn macro_def_head_all_is_unique_and_complete() {
19753 // Closed-set posture: `ALL` enumerates every reachable variant
19754 // EXACTLY ONCE — no duplicates, no omissions. The `[Self; 3]`
19755 // array literal in the declaration forces the arity at compile
19756 // time; this test catches the orthogonal failure modes — a
19757 // future variant added at the type without being added to ALL
19758 // (silently dropped from every consumer's sweep), or a typo
19759 // that duplicates an entry (silently double-counted). Same
19760 // truth-table pinning every sibling closed-set lift in the
19761 // workspace uses (SexpShape::ALL, AtomKind::ALL,
19762 // QuoteForm::ALL, UnquoteForm::ALL, RequestorKind::ALL,
19763 // ReceiptKind::ALL, ConditionKind::ALL, …).
19764 //
19765 // The `iter+map+collect+sort_unstable` quadruple this test
19766 // inlined pre-lift now binds at `<MacroDefHead as
19767 // ClosedSet>::sorted_labels()` — the canonical-ordered
19768 // candidate-list projection on the trait. Distinctness of the
19769 // sorted result is covered by
19770 // `assert_closed_set_well_formed::<MacroDefHead>()`.
19771 assert_eq!(MacroDefHead::ALL.len(), 3);
19772 assert_eq!(
19773 <MacroDefHead as tatara_closed_set::ClosedSet>::sorted_labels(),
19774 vec!["defcheck", "defmacro", "defpoint-template"],
19775 "MacroDefHead::ALL must cover all three macro-definition heads"
19776 );
19777 }
19778
19779 #[test]
19780 fn macro_def_head_keyword_round_trips_through_from_str() {
19781 // Bidirectional `keyword` ↔ `FromStr` contract: for every
19782 // variant in ALL, `head.keyword().parse() == Ok(head)`. A
19783 // regression that drifts the (variant, literal) pairing at
19784 // ONE arm of `keyword` (e.g. typo `defpoint_template` with an
19785 // underscore instead of `defpoint-template` with a hyphen) OR
19786 // at the `FromStr` decode body (off-by-one, missing variant in
19787 // the sweep) fails-loudly here. The canonical-literal site is
19788 // singular (`keyword`) so the round-trip is the only way the
19789 // typed surface and the rendered diagnostic literal can drift
19790 // apart — pinning it here means they cannot.
19791 for head in MacroDefHead::ALL {
19792 let parsed: MacroDefHead = head
19793 .keyword()
19794 .parse()
19795 .expect("every ALL variant's keyword must round-trip through FromStr");
19796 assert_eq!(
19797 parsed,
19798 head,
19799 "FromStr({}) must round-trip to the same variant",
19800 head.keyword()
19801 );
19802 }
19803 }
19804
19805 #[test]
19806 fn macro_def_head_from_keyword_matches_from_str_for_every_input() {
19807 // Cross-face contract: the `Option`-faced `from_keyword`
19808 // projection (`tatara_lisp::ast::Sexp::as_call_to_any`'s
19809 // decoder slot, signature `Fn(&str) -> Option<T>`) and the
19810 // typed-error-faced `FromStr` projection are the SAME closed-
19811 // set sweep with different rejection polarities. After the
19812 // lift `from_keyword` delegates to `parse().ok()`, so the
19813 // closed-set sweep lives at ONE site (the `FromStr` impl) and
19814 // both faces project the same accept/reject decision at every
19815 // input. Pinning this law means a future refactor that drifts
19816 // ONE face from the other (e.g., adding a fourth variant to
19817 // `keyword` but forgetting to bump `Self::ALL`, or branching
19818 // `from_keyword` against a hand-rolled match arm instead of
19819 // routing through the typed sweep) fails here.
19820 let inputs: &[&str] = &[
19821 // The three canonical heads — both faces accept.
19822 "defmacro",
19823 "defpoint-template",
19824 "defcheck",
19825 // Non-canonical capitalizations — both faces reject.
19826 "Defmacro",
19827 "DEFCHECK",
19828 "DefpointTemplate",
19829 // Near misses — both faces reject.
19830 "defmacroo",
19831 "defcheckk",
19832 "defpoint_template",
19833 "defpoint",
19834 "defpoint-templates",
19835 // Sibling authoring surfaces from other closed sets —
19836 // both faces reject (cross-set disjointness).
19837 "defmonitor",
19838 "defnotify",
19839 "defpoint",
19840 "defalertpolicy",
19841 // SexpShape labels (the structural-identity vocabulary on
19842 // a DIFFERENT axis) — both faces reject.
19843 "symbol",
19844 "list",
19845 "nil",
19846 // Punctuation from QuoteForm / UnquoteForm vocabularies —
19847 // both faces reject (closed sets live on disjoint axes).
19848 ",",
19849 ",@",
19850 "'",
19851 "`",
19852 // Edge cases — both faces reject.
19853 "",
19854 " ",
19855 " defmacro",
19856 "defmacro ",
19857 ];
19858 for s in inputs {
19859 let from_kw = MacroDefHead::from_keyword(s);
19860 let from_str = s.parse::<MacroDefHead>().ok();
19861 assert_eq!(
19862 from_kw,
19863 from_str,
19864 "`from_keyword` and `FromStr` must agree on {s:?}: from_keyword={from_kw:?}, FromStr={from_str:?}",
19865 );
19866 }
19867 }
19868
19869 #[test]
19870 fn unknown_macro_def_head_carries_offending_input_verbatim() {
19871 // Operator-facing diagnostic contract: the offending input
19872 // lands in the typed error verbatim — no normalization, no
19873 // truncation, no whitespace coercion. Pin the exact
19874 // `#[error(...)]` rendering AND the typed `.0` field
19875 // projection so a future refactor that normalizes (e.g.
19876 // `.trim()` / `.to_ascii_lowercase()`) before building the
19877 // error or that drops the input fails-loudly here. Symmetric
19878 // to every sibling `Unknown*` carrier in the workspace
19879 // ([`UnknownSexpShape`], [`crate::ast::UnknownAtomKind`],
19880 // [`UnknownUnquoteForm`], [`crate::ast::UnknownQuoteForm`],
19881 // `tatara_process::allocation::UnknownRequestorKind`, …).
19882 let err: UnknownMacroDefHead = "Defmacro"
19883 .parse::<MacroDefHead>()
19884 .expect_err("capitalized `Defmacro` is not a canonical macro-definition head");
19885 assert_eq!(err.0, "Defmacro");
19886 assert_eq!(format!("{err}"), "unknown macro definition head: Defmacro");
19887
19888 let err: UnknownMacroDefHead = "defmacroo"
19889 .parse::<MacroDefHead>()
19890 .expect_err("`defmacroo` is not a canonical macro-definition head");
19891 assert_eq!(err.0, "defmacroo");
19892 assert_eq!(format!("{err}"), "unknown macro definition head: defmacroo");
19893
19894 let err: UnknownMacroDefHead = ""
19895 .parse::<MacroDefHead>()
19896 .expect_err("empty input must NOT decode to a MacroDefHead");
19897 assert_eq!(err.0, "");
19898 assert_eq!(format!("{err}"), "unknown macro definition head: ");
19899
19900 // Whitespace-padded canonical keyword MUST reject — the
19901 // typed identity is byte-exact, the offending input is
19902 // returned verbatim with its padding intact (not trimmed).
19903 // The rendered diagnostic preserves the leading space, so
19904 // the bad value reaches the operator unmolested.
19905 let err: UnknownMacroDefHead = " defmacro"
19906 .parse::<MacroDefHead>()
19907 .expect_err("leading-space `defmacro` must reject — typed identity is byte-exact");
19908 assert_eq!(err.0, " defmacro");
19909 assert_eq!(
19910 format!("{err}"),
19911 "unknown macro definition head: defmacro",
19912 "leading whitespace is preserved verbatim in the rendered diagnostic",
19913 );
19914 }
19915
19916 #[test]
19917 fn macro_def_head_from_str_rejects_cross_axis_vocabularies() {
19918 // Cross-axis guard: a MacroDefHead is the head keyword of a
19919 // `(defmacro …)`-shaped form — distinct from every other
19920 // closed-set vocabulary in this crate. A `FromStr` that
19921 // silently accepted a SexpShape label, an UnquoteForm marker,
19922 // a QuoteForm prefix, or an AtomKind label would corrupt the
19923 // typed identity at the macro-definition-head boundary. Pin
19924 // BOTH directions: the three canonical keywords decode
19925 // through MacroDefHead, the sibling closed-set vocabularies
19926 // do NOT.
19927 assert_eq!(
19928 "defmacro".parse::<MacroDefHead>().unwrap(),
19929 MacroDefHead::Defmacro
19930 );
19931 assert_eq!(
19932 "defpoint-template".parse::<MacroDefHead>().unwrap(),
19933 MacroDefHead::DefpointTemplate
19934 );
19935 assert_eq!(
19936 "defcheck".parse::<MacroDefHead>().unwrap(),
19937 MacroDefHead::Defcheck
19938 );
19939
19940 // SexpShape labels — the structural-identity vocabulary
19941 // (twelve outer Sexp shapes) — share NO labels with the
19942 // macro-definition-head vocabulary. All must reject.
19943 for shape in SexpShape::ALL {
19944 assert!(
19945 shape.label().parse::<MacroDefHead>().is_err(),
19946 "SexpShape::{shape:?} label `{}` must NOT decode as a MacroDefHead",
19947 shape.label(),
19948 );
19949 }
19950
19951 // UnquoteForm punctuation markers (`,` / `,@`) belong to the
19952 // template-marker axis — they MUST reject here because
19953 // MacroDefHead is on the symbol-keyword axis.
19954 for form in UnquoteForm::ALL {
19955 assert!(
19956 form.marker().parse::<MacroDefHead>().is_err(),
19957 "UnquoteForm marker `{}` must NOT decode as a MacroDefHead",
19958 form.marker(),
19959 );
19960 }
19961
19962 // The `defpoint` authoring-surface keyword names a
19963 // `(defpoint …)` definition form — NOT a definition-template
19964 // form. The two are intentionally disjoint: `defpoint` is a
19965 // tatara-process domain authoring head, `defpoint-template`
19966 // is a parameterized-template head on the same surface.
19967 // Pinning the rejection here keeps the two from drifting.
19968 assert!("defpoint".parse::<MacroDefHead>().is_err());
19969 assert!("defmonitor".parse::<MacroDefHead>().is_err());
19970 assert!("defnotify".parse::<MacroDefHead>().is_err());
19971 assert!("defalertpolicy".parse::<MacroDefHead>().is_err());
19972
19973 // Common-Lisp-style aliases the substrate does NOT admit.
19974 // Pinning these rejects keeps a future refactor from
19975 // silently extending the vocabulary without bumping
19976 // `Self::ALL` first.
19977 assert!("def-macro".parse::<MacroDefHead>().is_err());
19978 assert!("defun".parse::<MacroDefHead>().is_err());
19979 assert!("define-syntax".parse::<MacroDefHead>().is_err());
19980 }
19981
19982 #[test]
19983 fn macro_def_head_is_well_formed_closed_set() {
19984 // Structural contract: MacroDefHead's three variants are
19985 // pairwise distinct, round-trip through the trait's `label` ↔
19986 // `parse_label`, and reject the empty string — the
19987 // workspace-wide `assert_closed_set_well_formed::<T>()` testkit
19988 // pinned across every `tatara-process` closed-set implementor
19989 // AND every prior tatara-lisp retrofit (`AtomKind`, `QuoteForm`).
19990 // The substrate-level assertion runs on the auto-derived
19991 // `impl ClosedSet for MacroDefHead` emitted by
19992 // `#[derive(tatara_closed_set::DeriveClosedSet)]` — a regression
19993 // that drifts the derive's `make_unknown` delegation, the
19994 // `via = "keyword"` projection (`"defmacro"` /
19995 // `"defpoint-template"` / `"defcheck"`), or the variant
19996 // listing forced through `Self::ALL` fails-loudly here in
19997 // isolation from the per-variant truth tables above.
19998 tatara_closed_set::assert_closed_set_well_formed::<MacroDefHead>();
19999 }
20000
20001 #[test]
20002 fn unquote_form_is_well_formed_closed_set() {
20003 // Structural contract: UnquoteForm's two variants are pairwise
20004 // distinct, round-trip through the trait's `label` ↔
20005 // `parse_label`, and reject the empty string. The substrate-
20006 // level assertion runs on the auto-derived `impl ClosedSet
20007 // for UnquoteForm` emitted by
20008 // `#[derive(tatara_closed_set::DeriveClosedSet)]` — a regression
20009 // that drifts the `via = "marker"` projection (`","` /
20010 // `",@"`) or conflates the punctuation-axis vocabulary with
20011 // the structural-axis SexpShape vocabulary
20012 // (`"unquote"` / `"unquote-splice"`) fails-loudly here. The
20013 // cross-axis disjointness check stays at
20014 // `unquote_form_from_str_rejects_sexp_shape_labels_on_template_marker_axis`;
20015 // THIS test pins the in-axis well-formedness floor.
20016 tatara_closed_set::assert_closed_set_well_formed::<UnquoteForm>();
20017 }
20018
20019 #[test]
20020 fn kwarg_path_kind_is_well_formed_closed_set() {
20021 // Structural contract: KwargPathKind's three variants are
20022 // pairwise distinct, round-trip through the trait's `label` ↔
20023 // `parse_label`, and reject the empty string. The substrate-
20024 // level assertion runs on the auto-derived `impl ClosedSet
20025 // for KwargPathKind` emitted by
20026 // `#[derive(tatara_closed_set::DeriveClosedSet)]` — a regression
20027 // that drifts the `via = "label"` projection (`"named"` /
20028 // `"item"` / `"slot"`) or the variant listing forced through
20029 // `Self::ALL` fails-loudly here.
20030 tatara_closed_set::assert_closed_set_well_formed::<KwargPathKind>();
20031 }
20032
20033 #[test]
20034 fn expected_kwarg_shape_is_well_formed_closed_set() {
20035 // Structural contract: ExpectedKwargShape's seven variants are
20036 // pairwise distinct, round-trip through the trait's `label` ↔
20037 // `parse_label`, and reject the empty string. The substrate-
20038 // level assertion runs on the auto-derived `impl ClosedSet
20039 // for ExpectedKwargShape` emitted by
20040 // `#[derive(tatara_closed_set::DeriveClosedSet)]` — a regression
20041 // that drifts the `via = "label"` projection (`"keyword"` /
20042 // `"string"` / `"int"` / `"number"` / `"bool"` / `"list"` /
20043 // `"list of strings"`) fails-loudly here.
20044 tatara_closed_set::assert_closed_set_well_formed::<ExpectedKwargShape>();
20045 }
20046
20047 #[test]
20048 fn sexp_shape_is_well_formed_closed_set() {
20049 // Structural contract: SexpShape's twelve variants are
20050 // pairwise distinct, round-trip through the trait's `label` ↔
20051 // `parse_label`, and reject the empty string. The substrate-
20052 // level assertion runs on the auto-derived `impl ClosedSet
20053 // for SexpShape` emitted by
20054 // `#[derive(tatara_closed_set::DeriveClosedSet)]` — a regression
20055 // that drifts the `via = "label"` projection (`"nil"` /
20056 // `"symbol"` / `"keyword"` / `"string"` / `"int"` / `"float"`
20057 // / `"bool"` / `"list"` / `"quote"` / `"quasiquote"` /
20058 // `"unquote"` / `"unquote-splice"`) or the variant listing
20059 // forced through `Self::ALL` (cardinality 12 = every reachable
20060 // outer `Sexp` shape) fails-loudly here.
20061 tatara_closed_set::assert_closed_set_well_formed::<SexpShape>();
20062 }
20063
20064 #[test]
20065 fn compiler_spec_io_stage_is_well_formed_closed_set() {
20066 // Structural contract: CompilerSpecIoStage's four variants are
20067 // pairwise distinct, round-trip through the trait's `label` ↔
20068 // `parse_label`, and reject the empty string. The substrate-
20069 // level assertion runs on the auto-derived `impl ClosedSet
20070 // for CompilerSpecIoStage` emitted by
20071 // `#[derive(tatara_closed_set::DeriveClosedSet)]` — a regression
20072 // that drifts the `via = "label"` projection (`"serialize"` /
20073 // `"write"` / `"read"` / `"deserialize"`) or the variant
20074 // listing forced through `Self::ALL` (cardinality 4 = every
20075 // reachable disk-persistence (operation, stage) pair) fails-
20076 // loudly here.
20077 //
20078 // Distinct from the sibling closed-set well-formedness assertions
20079 // above: this enum carries `#[closed_set(no_from_str)]`, so the
20080 // trait surface's `parse_label` keys on the SINGULAR label
20081 // (`"read"` → `LoadFromDiskRead`) while the inherent
20082 // `std::str::FromStr` impl below keys on the COMPOUND key
20083 // (`"load_from_disk: read"` → `LoadFromDiskRead`). The trait
20084 // surface's well-formedness floor — non-empty `ALL`, round-trip
20085 // through `label`, pairwise-distinct labels, empty-string
20086 // outside the set — STILL holds on the singular projection
20087 // because the four labels (`"serialize"` / `"write"` /
20088 // `"read"` / `"deserialize"`) ARE bijective with the four
20089 // variants by accident of the current closed set; the
20090 // compound-key shape is what `FromStr` enforces and is what
20091 // `compiler_spec_io_stage_compound_key_round_trips_through_from_str`
20092 // pins. The two surfaces are deliberately disjoint — the
20093 // `no_from_str` axis exists for exactly this case.
20094 tatara_closed_set::assert_closed_set_well_formed::<CompilerSpecIoStage>();
20095 }
20096
20097 #[test]
20098 fn structural_kind_all_is_unique_and_complete() {
20099 // Cardinality + uniqueness floor for the structural-residual
20100 // carving's typed algebra: `StructuralKind::ALL` has exactly
20101 // TWO entries (`Nil`, `List`) and both are pairwise distinct.
20102 // Sibling of the substrate-wide `*_all_is_unique_and_complete`
20103 // truth-table shape carried by every closed-set implementor.
20104 assert_eq!(
20105 StructuralKind::ALL.len(),
20106 2,
20107 "StructuralKind::ALL must have exactly 2 entries (Nil + List)",
20108 );
20109 let mut seen = Vec::new();
20110 for sk in StructuralKind::ALL {
20111 assert!(
20112 !seen.contains(&sk),
20113 "StructuralKind::{sk:?} repeated in ALL — variants must be pairwise distinct",
20114 );
20115 seen.push(sk);
20116 }
20117 }
20118
20119 #[test]
20120 fn structural_kind_sexp_shape_projects_canonical_variant_for_every_marker() {
20121 // Per-arm truth-table pin of the canonical (StructuralKind
20122 // variant, SexpShape variant) mapping: `StructuralKind::Nil →
20123 // SexpShape::Nil` and `StructuralKind::List → SexpShape::List`
20124 // byte-for-byte. Sibling of
20125 // `unquote_form_sexp_shape_emits_canonical_shape_for_every_marker`
20126 // on the residual-carving axis rather than the substitution-
20127 // subset axis.
20128 assert_eq!(
20129 StructuralKind::Nil.sexp_shape(),
20130 SexpShape::Nil,
20131 "StructuralKind::Nil.sexp_shape() drifted from SexpShape::Nil",
20132 );
20133 assert_eq!(
20134 StructuralKind::List.sexp_shape(),
20135 SexpShape::List,
20136 "StructuralKind::List.sexp_shape() drifted from SexpShape::List",
20137 );
20138 }
20139
20140 #[test]
20141 fn structural_kind_sexp_shape_round_trips_through_as_structural_kind() {
20142 // The embed/project section law on the structural-residual
20143 // carving: `StructuralKind::sexp_shape(sk).as_structural_kind()
20144 // == Some(sk)` for every `sk: StructuralKind::ALL`. Proves the
20145 // (embed, project) pair is an `Iso(StructuralKind,
20146 // StructuralShape ⊂ SexpShape)` — the section is total on
20147 // `StructuralKind`'s carving. Sibling round-trip to
20148 // `atom_kind_sexp_shape_round_trips_through_as_atom_kind`
20149 // (on the 6-of-12 atomic-payload carving),
20150 // `quote_form_sexp_shape_round_trips_through_as_quote_form`
20151 // (on the 4-of-12 quote-family carving), AND
20152 // `unquote_form_sexp_shape_round_trips_through_as_unquote_form`
20153 // (on the 2-of-12 substitution-subset carving). Closes the
20154 // third and final closed-set CARVING of `SexpShape` — the
20155 // structural-residual — as a round-trip identity on the typed
20156 // algebra.
20157 for sk in StructuralKind::ALL {
20158 let shape = sk.sexp_shape();
20159 let recovered = shape.as_structural_kind();
20160 assert_eq!(
20161 recovered,
20162 Some(sk),
20163 "StructuralKind::{sk:?} did NOT round-trip — sexp_shape().as_structural_kind() must recover the typed marker",
20164 );
20165 }
20166 }
20167
20168 #[test]
20169 fn as_structural_kind_projects_each_structural_shape_to_canonical_structural_kind_and_rejects_non_structural_shapes(
20170 ) {
20171 // Per-variant truth-table sweep across every `SexpShape::ALL`
20172 // entry — pins each variant's canonical mapping (the two
20173 // structural-residual shapes project to the matching
20174 // `StructuralKind`; every other shape — every atomic-payload
20175 // arm AND every quote-family wrapper — projects to `None`)
20176 // byte-for-byte. Sibling sweep to
20177 // `as_atom_kind_projects_each_atom_shape_to_canonical_atom_kind_and_rejects_non_atom_shapes`,
20178 // `as_quote_form_projects_each_quote_shape_to_canonical_quote_form_and_rejects_non_quote_shapes`,
20179 // and
20180 // `as_unquote_form_projects_each_unquote_shape_to_canonical_unquote_form_and_rejects_non_unquote_shapes`
20181 // on the structural-residual carving axis.
20182 for shape in SexpShape::ALL {
20183 let projected = shape.as_structural_kind();
20184 let expected = match shape {
20185 SexpShape::Nil => Some(StructuralKind::Nil),
20186 SexpShape::List => Some(StructuralKind::List),
20187 SexpShape::Symbol
20188 | SexpShape::Keyword
20189 | SexpShape::String
20190 | SexpShape::Int
20191 | SexpShape::Float
20192 | SexpShape::Bool
20193 | SexpShape::Quote
20194 | SexpShape::Quasiquote
20195 | SexpShape::Unquote
20196 | SexpShape::UnquoteSplice => None,
20197 };
20198 assert_eq!(
20199 projected, expected,
20200 "SexpShape::{shape:?}.as_structural_kind() drifted from canonical mapping",
20201 );
20202 }
20203 }
20204
20205 #[test]
20206 fn structural_kind_hash_discriminator_pins_legacy_cache_key_bytes() {
20207 // CACHE-KEY CONTRACT: pre-lift `Hash for Sexp` used the literal
20208 // byte values 0 for `Sexp::Nil` and 2 for `Sexp::List(_)` as
20209 // the two structural-residual arm discriminators. The
20210 // macro-expansion cache (`Expander::cache`) keys on Hash;
20211 // ANY change to a discriminator byte silently invalidates
20212 // every cached expansion across the substrate AND risks
20213 // collision with the reserved bytes the two sibling closed-
20214 // set carvings use (`1u8` for the `Sexp::Atom(_)` outer-carve
20215 // marker, `3..=6` for the quote-family via
20216 // `crate::ast::QuoteForm::hash_discriminator`). Pin the two
20217 // legacy values explicitly so a regression that re-numbers
20218 // them surfaces immediately — the `StructuralKind` algebra
20219 // MUST preserve the prior byte mapping bit-for-bit. Sibling
20220 // posture to
20221 // `atom_kind_hash_discriminator_pins_legacy_atom_cache_key_bytes`
20222 // on the atomic-payload axis (nested inner) AND
20223 // `quote_form_hash_discriminator_pins_legacy_cache_key_bytes`
20224 // on the quote-family axis (outer `Sexp` level).
20225 assert_eq!(StructuralKind::Nil.hash_discriminator(), 0);
20226 assert_eq!(StructuralKind::List.hash_discriminator(), 2);
20227 }
20228
20229 #[test]
20230 fn structural_kind_hash_discriminator_bytes_are_pairwise_disjoint() {
20231 // Closed-set injectivity: the two discriminator bytes must
20232 // partition their subset of the outer-`Sexp` discriminator
20233 // space injectively so `Sexp::Nil` and `Sexp::List(_)` never
20234 // produce the SAME hash discriminator — a violation here
20235 // means the cache could conflate the empty-result marker
20236 // with the empty-list container. Sibling pin to
20237 // `atom_kind_hash_discriminator_bytes_are_pairwise_disjoint`
20238 // on the atomic-payload axis.
20239 let bytes: Vec<u8> = StructuralKind::ALL
20240 .iter()
20241 .map(|sk| sk.hash_discriminator())
20242 .collect();
20243 let mut sorted = bytes.clone();
20244 sorted.sort_unstable();
20245 let mut deduped = sorted.clone();
20246 deduped.dedup();
20247 assert_eq!(
20248 sorted, deduped,
20249 "StructuralKind hash discriminator bytes must be pairwise disjoint",
20250 );
20251 assert_eq!(sorted, vec![0, 2]);
20252 }
20253
20254 #[test]
20255 fn structural_kind_hash_discriminator_disjoint_from_atom_outer_carve_byte_and_quote_form_hash_discriminator_partition(
20256 ) {
20257 // JOINT PARTITION CONTRACT: the outer-`Sexp` discriminator
20258 // space `{0, 1, 2, 3, 4, 5, 6}` MUST partition across the
20259 // three closed-set carvings AND the atomic-carve outer
20260 // marker byte with NO overlap — the substrate's cache-key
20261 // prefix-uniqueness contract binds bit-for-bit against this
20262 // partition. This test pins the disjointness of
20263 // `StructuralKind::hash_discriminator`'s `{0, 2}` against the
20264 // atomic-carve outer marker byte `1u8` (single-arm literal
20265 // inside `Hash for Sexp`) AND against
20266 // `crate::ast::QuoteForm::hash_discriminator`'s `{3, 4, 5,
20267 // 6}`. A regression that drifts ANY of the three carvings'
20268 // discriminator arms into another's byte space (e.g. a
20269 // future refactor that re-numbers `StructuralKind::List` to
20270 // `1u8`) surfaces here — the prefix-uniqueness contract
20271 // becomes a compile-time-verified theorem across the joint
20272 // partition, not a per-carving isolated invariant. Companion
20273 // to the on-axis `structural_kind_hash_discriminator_bytes_are_pairwise_disjoint`
20274 // pin (which enforces intra-carving injectivity); together
20275 // they close the three-carving-plus-atom-marker joint
20276 // disjointness contract.
20277 // Route the atomic-carve outer marker byte through
20278 // `crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR` (the typed
20279 // substrate primitive on the closed-set atomic-payload
20280 // algebra) rather than through a duplicated local `const
20281 // ATOM_OUTER_CARVE_BYTE: u8 = 1` fixture — the pre-lift local
20282 // const named the byte at ONE test site with no algebra
20283 // provenance, so a rename or renumbering at the algebra
20284 // required an inline byte drift at every joint-partition
20285 // fixture. Post-lift the byte lives at ONE
20286 // `pub(crate) const` on the [`crate::ast::AtomKind`] algebra;
20287 // this test's disjointness contract binds against the typed
20288 // constant so a regression that renumbers the primitive
20289 // fails-loudly at THIS test alongside the shape-level
20290 // collapse test.
20291 let structural: Vec<u8> = StructuralKind::ALL
20292 .iter()
20293 .map(|sk| sk.hash_discriminator())
20294 .collect();
20295 let quote_family: Vec<u8> = crate::ast::QuoteForm::ALL
20296 .iter()
20297 .map(|qf| qf.hash_discriminator())
20298 .collect();
20299 for byte in &structural {
20300 assert_ne!(
20301 *byte,
20302 crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
20303 "StructuralKind::hash_discriminator collided with the atomic-carve outer marker byte {marker}",
20304 marker = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
20305 );
20306 assert!(
20307 !quote_family.contains(byte),
20308 "StructuralKind::hash_discriminator byte {byte} collided with QuoteForm::hash_discriminator partition {quote_family:?}",
20309 );
20310 }
20311 let mut union: Vec<u8> = structural
20312 .iter()
20313 .copied()
20314 .chain(std::iter::once(
20315 crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
20316 ))
20317 .chain(quote_family.iter().copied())
20318 .collect();
20319 union.sort_unstable();
20320 let mut deduped = union.clone();
20321 deduped.dedup();
20322 assert_eq!(
20323 union, deduped,
20324 "the three-carving-plus-atom-marker joint discriminator space {union:?} must partition without overlap",
20325 );
20326 assert_eq!(union, vec![0, 1, 2, 3, 4, 5, 6]);
20327 }
20328
20329 #[test]
20330 fn structural_kind_hash_discriminators_pin_legacy_cache_key_bytes() {
20331 // Pin each per-role `pub(crate) const` at its exact canonical
20332 // `u8` byte. Sibling of
20333 // `structural_kind_hash_discriminator_pins_legacy_cache_key_bytes`
20334 // (which pins the method's projection) — this pin asserts the
20335 // `pub(crate) const` value itself, so a regression that drifts
20336 // the constant but leaves the method's arm literal in place
20337 // (unlikely post-lift but structurally distinct) surfaces
20338 // here. The cache-key partition `{0, 2}` is load-bearing for
20339 // the outer [`Hash for Sexp`](crate::ast::Sexp) prefix-
20340 // uniqueness contract — a `2u8` drift to `1u8` would silently
20341 // collide `Sexp::List(_)` with the reserved atomic-carve
20342 // outer marker and mis-hash every List through the Atom
20343 // arm's cache slot.
20344 assert_eq!(StructuralKind::NIL_HASH_DISCRIMINATOR, 0);
20345 assert_eq!(StructuralKind::LIST_HASH_DISCRIMINATOR, 2);
20346 }
20347
20348 #[test]
20349 fn structural_kind_hash_discriminator_routes_through_typed_per_role_constants() {
20350 // PATH-UNIFORMITY: `Self::hash_discriminator(self)` returns
20351 // the per-role `pub(crate) const` byte-for-byte per variant,
20352 // catching a regression that reverts ONE arm to an inline
20353 // `0` / `2` `u8` literal (or drifts one arm's byte silently).
20354 // Sibling posture to
20355 // `atom_kind_hash_discriminator_routes_through_typed_per_role_constants`
20356 // on the atomic-payload sub-carving AND to
20357 // `quote_form_hash_discriminator_routes_through_typed_per_role_constants`
20358 // on the quote-family sub-carving of the SAME outer-`Sexp`
20359 // Hash body.
20360 for (kind, expected) in [
20361 (StructuralKind::Nil, StructuralKind::NIL_HASH_DISCRIMINATOR),
20362 (
20363 StructuralKind::List,
20364 StructuralKind::LIST_HASH_DISCRIMINATOR,
20365 ),
20366 ] {
20367 let actual = kind.hash_discriminator();
20368 assert_eq!(
20369 actual, expected,
20370 "StructuralKind::{kind:?}.hash_discriminator() `{actual}` \
20371 drifted from per-role constant `{expected}` — the arm \
20372 must route through the typed `pub(crate) const` rather \
20373 than an inline `u8` literal",
20374 );
20375 }
20376 }
20377
20378 #[test]
20379 fn structural_kind_hash_discriminators_has_expected_cardinality() {
20380 // Cardinality contract: `Self::HASH_DISCRIMINATORS.len() == 2`
20381 // — pinned at the declaration site by rustc's forced-arity
20382 // check on `[u8; 2]`. This test surfaces the arity as a
20383 // fail-loud runtime pin so a future refactor that switches
20384 // the array type to `&[u8]` (dropping the compile-time arity
20385 // forcing) doesn't silently loosen the closed-set discipline
20386 // the family relies on. Sibling posture to
20387 // `structural_kind_labels_has_expected_cardinality` on the
20388 // diagnostic label axis of the SAME closed set, and to
20389 // `atom_kind_hash_discriminators_has_expected_cardinality` +
20390 // `quote_form_hash_discriminators_has_expected_cardinality`
20391 // on the two sibling carvings' cache-key-byte peers.
20392 assert_eq!(
20393 StructuralKind::HASH_DISCRIMINATORS.len(),
20394 2,
20395 "StructuralKind::HASH_DISCRIMINATORS cardinality drifted \
20396 from 2 — the closed structural-residual domain admits \
20397 exactly two kinds by construction; a third extension \
20398 surfaces here"
20399 );
20400 }
20401
20402 #[test]
20403 fn structural_kind_hash_discriminators_align_with_all_by_index() {
20404 // ALIGNMENT CONTRACT: `Self::HASH_DISCRIMINATORS[i] ==
20405 // Self::ALL[i].hash_discriminator()` element-wise. Pins that
20406 // the typed variant ALL and the `u8` HASH_DISCRIMINATORS ALL
20407 // stay in lockstep under any reorder — a regression that
20408 // reorders ONE array without reordering the other silently
20409 // misaligns every `zip(ALL, HASH_DISCRIMINATORS)` consumer.
20410 // Sibling posture to
20411 // `structural_kind_labels_align_with_all_by_index` on the
20412 // diagnostic label axis of the SAME closed set.
20413 for (i, kind) in StructuralKind::ALL.iter().enumerate() {
20414 assert_eq!(
20415 StructuralKind::HASH_DISCRIMINATORS[i],
20416 kind.hash_discriminator(),
20417 "StructuralKind::HASH_DISCRIMINATORS[{i}] `{disc}` \
20418 drifted from StructuralKind::ALL[{i}] ({kind:?}).hash_discriminator() \
20419 `{via_variant}` — the canonical declaration order of \
20420 the ALL array and the hash_discriminator projection \
20421 must match element-wise",
20422 disc = StructuralKind::HASH_DISCRIMINATORS[i],
20423 via_variant = kind.hash_discriminator(),
20424 );
20425 }
20426 }
20427
20428 #[test]
20429 fn structural_kind_hash_discriminators_pairwise_distinct() {
20430 // PAIRWISE DISJOINTNESS: every entry of the
20431 // `HASH_DISCRIMINATORS` array must differ so the outer-`Sexp`
20432 // Hash body cannot route two structural-residual variants
20433 // through the same cache-key byte — a collision would
20434 // silently mis-hash two structurally-distinct residual shapes
20435 // (`Sexp::Nil` and `Sexp::List(_)`) to the same
20436 // `Expander::cache` slot. Family-wide sweep over
20437 // `HASH_DISCRIMINATORS × HASH_DISCRIMINATORS` — supersedes any
20438 // per-pair pin and picks up new discriminators mechanically.
20439 // Sibling posture to
20440 // `structural_kind_hash_discriminator_bytes_are_pairwise_disjoint`
20441 // (which sweeps via the projection) — this sweep pins the
20442 // array itself so a regression that lifts a fresh entry with
20443 // a colliding byte surfaces at the ALL sweep.
20444 for (i, a) in StructuralKind::HASH_DISCRIMINATORS.iter().enumerate() {
20445 for (j, b) in StructuralKind::HASH_DISCRIMINATORS.iter().enumerate() {
20446 if i == j {
20447 continue;
20448 }
20449 assert_ne!(
20450 a, b,
20451 "StructuralKind::HASH_DISCRIMINATORS[{i}] `{a}` \
20452 collides with StructuralKind::HASH_DISCRIMINATORS[{j}] \
20453 `{b}` — the outer-Sexp Hash body's cache-key \
20454 partition would route two structural-residual \
20455 variants through the same slot"
20456 );
20457 }
20458 }
20459 }
20460
20461 #[test]
20462 fn structural_kind_label_matches_sexp_shape_label_via_composition() {
20463 // Composition-routing pin: for every `sk: StructuralKind`,
20464 // `sk.label()` and `sk.sexp_shape().label()` agree byte-for-
20465 // byte AND at the SAME `&'static str` pointer (the
20466 // composition returns the SAME literal `SexpShape::label`
20467 // emits, without a parallel arm-table on the subset). A
20468 // regression that re-inlines the two arms as a parallel
20469 // match-table (`Self::Nil => "nil"`, `Self::List => "list"`)
20470 // still passes the label-equality sweep but fails the pointer
20471 // pin — the residual-carving's label vocabulary is no longer
20472 // derived from the superset's canonical site. Sibling-shape
20473 // pin to `atom_kind_label_routes_through_sexp_shape_label_via_sexp_shape_projection`
20474 // (the 6-of-12 atomic-payload carving) and
20475 // `unquote_form_marker_routes_through_to_quote_form_prefix_via_composition`
20476 // (the 2-of-4 substitution-subset carving).
20477 for sk in StructuralKind::ALL {
20478 let from_direct = sk.label();
20479 let from_composition = sk.sexp_shape().label();
20480 assert_eq!(
20481 from_direct, from_composition,
20482 "StructuralKind::{sk:?}.label() drifted from .sexp_shape().label() — the residual carving's label vocabulary is no longer derived from the superset's canonical site",
20483 );
20484 assert!(
20485 std::ptr::eq(from_direct.as_ptr(), from_composition.as_ptr()),
20486 "StructuralKind::{sk:?}.label() and .sexp_shape().label() must point at the SAME `&'static str` — a parallel inline table has a different pointer",
20487 );
20488 }
20489 }
20490
20491 #[test]
20492 fn structural_kind_label_round_trips_through_from_str() {
20493 // `label` ↔ `FromStr` round-trip pin: for every `sk:
20494 // StructuralKind::ALL`, `sk.label().parse::<StructuralKind>()
20495 // == Ok(sk)`. The auto-derived `impl FromStr` (via
20496 // `#[derive(ClosedSet)]` + the default `parse_label`) sweeps
20497 // over `Self::ALL` keyed on `self.label()`, so a drift in
20498 // either direction (a label typo or a variant-set drift)
20499 // surfaces here. Sibling round-trip to
20500 // `sexp_shape_label_round_trips_through_from_str` and
20501 // `atom_kind_label_round_trips_through_from_str`.
20502 for sk in StructuralKind::ALL {
20503 let parsed = sk.label().parse::<StructuralKind>();
20504 assert_eq!(
20505 parsed,
20506 Ok(sk),
20507 "StructuralKind::{sk:?}.label() must round-trip through FromStr",
20508 );
20509 }
20510 }
20511
20512 #[test]
20513 fn structural_kind_from_str_rejects_non_structural_sexp_shape_labels() {
20514 // Cross-axis rejection pin: the `FromStr` decode on
20515 // StructuralKind MUST reject every `SexpShape` label that lies
20516 // OUTSIDE the residual carving (the ten `symbol` / `keyword` /
20517 // `string` / `int` / `float` / `bool` / `quote` / `quasiquote`
20518 // / `unquote` / `unquote-splice` labels — every atomic-payload
20519 // AND quote-family label). The two residual labels (`nil`,
20520 // `list`) accept because they lie INSIDE the carving; every
20521 // other `SexpShape` label rejects with `Err(UnknownStructuralKind)`.
20522 // Sibling of
20523 // `unquote_form_from_str_rejects_sexp_shape_labels_on_template_marker_axis`
20524 // and
20525 // `sexp_shape_from_str_accepts_only_canonical_labels` — the
20526 // pattern the substrate carries at every closed-set-carving
20527 // boundary.
20528 let residual_labels: Vec<&'static str> =
20529 StructuralKind::ALL.iter().map(|sk| sk.label()).collect();
20530 for shape in SexpShape::ALL {
20531 let label = shape.label();
20532 let parsed = label.parse::<StructuralKind>();
20533 if residual_labels.contains(&label) {
20534 assert!(
20535 parsed.is_ok(),
20536 "SexpShape::{shape:?}.label() = {label:?} is a residual label — StructuralKind::from_str must accept",
20537 );
20538 } else {
20539 assert!(
20540 parsed.is_err(),
20541 "SexpShape::{shape:?}.label() = {label:?} lies outside the residual carving — StructuralKind::from_str must reject",
20542 );
20543 }
20544 }
20545 }
20546
20547 #[test]
20548 fn structural_kind_is_well_formed_closed_set() {
20549 // Structural contract: StructuralKind's two variants are
20550 // pairwise distinct, round-trip through the trait's `label` ↔
20551 // `parse_label`, and reject the empty string — the
20552 // workspace-wide `assert_closed_set_well_formed::<T>()` testkit
20553 // pinned across every prior closed-set implementor
20554 // (`AtomKind`, `QuoteForm`, `UnquoteForm`, `SexpShape`,
20555 // `MacroDefHead`, `ExpectedKwargShape`, `KwargPathKind`,
20556 // `CompilerSpecIoStage`). The substrate-level assertion runs
20557 // on the auto-derived `impl ClosedSet for StructuralKind`
20558 // emitted by `#[derive(tatara_closed_set::DeriveClosedSet)]` — a
20559 // regression that drifts the `via = "label"` projection
20560 // (`"nil"` / `"list"`) or the variant listing forced through
20561 // `Self::ALL` (cardinality 2 = every structural-residual
20562 // outer shape) fails-loudly here.
20563 tatara_closed_set::assert_closed_set_well_formed::<StructuralKind>();
20564 }
20565
20566 #[test]
20567 fn structural_kind_per_role_labels_alias_sexp_shape_per_role_labels_byte_for_byte() {
20568 // ALIAS CONTRACT: pin every one of the two per-role `pub const
20569 // StructuralKind::*_LABEL` aliases equals the corresponding
20570 // `pub const SexpShape::*_LABEL` byte-for-byte — so the
20571 // StructuralKind ⊂ SexpShape marker-vocabulary containment
20572 // routes through the typed `pub const StructuralKind::V_LABEL:
20573 // &'static str = SexpShape::V_LABEL` alias chain rather than
20574 // through two independent literal-discipline sites. Sibling-
20575 // shape pin to
20576 // `atom_kind_per_role_labels_alias_sexp_shape_per_role_labels_byte_for_byte`
20577 // on the peer 6-of-12 atomic-payload carving — the two pins
20578 // together bind the two SexpShape sub-carvings' per-role
20579 // alias chains at the SAME shape (parent's canonical bytes on
20580 // the RHS, subset's alias on the LHS). A regression that
20581 // renames the SexpShape side without updating the
20582 // StructuralKind alias pointing at it fails-loudly here with
20583 // the exact axis identified (NIL / LIST); a regression that
20584 // re-inlines the StructuralKind constant to a fresh literal
20585 // still passes this pin but loses the alias-chain typing
20586 // (which is what
20587 // `structural_kind_label_arms_route_through_per_role_labels_for_every_variant`
20588 // + `structural_kind_labels_align_with_all_by_index` catch in
20589 // combination).
20590 assert_eq!(StructuralKind::NIL_LABEL, SexpShape::NIL_LABEL);
20591 assert_eq!(StructuralKind::LIST_LABEL, SexpShape::LIST_LABEL);
20592 }
20593
20594 #[test]
20595 fn structural_kind_label_arms_route_through_per_role_labels_for_every_variant() {
20596 // PATH-UNIFORMITY: `StructuralKind::V.label()` MUST equal the
20597 // per-role `pub const StructuralKind::V_LABEL` for every `v:
20598 // StructuralKind`. Pre-lift the two structural-residual marker
20599 // bytes were reachable through `StructuralKind::label` (the
20600 // composition `self.sexp_shape().label()` — routing into
20601 // `SexpShape::*_LABEL`) OR through direct
20602 // `SexpShape::*_LABEL` reach-across; post-lift each variant's
20603 // canonical bytes are reachable through the per-role
20604 // `StructuralKind::*_LABEL` alias too. Pin the byte-equality
20605 // between the runtime projection and the compile-time alias
20606 // so a regression that renames the alias without updating the
20607 // arm (or vice versa) fails-loudly at the exact axis.
20608 //
20609 // Sibling-shape pin to
20610 // `atom_kind_label_arms_route_through_per_role_labels_for_every_variant`
20611 // on the peer 6-of-12 atomic-payload carving — the two pins
20612 // together bind BOTH SexpShape sub-carvings' per-role alias
20613 // chains against their respective label projections.
20614 assert_eq!(StructuralKind::Nil.label(), StructuralKind::NIL_LABEL);
20615 assert_eq!(StructuralKind::List.label(), StructuralKind::LIST_LABEL);
20616 }
20617
20618 #[test]
20619 fn structural_kind_labels_has_expected_cardinality() {
20620 // Cardinality pin: `LABELS.len() == 2` matches `ALL.len()` so
20621 // a refactor that loosens the type to `&'static [&'static
20622 // str]` fails HERE (the `[_; 2]` slot cannot be sliced
20623 // silently), and a variant added to `ALL` without a matching
20624 // `LABELS` row fails the pair-arity gate at the array literal
20625 // itself before this test even runs. The pin doubles as an
20626 // operator-visible mark of the family's cardinality across
20627 // the substrate — two structural-residual markers, matching
20628 // the two-arm carving of the parent `SexpShape::LABELS` (the
20629 // residual subset of the twelve canonical outer-shape labels).
20630 assert_eq!(StructuralKind::LABELS.len(), 2);
20631 assert_eq!(StructuralKind::LABELS.len(), StructuralKind::ALL.len());
20632 }
20633
20634 #[test]
20635 fn structural_kind_labels_align_with_all_by_index() {
20636 // ALIGNMENT PIN: sweep `LABELS[i] == ALL[i].label()` so any
20637 // `zip(ALL, LABELS)` consumer reads a coherent (variant,
20638 // label) pair off ONE forced-arity array pair. The
20639 // declaration-order pin makes a family-wide consumer that
20640 // walks the ALL / LABELS pair in lockstep (an LSP completion
20641 // bar keyed on `StructuralKind::LABELS`, a Sekiban metric
20642 // emitter labeling `tatara_lisp_structural_shape_total{kind}`
20643 // by the per-index label) read one canonical (variant, bytes)
20644 // pair per slot rather than routing through per-consumer
20645 // paired-iteration. A regression that reorders LABELS without
20646 // also reordering ALL (or vice versa) fails-loudly at the
20647 // exact index that drifted.
20648 assert_eq!(StructuralKind::LABELS.len(), StructuralKind::ALL.len());
20649 for (i, kind) in StructuralKind::ALL.iter().enumerate() {
20650 assert_eq!(
20651 StructuralKind::LABELS[i],
20652 kind.label(),
20653 "StructuralKind::LABELS[{i}] `{lbl}` drifted from \
20654 StructuralKind::ALL[{i}].label() `{via_variant}` — \
20655 the canonical ALL ordering and the LABELS ordering \
20656 must match element-wise",
20657 lbl = StructuralKind::LABELS[i],
20658 via_variant = kind.label(),
20659 );
20660 }
20661 }
20662
20663 #[test]
20664 fn structural_kind_labels_pairwise_distinct() {
20665 // 2x2 pairwise sweep so a collision between the two labels
20666 // (which would silently degrade two distinct structural-
20667 // residual markers to the SAME diagnostic bytes and violate
20668 // the closed-set FromStr round-trip) fails-loudly at the
20669 // exact pair. Distinctness is already enforced structurally
20670 // by `assert_closed_set_well_formed::<StructuralKind>()`
20671 // (clause 3), so this pin is a secondary guard focused on
20672 // the per-role `pub const` surface directly rather than the
20673 // runtime projection through the trait's default `labels()`.
20674 for (i, a) in StructuralKind::LABELS.iter().enumerate() {
20675 for (j, b) in StructuralKind::LABELS.iter().enumerate() {
20676 if i == j {
20677 continue;
20678 }
20679 assert_ne!(
20680 a, b,
20681 "StructuralKind::LABELS[{i}] ({a:?}) collides with \
20682 StructuralKind::LABELS[{j}] ({b:?}) — two distinct \
20683 structural-residual markers cannot share \
20684 diagnostic bytes",
20685 );
20686 }
20687 }
20688 }
20689
20690 #[test]
20691 fn structural_kind_labels_match_sexp_shape_labels_element_wise_via_alias_chain() {
20692 // CROSS-AXIS PIN: sweep `StructuralKind::LABELS[i] ==
20693 // StructuralKind::ALL[i].sexp_shape().label()` so the ALL
20694 // array projected through the [`Self::sexp_shape`] embed +
20695 // [`SexpShape::label`] composition agrees with LABELS byte-
20696 // for-byte at every index. The pin closes the alias-chain
20697 // identity at the family-wide array level: the LABELS array
20698 // is NOT a fresh literal table but the projection of ALL
20699 // through the composition, materialized once at declaration
20700 // time through the two per-role `pub const *_LABEL` aliases.
20701 // A regression that re-inlines LABELS to fresh literals still
20702 // passes the pairwise-distinct + cardinality + alignment
20703 // pins but drifts the alias chain — this pin catches that
20704 // drift by comparing against the composition on the SAME
20705 // ALL ordering.
20706 //
20707 // Sibling-shape pin to the peer 6-of-12 atomic-payload
20708 // carving's family-wide alias-chain pin (implicit in
20709 // `atom_kind_labels_align_with_all_by_index` +
20710 // `atom_kind_per_role_labels_alias_sexp_shape_per_role_labels_byte_for_byte`
20711 // together); this pin binds the 2-of-12 residual carving's
20712 // family-wide alias chain in ONE test at the family-wide
20713 // array level.
20714 for (i, kind) in StructuralKind::ALL.iter().enumerate() {
20715 assert_eq!(
20716 StructuralKind::LABELS[i],
20717 kind.sexp_shape().label(),
20718 "StructuralKind::LABELS[{i}] drifted from \
20719 StructuralKind::ALL[{i}].sexp_shape().label() — the \
20720 LABELS array must project through the sexp_shape + \
20721 SexpShape::label composition byte-for-byte",
20722 );
20723 }
20724 }
20725
20726 #[test]
20727 fn structural_kind_per_role_shapes_pin_canonical_sexp_shape_variants() {
20728 // PER-ROLE ALIAS CONTRACT: each `StructuralKind::*_SHAPE` per-
20729 // role `pub const` binds byte-for-byte to its canonical
20730 // `SexpShape` variant on the StructuralKind ⊂ SexpShape 2-of-12
20731 // carving. Pin so a regression that swaps ONE alias (e.g.
20732 // re-aims `LIST_SHAPE` at `SexpShape::Nil`) surfaces at rustc
20733 // / test time rather than as a silent operator-facing
20734 // diagnostic drift at every consumer keyed on the typed embed
20735 // target. Sibling posture to
20736 // `structural_kind_per_role_labels_alias_sexp_shape_per_role_labels_byte_for_byte`
20737 // on the diagnostic label axis of the SAME closed set — this
20738 // pin is the peer on the `SexpShape` embed-target axis.
20739 assert_eq!(StructuralKind::NIL_SHAPE, SexpShape::Nil);
20740 assert_eq!(StructuralKind::LIST_SHAPE, SexpShape::List);
20741 }
20742
20743 #[test]
20744 fn structural_kind_shapes_has_expected_cardinality() {
20745 // CLOSED-SET CARDINALITY CONTRACT: `StructuralKind::SHAPES`
20746 // carries exactly TWO entries — one per variant in the
20747 // closed-set structural-residual carving. Runtime companion to
20748 // the `[SexpShape; 2]` type annotation's compile-time forced
20749 // arity. A regression that widens the array without adding a
20750 // matching per-role `*_SHAPE` alias would fail the
20751 // `[SexpShape; 2]` type check at rustc time; this runtime pin
20752 // catches a silent shrink or duplicate-arm coalesce. Sibling
20753 // posture to `structural_kind_labels_has_expected_cardinality`
20754 // and `structural_kind_hash_discriminators_has_expected_cardinality`
20755 // on the other two per-role axes of the SAME closed set.
20756 assert_eq!(StructuralKind::SHAPES.len(), 2);
20757 assert_eq!(StructuralKind::SHAPES.len(), StructuralKind::ALL.len());
20758 }
20759
20760 #[test]
20761 fn structural_kind_shapes_align_with_all_by_index() {
20762 // ALIGNMENT CONTRACT: `Self::SHAPES[i] ==
20763 // Self::ALL[i].sexp_shape()` element-wise. Pins that the typed
20764 // variant ALL and the `SexpShape` SHAPES ALL stay in lockstep
20765 // under any reorder — a regression that reorders ONE array
20766 // without reordering the other silently misaligns every
20767 // `zip(ALL, SHAPES)` consumer that wants to project each
20768 // structural-residual variant to its canonical outer-shape
20769 // identity (LSP completion, `tatara-check` predicate over the
20770 // structural ⊂ outer partition, Sekiban audit-trail metric
20771 // jointly labeled by the embed target). Sibling posture to
20772 // `structural_kind_labels_align_with_all_by_index` and
20773 // `structural_kind_hash_discriminators_align_with_all_by_index`
20774 // on the other two per-role axes of the SAME closed set.
20775 for (i, kind) in StructuralKind::ALL.iter().enumerate() {
20776 assert_eq!(
20777 StructuralKind::SHAPES[i],
20778 kind.sexp_shape(),
20779 "StructuralKind::SHAPES[{i}] `{shape:?}` drifted from \
20780 StructuralKind::ALL[{i}] ({kind:?}).sexp_shape() \
20781 `{via_variant:?}` — the canonical declaration order \
20782 of the ALL array and the sexp_shape projection must \
20783 match element-wise",
20784 shape = StructuralKind::SHAPES[i],
20785 via_variant = kind.sexp_shape(),
20786 );
20787 }
20788 }
20789
20790 #[test]
20791 fn structural_kind_shapes_align_with_all_by_index_through_as_structural_kind() {
20792 // ROUND-TRIP CONTRACT: `Self::SHAPES[i].as_structural_kind() ==
20793 // Some(Self::ALL[i])` element-wise. Pins the embed / project
20794 // section of the (`StructuralKind::sexp_shape`,
20795 // `SexpShape::as_structural_kind`) `Iso(StructuralKind,
20796 // StructuralShape ⊂ SexpShape)` as a family-wide array-indexed
20797 // law rather than as a per-variant assertion sweep. A
20798 // regression that drifts EITHER the `SHAPES` array entries OR
20799 // the peer inverse `as_structural_kind` arms silently breaks
20800 // the (embed, project) section — this pin catches both
20801 // directions at ONCE. Sibling posture to
20802 // `atom_kind_shapes_align_with_all_by_index_through_as_atom_kind`
20803 // on the peer 6-of-12 atomic-payload carving and
20804 // `quote_form_shapes_align_with_all_by_index_through_as_quote_form`
20805 // on the peer 4-of-12 quote-family carving — this pin closes
20806 // the third and final peer round-trip.
20807 for (i, kind) in StructuralKind::ALL.iter().enumerate() {
20808 assert_eq!(
20809 StructuralKind::SHAPES[i].as_structural_kind(),
20810 Some(*kind),
20811 "StructuralKind::SHAPES[{i}] `{shape:?}`.as_structural_kind() = \
20812 {actual:?} drifted from Some(StructuralKind::ALL[{i}]) = \
20813 Some({expected:?}) — the (SHAPES entry, ALL variant) \
20814 round-trip through the peer inverse \
20815 SexpShape::as_structural_kind must hold element-wise",
20816 shape = StructuralKind::SHAPES[i],
20817 actual = StructuralKind::SHAPES[i].as_structural_kind(),
20818 expected = kind,
20819 );
20820 }
20821 }
20822
20823 #[test]
20824 fn structural_kind_sexp_shape_routes_through_typed_per_role_constants() {
20825 // ROUTING CONTRACT: `StructuralKind::sexp_shape()`'s two arms
20826 // bind through the per-role `pub const *_SHAPE` aliases rather
20827 // than through inline `SexpShape::X` literals. Pin so a
20828 // regression that re-inlines the two literals here (and gains
20829 // its own drift surface separate from the canonical per-role
20830 // alias site) surfaces immediately at variant equality.
20831 // Sibling posture to
20832 // `structural_kind_label_arms_route_through_per_role_labels_for_every_variant`
20833 // (which pins label routing through the per-role LABEL aliases)
20834 // and `atom_kind_sexp_shape_routes_through_typed_per_role_constants`
20835 // / `quote_form_sexp_shape_routes_through_typed_per_role_constants`
20836 // on the peer 6-of-12 atomic-payload and 4-of-12 quote-family
20837 // carvings.
20838 assert_eq!(StructuralKind::Nil.sexp_shape(), StructuralKind::NIL_SHAPE);
20839 assert_eq!(
20840 StructuralKind::List.sexp_shape(),
20841 StructuralKind::LIST_SHAPE
20842 );
20843 }
20844
20845 #[test]
20846 fn structural_kind_shapes_pairwise_distinct() {
20847 // PAIRWISE DISJOINTNESS: every entry of the `SHAPES` array
20848 // must differ so the (StructuralKind variant, SexpShape embed
20849 // target) mapping stays injective — a collision would silently
20850 // route two distinct StructuralKind variants through the SAME
20851 // outer-shape identity, breaking the StructuralKind ⊂
20852 // SexpShape 2-of-12 carving. Family-wide sweep over `SHAPES ×
20853 // SHAPES` — supersedes any per-pair pin and picks up new embed
20854 // targets mechanically. Sibling posture to
20855 // `atom_kind_shapes_pairwise_distinct` and
20856 // `quote_form_shapes_pairwise_distinct` on the peer 6-of-12
20857 // atomic-payload and 4-of-12 quote-family carvings — this pin
20858 // closes the third and final peer pairwise-distinct sweep.
20859 for (i, a) in StructuralKind::SHAPES.iter().enumerate() {
20860 for (j, b) in StructuralKind::SHAPES.iter().enumerate() {
20861 if i == j {
20862 continue;
20863 }
20864 assert_ne!(
20865 a, b,
20866 "StructuralKind::SHAPES[{i}] `{a:?}` collides with \
20867 StructuralKind::SHAPES[{j}] `{b:?}` — the \
20868 StructuralKind ⊂ SexpShape 2-of-12 carving would \
20869 route two structural-residual variants through the \
20870 same outer-shape identity"
20871 );
20872 }
20873 }
20874 }
20875
20876 #[test]
20877 fn sexp_shape_hash_discriminator_pins_legacy_outer_cache_key_bytes() {
20878 // CACHE-KEY CONTRACT (shape-level peer): the shape-level
20879 // `SexpShape::hash_discriminator` MUST project each of the twelve
20880 // outer-shape arms to the SAME outer cache-key byte the
20881 // corresponding `Sexp` variant surfaces through
20882 // `crate::ast::Sexp::hash_discriminator`. Pin the twelve-arm →
20883 // seven-byte collapse: Nil→0, {Symbol/Keyword/String/Int/Float/
20884 // Bool}→1 (the outer Atom marker byte; the per-atom-kind inner
20885 // byte lives INSIDE `Hash for Atom` via
20886 // `AtomKind::hash_discriminator`'s `{0..=5}`, NOT surfaced
20887 // through this shape-level method), List→2, Quote→3,
20888 // Quasiquote→4, Unquote→5, UnquoteSplice→6. Any regression that
20889 // renumbers a byte here silently drifts the outer-`Sexp` cache-
20890 // key partition since `Sexp::hash_discriminator` composes
20891 // through `self.shape().hash_discriminator()` post-lift.
20892 // Sibling posture to
20893 // `structural_kind_hash_discriminator_pins_legacy_cache_key_bytes`
20894 // on the 2-arm sub-carving, `crate::ast::AtomKind`'s
20895 // `atom_kind_hash_discriminator_pins_legacy_atom_cache_key_bytes`
20896 // on the 6-arm sub-carving, and
20897 // `crate::ast::QuoteForm`'s
20898 // `quote_form_hash_discriminator_pins_legacy_cache_key_bytes`
20899 // on the 4-arm sub-carving.
20900 assert_eq!(SexpShape::Nil.hash_discriminator(), 0);
20901 assert_eq!(SexpShape::Symbol.hash_discriminator(), 1);
20902 assert_eq!(SexpShape::Keyword.hash_discriminator(), 1);
20903 assert_eq!(SexpShape::String.hash_discriminator(), 1);
20904 assert_eq!(SexpShape::Int.hash_discriminator(), 1);
20905 assert_eq!(SexpShape::Float.hash_discriminator(), 1);
20906 assert_eq!(SexpShape::Bool.hash_discriminator(), 1);
20907 assert_eq!(SexpShape::List.hash_discriminator(), 2);
20908 assert_eq!(SexpShape::Quote.hash_discriminator(), 3);
20909 assert_eq!(SexpShape::Quasiquote.hash_discriminator(), 4);
20910 assert_eq!(SexpShape::Unquote.hash_discriminator(), 5);
20911 assert_eq!(SexpShape::UnquoteSplice.hash_discriminator(), 6);
20912 }
20913
20914 #[test]
20915 fn sexp_shape_hash_discriminator_covers_outer_partition_zero_through_six() {
20916 // PARTITION-COMPLETENESS CONTRACT (shape-level peer): the
20917 // 12-arm sweep of `SexpShape::ALL` through
20918 // `SexpShape::hash_discriminator` MUST cover exactly the outer
20919 // cache-key partition `{0..=6}` — no byte outside the outer
20920 // partition, no byte inside missing. The projection is
20921 // surjective onto `{0..=6}` (every outer byte reached by at
20922 // least one shape arm) with the atomic-shape collapse
20923 // ({Symbol/Keyword/String/Int/Float/Bool} → 1) covering the
20924 // outer Atom marker byte. Sibling posture to
20925 // `sexp_hash_discriminator_partitions_the_full_outer_discriminator_space_zero_through_six`
20926 // on the outer-`Sexp` algebra — that pin binds the outer-Sexp
20927 // partition; this pin binds the shape-level partition. Post-
20928 // lift the two partitions coincide byte-for-byte through
20929 // `Sexp::hash_discriminator` = `self.shape().hash_discriminator()`.
20930 let outer_bytes: std::collections::BTreeSet<u8> = SexpShape::ALL
20931 .iter()
20932 .map(|shape| shape.hash_discriminator())
20933 .collect();
20934 let expected: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
20935 assert_eq!(
20936 outer_bytes, expected,
20937 "SexpShape::hash_discriminator must cover exactly the outer discriminator space {{0..=6}}",
20938 );
20939 }
20940
20941 #[test]
20942 fn sexp_shape_hash_discriminator_atomic_arms_collapse_to_outer_atom_marker() {
20943 // OUTER-CARVING CONTRACT (shape-level atomic arm): every
20944 // `SexpShape` variant whose `as_atom_kind()` projection returns
20945 // `Some(_)` MUST project through `SexpShape::hash_discriminator`
20946 // to the SAME outer discriminator byte `1u8` — the atomic outer
20947 // arm is a single-byte marker on the outer partition, with the
20948 // per-atom-kind inner byte
20949 // (`crate::ast::AtomKind::hash_discriminator`'s `{0..=5}`)
20950 // nested INSIDE `Hash for Atom` — NOT surfaced through this
20951 // shape-level method. Pin the six-way collapse so a regression
20952 // that drifts ONE atomic-shape arm's outer routing (e.g. routes
20953 // `SexpShape::Int` to `7u8` inline) surfaces here immediately.
20954 // Sibling posture to
20955 // `sexp_hash_discriminator_atom_arm_collapses_over_every_atom_kind`
20956 // on the outer-`Sexp` algebra — that pin binds the outer-Sexp
20957 // atom-arm collapse; this pin binds the shape-level atomic-arm
20958 // collapse. Post-lift the two collapses coincide byte-for-byte
20959 // through `Sexp::hash_discriminator` =
20960 // `self.shape().hash_discriminator()`.
20961 for shape in SexpShape::ALL {
20962 if shape.as_atom_kind().is_some() {
20963 assert_eq!(
20964 shape.hash_discriminator(),
20965 crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
20966 "SexpShape::{shape:?} is an atomic-payload shape but did not collapse to outer byte {marker}",
20967 marker = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
20968 );
20969 }
20970 }
20971 }
20972
20973 #[test]
20974 fn sexp_shape_hash_discriminator_atomic_arms_route_through_atom_kind_outer_hash_discriminator()
20975 {
20976 // PATH-UNIFORMITY (shape-level atomic-carve outer marker):
20977 // for every `SexpShape` variant whose `as_atom_kind()`
20978 // projection returns `Some(_)`, `SexpShape::hash_discriminator`
20979 // MUST bind byte-for-byte to
20980 // `crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR` — the
20981 // atomic-carve outer marker binds through the typed
20982 // substrate primitive rather than through an inline `1u8`
20983 // literal (which is what pre-lift wrote at the six-arm
20984 // atomic collapse arm). Pin the routing-identity across the
20985 // closed set so a regression that reverts the atomic
20986 // collapse to `Self::Symbol | ... | Self::Bool => 1` (or
20987 // drifts the routing byte silently while leaving the const
20988 // in place) fails-loudly here. Sibling posture to
20989 // `sexp_shape_hash_discriminator_structural_arms_route_through_structural_kind`
20990 // on the structural-residual sub-carving AND
20991 // `sexp_shape_hash_discriminator_quote_family_arms_route_through_quote_form`
20992 // on the quote-family sub-carving — every one of the three
20993 // shape-level carvings' six-plus-two-plus-four arms routes
20994 // through the typed substrate primitive on its owning
20995 // closed-set algebra rather than an inline `u8` literal
20996 // scattered across `SexpShape::hash_discriminator`. Together
20997 // with the two pre-existing companion pins this pin closes
20998 // the shape-level projection's TWELVE-ARM routing across the
20999 // FOUR typed byte primitives (three sub-algebra methods +
21000 // the outer-Atom scalar) at ONE byte-equality contract per
21001 // arm.
21002 //
21003 // The atomic-carve outer marker is a SCALAR (single byte),
21004 // NOT an ALL-array sweep like the other two carvings —
21005 // reflected in the assertion body: all six atomic-payload
21006 // shapes assert against the SAME
21007 // `AtomKind::OUTER_HASH_DISCRIMINATOR` constant rather than
21008 // zip against a `HASH_DISCRIMINATORS` array. This scalar-vs-
21009 // array asymmetry is intrinsic to the carve semantics: all
21010 // six atomic-payload arms of `SexpShape` COLLAPSE to the
21011 // SAME outer byte (the outer-Sexp distinguisher is
21012 // variant-level: `Sexp::Atom(_)` vs the six sibling `Sexp`
21013 // variants); per-atom-kind specialisation lives at the
21014 // NESTED INNER `AtomKind::HASH_DISCRIMINATORS` carve inside
21015 // `Hash for Atom`.
21016 for shape in SexpShape::ALL {
21017 if shape.as_atom_kind().is_some() {
21018 assert_eq!(
21019 shape.hash_discriminator(),
21020 crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
21021 "SexpShape::{shape:?} atomic arm must route \
21022 through the typed `AtomKind::OUTER_HASH_DISCRIMINATOR` \
21023 rather than an inline `1u8` literal — the arm's \
21024 projection `{proj}` diverged from the typed \
21025 primitive `{expected}`",
21026 proj = shape.hash_discriminator(),
21027 expected = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
21028 );
21029 }
21030 }
21031 }
21032
21033 #[test]
21034 fn atom_kind_outer_hash_discriminator_closes_outer_sexp_partition_with_structural_and_quote_form(
21035 ) {
21036 // JOINT PARTITION CONTRACT (four-way typed closure): the
21037 // outer-`Sexp` cache-key partition `{0..=6}` MUST close
21038 // exactly under the FOUR typed byte primitives — scalar
21039 // `AtomKind::OUTER_HASH_DISCRIMINATOR` (`{1}`), array
21040 // `StructuralKind::HASH_DISCRIMINATORS` (`{0, 2}`), array
21041 // `QuoteForm::HASH_DISCRIMINATORS` (`{3..=6}`). The union
21042 // covers exactly `{0..=6}` with NO overlap. Pre-lift the
21043 // partition closure was pinned INDIRECTLY through the
21044 // `structural_kind_hash_discriminator_disjoint_from_atom_outer_carve_byte_and_quote_form_hash_discriminator_partition`
21045 // sweep which used a duplicated local `const
21046 // ATOM_OUTER_CARVE_BYTE: u8 = 1` fixture for the atom side;
21047 // post-lift THIS pin binds the four-way closure DIRECTLY
21048 // through the typed substrate primitives. A regression that
21049 // renumbers `AtomKind::OUTER_HASH_DISCRIMINATOR` into
21050 // `{0, 2}` (StructuralKind's space) or `{3..=6}`
21051 // (QuoteForm's space) fails-loudly at THIS test as a
21052 // partition-collision rather than at a downstream cache
21053 // mis-hash. Sibling posture to
21054 // `structural_kind_hash_discriminator_disjoint_from_atom_outer_carve_byte_and_quote_form_hash_discriminator_partition`
21055 // — that pin sweeps the three-carving-plus-atom-marker
21056 // union bottom-up from `StructuralKind`; this pin closes
21057 // the same partition top-down from `AtomKind`'s outer scalar
21058 // as the sole SCALAR entry alongside the two ARRAY entries.
21059 // Together the two pins bind the four-way closure at BOTH
21060 // directions.
21061 let atomic: std::collections::BTreeSet<u8> =
21062 std::iter::once(crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR).collect();
21063 let structural: std::collections::BTreeSet<u8> = StructuralKind::HASH_DISCRIMINATORS
21064 .iter()
21065 .copied()
21066 .collect();
21067 let quote_family: std::collections::BTreeSet<u8> =
21068 crate::ast::QuoteForm::HASH_DISCRIMINATORS
21069 .iter()
21070 .copied()
21071 .collect();
21072 assert!(
21073 atomic.is_disjoint(&structural),
21074 "atomic-carve outer marker `{atomic:?}` and structural-carve \
21075 partition `{structural:?}` MUST be disjoint on the outer-Sexp \
21076 cache-key space `{{0..=6}}`",
21077 );
21078 assert!(
21079 atomic.is_disjoint("e_family),
21080 "atomic-carve outer marker `{atomic:?}` and quote-family carve \
21081 partition `{quote_family:?}` MUST be disjoint on the outer-Sexp \
21082 cache-key space `{{0..=6}}`",
21083 );
21084 assert!(
21085 structural.is_disjoint("e_family),
21086 "structural-carve partition `{structural:?}` and quote-family \
21087 carve partition `{quote_family:?}` MUST be disjoint on the \
21088 outer-Sexp cache-key space `{{0..=6}}`",
21089 );
21090 let union: std::collections::BTreeSet<u8> = atomic
21091 .iter()
21092 .chain(structural.iter())
21093 .chain(quote_family.iter())
21094 .copied()
21095 .collect();
21096 let expected: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
21097 assert_eq!(
21098 union, expected,
21099 "the four-way typed closure `atomic ∪ structural ∪ quote_family` \
21100 = `{union:?}` MUST equal exactly the outer-Sexp cache-key \
21101 partition `{{0..=6}}` — a regression that drifts ANY of the four \
21102 primitives into an outer byte outside `{{0..=6}}` OR leaves \
21103 ANY outer byte unclaimed fails-loudly here",
21104 );
21105 }
21106
21107 #[test]
21108 fn sexp_shape_hash_discriminator_structural_arms_route_through_structural_kind() {
21109 // COMPOSITION-ROUTING CONTRACT (shape-level structural-residual
21110 // arm): for every `SexpShape` variant whose `as_structural_kind()`
21111 // projection returns `Some(sk)`, `SexpShape::hash_discriminator`
21112 // MUST agree byte-for-byte with `sk.hash_discriminator()` — the
21113 // two structural-residual arms (`Nil`, `List`) DELEGATE to the
21114 // typed sub-carving rather than inline two byte literals. Pin
21115 // the delegation-identity across the closed set so a regression
21116 // that inlines a byte at ONE arm (e.g. routes `SexpShape::Nil`
21117 // to `7u8` inline instead of through
21118 // `StructuralKind::Nil.hash_discriminator()`) fails-loudly here
21119 // — it would type-check but silently drift if the sub-algebra's
21120 // byte is renumbered. Sibling posture to the outer-`Sexp` axis
21121 // where `Sexp::Nil` / `Sexp::List(_)` also compose through
21122 // `StructuralKind::hash_discriminator` (pre-lift on that axis;
21123 // post-lift the same composition emerges through `self.shape()`
21124 // routing).
21125 for shape in SexpShape::ALL {
21126 if let Some(sk) = shape.as_structural_kind() {
21127 assert_eq!(
21128 shape.hash_discriminator(),
21129 sk.hash_discriminator(),
21130 "SexpShape::{shape:?} structural arm must delegate to StructuralKind::{sk:?}.hash_discriminator()",
21131 );
21132 }
21133 }
21134 }
21135
21136 #[test]
21137 fn sexp_shape_hash_discriminator_quote_family_arms_route_through_quote_form() {
21138 // COMPOSITION-ROUTING CONTRACT (shape-level quote-family arm):
21139 // for every `SexpShape` variant whose `as_quote_form()`
21140 // projection returns `Some(qf)`, `SexpShape::hash_discriminator`
21141 // MUST agree byte-for-byte with `qf.hash_discriminator()` — the
21142 // four quote-family arms DELEGATE to the sub-algebra's
21143 // discriminator method rather than inline four literals. Pin the
21144 // delegation-identity across the closed set so a regression that
21145 // inlines a byte at ONE arm (e.g. routes
21146 // `SexpShape::UnquoteSplice` to `6u8` inline instead of through
21147 // `QuoteForm::UnquoteSplice.hash_discriminator()`) fails-loudly
21148 // here — it would type-check but silently drift if the sub-
21149 // algebra's byte is renumbered. Sibling posture to
21150 // `sexp_hash_discriminator_quote_arm_delegates_to_quote_form_hash_discriminator`
21151 // on the outer-`Sexp` algebra — that pin binds the outer-Sexp
21152 // quote-arm delegation; this pin binds the shape-level quote-
21153 // family delegation.
21154 for shape in SexpShape::ALL {
21155 if let Some(qf) = shape.as_quote_form() {
21156 assert_eq!(
21157 shape.hash_discriminator(),
21158 qf.hash_discriminator(),
21159 "SexpShape::{shape:?} quote-family arm must delegate to QuoteForm::{qf:?}.hash_discriminator()",
21160 );
21161 }
21162 }
21163 }
21164
21165 #[test]
21166 fn sexp_shape_hash_discriminator_partitions_by_three_way_carving_disjointly() {
21167 // THREE-WAY CARVING PARTITION CONTRACT: for every `SexpShape`
21168 // variant, EXACTLY ONE of the three closed-set carvings
21169 // (`as_atom_kind`, `as_quote_form`, `as_structural_kind`)
21170 // projects to `Some(_)`, AND the outer discriminator byte the
21171 // shape-level `hash_discriminator` returns partitions across
21172 // the three carvings' image spaces: atomic → `{1}` (outer Atom
21173 // marker), quote-family → `{3, 4, 5, 6}`, structural → `{0, 2}`.
21174 // A regression that drifts a carving assignment (e.g. routes
21175 // `SexpShape::Nil` through `as_atom_kind()` due to a bogus
21176 // typed-shape lattice edit) fails-loudly here — the partition
21177 // completeness is a compile-time-verified theorem across the
21178 // three carvings via rustc's exhaustiveness on each carving's
21179 // `match`. Sibling posture to
21180 // `structural_kind_hash_discriminator_disjoint_from_atom_outer_carve_byte_and_quote_form_hash_discriminator_partition`
21181 // — that pin binds the sub-carving byte-space disjointness;
21182 // this pin binds the shape-level three-way carving assignment
21183 // AND its image-space partition.
21184 let atomic_image: std::collections::BTreeSet<u8> = SexpShape::ALL
21185 .iter()
21186 .filter(|s| s.as_atom_kind().is_some())
21187 .map(|s| s.hash_discriminator())
21188 .collect();
21189 let quote_image: std::collections::BTreeSet<u8> = SexpShape::ALL
21190 .iter()
21191 .filter(|s| s.as_quote_form().is_some())
21192 .map(|s| s.hash_discriminator())
21193 .collect();
21194 let structural_image: std::collections::BTreeSet<u8> = SexpShape::ALL
21195 .iter()
21196 .filter(|s| s.as_structural_kind().is_some())
21197 .map(|s| s.hash_discriminator())
21198 .collect();
21199 let expected_atomic: std::collections::BTreeSet<u8> =
21200 std::iter::once(crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR).collect();
21201 let expected_quote: std::collections::BTreeSet<u8> = (3u8..=6u8).collect();
21202 let expected_structural: std::collections::BTreeSet<u8> = [0u8, 2u8].into_iter().collect();
21203 assert_eq!(
21204 atomic_image, expected_atomic,
21205 "the atomic-shape carving's `hash_discriminator` image must be {{1}} (the outer Atom marker byte)",
21206 );
21207 assert_eq!(
21208 quote_image, expected_quote,
21209 "the quote-family carving's `hash_discriminator` image must be {{3, 4, 5, 6}}",
21210 );
21211 assert_eq!(
21212 structural_image, expected_structural,
21213 "the structural-residual carving's `hash_discriminator` image must be {{0, 2}}",
21214 );
21215 assert!(
21216 atomic_image.is_disjoint("e_image),
21217 "atomic and quote-family carvings' `hash_discriminator` images must be disjoint",
21218 );
21219 assert!(
21220 atomic_image.is_disjoint(&structural_image),
21221 "atomic and structural-residual carvings' `hash_discriminator` images must be disjoint",
21222 );
21223 assert!(
21224 quote_image.is_disjoint(&structural_image),
21225 "quote-family and structural-residual carvings' `hash_discriminator` images must be disjoint",
21226 );
21227 let mut union: std::collections::BTreeSet<u8> = atomic_image;
21228 union.extend("e_image);
21229 union.extend(&structural_image);
21230 let full_outer: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
21231 assert_eq!(
21232 union, full_outer,
21233 "the three-carving `hash_discriminator` image union must exactly cover {{0..=6}}",
21234 );
21235 }
21236
21237 #[test]
21238 fn sexp_shape_hash_discriminators_pin_legacy_outer_cache_key_bytes() {
21239 // Pin each per-role `pub(crate) const *_HASH_DISCRIMINATOR` on
21240 // `SexpShape` at its exact canonical outer-`Sexp` cache-key byte
21241 // — sibling of the pre-existing
21242 // `sexp_shape_hash_discriminator_pins_legacy_outer_cache_key_bytes`
21243 // (which pins the projection method's returns). Together the two
21244 // pins close the (per-role const, projection method) pairing on
21245 // the outer-shape algebra. Post-lift the twelve per-role bytes
21246 // live at ONE `pub(crate) const` per role on the `SexpShape`
21247 // algebra rather than at twelve inline `u8` literals scattered
21248 // across the projection method's match arms.
21249 assert_eq!(SexpShape::NIL_HASH_DISCRIMINATOR, 0);
21250 assert_eq!(SexpShape::SYMBOL_HASH_DISCRIMINATOR, 1);
21251 assert_eq!(SexpShape::KEYWORD_HASH_DISCRIMINATOR, 1);
21252 assert_eq!(SexpShape::STRING_HASH_DISCRIMINATOR, 1);
21253 assert_eq!(SexpShape::INT_HASH_DISCRIMINATOR, 1);
21254 assert_eq!(SexpShape::FLOAT_HASH_DISCRIMINATOR, 1);
21255 assert_eq!(SexpShape::BOOL_HASH_DISCRIMINATOR, 1);
21256 assert_eq!(SexpShape::LIST_HASH_DISCRIMINATOR, 2);
21257 assert_eq!(SexpShape::QUOTE_HASH_DISCRIMINATOR, 3);
21258 assert_eq!(SexpShape::QUASIQUOTE_HASH_DISCRIMINATOR, 4);
21259 assert_eq!(SexpShape::UNQUOTE_HASH_DISCRIMINATOR, 5);
21260 assert_eq!(SexpShape::UNQUOTE_SPLICE_HASH_DISCRIMINATOR, 6);
21261 }
21262
21263 #[test]
21264 fn sexp_shape_hash_discriminator_routes_through_typed_per_role_constants() {
21265 // PATH-UNIFORMITY (shape-level per-role): each of the twelve arms
21266 // of `SexpShape::hash_discriminator` MUST return the corresponding
21267 // `SexpShape::*_HASH_DISCRIMINATOR` byte-for-byte — catches a
21268 // regression that reverts ONE arm to an inline `u8` literal OR
21269 // to a `.hash_discriminator()` runtime dispatch through a sub-
21270 // carving variant (both of which type-check but bypass the typed
21271 // per-role primitive). Sibling posture to
21272 // `atom_kind_hash_discriminator_routes_through_typed_per_role_constants`,
21273 // `quote_form_hash_discriminator_routes_through_typed_per_role_constants`,
21274 // and `structural_kind_hash_discriminator_routes_through_typed_per_role_constants`
21275 // on the three sub-carvings — this pin closes the same routing
21276 // contract at the outer twelve-arm shape level.
21277 let pairs = [
21278 (SexpShape::Nil, SexpShape::NIL_HASH_DISCRIMINATOR),
21279 (SexpShape::Symbol, SexpShape::SYMBOL_HASH_DISCRIMINATOR),
21280 (SexpShape::Keyword, SexpShape::KEYWORD_HASH_DISCRIMINATOR),
21281 (SexpShape::String, SexpShape::STRING_HASH_DISCRIMINATOR),
21282 (SexpShape::Int, SexpShape::INT_HASH_DISCRIMINATOR),
21283 (SexpShape::Float, SexpShape::FLOAT_HASH_DISCRIMINATOR),
21284 (SexpShape::Bool, SexpShape::BOOL_HASH_DISCRIMINATOR),
21285 (SexpShape::List, SexpShape::LIST_HASH_DISCRIMINATOR),
21286 (SexpShape::Quote, SexpShape::QUOTE_HASH_DISCRIMINATOR),
21287 (
21288 SexpShape::Quasiquote,
21289 SexpShape::QUASIQUOTE_HASH_DISCRIMINATOR,
21290 ),
21291 (SexpShape::Unquote, SexpShape::UNQUOTE_HASH_DISCRIMINATOR),
21292 (
21293 SexpShape::UnquoteSplice,
21294 SexpShape::UNQUOTE_SPLICE_HASH_DISCRIMINATOR,
21295 ),
21296 ];
21297 for (shape, expected) in pairs {
21298 assert_eq!(
21299 shape.hash_discriminator(),
21300 expected,
21301 "SexpShape::hash_discriminator arm for {shape:?} must route through the typed \
21302 `pub(crate) const` per-role byte",
21303 );
21304 }
21305 }
21306
21307 #[test]
21308 fn sexp_shape_hash_discriminators_has_expected_cardinality() {
21309 // Cardinality contract: `Self::HASH_DISCRIMINATORS.len() == 12`
21310 // matching `Self::ALL.len() == 12`. Arity is forced at the
21311 // declaration site by rustc's `[u8; 12]` check; this runtime pin
21312 // fails-loud so a future refactor that switches the array type
21313 // to `&[u8]` doesn't silently loosen the closed-set discipline.
21314 // Sibling posture to `sexp_shape_labels_has_expected_cardinality`
21315 // on the diagnostic-label axis of the same twelve-variant closed
21316 // set.
21317 assert_eq!(SexpShape::HASH_DISCRIMINATORS.len(), 12);
21318 assert_eq!(
21319 SexpShape::HASH_DISCRIMINATORS.len(),
21320 SexpShape::ALL.len(),
21321 "SexpShape::HASH_DISCRIMINATORS cardinality drifted from SexpShape::ALL",
21322 );
21323 }
21324
21325 #[test]
21326 fn sexp_shape_hash_discriminators_align_with_all_by_index() {
21327 // ALIGNMENT CONTRACT: `Self::HASH_DISCRIMINATORS[i] ==
21328 // Self::ALL[i].hash_discriminator()` element-wise — the ALL
21329 // array's variant order MUST align with the HASH_DISCRIMINATORS
21330 // array's byte order. A regression that reorders ONE array
21331 // without reordering the other silently misaligns every
21332 // `zip(ALL, HASH_DISCRIMINATORS)` consumer. Sibling posture to
21333 // `structural_kind_hash_discriminators_align_with_all_by_index`,
21334 // `quote_form_hash_discriminators_align_with_all_by_index`, and
21335 // `atom_kind_hash_discriminators_align_with_all_by_index` on the
21336 // three sub-carvings — this pin closes the same alignment
21337 // contract at the outer twelve-arm shape level.
21338 for (i, shape) in SexpShape::ALL.iter().copied().enumerate() {
21339 let disc = shape.hash_discriminator();
21340 assert_eq!(
21341 SexpShape::HASH_DISCRIMINATORS[i],
21342 disc,
21343 "SexpShape::HASH_DISCRIMINATORS[{i}] `{disc}` != \
21344 SexpShape::ALL[{i}].hash_discriminator() `{disc}` — array-vs-projection drift",
21345 );
21346 }
21347 }
21348
21349 #[test]
21350 fn sexp_shape_hash_discriminators_cover_outer_partition_zero_through_six() {
21351 // PARTITION-COMPLETENESS CONTRACT (array level): the twelve-byte
21352 // set of `Self::HASH_DISCRIMINATORS` MUST cover exactly the
21353 // outer cache-key partition `{0..=6}` — no byte outside, no byte
21354 // inside missing. Sibling to
21355 // `sexp_shape_hash_discriminator_covers_outer_partition_zero_through_six`
21356 // (which closes the same partition through the projection method)
21357 // — this pin closes it at the ARRAY level so a regression that
21358 // drifts the array's contents without changing the method
21359 // surfaces here rather than only through the projection sweep.
21360 let bytes: std::collections::BTreeSet<u8> =
21361 SexpShape::HASH_DISCRIMINATORS.iter().copied().collect();
21362 let expected: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
21363 assert_eq!(
21364 bytes, expected,
21365 "SexpShape::HASH_DISCRIMINATORS must cover exactly the outer discriminator space {{0..=6}}",
21366 );
21367 }
21368
21369 #[test]
21370 fn sexp_shape_hash_discriminators_align_with_typed_per_role_constants_by_index() {
21371 // PER-ROLE ROUTING CONTRACT (family-wide array level): for every
21372 // `i in 0..12`, `SexpShape::HASH_DISCRIMINATORS[i]` MUST equal the
21373 // corresponding twelve-arm `SexpShape::*_HASH_DISCRIMINATOR`
21374 // per-role `pub(crate) const` at the SAME position in
21375 // `SexpShape::ALL`. Catches a regression that inlines byte
21376 // LITERALS in the `HASH_DISCRIMINATORS` array definition (e.g.
21377 // rewrites `[Self::NIL_HASH_DISCRIMINATOR, Self::SYMBOL_HASH_DISCRIMINATOR,
21378 // ...]` to `[0u8, 1u8, ...]`) — that regression would still
21379 // byte-equal the outer partition `{0..=6}`, still byte-equal the
21380 // sub-carving projections, and still byte-equal
21381 // `SexpShape::ALL[i].hash_discriminator()`, so every existing pin
21382 // (`_pin_legacy_outer_cache_key_bytes`, `_align_with_all_by_index`,
21383 // `_cover_outer_partition_zero_through_six`,
21384 // `_align_with_sub_carvings_by_projection`) would still PASS while
21385 // the FAMILY-WIDE array silently DRIFTED from the per-role
21386 // primitives it aliases through. Pin the alias-identity across
21387 // the twelve positions so the outer-shape ALL-array binds to the
21388 // per-role sources of truth at rustc-time PLUS runtime by ONE
21389 // typed pairing per position.
21390 //
21391 // Sibling posture to `sexp_shape_hash_discriminator_routes_through_typed_per_role_constants`
21392 // on the METHOD-side per-role routing axis — that pin binds the
21393 // twelve-arm projection method's per-role routing; this pin binds
21394 // the twelve-position family-wide array's per-role routing. Together
21395 // the two pins close the per-role routing contract at BOTH the
21396 // METHOD-side and the ARRAY-side of the shape algebra, in the SAME
21397 // shape as the sibling `SexpShape::LABELS` axis's
21398 // `sexp_shape_labels_align_with_all_by_index` pin (via
21399 // `SexpShape::ALL[i].label()`) — both axes now pin BOTH sides of
21400 // the (method, array) pair to the SAME per-role source of truth.
21401 let per_role = [
21402 SexpShape::NIL_HASH_DISCRIMINATOR,
21403 SexpShape::SYMBOL_HASH_DISCRIMINATOR,
21404 SexpShape::KEYWORD_HASH_DISCRIMINATOR,
21405 SexpShape::STRING_HASH_DISCRIMINATOR,
21406 SexpShape::INT_HASH_DISCRIMINATOR,
21407 SexpShape::FLOAT_HASH_DISCRIMINATOR,
21408 SexpShape::BOOL_HASH_DISCRIMINATOR,
21409 SexpShape::LIST_HASH_DISCRIMINATOR,
21410 SexpShape::QUOTE_HASH_DISCRIMINATOR,
21411 SexpShape::QUASIQUOTE_HASH_DISCRIMINATOR,
21412 SexpShape::UNQUOTE_HASH_DISCRIMINATOR,
21413 SexpShape::UNQUOTE_SPLICE_HASH_DISCRIMINATOR,
21414 ];
21415 assert_eq!(
21416 per_role.len(),
21417 SexpShape::HASH_DISCRIMINATORS.len(),
21418 "the twelve per-role SexpShape::*_HASH_DISCRIMINATOR constants \
21419 must cover the same cardinality as SexpShape::HASH_DISCRIMINATORS",
21420 );
21421 for (i, expected) in per_role.iter().copied().enumerate() {
21422 assert_eq!(
21423 SexpShape::HASH_DISCRIMINATORS[i],
21424 expected,
21425 "SexpShape::HASH_DISCRIMINATORS[{i}] `{actual}` drifted from \
21426 the per-role SexpShape::*_HASH_DISCRIMINATOR constant `{expected}` \
21427 for {shape:?} — the family-wide array MUST alias through the \
21428 per-role source of truth rather than inline a byte literal",
21429 actual = SexpShape::HASH_DISCRIMINATORS[i],
21430 shape = SexpShape::ALL[i],
21431 );
21432 }
21433 }
21434
21435 #[test]
21436 fn sexp_shape_hash_discriminators_align_with_sub_carvings_by_projection() {
21437 // CROSS-ALGEBRA COMPOSITION CONTRACT: for every `i in 0..12`,
21438 // `Self::HASH_DISCRIMINATORS[i]` MUST equal the byte the SUB-
21439 // CARVING projection returns for `Self::ALL[i]` — i.e. the
21440 // outer-shape ALL-array byte at every position is byte-identical
21441 // to (a) `AtomKind::OUTER_HASH_DISCRIMINATOR` on the atomic-
21442 // payload variants, (b) `StructuralKind::HASH_DISCRIMINATORS`'s
21443 // byte on structural-residual variants (routed via
21444 // `as_structural_kind`), and (c)
21445 // `QuoteForm::HASH_DISCRIMINATORS`'s byte on quote-family
21446 // variants (routed via `as_quote_form`). Closes the load-
21447 // bearing invariant that the outer-shape peer alias equals the
21448 // sub-carving's canonical byte at rustc-time through the `const`
21449 // initializer. A regression that drifts ONE sub-carving's byte
21450 // without updating the outer-shape peer alias fails to compile
21451 // (the `const` initializer references the same primitive); a
21452 // regression that drifts the OUTER alias byte-order in the
21453 // array fails-loudly here.
21454 for (i, shape) in SexpShape::ALL.iter().copied().enumerate() {
21455 let expected = if shape.as_atom_kind().is_some() {
21456 crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR
21457 } else if let Some(sk) = shape.as_structural_kind() {
21458 sk.hash_discriminator()
21459 } else if let Some(qf) = shape.as_quote_form() {
21460 qf.hash_discriminator()
21461 } else {
21462 unreachable!(
21463 "SexpShape::{shape:?} lies outside all three sub-carvings — \
21464 the three-carving partition (Atom + Structural + Quote) is not exhaustive",
21465 );
21466 };
21467 assert_eq!(
21468 SexpShape::HASH_DISCRIMINATORS[i], expected,
21469 "SexpShape::HASH_DISCRIMINATORS[{i}] must equal the sub-carving projection for {shape:?}",
21470 );
21471 }
21472 }
21473
21474 #[test]
21475 fn sexp_shape_iac_forge_tag_pins_canonical_cl_tags_for_every_quote_family_arm() {
21476 // CANONICAL-TAG CONTRACT (shape-level peer): the shape-level
21477 // `SexpShape::iac_forge_tag` MUST project each of the four
21478 // quote-family arms to the SAME canonical Common-Lisp tag
21479 // string `crate::ast::QuoteForm::iac_forge_tag` projects at the
21480 // sub-carving level — `Quote → "quote"`, `Quasiquote →
21481 // "quasiquote"`, `Unquote → "unquote"`, `UnquoteSplice →
21482 // "unquote-splicing"`. A regression that inlines a byte-drifted
21483 // spelling here (e.g. `SexpShape::UnquoteSplice → Some(
21484 // "unquote-splice")` conflating the substrate's shorter
21485 // diagnostic label with the CL canonical form) silently breaks
21486 // every cross-crate iac-forge consumer keyed on `(unquote-
21487 // splicing ...)`. Sibling posture to
21488 // `quote_form_iac_forge_tag_pins_canonical_lisp_tag_strings_for_every_variant`
21489 // (in `crate::ast`) on the 4-arm sub-carving — that pin binds
21490 // the sub-carving's canonical tag surface; this pin binds the
21491 // shape-level projection's canonical tag surface AND its
21492 // Option-partial shape.
21493 assert_eq!(SexpShape::Quote.iac_forge_tag(), Some("quote"));
21494 assert_eq!(SexpShape::Quasiquote.iac_forge_tag(), Some("quasiquote"));
21495 assert_eq!(SexpShape::Unquote.iac_forge_tag(), Some("unquote"));
21496 assert_eq!(
21497 SexpShape::UnquoteSplice.iac_forge_tag(),
21498 Some("unquote-splicing"),
21499 );
21500 }
21501
21502 #[test]
21503 fn sexp_shape_iac_forge_tag_returns_none_on_every_non_quote_family_shape() {
21504 // PARTIAL-PROJECTION KERNEL CONTRACT (shape-level peer): every
21505 // `SexpShape` variant OUTSIDE the four-arm quote-family carving
21506 // MUST project through `SexpShape::iac_forge_tag` to `None` —
21507 // the eight-shape kernel coincides byte-for-byte with the
21508 // kernel of `SexpShape::as_quote_form`. Pin the eight-shape
21509 // sweep (six atomic-payload variants + `Nil` + `List`) so a
21510 // regression that surfaces a bogus tag for a non-quote-family
21511 // arm (e.g. `SexpShape::List → Some("list")` conflating the
21512 // outer-shape diagnostic label with the quote-family canonical
21513 // form) fails-loudly here. Sibling posture to the projection's
21514 // parent-carving `sexp_shape_as_quote_form` returning `None`
21515 // on the eight-shape kernel.
21516 assert_eq!(SexpShape::Nil.iac_forge_tag(), None);
21517 assert_eq!(SexpShape::Symbol.iac_forge_tag(), None);
21518 assert_eq!(SexpShape::Keyword.iac_forge_tag(), None);
21519 assert_eq!(SexpShape::String.iac_forge_tag(), None);
21520 assert_eq!(SexpShape::Int.iac_forge_tag(), None);
21521 assert_eq!(SexpShape::Float.iac_forge_tag(), None);
21522 assert_eq!(SexpShape::Bool.iac_forge_tag(), None);
21523 assert_eq!(SexpShape::List.iac_forge_tag(), None);
21524 }
21525
21526 #[test]
21527 fn sexp_shape_iac_forge_tag_composes_through_as_quote_form_for_every_variant() {
21528 // COMPOSITION-LAW CONTRACT (shape-level peer):
21529 // `SexpShape::iac_forge_tag() ==
21530 // SexpShape::as_quote_form().map(QuoteForm::iac_forge_tag)` for
21531 // every variant in `SexpShape::ALL`. The (outer shape, canonical
21532 // iac-forge tag) pairing binds through the pre-existing quote-
21533 // family carving composed with the closed-set tag projection
21534 // rather than at a parallel four-arm inline match — this pin
21535 // enforces the composition-identity across the closed twelve-arm
21536 // sweep so a regression that drifts ONE arm's inline projection
21537 // from the composition (e.g. re-inlining `SexpShape::Quote →
21538 // Some("quote-old")` without updating `QuoteForm::iac_forge_tag`)
21539 // fails-loudly here. Sibling posture to
21540 // `sexp_shape_hash_discriminator_quote_family_arms_route_through_quote_form`
21541 // (delegation-identity on the byte-discriminator surface) — that
21542 // pin binds the sub-carving byte routing; this pin binds the
21543 // sub-carving tag routing.
21544 for shape in SexpShape::ALL {
21545 let via_method = shape.iac_forge_tag();
21546 let via_composition = shape
21547 .as_quote_form()
21548 .map(crate::ast::QuoteForm::iac_forge_tag);
21549 assert_eq!(
21550 via_method, via_composition,
21551 "SexpShape::{shape:?}.iac_forge_tag() drifted from \
21552 .as_quote_form().map(QuoteForm::iac_forge_tag) — the shape-level tag projection is no longer derived from the sub-carving's canonical site",
21553 );
21554 }
21555 }
21556
21557 #[test]
21558 fn sexp_shape_iac_forge_tag_partitions_quote_family_and_kernel_disjointly() {
21559 // IMAGE-PARTITION CONTRACT (shape-level peer): sweeping
21560 // `SexpShape::ALL` through `SexpShape::iac_forge_tag` MUST
21561 // partition into EXACTLY the four-arm quote-family image (four
21562 // distinct canonical CL tag strings — the pre-image of `Some(_)`)
21563 // AND the eight-shape kernel (all `None` — six atomic-payload
21564 // variants + `Nil` + `List`). The image's `is_some()` count
21565 // MUST be four (surjective onto the four-tag closed set), the
21566 // kernel's `is_none()` count MUST be eight, and the sum MUST
21567 // hit `SexpShape::ALL.len()` (twelve) — a regression that leaks
21568 // a bogus `Some(_)` from a non-quote-family arm or drops a
21569 // `Some(_)` from a quote-family arm fails-loudly here on the
21570 // partition-cardinality axis before any downstream iac-forge
21571 // consumer would surface the drift. Sibling posture to the
21572 // three-way carving `sexp_shape_hash_discriminator_partitions_by_three_way_carving_disjointly`
21573 // pinning the byte-image partition; this pin binds the shape-
21574 // level tag-image partition on the four-arm quote-family
21575 // carving.
21576 let tag_image: std::collections::BTreeSet<&'static str> = SexpShape::ALL
21577 .iter()
21578 .filter_map(|shape| shape.iac_forge_tag())
21579 .collect();
21580 let expected_tag_image: std::collections::BTreeSet<&'static str> =
21581 ["quote", "quasiquote", "unquote", "unquote-splicing"]
21582 .into_iter()
21583 .collect();
21584 assert_eq!(
21585 tag_image, expected_tag_image,
21586 "SexpShape::iac_forge_tag image must exactly cover the four canonical CL quote-family tags",
21587 );
21588 let some_count = SexpShape::ALL
21589 .iter()
21590 .filter(|shape| shape.iac_forge_tag().is_some())
21591 .count();
21592 assert_eq!(
21593 some_count, 4,
21594 "SexpShape::iac_forge_tag must return `Some(_)` on exactly the four-arm quote-family carving",
21595 );
21596 let none_count = SexpShape::ALL
21597 .iter()
21598 .filter(|shape| shape.iac_forge_tag().is_none())
21599 .count();
21600 assert_eq!(
21601 none_count, 8,
21602 "SexpShape::iac_forge_tag must return `None` on exactly the eight-shape non-quote-family kernel",
21603 );
21604 assert_eq!(
21605 some_count + none_count,
21606 SexpShape::ALL.len(),
21607 "SexpShape::iac_forge_tag's image + kernel must partition SexpShape::ALL exactly",
21608 );
21609 }
21610
21611 #[test]
21612 fn sexp_shape_prefix_pins_canonical_reader_prefixes_for_every_quote_family_arm() {
21613 // CANONICAL-PREFIX CONTRACT (shape-level peer): the shape-level
21614 // `SexpShape::prefix` MUST project each of the four quote-family
21615 // arms to the SAME canonical reader-punctuation string
21616 // `crate::ast::QuoteForm::prefix` projects at the sub-carving
21617 // level — `Quote → "'"`, `Quasiquote → "`"`, `Unquote → ","`,
21618 // `UnquoteSplice → ",@"`. A regression that inlines a byte-
21619 // drifted spelling here (e.g. `SexpShape::Quote → Some("`")`
21620 // swapping the single-quote and backtick prefixes, or
21621 // `SexpShape::UnquoteSplice → Some("@,")` swapping the two-byte
21622 // splice prefix's byte order) silently breaks every reader /
21623 // LSP / REPL consumer keyed on the source-punctuation
21624 // vocabulary. Sibling posture to
21625 // `sexp_shape_iac_forge_tag_pins_canonical_cl_tags_for_every_quote_family_arm`
21626 // on the cross-crate canonical-form axis — that pin binds the
21627 // iac-forge tag surface; this pin binds the reader-punctuation
21628 // prefix surface AND its Option-partial shape.
21629 assert_eq!(SexpShape::Quote.prefix(), Some("'"));
21630 assert_eq!(SexpShape::Quasiquote.prefix(), Some("`"));
21631 assert_eq!(SexpShape::Unquote.prefix(), Some(","));
21632 assert_eq!(SexpShape::UnquoteSplice.prefix(), Some(",@"));
21633 }
21634
21635 #[test]
21636 fn sexp_shape_prefix_returns_none_on_every_non_quote_family_shape() {
21637 // PARTIAL-PROJECTION KERNEL CONTRACT (shape-level peer): every
21638 // `SexpShape` variant OUTSIDE the four-arm quote-family carving
21639 // MUST project through `SexpShape::prefix` to `None` — the
21640 // eight-shape kernel coincides byte-for-byte with the kernel of
21641 // `SexpShape::as_quote_form` AND `SexpShape::iac_forge_tag`
21642 // (there is NO homoiconic reader-punctuation prefix for the
21643 // atomic-payload variants `Symbol`/`Keyword`/`String`/`Int`/
21644 // `Float`/`Bool` — those render via their own atomic syntax —
21645 // nor for the structural residuals `Nil`/`List` which render as
21646 // `nil`/`(...)` respectively, neither via a prefix character).
21647 // Pin the eight-shape sweep so a regression that surfaces a
21648 // bogus prefix for a non-quote-family arm (e.g. `SexpShape::Nil
21649 // → Some("()")` conflating the structural-residual Display with
21650 // the reader-punctuation vocabulary) fails-loudly here. Sibling
21651 // posture to
21652 // `sexp_shape_iac_forge_tag_returns_none_on_every_non_quote_family_shape`
21653 // one vocabulary axis over.
21654 assert_eq!(SexpShape::Nil.prefix(), None);
21655 assert_eq!(SexpShape::Symbol.prefix(), None);
21656 assert_eq!(SexpShape::Keyword.prefix(), None);
21657 assert_eq!(SexpShape::String.prefix(), None);
21658 assert_eq!(SexpShape::Int.prefix(), None);
21659 assert_eq!(SexpShape::Float.prefix(), None);
21660 assert_eq!(SexpShape::Bool.prefix(), None);
21661 assert_eq!(SexpShape::List.prefix(), None);
21662 }
21663
21664 #[test]
21665 fn sexp_shape_prefix_composes_through_as_quote_form_for_every_variant() {
21666 // COMPOSITION-LAW CONTRACT (shape-level peer): `SexpShape::prefix()
21667 // == SexpShape::as_quote_form().map(QuoteForm::prefix)` for every
21668 // variant in `SexpShape::ALL`. The (outer shape, reader-
21669 // punctuation prefix) pairing binds through the pre-existing
21670 // quote-family carving composed with the closed-set prefix
21671 // projection rather than at a parallel four-arm inline match —
21672 // this pin enforces the composition-identity across the closed
21673 // twelve-arm sweep so a regression that drifts ONE arm's inline
21674 // projection from the composition (e.g. re-inlining
21675 // `SexpShape::Quote → Some("’")` — a fancy-quote look-alike —
21676 // without updating `QuoteForm::prefix`) fails-loudly here.
21677 // Sibling posture to
21678 // `sexp_shape_iac_forge_tag_composes_through_as_quote_form_for_every_variant`
21679 // one vocabulary axis over.
21680 for shape in SexpShape::ALL {
21681 let via_method = shape.prefix();
21682 let via_composition = shape.as_quote_form().map(crate::ast::QuoteForm::prefix);
21683 assert_eq!(
21684 via_method, via_composition,
21685 "SexpShape::{shape:?}.prefix() drifted from \
21686 .as_quote_form().map(QuoteForm::prefix) — the shape-level prefix projection is no longer derived from the sub-carving's canonical site",
21687 );
21688 }
21689 }
21690
21691 #[test]
21692 fn sexp_shape_prefix_partitions_quote_family_and_kernel_disjointly() {
21693 // IMAGE-PARTITION CONTRACT (shape-level peer): sweeping
21694 // `SexpShape::ALL` through `SexpShape::prefix` MUST partition
21695 // into EXACTLY the four-arm quote-family image (four distinct
21696 // canonical reader-punctuation strings — the pre-image of
21697 // `Some(_)`) AND the eight-shape kernel (all `None` — six
21698 // atomic-payload variants + `Nil` + `List`). The image's
21699 // `is_some()` count MUST be four (surjective onto the four-
21700 // prefix closed set), the kernel's `is_none()` count MUST be
21701 // eight, and the sum MUST hit `SexpShape::ALL.len()` (twelve)
21702 // — a regression that leaks a bogus `Some(_)` from a non-
21703 // quote-family arm or drops a `Some(_)` from a quote-family
21704 // arm fails-loudly here on the partition-cardinality axis
21705 // before any downstream reader / LSP / REPL consumer would
21706 // surface the drift. Sibling posture to
21707 // `sexp_shape_iac_forge_tag_partitions_quote_family_and_kernel_disjointly`
21708 // pinning the tag-image partition; this pin binds the shape-
21709 // level prefix-image partition on the four-arm quote-family
21710 // carving. Disjoint vocabulary from the iac-forge partition
21711 // (`"'"` / `"`"` / `","` / `",@"` vs. `"quote"` / `"quasiquote"`
21712 // / `"unquote"` / `"unquote-splicing"`) — the same closed twelve-
21713 // arm shape algebra carries THREE distinct `&'static str`
21714 // vocabularies keyed on the same four-arm quote-family carving,
21715 // and a regression that conflates any two lands here.
21716 let prefix_image: std::collections::BTreeSet<&'static str> = SexpShape::ALL
21717 .iter()
21718 .filter_map(|shape| shape.prefix())
21719 .collect();
21720 let expected_prefix_image: std::collections::BTreeSet<&'static str> =
21721 ["'", "`", ",", ",@"].into_iter().collect();
21722 assert_eq!(
21723 prefix_image, expected_prefix_image,
21724 "SexpShape::prefix image must exactly cover the four canonical reader-punctuation quote-family prefixes",
21725 );
21726 let some_count = SexpShape::ALL
21727 .iter()
21728 .filter(|shape| shape.prefix().is_some())
21729 .count();
21730 assert_eq!(
21731 some_count, 4,
21732 "SexpShape::prefix must return `Some(_)` on exactly the four-arm quote-family carving",
21733 );
21734 let none_count = SexpShape::ALL
21735 .iter()
21736 .filter(|shape| shape.prefix().is_none())
21737 .count();
21738 assert_eq!(
21739 none_count, 8,
21740 "SexpShape::prefix must return `None` on exactly the eight-shape non-quote-family kernel",
21741 );
21742 assert_eq!(
21743 some_count + none_count,
21744 SexpShape::ALL.len(),
21745 "SexpShape::prefix's image + kernel must partition SexpShape::ALL exactly",
21746 );
21747 }
21748
21749 /// Runtime cross-check for the 13 module-level `const _: () =
21750 /// crate::ast::assert_str_array_pairwise_distinct(&…)` witnesses
21751 /// declared at the top of `error.rs`. Runs the same const-fn
21752 /// helper on every family-wide `[&'static str; N]` array on this
21753 /// module's closed-set diagnostic / classification algebras at
21754 /// `cargo test` time — the runtime path pairs the compile-time
21755 /// path so a build that runs tests catches the regression a
21756 /// second time as a safety net if the const-eval sweep is ever
21757 /// silently dropped.
21758 ///
21759 /// Sibling to
21760 /// `assert_str_array_pairwise_distinct_accepts_every_family_
21761 /// wide_substrate_array` on `ast.rs`'s tests module — that
21762 /// pair covers the FIVE family-wide `[&'static str; N]` arrays
21763 /// on `ast.rs`'s closed-set outer algebras (`Atom::BOOL_LITERALS`,
21764 /// `AtomKind::LABELS`, `QuoteForm::PREFIXES`,
21765 /// `QuoteForm::IAC_FORGE_TAGS`, `QuoteForm::LABELS`); this test
21766 /// covers the THIRTEEN peer arrays on `error.rs`'s closed-set
21767 /// diagnostic / classification algebras. Together the two tests
21768 /// runtime-verify every ONE of the workspace-wide EIGHTEEN
21769 /// pairwise-distinct family-wide `[&'static str; N]` arrays
21770 /// through the SAME `assert_str_array_pairwise_distinct` const-
21771 /// fn primitive.
21772 ///
21773 /// `CompilerSpecIoStage::OPERATIONS` (`[&'static str; 4]`) is
21774 /// intentionally omitted — it is a variant-aligned projection
21775 /// with two-fold duplicated entries (`[REALIZE_TO_DISK,
21776 /// REALIZE_TO_DISK, LOAD_FROM_DISK, LOAD_FROM_DISK]`), so
21777 /// enrolling it here would panic; the intended invariant on
21778 /// `OPERATIONS` is "variant-index alignment with
21779 /// `CompilerSpecIoStage::ALL`", a distinct theorem covered by
21780 /// its own tests.
21781 #[test]
21782 fn assert_str_array_pairwise_distinct_accepts_every_family_wide_error_module_array() {
21783 use crate::ast::assert_str_array_pairwise_distinct;
21784 assert_str_array_pairwise_distinct(&CompilerSpecIoStage::LABELS);
21785 assert_str_array_pairwise_distinct(&TemplateInvariantKind::STATIC_MESSAGES);
21786 assert_str_array_pairwise_distinct(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
21787 assert_str_array_pairwise_distinct(&MacroDefHead::KEYWORDS);
21788 assert_str_array_pairwise_distinct(&OptionalParamMalformedReason::STATIC_LABELS);
21789 assert_str_array_pairwise_distinct(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
21790 assert_str_array_pairwise_distinct(&UnquoteForm::MARKERS);
21791 assert_str_array_pairwise_distinct(&UnquoteForm::IAC_FORGE_TAGS);
21792 assert_str_array_pairwise_distinct(&UnquoteForm::LABELS);
21793 assert_str_array_pairwise_distinct(&KwargPathKind::LABELS);
21794 assert_str_array_pairwise_distinct(&ExpectedKwargShape::LABELS);
21795 assert_str_array_pairwise_distinct(&SexpShape::LABELS);
21796 assert_str_array_pairwise_distinct(&StructuralKind::LABELS);
21797 }
21798
21799 /// Runtime cross-check that the SAME thirteen family-wide
21800 /// `[&'static str; N]` arrays declared under this module that
21801 /// the compile-time `const _: () = crate::ast::
21802 /// assert_str_array_all_ascii(&…)` witness block above pins at
21803 /// `cargo check` time are all-ASCII at runtime too. Sibling to
21804 /// `assert_str_array_pairwise_distinct_accepts_every_family_
21805 /// wide_error_module_array` on the (contract-shape) column —
21806 /// the two together pin BOTH the INJECTIVITY axis AND the
21807 /// ASCII-BYTE-RANGE axis on the SAME thirteen arrays, at TWO
21808 /// stages of the toolchain (compile-time `const _` line + this
21809 /// runtime safety-net). Includes `CompilerSpecIoStage::
21810 /// OPERATIONS` — which is DELIBERATELY excluded from the
21811 /// pairwise-distinct sibling because its four entries are
21812 /// intentionally-collapsed onto two per-operation labels but
21813 /// is enrolled here because its two distinct entries
21814 /// (`"realize_to_disk"`, `"load_from_disk"`) are ASCII by
21815 /// construction.
21816 #[test]
21817 fn assert_str_array_all_ascii_accepts_every_family_wide_error_module_array() {
21818 use crate::ast::assert_str_array_all_ascii;
21819 assert_str_array_all_ascii(&CompilerSpecIoStage::LABELS);
21820 assert_str_array_all_ascii(&CompilerSpecIoStage::OPERATIONS);
21821 assert_str_array_all_ascii(&TemplateInvariantKind::STATIC_MESSAGES);
21822 assert_str_array_all_ascii(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
21823 assert_str_array_all_ascii(&MacroDefHead::KEYWORDS);
21824 assert_str_array_all_ascii(&OptionalParamMalformedReason::STATIC_LABELS);
21825 assert_str_array_all_ascii(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
21826 assert_str_array_all_ascii(&UnquoteForm::MARKERS);
21827 assert_str_array_all_ascii(&UnquoteForm::IAC_FORGE_TAGS);
21828 assert_str_array_all_ascii(&UnquoteForm::LABELS);
21829 assert_str_array_all_ascii(&KwargPathKind::LABELS);
21830 assert_str_array_all_ascii(&ExpectedKwargShape::LABELS);
21831 assert_str_array_all_ascii(&SexpShape::LABELS);
21832 assert_str_array_all_ascii(&StructuralKind::LABELS);
21833 }
21834
21835 /// Confirm `CompilerSpecIoStage::OPERATIONS` — the ONE family-
21836 /// wide `[&'static str; N]` array on this module that is
21837 /// DELIBERATELY excluded from the compile-time pairwise-
21838 /// distinctness witness block above — panics on the same helper
21839 /// (`crate::ast::assert_str_array_pairwise_distinct`) at
21840 /// runtime. Two positional pairs collide byte-for-byte on this
21841 /// array by design: positions 0+1 (both
21842 /// `Self::REALIZE_TO_DISK_OPERATION` = `"realize_to_disk"`) AND
21843 /// positions 2+3 (both `Self::LOAD_FROM_DISK_OPERATION` =
21844 /// `"load_from_disk"`), and the array's intended invariant is
21845 /// variant-index alignment with `Self::ALL`, not injectivity.
21846 ///
21847 /// This test doubles as a documented boundary marker — if a
21848 /// future refactor DID sharpen `OPERATIONS` to a distinct set
21849 /// (e.g. by carving the two-per-operation collapse into a
21850 /// separate `[&'static str; 2]` array), enrolling the sharpened
21851 /// array in the `const _` block above would then be legal, and
21852 /// this test would need updating to match. Pinning the "is
21853 /// intentionally non-distinct" property here surfaces that
21854 /// refactor at test time rather than leaving it as a silent
21855 /// module-level tribal-knowledge assumption.
21856 #[test]
21857 #[should_panic(expected = "assert_str_array_pairwise_distinct")]
21858 fn assert_str_array_pairwise_distinct_panics_on_compiler_spec_io_stage_operations() {
21859 use crate::ast::assert_str_array_pairwise_distinct;
21860 assert_str_array_pairwise_distinct(&CompilerSpecIoStage::OPERATIONS);
21861 }
21862
21863 /// Runtime cross-check that the TWO
21864 /// (`_`, `StructuralKind::LABELS`) disjointness pairs the
21865 /// module-level `const _: () = crate::ast::assert_str_arrays_
21866 /// disjoint::<N, 2>(...)` witnesses pin at COMPILE time are
21867 /// proper DISJOINTNESS embeddings at runtime too. Peer to
21868 /// `assert_str_arrays_disjoint_accepts_each_family_wide_
21869 /// substrate_pair` in `ast.rs` (which sweeps the FOUR pairs
21870 /// whose two hosts both live in `ast.rs`): together the two
21871 /// tests sweep all SIX substrate-pinned `&'static str`
21872 /// disjointness pairs (`ast.rs` × 4 + `error.rs` × 2 = 6) at
21873 /// runtime as a safety net. The compile-time `const _` sweep
21874 /// fires FIRST at `cargo check`; this test catches drift again
21875 /// at `cargo test` if the const-eval sweep is ever silently
21876 /// dropped.
21877 ///
21878 /// Composed with the ast.rs-pinned
21879 /// (`AtomKind::LABELS`, `QuoteForm::LABELS`) pair — the third
21880 /// pair on the three-element sub-vocabulary triple — these two
21881 /// checks close the DISJOINT-UNION theorem
21882 /// `SexpShape::LABELS ≡ AtomKind::LABELS ⊕ QuoteForm::LABELS ⊕
21883 /// StructuralKind::LABELS` at runtime as the sibling proof to
21884 /// the three subset-embedding witnesses on the same triple.
21885 #[test]
21886 fn assert_str_arrays_disjoint_accepts_each_structural_kind_partition_pair() {
21887 use crate::ast::assert_str_arrays_disjoint;
21888 assert_str_arrays_disjoint::<6, 2>(&crate::ast::AtomKind::LABELS, &StructuralKind::LABELS);
21889 assert_str_arrays_disjoint::<4, 2>(&crate::ast::QuoteForm::LABELS, &StructuralKind::LABELS);
21890 }
21891
21892 /// Runtime safety-net sibling of the module-level compile-time
21893 /// STR-DISJOINTNESS witnesses on the STATIC ⊕ DYNAMIC 2-of-2 face
21894 /// of the substrate's TWO peer diagnostic algebras
21895 /// (`TemplateInvariantKind` at 2 ⊕ 2 asymmetric widths;
21896 /// `OptionalParamMalformedReason` at 3 ⊕ 1 asymmetric widths).
21897 /// Re-runs each `const _: () = crate::ast::assert_str_arrays_
21898 /// disjoint::<N, M>(&<algebra>::STATIC_*, &<algebra>::DYNAMIC_
21899 /// DESCRIPTORS)` witness pinned above the `LispError` enum at
21900 /// runtime as a safety net so a build that skips `cargo check`'s
21901 /// const-eval sweep still catches drift here. Peer to
21902 /// `assert_str_arrays_disjoint_accepts_each_structural_kind_partition_pair`
21903 /// above (which sweeps the SexpShape 3-way sub-vocabulary triple's
21904 /// two `error.rs`-hosted disjointness pairs); together the two
21905 /// tests sweep all FOUR `error.rs`-hosted `assert_str_arrays_
21906 /// disjoint` witnesses at runtime. The compile-time `const _`
21907 /// sweep at the top of this module fires FIRST at `cargo check`;
21908 /// this test catches drift again at `cargo test` if the const-
21909 /// eval sweep is ever silently dropped.
21910 #[test]
21911 fn assert_str_arrays_disjoint_accepts_each_static_dynamic_carving_pair() {
21912 use crate::ast::assert_str_arrays_disjoint;
21913 assert_str_arrays_disjoint::<2, 2>(
21914 &TemplateInvariantKind::STATIC_MESSAGES,
21915 &TemplateInvariantKind::DYNAMIC_DESCRIPTORS,
21916 );
21917 assert_str_arrays_disjoint::<3, 1>(
21918 &OptionalParamMalformedReason::STATIC_LABELS,
21919 &OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
21920 );
21921 }
21922
21923 /// Runtime safety-net sibling of the module-level compile-time
21924 /// STR-BLOCK-CONSTANCY witness on the MANY-TO-ONE variant →
21925 /// canonical-projection surface at
21926 /// `CompilerSpecIoStage::OPERATIONS`. Re-runs the `const _: () =
21927 /// crate::ast::assert_str_array_is_concatenation_of_two_scalar_
21928 /// replicas::<4, 2>(&OPERATIONS, REALIZE_TO_DISK_OPERATION,
21929 /// LOAD_FROM_DISK_OPERATION)` witness pinned above the
21930 /// `LispError` enum at runtime as a safety net so a build that
21931 /// skips `cargo check`'s const-eval sweep still catches the
21932 /// block-constant drift here. Peer to
21933 /// `compiler_spec_io_stage_operations_align_with_all_by_index`
21934 /// (which pins per-position projection equality through
21935 /// `stage.operation()`) and to
21936 /// `compiler_spec_io_stage_operations_partition_all_two_ways`
21937 /// (which pins the two operation-label multiplicities) — the
21938 /// three tests jointly sweep the block-constant partition at
21939 /// THREE distinct runtime angles (per-position equality with
21940 /// ALL, multiplicity count of each block scalar, ARRAY-LEVEL
21941 /// block-constancy structural pattern) while the compile-time
21942 /// witness above binds the ARRAY-LEVEL structural pattern at
21943 /// `cargo check` time as the FIRST-firing arm.
21944 #[test]
21945 fn assert_str_array_is_concatenation_of_two_scalar_replicas_accepts_compiler_spec_io_stage_operations(
21946 ) {
21947 use crate::ast::assert_str_array_is_concatenation_of_two_scalar_replicas;
21948 assert_str_array_is_concatenation_of_two_scalar_replicas::<4, 2>(
21949 &CompilerSpecIoStage::OPERATIONS,
21950 CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
21951 CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
21952 );
21953 }
21954
21955 /// Runtime cross-check that the SAME five (str)-row FULL-ARRAY
21956 /// LITERAL witnesses covered at COMPILE time by the module-level
21957 /// `const _: () = crate::ast::assert_str_array_slice_equals_str_
21958 /// array::<N, N, 0>(&…, &[literal strs; N])` cluster immediately
21959 /// below the `assert_str_array_is_concatenation_of_two_scalar_
21960 /// replicas::<4, 2>(&CompilerSpecIoStage::OPERATIONS, …)` block
21961 /// witness — pinning `ExpectedKwargShape::LABELS`,
21962 /// `KwargPathKind::LABELS`, `MacroDefHead::KEYWORDS`,
21963 /// `CompilerSpecIoStage::LABELS`, and `StructuralKind::LABELS`
21964 /// against their canonical literal-str listings — also hold when
21965 /// exercised at runtime. A regression that removes ONE of the
21966 /// const witnesses would still leave THIS runtime pin as a
21967 /// safety net; the const witness fires FIRST at `cargo check`,
21968 /// this runtime pin catches the positionwise drift at `cargo
21969 /// test`. The pair enforces the theorem at TWO stages of the
21970 /// toolchain.
21971 ///
21972 /// Sibling posture to
21973 /// `assert_str_array_slice_equals_str_array_accepts_every_family_wide_substrate_array_full_array_literal_listing`
21974 /// in `ast.rs` (the ast-side (str)-row cluster's runtime peer
21975 /// on `Atom::BOOL_LITERALS` / `AtomKind::LABELS` /
21976 /// `QuoteForm::PREFIXES` / `QuoteForm::LABELS` /
21977 /// `QuoteForm::IAC_FORGE_TAGS`) — together the two runtime
21978 /// tests sweep TEN family-wide `[&'static str; N]` label
21979 /// vocabularies on the substrate at `cargo test` time as
21980 /// safety-net peers to the ten compile-time `const _` witnesses.
21981 ///
21982 /// A regression that (a) reorders one of the outer arrays' slots
21983 /// away from canonical declaration order, or (b) drifts one of
21984 /// the per-role scalar `pub const *_LABEL` / `*_KEYWORD` aliases
21985 /// the outer array's slots re-export, fails HERE with the
21986 /// `STR-SLICE-EQUALS-ARRAY-VIOLATION` axis panic naming the
21987 /// drifted position.
21988 #[test]
21989 fn assert_str_array_slice_equals_str_array_accepts_every_family_wide_error_module_array_full_array_literal_listing(
21990 ) {
21991 use crate::ast::assert_str_array_slice_equals_str_array;
21992 assert_str_array_slice_equals_str_array::<7, 7, 0>(
21993 &ExpectedKwargShape::LABELS,
21994 &[
21995 "keyword",
21996 "string",
21997 "int",
21998 "number",
21999 "bool",
22000 "list",
22001 "list of strings",
22002 ],
22003 );
22004 assert_str_array_slice_equals_str_array::<3, 3, 0>(
22005 &KwargPathKind::LABELS,
22006 &["named", "item", "slot"],
22007 );
22008 assert_str_array_slice_equals_str_array::<3, 3, 0>(
22009 &MacroDefHead::KEYWORDS,
22010 &["defmacro", "defpoint-template", "defcheck"],
22011 );
22012 assert_str_array_slice_equals_str_array::<4, 4, 0>(
22013 &CompilerSpecIoStage::LABELS,
22014 &["serialize", "write", "read", "deserialize"],
22015 );
22016 assert_str_array_slice_equals_str_array::<2, 2, 0>(
22017 &StructuralKind::LABELS,
22018 &["nil", "list"],
22019 );
22020 }
22021
22022 /// Runtime DISJOINT-UNION theorem — the twelve-arm outer
22023 /// `SexpShape::LABELS` array is byte-for-byte the union of the
22024 /// three sub-vocabulary arrays (`AtomKind::LABELS` ∪
22025 /// `QuoteForm::LABELS` ∪ `StructuralKind::LABELS`) up to
22026 /// permutation, sweeping both directions of the SET-equality
22027 /// obligation:
22028 ///
22029 /// (⊆) every outer label appears in EXACTLY ONE sub-
22030 /// vocabulary (existence + uniqueness);
22031 /// (⊇) every sub-vocabulary label appears in the outer
22032 /// vocabulary (SUBSET-embedding, already pinned above at
22033 /// rustc time by the three `assert_str_array_within_str_
22034 /// finite_set` witnesses; swept here again as a runtime
22035 /// safety net).
22036 ///
22037 /// Cardinality composition (6 + 4 + 2 = 12) is enforced
22038 /// STRUCTURALLY by rustc through the `[&'static str; N]` array
22039 /// arities; this test pins the byte-level partition INDEPENDENT
22040 /// of the sub-vocabulary array declarations, so a regression
22041 /// that silently split a two-arm sub-vocabulary into
22042 /// (`[&; 1]`, `[&; 1]`) while keeping the total at 12 would
22043 /// still catch here even if the arity sum remained legal.
22044 #[test]
22045 fn sexp_shape_labels_is_disjoint_union_of_three_sub_vocabularies() {
22046 for outer in SexpShape::LABELS.iter() {
22047 let hits_atom = crate::ast::AtomKind::LABELS
22048 .iter()
22049 .filter(|s| s == &outer)
22050 .count();
22051 let hits_quote = crate::ast::QuoteForm::LABELS
22052 .iter()
22053 .filter(|s| s == &outer)
22054 .count();
22055 let hits_struct = StructuralKind::LABELS
22056 .iter()
22057 .filter(|s| s == &outer)
22058 .count();
22059 let total = hits_atom + hits_quote + hits_struct;
22060 assert_eq!(
22061 total, 1,
22062 "outer SexpShape label {outer:?} must appear in EXACTLY ONE \
22063 sub-vocabulary (atom={hits_atom}, quote={hits_quote}, \
22064 struct={hits_struct}); the twelve-arm partition triple \
22065 (AtomKind::LABELS, QuoteForm::LABELS, StructuralKind::LABELS) \
22066 must be a DISJOINT UNION of SexpShape::LABELS"
22067 );
22068 }
22069 for inner in crate::ast::AtomKind::LABELS
22070 .iter()
22071 .chain(crate::ast::QuoteForm::LABELS.iter())
22072 .chain(StructuralKind::LABELS.iter())
22073 {
22074 assert!(
22075 SexpShape::LABELS.iter().any(|s| s == inner),
22076 "sub-vocabulary label {inner:?} must be embedded in the \
22077 outer SexpShape::LABELS vocabulary — the (⊇) direction \
22078 of the disjoint-union theorem"
22079 );
22080 }
22081 }
22082
22083 /// Runtime SLICE-EQUALS-ARRAY safety net for the (UnquoteForm ⊂
22084 /// QuoteForm) 2-of-4 sub-carve on the three sibling `[&'static str;
22085 /// N]` vocabulary axes — sweeps the same three
22086 /// `assert_str_array_slice_equals_str_array::<4, 2, 2>` invocations
22087 /// pinned at compile time in the module-level `const _` block
22088 /// immediately below the peer SUBSET-embedding witness cluster.
22089 /// The `const _` witnesses fire FIRST at `cargo check`; this
22090 /// runtime pin catches the ARRAY-LEVEL positionwise drift at
22091 /// `cargo test` as a safety net enforcing the theorem at BOTH
22092 /// stages of the toolchain.
22093 ///
22094 /// A regression that (a) swaps the two per-role slot bindings in
22095 /// one of the `UnquoteForm::MARKERS` / `LABELS` / `IAC_FORGE_TAGS`
22096 /// initializers, or (b) reorders the four-arm `QuoteForm::PREFIXES`
22097 /// / `LABELS` / `IAC_FORGE_TAGS` superset such that the two
22098 /// UnquoteForm-family arms no longer sit contiguously at slots
22099 /// `[2..4)`, fails HERE with the `STR-SLICE-EQUALS-ARRAY-VIOLATION`
22100 /// axis panic naming the drifted position — where the sibling
22101 /// SUBSET-embedding safety-net pins stay silent (both aliases still
22102 /// appear in the superset, just at different positions).
22103 #[test]
22104 fn assert_str_array_slice_equals_str_array_accepts_unquote_form_sub_carve_of_quote_form() {
22105 use crate::ast::assert_str_array_slice_equals_str_array;
22106 assert_str_array_slice_equals_str_array::<4, 2, 2>(
22107 &crate::ast::QuoteForm::LABELS,
22108 &UnquoteForm::LABELS,
22109 );
22110 assert_str_array_slice_equals_str_array::<4, 2, 2>(
22111 &crate::ast::QuoteForm::PREFIXES,
22112 &UnquoteForm::MARKERS,
22113 );
22114 assert_str_array_slice_equals_str_array::<4, 2, 2>(
22115 &crate::ast::QuoteForm::IAC_FORGE_TAGS,
22116 &UnquoteForm::IAC_FORGE_TAGS,
22117 );
22118 }
22119}