Skip to main content

tatara_lisp/
macro_expand.rs

1//! Macro expander — rewrites `defmacro` / `defpoint-template` calls into
2//! their quasi-quoted templates.
3//!
4//! Semantics (v0, no evaluator):
5//!
6//! ```lisp
7//! (defmacro wrap (x) `(list ,x ,x))      ; or defpoint-template
8//! (wrap hello)                            ; expands to (list hello hello)
9//! ```
10//!
11//! Supported:
12//!   - Required params:      `(name a b c)`
13//!   - Optional params:      `(name a &optional b c)` — unsupplied bind to `()`
14//!   - Rest param:           `(name a &rest rest)`
15//!   - Quasi-quote body:     `` `(…) ``
16//!   - Unquote substitution: `,x`
17//!   - Splice substitution:  `,@x` (splices a bound list into the outer list)
18//!   - Recursive expansion: macro bodies may call other macros.
19//!
20//! Not yet supported (no evaluator):
21//!   - Arbitrary expressions under `,` — only bound symbol lookups.
22//!   - Nested quasi-quotes.
23//!   - Hygiene / gensym — param names capture aggressively.
24
25use std::cmp::Ordering;
26use std::collections::hash_map::DefaultHasher;
27use std::collections::HashMap;
28use std::hash::{Hash, Hasher};
29use std::sync::{Arc, Mutex};
30
31use crate::ast::Sexp;
32use crate::error::{LispError, MacroDefHead, Result, TemplateInvariantKind, UnquoteForm};
33
34/// Default ceiling on `Expander::expand`'s recursive re-entry into a
35/// macro-call form. A macro whose expansion contains another call to
36/// itself — the canonical runaway `(defmacro loop (x) `(loop ,x))` —
37/// pre-lift stack-overflowed the process (a below-floor abort with no
38/// `Result` witness). The ceiling turns that abort into a
39/// [`LispError::ExpansionDepthExceeded`] rejection at the expander
40/// boundary. `256` is large enough that no lawful hand-authored
41/// expansion nesting hits it (nested `when` / `let` / template
42/// compositions cap out in single digits in practice) and small
43/// enough that a runaway is rejected in microseconds instead of
44/// consuming a process stack. Consumers that need a tighter or looser
45/// ceiling call [`Expander::set_max_expansion_depth`].
46pub const DEFAULT_MAX_EXPANSION_DEPTH: usize = 256;
47
48/// Default ceiling on the expansion cache's entry count. Peer to
49/// [`DEFAULT_MAX_EXPANSION_DEPTH`] one RESOURCE axis over on the
50/// expander surface — where the depth ceiling bounds RECURSION (a
51/// runaway macro's stack), this ceiling bounds MEMORY (the memoized
52/// `apply(macro, args)` result table). Pre-lift the cache was an
53/// unbounded [`HashMap`] shared across [`Expander`] clones via
54/// [`std::sync::Arc`]; a long-running host (REPL, LSP,
55/// `tatara-check`, MCP server) that expanded many distinct
56/// `(macro, args)` pairs would accrete cache entries without bound —
57/// the same below-floor resource-drift the depth ceiling closed on
58/// the recursion axis. Post-lift the [`Expander::apply`] insert path
59/// consults this ceiling before growing the cache; once the entry
60/// count reaches the ceiling, new expansions still succeed (the
61/// cache remains a pure PERFORMANCE optimization — never a
62/// CORRECTNESS gate) but skip cache insertion until the operator
63/// clears the cache via [`Expander::clear_cache`]. Consumers that
64/// need a tighter ceiling (embedded / test harnesses) call
65/// [`Expander::set_max_cache_entries`] with a lower cap; consumers
66/// that want unbounded caching (batch compilation runs where memory
67/// is not the concern) call it with [`usize::MAX`].
68///
69/// `8192` is large enough that lawful macro authoring — a small
70/// closed set of macros, each expanded against a bounded set of
71/// argument shapes — never brushes the ceiling on a session-length
72/// workload (a typescape with 20 macros × 32 distinct arg-shapes ×
73/// several call sites still sits under 1K entries), and small
74/// enough that a pathological expansion loop (a macro emitting
75/// unique arg-shapes on every call) is rejected before it exhausts
76/// process memory. The default is deliberately generous — the
77/// ceiling exists to prevent unbounded drift, not to force operators
78/// to tune it. Consumers that want a tighter policy configure it
79/// explicitly.
80pub const DEFAULT_MAX_CACHE_ENTRIES: usize = 8192;
81
82/// Default ceiling on a single macro-expansion output's structural
83/// node count. Peer to [`DEFAULT_MAX_EXPANSION_DEPTH`] and
84/// [`DEFAULT_MAX_CACHE_ENTRIES`] one RESOURCE-DIMENSION axis over on
85/// the expander surface — where the depth ceiling bounds RECURSION
86/// LENGTH (a runaway macro's stack) and the cache ceiling bounds
87/// MEMOIZATION WIDTH (the memoized `apply(macro, args)` table's
88/// entry count), this ceiling bounds OUTPUT SIZE (the "expansion
89/// bomb" axis: a well-defined macro whose depth stays small while
90/// the produced tree grows past any reasonable working-set budget).
91/// Pre-lift the canonical
92/// `(defmacro bomb (x) `(list ,x ,x ,x ,x ,x ,x ,x ,x))` composed
93/// under itself a handful of times exhausted the process heap while
94/// the depth counter was still at low single digits and the cache
95/// entry count sat under any reasonable ceiling — a below-floor
96/// resource-drift the depth AND cache ceilings both admitted through.
97/// Post-lift the [`Expander::expand_with_depth`] site consults this
98/// ceiling on every freshly-applied macro output via
99/// [`crate::ast::Sexp::node_count`]; once an `apply` result exceeds
100/// the ceiling the expander returns
101/// [`crate::error::LispError::ExpansionSizeExceeded`] with
102/// `macro_name`, `size`, and `limit` populated. Consumers that need
103/// a tighter or looser ceiling call
104/// [`Expander::set_max_expansion_size`].
105///
106/// `65_536` is large enough that lawful macro-authored output — a
107/// domain-language body composed from a few dozen typed primitives,
108/// each expanded against a bounded set of arg-shapes — never brushes
109/// the ceiling on a session-length workload (a typical `defpoint` /
110/// `defmonitor` expansion sits under 100 nodes, and even a
111/// hand-authored `defcheck` cascade tops out in the low thousands),
112/// and small enough that a pathological "output bomb" (a macro
113/// producing a 2^N-node blob from a small input) is rejected in
114/// milliseconds instead of consuming multiple gigabytes of process
115/// memory in the worst case. The default is deliberately generous —
116/// the ceiling exists to prevent unbounded drift, not to force
117/// operators to tune it. Consumers that want a tighter policy
118/// configure it explicitly.
119pub const DEFAULT_MAX_EXPANSION_SIZE: usize = 65_536;
120
121/// Default ceiling on a registered macro's BODY structural node count.
122/// Peer to [`DEFAULT_MAX_EXPANSION_DEPTH`], [`DEFAULT_MAX_CACHE_ENTRIES`],
123/// and [`DEFAULT_MAX_EXPANSION_SIZE`] one PIPELINE-STAGE axis over on
124/// the expander surface — the three prior guards close the RESOURCE
125/// surface at EXPAND time (recursion length, memoization width,
126/// single-`apply` output size); this ceiling closes it at REGISTER time
127/// (the SIZE the body itself contributes to storage in
128/// [`Expander::macros`], to [`compile_template`]'s pre-expansion walk,
129/// and to every subsequent [`substitute`] walk that recurses through
130/// the body-shaped template). Pre-lift a pathological
131/// `(defmacro huge (x) `<template-of-N-million-nodes>)` would land the
132/// body in the macros table before any `apply` ran and inflate the
133/// per-registration cost proportional to `N` — a below-floor
134/// resource-drift the three expand-time ceilings all admitted through
135/// because none of them fires at the registration boundary. Post-lift
136/// the [`Expander::register_macro_def`] site consults this ceiling
137/// against `def.body.node_count()`; once the observed size exceeds
138/// the ceiling the expander returns
139/// [`crate::error::LispError::MacroBodySizeExceeded`] with
140/// `macro_name`, `size`, and `limit` populated, and BOTH the
141/// [`Expander::macros`] AND [`Expander::templates`] tables stay
142/// exactly as they were — no partial-write window in which the
143/// registration succeeded halfway.
144///
145/// `16_384` is large enough that lawful hand-authored macro bodies
146/// (an outer quasi-quote wrapping a few dozen nested lists with a
147/// handful of substitution slots) never brush the ceiling on any
148/// realistic authoring workload — even a heavily-composed
149/// `defalertpolicy` template with multi-page cascades tops out in the
150/// low thousands — and small enough that a pathological "authoring
151/// bomb" (a code-generator that unrolls a `Vec<TypedEntity>` into a
152/// giant literal body) is rejected at the registration boundary in
153/// microseconds instead of leaking a multi-megabyte template into the
154/// live macros table. The default is deliberately generous — the
155/// ceiling exists to prevent unbounded drift, not to force operators
156/// to tune it. Consumers that want a tighter or looser ceiling call
157/// [`Expander::set_max_macro_body_size`].
158pub const DEFAULT_MAX_MACRO_BODY_SIZE: usize = 16_384;
159
160/// Default ceiling on the [`Expander::macros`] table's entry count.
161/// Peer to [`DEFAULT_MAX_MACRO_BODY_SIZE`] one RESOURCE-DIMENSION axis
162/// over on the REGISTER-time surface — where the body-size ceiling
163/// bounds a single macro's BODY SIZE (the per-registration authoring
164/// bomb), this ceiling bounds TABLE ENTRY COUNT (the cumulative
165/// registration bomb: a code-generator emitting unbounded fresh
166/// `(defmacro N (x) `(list ,x))` heads whose bodies each sit
167/// comfortably under [`DEFAULT_MAX_MACRO_BODY_SIZE`] yet collectively
168/// saturate process memory). Also peer to [`DEFAULT_MAX_CACHE_ENTRIES`]
169/// one PIPELINE-STAGE axis over — where the cache-entries ceiling
170/// bounds EXPAND-time memoization width, this ceiling bounds
171/// REGISTER-time macro-table width. The two entry-count guards close
172/// the two pipeline stages symmetrically. Pre-lift the macros table
173/// was an unbounded [`HashMap`]; a long-running host (REPL, LSP,
174/// `tatara-check`, MCP server) that ingested a source stream of
175/// distinct `(defmacro …)` heads would accrete entries without
176/// bound — a below-floor resource-drift the four prior guards (depth,
177/// cache-entries, expansion-size, body-size) all admitted through
178/// because none of them fires against the CUMULATIVE registration
179/// count.
180///
181/// Overwrites (a re-registration of an already-registered key) do not
182/// grow the table and never trigger this ceiling — operators can
183/// redefine a macro at capacity without hitting the gate. Only FRESH
184/// keys are counted against the ceiling.
185///
186/// `4096` is large enough that lawful hand-authored typescapes never
187/// brush the ceiling on any realistic authoring workload — a
188/// typescape with 20-100 macros × several call sites sits well under
189/// 1K entries, and even a library-heavy project composing hundreds of
190/// domain macros stays under a fraction of the default — and small
191/// enough that a pathological registration bomb (a code-generator
192/// unrolling a `Vec<TypedEntity>` into a giant `(defmacro fresh-N …)`
193/// stream) is rejected at the registration boundary in microseconds
194/// instead of leaking a multi-megabyte macros table into the live
195/// expander. The default is deliberately generous — the ceiling
196/// exists to prevent unbounded drift, not to force operators to tune
197/// it. Consumers that want a tighter or looser ceiling call
198/// [`Expander::set_max_registered_macros`].
199pub const DEFAULT_MAX_REGISTERED_MACROS: usize = 4096;
200
201/// Default ceiling on a registered macro's lambda-list ARITY — the
202/// count of parameter slots (`required` + `optional` + `rest?`) the
203/// [`MacroDef`] declares. Peer to [`DEFAULT_MAX_MACRO_BODY_SIZE`] one
204/// RESOURCE-DIMENSION axis over on the REGISTER-time surface — where
205/// body-size bounds a per-registration BODY node count (the AST-nodes
206/// of the template a macro rewrites INTO), this ceiling bounds a
207/// per-registration PARAM slot count (the fresh symbols
208/// [`compile_template`]'s `,name`-index resolution AND
209/// [`MacroParams::bind`]'s per-index binder walk have to thread through
210/// on every call). Peer to [`DEFAULT_MAX_REGISTERED_MACROS`] one
211/// RESOURCE-DIMENSION axis over on the REGISTER-time surface — where
212/// the registered-macros ceiling bounds cumulative registration COUNT
213/// (the table-scoped resource), this ceiling bounds a per-registration
214/// PARAM-LIST WIDTH (the per-entry resource). Together the three
215/// REGISTER-time ceilings close the (per-body SIZE, per-body ARITY,
216/// per-table COUNT) three-corner surface — a `Vec<TypedEntity>`
217/// arity-bomb code-generator emitting a
218/// `(defmacro huge (a-1 a-2 … a-N-million) `,a-1)` head with a
219/// single-node body slips past `max_macro_body_size` and past
220/// `max_registered_macros` (one fresh key), yet each subsequent call
221/// pays O(N) walking the required-run binder — a below-floor
222/// resource-drift the two prior REGISTER-time guards admit through
223/// because neither of them fires against the PER-REGISTRATION
224/// PARAM-LIST WIDTH.
225///
226/// The default is deliberately generous — lawful hand-authored macros
227/// carry a handful of required params + a small optional run + at
228/// most a single `&rest` slot (`observability-stack`'s `defalertpolicy`
229/// carries under 8 slots; even keyword-heavy authoring surfaces sit
230/// well under 32 slots). `128` sits an order of magnitude past any
231/// realistic authoring workload yet an order of magnitude BELOW a
232/// pathological code-generator's arity-bomb output, so the ceiling
233/// gates the drift without brushing lawful traffic. Consumers that
234/// want a tighter or looser ceiling call
235/// [`Expander::set_max_macro_arity`].
236///
237/// Frontier inspiration: Common Lisp's `LAMBDA-PARAMETERS-LIMIT`
238/// standard variable (CLHS §3.4.1) — the runtime-reflectable
239/// "upper exclusive bound on the number of parameters that may
240/// appear in a lambda list" every conforming implementation carries.
241/// SBCL's is `4611686018427387903`, ECL's is `4096`, CLisp's is `4096`.
242/// `DEFAULT_MAX_MACRO_ARITY` is the substrate's typed-Rust peer at
243/// compile time, translated through pleme-io primitives: a
244/// `pub const usize` on the typed macro-expander algebra rather than
245/// a runtime-mutable global, wired into the substrate's typed
246/// [`crate::error::LispError`] Result algebra at
247/// [`Expander::register_macro_def`] rather than raised as an
248/// implementation-defined condition.
249pub const DEFAULT_MAX_MACRO_ARITY: usize = 128;
250
251/// Typed snapshot of the six `Expander` resource ceilings — the
252/// bundled peer of the six [`Expander::max_expansion_depth`] /
253/// [`Expander::max_cache_entries`] / [`Expander::max_expansion_size`] /
254/// [`Expander::max_macro_body_size`] / [`Expander::max_registered_macros`] /
255/// [`Expander::max_macro_arity`] individual getters. The six knobs form
256/// ONE resource surface (the (PIPELINE-STAGE × RESOURCE-DIMENSION)
257/// grid the last six commits closed corner-by-corner) yet the
258/// pre-lift `Expander` surface exposed them only as SIX independent
259/// projections — inspecting the full posture required six calls, and
260/// bulk-configuring the expander (a preset for CI, a preset for the
261/// REPL, a preset for a test harness) required six independent
262/// setter invocations whose ordering the type system did not gate.
263///
264/// Post-lift the typed bundle binds the six knobs at ONE typed value
265/// on the `Expander` surface. A `#[derive(Clone, Copy, Debug,
266/// PartialEq, Eq)]` posture — every ceiling is a `Copy` scalar —
267/// gives callers a cheap snapshot they can serialize, compare, or
268/// pass around; the `Copy` implementation guarantees the bundle
269/// never carries a hidden allocation. Adding a SEVENTH ceiling
270/// extends this struct AND [`DEFAULT_RESOURCE_LIMITS`] in lockstep
271/// via rustc's field-exhaustiveness on the `..Default::default()`-free
272/// literal construction — no independent-field-drift window.
273///
274/// Peer of the six `DEFAULT_MAX_*` module constants one AGGREGATION
275/// axis over: where each constant carries ONE ceiling, this struct
276/// carries the SIX-fold cross-product. [`DEFAULT_RESOURCE_LIMITS`]
277/// pins the (defaults × aggregation) corner so a consumer that wants
278/// the shipped posture in one value does not re-derive the six
279/// individual constants at its call site.
280///
281/// Theory grounding: THEORY.md §II.1 — typed entry / typed exit for
282/// the resource-limit configuration surface. The pre-lift six
283/// independent knobs were a set of typed scalars but their
284/// composition into an "Expander resource posture" was untyped —
285/// callers assembled the posture at their call site with no
286/// compile-time exhaustiveness check that all six were considered.
287/// Frontier inspiration: `tokio::runtime::Builder`'s bundled knob
288/// struct — a runtime's ceiling posture is ONE typed value rather
289/// than a chain of independent method calls; translation through
290/// pleme-io primitives is the plain `Copy` snapshot below, no
291/// builder-pattern indirection.
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub struct ResourceLimits {
294    /// See [`Expander::max_expansion_depth`].
295    pub max_expansion_depth: usize,
296    /// See [`Expander::max_cache_entries`].
297    pub max_cache_entries: usize,
298    /// See [`Expander::max_expansion_size`].
299    pub max_expansion_size: usize,
300    /// See [`Expander::max_macro_body_size`].
301    pub max_macro_body_size: usize,
302    /// See [`Expander::max_registered_macros`].
303    pub max_registered_macros: usize,
304    /// See [`Expander::max_macro_arity`].
305    pub max_macro_arity: usize,
306}
307
308impl Default for ResourceLimits {
309    /// The shipped posture — every field seeded from its matching
310    /// `DEFAULT_MAX_*` module constant. Pinned equal to
311    /// [`DEFAULT_RESOURCE_LIMITS`] as a compile-time identity so a
312    /// future re-tuning of ONE default lands in ONE place and both
313    /// projections pick it up mechanically.
314    fn default() -> Self {
315        DEFAULT_RESOURCE_LIMITS
316    }
317}
318
319/// The shipped [`ResourceLimits`] posture — every field bound to its
320/// matching `DEFAULT_MAX_*` module constant at ONE typed value. This
321/// is the `const` peer of [`ResourceLimits::default`]; the two are
322/// pinned equal so a call-site that wants the shipped posture as a
323/// compile-time value binds through the `const` and a call-site that
324/// wants it as a runtime `Default::default()` value binds through the
325/// trait impl — both routes reach the SAME six numbers.
326///
327/// Adding a SEVENTH ceiling extends this constant AND the struct AND
328/// [`Expander`]'s constructors in lockstep — rustc's
329/// field-exhaustiveness on this `..Default::default()`-free literal
330/// forces the new field to appear here, and the trait impl above
331/// reuses this constant so no independent-default-drift window opens
332/// between the two projections.
333pub const DEFAULT_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
334    max_expansion_depth: DEFAULT_MAX_EXPANSION_DEPTH,
335    max_cache_entries: DEFAULT_MAX_CACHE_ENTRIES,
336    max_expansion_size: DEFAULT_MAX_EXPANSION_SIZE,
337    max_macro_body_size: DEFAULT_MAX_MACRO_BODY_SIZE,
338    max_registered_macros: DEFAULT_MAX_REGISTERED_MACROS,
339    max_macro_arity: DEFAULT_MAX_MACRO_ARITY,
340};
341
342/// The unbounded [`ResourceLimits`] posture — every ceiling seeded to
343/// [`usize::MAX`] at ONE typed value. This is the CEILING-LIFTED peer of
344/// [`DEFAULT_RESOURCE_LIMITS`] one PRESET-POSTURE axis over on the
345/// named-preset surface: where the default constant binds each field to
346/// the shipped `DEFAULT_MAX_*` module constant, this constant binds
347/// each field to the raw `usize::MAX` sentinel that every
348/// [`Expander::set_max_expansion_depth`] / [`Expander::set_max_cache_entries`] /
349/// [`Expander::set_max_expansion_size`] / [`Expander::set_max_macro_body_size`] /
350/// [`Expander::set_max_registered_macros`] / [`Expander::set_max_macro_arity`]
351/// setter's doc names as "the ceiling is effectively lifted" —
352/// admitting any lawful value the type system supports.
353///
354/// A test-harness fixture that wants "exercise the raw expander with no
355/// ceiling gating anything" pre-lift composed the posture at its call
356/// site as six independent [`usize::MAX`] literals (or six independent
357/// setter invocations post-`Expander::new`) whose exhaustiveness the
358/// type system did NOT gate — a copy-paste that dropped one of the six
359/// lifts left THAT ceiling at its default and gated the fixture
360/// silently. Post-lift the posture is ONE named typed constant every
361/// such fixture routes through, AND rustc's field-exhaustiveness on
362/// the `..Default::default()`-free literal here forces a SEVENTH
363/// ceiling extension to appear at this site in lockstep with
364/// [`DEFAULT_RESOURCE_LIMITS`] and the [`ResourceLimits`] struct
365/// itself — no independent-preset-drift window.
366///
367/// Peer to [`DEFAULT_RESOURCE_LIMITS`] on the (posture × aggregation)
368/// grid: both constants live at the AGGREGATION corner (six ceilings
369/// as ONE typed value); the posture axis distinguishes the SHIPPED
370/// default from the CEILING-LIFTED unbounded preset. Disagrees with
371/// [`DEFAULT_RESOURCE_LIMITS`] on every field (every default is a
372/// concrete non-[`usize::MAX`] positive constant, so the six-way
373/// disagreement is structurally exhaustive) — pinned as a typed
374/// theorem in the [`ResourceLimits`] test cohort.
375///
376/// Frontier inspiration: `tokio::runtime::Builder`'s pattern of
377/// exposing named preset thunks (`multi_thread`, `current_thread`)
378/// alongside the field-level configurators, so a caller that wants a
379/// standard posture binds through ONE named entry rather than
380/// composing the field literals at the call site. Translation
381/// through pleme-io primitives is the plain `const` snapshot below,
382/// no builder-pattern indirection — the `Copy` posture on
383/// [`ResourceLimits`] already gives callers the struct-update
384/// literal (`ResourceLimits { max_expansion_depth: 4,
385/// ..UNBOUNDED_RESOURCE_LIMITS }`) that isolates the ONE ceiling
386/// still gating from the five lifted.
387pub const UNBOUNDED_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
388    max_expansion_depth: usize::MAX,
389    max_cache_entries: usize::MAX,
390    max_expansion_size: usize::MAX,
391    max_macro_body_size: usize::MAX,
392    max_registered_macros: usize::MAX,
393    max_macro_arity: usize::MAX,
394};
395
396/// The zero [`ResourceLimits`] posture — every ceiling seeded to `0` at
397/// ONE typed value. This is the BOTTOM peer of
398/// [`UNBOUNDED_RESOURCE_LIMITS`] one LATTICE-POLE axis over on the
399/// (bottom, top) bounded-lattice preset-pair surface: where the
400/// ceiling-lifted preset carries every gate at [`usize::MAX`]
401/// (admitting every lawful `usize`-indexed value the type system
402/// supports), this preset carries every gate at `0` (rejecting every
403/// non-empty input past the first depth step, cache slot, macro-body
404/// node, macro registration, or arity slot). Together the two close
405/// the (BOTTOM, TOP) preset-posture pair on the bounded-lattice
406/// diagonal, and [`DEFAULT_RESOURCE_LIMITS`] sits strictly between
407/// them (`EMPTY.leq(DEFAULT) && DEFAULT.leq(UNBOUNDED)`).
408///
409/// **Bounded-lattice identity elements**: [`ResourceLimits`] under the
410/// pointwise `min` / `max` operations forms a BOUNDED lattice; each
411/// operation acquires its identity element from the OPPOSITE pole of
412/// the preset-pair diagonal, and its ANNIHILATOR from the SAME pole.
413///   * [`Self::strictest`] (meet, pointwise `min`) —
414///     [`UNBOUNDED_RESOURCE_LIMITS`] is the identity
415///     (`a.strictest(UNBOUNDED) == a` for every posture, since pointwise
416///     `min` against [`usize::MAX`] returns `a`); this constant is the
417///     ANNIHILATOR (`a.strictest(EMPTY) == EMPTY` for every posture,
418///     since pointwise `min` against `0` returns `0`).
419///   * [`Self::most_permissive`] (join, pointwise `max`) — this
420///     constant is the identity (`a.most_permissive(EMPTY) == a` for
421///     every posture, since pointwise `max` against `0` returns `a`);
422///     [`UNBOUNDED_RESOURCE_LIMITS`] is the ANNIHILATOR
423///     (`a.most_permissive(UNBOUNDED) == UNBOUNDED`).
424///   * [`Self::leq`] (partial order) — this constant is the MINIMUM
425///     (`EMPTY.leq(a) == true` for every posture, since `0 <= x` holds
426///     for every `usize x`); [`UNBOUNDED_RESOURCE_LIMITS`] is the
427///     MAXIMUM (`a.leq(UNBOUNDED) == true` for every posture, since
428///     `x <= usize::MAX` holds for every `usize x`).
429///
430/// **Fold-identity use case**: a caller aggregating a slice of
431/// postures through [`Self::most_permissive`] (computing the pointwise
432/// least upper bound across the slice) seeds the fold from this
433/// constant — the join-identity property guarantees the seed does not
434/// distort the fold's result:
435///
436/// ```rust,ignore
437/// let joined = postures.iter().copied()
438///     .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
439/// ```
440///
441/// The dual pattern (folding through [`Self::strictest`] to compute
442/// the pointwise greatest lower bound) seeds from
443/// [`UNBOUNDED_RESOURCE_LIMITS`] — the meet-identity from the OPPOSITE
444/// pole. Pre-lift, an aggregator either (a) picked ONE arbitrary
445/// element of the slice as the seed and folded through the tail — a
446/// non-total fixup that panicked on an empty slice — or (b) seeded
447/// from `Some(first)`-style option wrapper — a runtime `Option` that
448/// carried a type-level admission the aggregate might be absent even
449/// though the algebra defines it exhaustively via its identity. Post-
450/// lift the fold takes ONE typed identity element and handles empty
451/// slices as the algebraic identity value — a boundary case which the
452/// bounded-lattice structure resolves at TYPE level rather than the
453/// caller resolving at CALL SITE with a wrapper type.
454///
455/// Adding a SEVENTH ceiling extends this constant AND the struct AND
456/// [`DEFAULT_RESOURCE_LIMITS`] AND [`UNBOUNDED_RESOURCE_LIMITS`] in
457/// lockstep — rustc's field-exhaustiveness on this
458/// `..Default::default()`-free literal forces the new field to appear
459/// at this site, and the identity-law tests pin `0` as the seventh
460/// field's value at compile time via the const-fn peer of the
461/// existing lattice-law const-fn pins.
462///
463/// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
464/// proofs; the identity element of a lattice operation is itself a
465/// typed named entry whose composition with any other element is the
466/// element (join) or the identity (meet). THEORY.md §V.1 — knowable
467/// platform; the bounded-lattice bottom binds at ONE typed constant
468/// rather than a six-primitive inline literal every consumer that
469/// needs the fold identity carries at its call site. Frontier
470/// inspiration: the algebraic-datatypes tradition of a `Monoid` typeclass
471/// (Haskell's `mempty`, Scala's `Monoid.empty`, Rust's various
472/// `Default::default` for `Sum`/`Product` newtypes) — a caller
473/// aggregating values through a binary combinator binds its identity
474/// element at ONE named entry rather than composing the identity
475/// literal at every fold site. Translation through pleme-io primitives
476/// is the plain `const` snapshot below, no typeclass indirection — the
477/// `Copy` posture on [`ResourceLimits`] already gives callers the
478/// struct-update literal (`ResourceLimits { max_macro_arity: 3,
479/// ..EMPTY_RESOURCE_LIMITS }`) that isolates the ONE ceiling admitting
480/// non-zero input from the five sealed at `0`.
481pub const EMPTY_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
482    max_expansion_depth: 0,
483    max_cache_entries: 0,
484    max_expansion_size: 0,
485    max_macro_body_size: 0,
486    max_registered_macros: 0,
487    max_macro_arity: 0,
488};
489
490/// Pointwise `min` / `max` primitives the [`ResourceLimits`] lattice binds
491/// its meet ([`ResourceLimits::strictest`]) and join
492/// ([`ResourceLimits::most_permissive`]) operations to. Named at module
493/// scope as `const fn` so both operations are themselves `const fn` on
494/// stable — composing two postures at compile time (a caller stitching a
495/// new `pub const` preset from two shipped presets) needs no runtime
496/// evaluation. Ternary form rather than [`usize::min`] / [`usize::max`]
497/// so the two `const fn` remain valid on any tatara MSRV whose stability
498/// on the primitive const-fn methods has not been established.
499const fn min_usize(a: usize, b: usize) -> usize {
500    if a <= b {
501        a
502    } else {
503        b
504    }
505}
506
507const fn max_usize(a: usize, b: usize) -> usize {
508    if a >= b {
509        a
510    } else {
511        b
512    }
513}
514
515impl ResourceLimits {
516    /// Pointwise-`min` across the six ceilings — the STRICTEST posture
517    /// whose admissible input set is the intersection of `self`'s and
518    /// `other`'s admissible input sets. `a.strictest(b)` admits an
519    /// input iff BOTH `a` AND `b` admit it: on every field the
520    /// per-axis ceiling is the smaller of the two, so a value that
521    /// clears the meet clears BOTH postures, and a value that any
522    /// posture rejects the meet also rejects.
523    ///
524    /// The meet of the `ResourceLimits` lattice under the pointwise
525    /// partial order `a ≤ b iff every field of a ≤ every field of b`
526    /// (tighter-first — a POSTURE admits FEWER inputs on that field's
527    /// axis when the ceiling is smaller). Peer of
528    /// [`Self::most_permissive`] one COMBINATOR axis over on the
529    /// lattice-algebra surface: where `most_permissive` is the join
530    /// (pointwise-`max`, LUB), this is the meet (pointwise-`min`, GLB).
531    /// Together the two operations close the (meet, join) lattice
532    /// operator pair on the [`ResourceLimits`] posture algebra.
533    ///
534    /// The concrete identity on the shipped preset pair holds
535    /// structurally: [`DEFAULT_RESOURCE_LIMITS`]`.strictest(`
536    /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == `[`DEFAULT_RESOURCE_LIMITS`].
537    /// Every `DEFAULT_MAX_*` module constant is a concrete positive
538    /// value strictly less than [`usize::MAX`], so on every axis the
539    /// pointwise-`min` picks the DEFAULT side; the six-axis pin holds
540    /// as a typed theorem in the test cohort.
541    ///
542    /// Idempotent (`a.strictest(a) == a`), commutative
543    /// (`a.strictest(b) == b.strictest(a)`), associative
544    /// (`(a.strictest(b)).strictest(c) == a.strictest(b.strictest(c))`),
545    /// and satisfies the absorption identity with the join
546    /// (`a.strictest(a.most_permissive(b)) == a`) — all pinned as
547    /// typed theorems in the test cohort. Distributive over the join
548    /// (`a.strictest(b.most_permissive(c)) ==
549    /// a.strictest(b).most_permissive(a.strictest(c))`) because
550    /// pointwise-`min` and pointwise-`max` on [`usize`] each
551    /// distribute over the other on every axis.
552    ///
553    /// `const fn` so a caller can compose two named presets into a
554    /// third at compile time (`pub const CI_RESOURCE_LIMITS:
555    /// ResourceLimits = SOME_OPERATOR_PRESET.strictest(
556    /// SOME_CI_PRESET);`) rather than deferring the composition to
557    /// a runtime `Default::default()` chain.
558    ///
559    /// Pre-lift, a caller wanting the tightest common posture across
560    /// two shipped presets composed the intersection at its call site
561    /// as six independent `usize::min(a.max_X, b.max_X)` invocations
562    /// stitched into a fresh `ResourceLimits { ... }` literal — the
563    /// same six-inline-primitive shape the pre-`ResourceLimits`
564    /// bundled `Expander` posture required its callers to carry, and
565    /// the same exhaustiveness gap the `..Default::default()`-free
566    /// literal on the bundled struct exists to close: a copy-paste
567    /// that dropped ONE of the six `min` invocations left THAT
568    /// ceiling at whichever posture the caller happened to seed the
569    /// literal with, and the type system did not gate the drop.
570    /// Post-lift the composition binds at ONE typed method on the
571    /// posture algebra — a caller writes
572    /// `operator_preset.strictest(ci_preset)` and rustc's
573    /// field-exhaustiveness at the const's construction guarantees
574    /// every ceiling is threaded through the `min` primitive.
575    ///
576    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
577    /// preserves proofs; the tightest posture of two preset-carried
578    /// resource proofs is itself a `ResourceLimits` whose proof
579    /// content is the intersection of the two, and this method is
580    /// the typed named entry composing them. THEORY.md §V.1 —
581    /// knowable platform; the pointwise-`min` combinator becomes a
582    /// TYPE-level operation on the posture algebra rather than an
583    /// inline six-primitive cascade at every consumer that composes
584    /// two presets. `tatara-lattice`'s `meet` / `join` operators on
585    /// `Classification` (see `pleme-io/tatara` workspace crate
586    /// docstring: "Lattice algebra over Classification — `meet` /
587    /// `join` / `leq` / `Baseline`") close the SAME shape one layer
588    /// down: an entity's classification is a lattice; a resource
589    /// posture is a lattice; both algebras carry the (meet, join)
590    /// operator pair as their fundamental composition primitives.
591    #[must_use]
592    pub const fn strictest(self, other: Self) -> Self {
593        Self {
594            max_expansion_depth: min_usize(self.max_expansion_depth, other.max_expansion_depth),
595            max_cache_entries: min_usize(self.max_cache_entries, other.max_cache_entries),
596            max_expansion_size: min_usize(self.max_expansion_size, other.max_expansion_size),
597            max_macro_body_size: min_usize(self.max_macro_body_size, other.max_macro_body_size),
598            max_registered_macros: min_usize(
599                self.max_registered_macros,
600                other.max_registered_macros,
601            ),
602            max_macro_arity: min_usize(self.max_macro_arity, other.max_macro_arity),
603        }
604    }
605
606    /// Pointwise-`max` across the six ceilings — the MOST-PERMISSIVE
607    /// posture whose admissible input set is the union of `self`'s
608    /// and `other`'s admissible input sets. `a.most_permissive(b)`
609    /// admits an input iff EITHER `a` OR `b` admits it: on every
610    /// field the per-axis ceiling is the larger of the two, so a
611    /// value that either posture accepts the join accepts, and a
612    /// value the join rejects both postures reject.
613    ///
614    /// The join of the `ResourceLimits` lattice under the pointwise
615    /// partial order `a ≤ b iff every field of a ≤ every field of b`
616    /// (tighter-first). Peer of [`Self::strictest`] one COMBINATOR
617    /// axis over on the lattice-algebra surface: where `strictest` is
618    /// the meet (pointwise-`min`, GLB), this is the join
619    /// (pointwise-`max`, LUB). Together the two operations close the
620    /// (meet, join) lattice operator pair on the [`ResourceLimits`]
621    /// posture algebra.
622    ///
623    /// The concrete identity on the shipped preset pair holds
624    /// structurally: [`DEFAULT_RESOURCE_LIMITS`]`.most_permissive(`
625    /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == `[`UNBOUNDED_RESOURCE_LIMITS`].
626    /// Every `DEFAULT_MAX_*` module constant is a concrete positive
627    /// value strictly less than [`usize::MAX`], so on every axis the
628    /// pointwise-`max` picks the UNBOUNDED side; the six-axis pin
629    /// holds as a typed theorem in the test cohort.
630    ///
631    /// Idempotent, commutative, associative, and satisfies the
632    /// absorption identity with the meet
633    /// (`a.most_permissive(a.strictest(b)) == a`) — all pinned as
634    /// typed theorems in the test cohort. Distributive over the meet
635    /// (`a.most_permissive(b.strictest(c)) ==
636    /// a.most_permissive(b).strictest(a.most_permissive(c))`).
637    ///
638    /// `const fn` so a caller can compose two named presets into a
639    /// third at compile time rather than deferring the composition
640    /// to a runtime `Default::default()` chain.
641    ///
642    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
643    /// preserves proofs; the most-permissive posture of two preset-
644    /// carried resource proofs is itself a `ResourceLimits` whose
645    /// proof content is the union of the two, and this method is the
646    /// typed named entry composing them. THEORY.md §V.1 — knowable
647    /// platform; the pointwise-`max` combinator becomes a TYPE-level
648    /// operation on the posture algebra rather than an inline
649    /// six-primitive cascade at every consumer that composes two
650    /// presets.
651    #[must_use]
652    pub const fn most_permissive(self, other: Self) -> Self {
653        Self {
654            max_expansion_depth: max_usize(self.max_expansion_depth, other.max_expansion_depth),
655            max_cache_entries: max_usize(self.max_cache_entries, other.max_cache_entries),
656            max_expansion_size: max_usize(self.max_expansion_size, other.max_expansion_size),
657            max_macro_body_size: max_usize(self.max_macro_body_size, other.max_macro_body_size),
658            max_registered_macros: max_usize(
659                self.max_registered_macros,
660                other.max_registered_macros,
661            ),
662            max_macro_arity: max_usize(self.max_macro_arity, other.max_macro_arity),
663        }
664    }
665
666    /// Pointwise partial-order relation across the six ceilings — `self`
667    /// is at least as tight as `other` iff every field of `self` is at
668    /// most the matching field of `other`. `a.leq(b)` holds iff every
669    /// input the `a` posture admits the `b` posture also admits: `a`'s
670    /// per-axis ceilings all sit at or below `b`'s per-axis ceilings, so
671    /// any value that clears `a`'s tighter gates also clears `b`'s
672    /// looser gates.
673    ///
674    /// The partial order underneath the [`ResourceLimits`] lattice.
675    /// Peer of [`Self::strictest`] (meet, GLB) and [`Self::most_permissive`]
676    /// (join, LUB) one PRIMITIVE-KIND axis over on the lattice-algebra
677    /// surface: where those two are the (meet, join) COMBINATORS, this
678    /// is the RELATION they are defined against. Together the three
679    /// close the (meet, join, leq) primitive triple on the
680    /// [`ResourceLimits`] posture algebra — the same triple
681    /// `tatara-lattice`'s [`Lattice`] trait exposes as its three
682    /// required methods for every classification lattice one crate over
683    /// (see `tatara-lattice/src/lib.rs`).
684    ///
685    /// **Meet-agreement** (`a.leq(b) ⇔ a.strictest(b) == a`): if `a` is
686    /// tighter on every axis than `b`, then the pointwise `min` picks
687    /// `a` on every axis, so `a.strictest(b) == a`; conversely, if
688    /// `a.strictest(b) == a`, every field's `min` picked `a`, so every
689    /// field of `a` is at most the matching field of `b`. Pinned as
690    /// `resource_limits_leq_agrees_with_meet` in the test cohort.
691    ///
692    /// **Join-agreement** (`a.leq(b) ⇔ a.most_permissive(b) == b`):
693    /// symmetric statement on the LUB side; pinned as
694    /// `resource_limits_leq_agrees_with_join`. Together the two
695    /// agreements are the canonical lattice cross-check axiom
696    /// `a ≤ b ⇔ a ⊓ b = a ⇔ a ⊔ b = b` documented in
697    /// `tatara-lattice/src/lib.rs`'s preamble.
698    ///
699    /// Reflexive (`a.leq(a) == true`), antisymmetric (`a.leq(b) &&
700    /// b.leq(a) → a == b`), transitive (`a.leq(b) && b.leq(c) →
701    /// a.leq(c)`) — all pinned as typed theorems in the test cohort.
702    /// NOT total: two postures with reversed per-axis orderings (some
703    /// axes where `a` is tighter, some where `b` is tighter) are
704    /// incomparable — `!a.leq(b) && !b.leq(a)` — pinned by
705    /// `resource_limits_leq_is_not_total_on_asymmetric_postures`.
706    ///
707    /// **Bounds pins in terms of leq**:
708    ///   * `a.strictest(b).leq(a) && a.strictest(b).leq(b)` (meet is
709    ///     the greatest lower bound) — pinned by
710    ///     `resource_limits_strictest_is_leq_both_operands`, the typed
711    ///     lattice-relation companion to the per-axis
712    ///     `resource_limits_strictest_is_dominated_by_both_operands_pointwise`.
713    ///   * `a.leq(a.most_permissive(b)) && b.leq(a.most_permissive(b))`
714    ///     (join is the least upper bound) — pinned by
715    ///     `resource_limits_most_permissive_is_geq_both_operands`, the
716    ///     typed lattice-relation companion to the per-axis
717    ///     `resource_limits_most_permissive_dominates_both_operands_pointwise`.
718    ///
719    /// **Concrete-preset pin**: [`DEFAULT_RESOURCE_LIMITS`]`.leq(`
720    /// [`UNBOUNDED_RESOURCE_LIMITS`]`) == true` (every
721    /// `DEFAULT_MAX_*` module constant is a concrete positive value at
722    /// most [`usize::MAX`]) AND
723    /// `!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS)` (every
724    /// `DEFAULT_MAX_*` is strictly less than [`usize::MAX`], so on
725    /// every axis [`usize::MAX`] exceeds the matching default) — the
726    /// partial order is a STRICT order on the shipped preset pair.
727    /// Pinned by `resource_limits_leq_of_default_and_unbounded_is_a_strict_order`.
728    ///
729    /// `const fn` so a caller can pin a preset-relation identity at
730    /// compile time (`const _: () = assert!(DEFAULT_RESOURCE_LIMITS
731    /// .leq(UNBOUNDED_RESOURCE_LIMITS));`) — the typed lattice-relation
732    /// peer of the `const fn` composition ability on
733    /// [`Self::strictest`] / [`Self::most_permissive`].
734    ///
735    /// Pre-lift, a caller wanting to decide whether one posture was
736    /// tighter than another either (a) composed six independent
737    /// per-axis `<=` comparisons at the call site stitched into a
738    /// six-fold `&&` — the same six-inline-primitive shape the
739    /// pre-`ResourceLimits` bundled Expander posture required its
740    /// callers to carry, and the same exhaustiveness gap the
741    /// `..Default::default()`-free literal on the bundled struct exists
742    /// to close: a copy-paste that dropped ONE of the six comparisons
743    /// silently admitted a posture that violated tightness on THAT
744    /// axis — or (b) invoked [`Self::strictest`] and compared the result
745    /// to `self` — a correct but roundabout composition of the partial-
746    /// order primitive against its lattice-companion. Post-lift the
747    /// relation binds at ONE typed method on the posture algebra; a
748    /// caller writes `tighter.leq(looser)` and rustc's exhaustiveness
749    /// on the six-field conjunction below guarantees every ceiling is
750    /// threaded through the `<=` primitive.
751    ///
752    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
753    /// preserves proofs; the partial-order relation on two preset-
754    /// carried resource proofs is itself a typed named entry composing
755    /// their tightness ordering. THEORY.md §V.1 — knowable platform;
756    /// the pointwise `<=` combinator becomes a TYPE-level operation on
757    /// the posture algebra rather than an inline six-primitive
758    /// conjunction at every consumer that compares two presets.
759    /// `tatara-lattice`'s [`Lattice::leq`] method on `Classification`
760    /// closes the SAME shape one layer down — an entity's
761    /// classification is a lattice with a `leq` partial order; a
762    /// resource posture is a lattice with a `leq` partial order; both
763    /// algebras carry the (meet, join, leq) primitive triple as their
764    /// fundamental relation-and-combinator surface.
765    #[must_use]
766    pub const fn leq(self, other: Self) -> bool {
767        self.max_expansion_depth <= other.max_expansion_depth
768            && self.max_cache_entries <= other.max_cache_entries
769            && self.max_expansion_size <= other.max_expansion_size
770            && self.max_macro_body_size <= other.max_macro_body_size
771            && self.max_registered_macros <= other.max_registered_macros
772            && self.max_macro_arity <= other.max_macro_arity
773    }
774
775    /// Pointwise-`min` fold across a slice of postures — the STRICTEST
776    /// posture whose admissible input set is the intersection of every
777    /// operand's admissible input sets. `ResourceLimits::strictest_of(&[a,
778    /// b, c])` admits an input iff EVERY operand admits it: on every
779    /// field the per-axis ceiling is the smallest across the whole slice,
780    /// so a value that clears the aggregated meet clears EVERY operand,
781    /// and a value that ANY operand rejects the aggregated meet also
782    /// rejects.
783    ///
784    /// The N-ary MEET aggregation on the [`ResourceLimits`] lattice —
785    /// the extension of [`Self::strictest`] from a 2-input pairwise
786    /// combinator to an N-input fold across a slice. Seeded from
787    /// [`UNBOUNDED_RESOURCE_LIMITS`] (the meet-identity — its per-axis
788    /// [`usize::MAX`] never wins the pointwise `min` against a concrete
789    /// positive value, so the seed contributes nothing to the fold's
790    /// result), which makes the empty-slice case aggregate to the
791    /// identity element rather than requiring an `Option<Self>` wrapper
792    /// the caller inspects at every consumption site. Peer of
793    /// [`Self::most_permissive_of`] one COMBINATOR axis over on the
794    /// N-ary aggregation surface: where `most_permissive_of` is the
795    /// N-ary JOIN (LUB across the slice, seeded from the join-identity
796    /// [`EMPTY_RESOURCE_LIMITS`]), this is the N-ary MEET (GLB across
797    /// the slice, seeded from the meet-identity). Together the two
798    /// close the (N-ary meet, N-ary join) aggregation pair the pairwise
799    /// combinators [`Self::strictest`] / [`Self::most_permissive`]
800    /// extend from 2 to N.
801    ///
802    /// **Empty-slice identity**: `strictest_of(&[]) ==
803    /// UNBOUNDED_RESOURCE_LIMITS` — the null aggregate binds structurally
804    /// to the meet-identity element. Pre-lift a caller composing "the
805    /// tightest posture across THIS SLICE" from the pairwise `strictest`
806    /// primitive either (a) seeded the fold from a non-empty first
807    /// element and panicked on empty input, or (b) wrapped the aggregate
808    /// in `Option<Self>` — a runtime type-level admission the aggregate
809    /// might be absent even though the algebra's bounded-lattice
810    /// structure defines the null aggregate exhaustively via its
811    /// identity element. Post-lift the null aggregate is
812    /// [`UNBOUNDED_RESOURCE_LIMITS`] at the TYPE level, not a wrapper
813    /// the consumer inspects.
814    ///
815    /// **Single-element identity**: `strictest_of(&[a]) == a` — the
816    /// 1-input aggregate is the input verbatim, since
817    /// `UNBOUNDED.strictest(a) == a` for every posture `a` by the
818    /// meet-identity law.
819    ///
820    /// **Two-element identity**: `strictest_of(&[a, b]) ==
821    /// a.strictest(b)` — the 2-input aggregate reduces to the pairwise
822    /// combinator, since `UNBOUNDED.strictest(a).strictest(b) ==
823    /// a.strictest(b)`.
824    ///
825    /// **Order-independent**: `strictest_of(&[a, b, c]) ==
826    /// strictest_of(&[c, b, a])` — the fold inherits the commutativity
827    /// AND associativity of the pairwise `strictest`, so any permutation
828    /// of the slice yields the same aggregate.
829    ///
830    /// `const fn` so a caller can compose an arbitrary-length preset
831    /// slice into a third preset at compile time (`pub const
832    /// COMBINED_PRESET: ResourceLimits = ResourceLimits::strictest_of(
833    /// &[SOME_OPERATOR_PRESET, SOME_CI_PRESET, SOME_HOSTILE_PRESET]);`)
834    /// rather than deferring the composition to a runtime chain — the
835    /// N-ary const-fn peer of the pairwise const-fn on
836    /// [`Self::strictest`]. `Copy` on [`ResourceLimits`] lets the const-
837    /// fn loop index the slice by value without an explicit `.clone()`
838    /// (which const-fn would not permit anyway).
839    ///
840    /// Pre-lift, the SAME `.iter().copied().fold(UNBOUNDED, strictest)`
841    /// three-primitive cascade appeared verbatim in
842    /// `unbounded_resource_limits_seeds_strictest_fold_over_slice` AND at
843    /// every future consumer that would compose "the tightest across
844    /// this set of presets" — a PRIME DIRECTIVE ≥2 trigger, since the
845    /// fold docstring on [`UNBOUNDED_RESOURCE_LIMITS`] explicitly names
846    /// this pattern as the constant's fold-identity use case. Post-lift
847    /// the N-ary aggregation binds at ONE typed method the algebra
848    /// exposes, and every future consumer routes through ONE name whose
849    /// signature carries the identity-element choice INTO the method
850    /// (rather than the consumer having to remember which seed pairs
851    /// with which combinator at each call site).
852    ///
853    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
854    /// preserves proofs; the N-ary aggregation of N preset-carried
855    /// resource proofs is itself a `ResourceLimits` whose proof content
856    /// is the intersection of every operand's, and this method is the
857    /// typed named entry composing them. THEORY.md §V.1 — knowable
858    /// platform; the N-ary meet becomes a TYPE-level operation on the
859    /// posture algebra rather than an inline fold cascade at every
860    /// consumer that aggregates a slice of presets — with the
861    /// meet-identity seed baked in so the consumer cannot pair the join-
862    /// identity (`EMPTY`) with the meet combinator (a silent
863    /// distortion that would collapse every axis to `0`). Frontier
864    /// inspiration: the algebraic-datatypes tradition of a `Monoid`
865    /// typeclass exposing `mconcat` / `Monoid.combineAll` alongside the
866    /// pairwise `mappend` — the N-ary aggregation of a collection
867    /// through a binary combinator is a first-class named method the
868    /// typeclass carries. Translation through pleme-io primitives is
869    /// the plain `const fn` slice-fold below, no typeclass indirection
870    /// — the meet-identity element already exists as a named `pub const`
871    /// on the algebra so the fold picks it up structurally.
872    #[must_use]
873    pub const fn strictest_of(postures: &[Self]) -> Self {
874        let mut acc = UNBOUNDED_RESOURCE_LIMITS;
875        let mut i = 0;
876        while i < postures.len() {
877            acc = acc.strictest(postures[i]);
878            i += 1;
879        }
880        acc
881    }
882
883    /// Pointwise-`max` fold across a slice of postures — the MOST-
884    /// PERMISSIVE posture whose admissible input set is the union of
885    /// every operand's admissible input sets.
886    /// `ResourceLimits::most_permissive_of(&[a, b, c])` admits an input
887    /// iff ANY operand admits it: on every field the per-axis ceiling
888    /// is the largest across the whole slice.
889    ///
890    /// The N-ary JOIN aggregation on the [`ResourceLimits`] lattice —
891    /// the extension of [`Self::most_permissive`] from a 2-input
892    /// pairwise combinator to an N-input fold across a slice. Seeded
893    /// from [`EMPTY_RESOURCE_LIMITS`] (the join-identity — every field
894    /// at `0`, which never wins the pointwise `max` against any
895    /// non-zero value, so the seed contributes nothing to the fold's
896    /// result), which makes the empty-slice case aggregate to the
897    /// identity element. Peer of [`Self::strictest_of`] one COMBINATOR
898    /// axis over on the N-ary aggregation surface: where `strictest_of`
899    /// is the N-ary MEET (seeded from the meet-identity
900    /// [`UNBOUNDED_RESOURCE_LIMITS`]), this is the N-ary JOIN (seeded
901    /// from the join-identity). Together the two close the (N-ary meet,
902    /// N-ary join) aggregation pair.
903    ///
904    /// **Empty-slice identity**: `most_permissive_of(&[]) ==
905    /// EMPTY_RESOURCE_LIMITS` — the null aggregate binds structurally
906    /// to the join-identity element.
907    ///
908    /// **Single-element identity**: `most_permissive_of(&[a]) == a` —
909    /// the 1-input aggregate is the input verbatim, since
910    /// `EMPTY.most_permissive(a) == a` for every posture `a` by the
911    /// join-identity law.
912    ///
913    /// **Two-element identity**: `most_permissive_of(&[a, b]) ==
914    /// a.most_permissive(b)` — the 2-input aggregate reduces to the
915    /// pairwise combinator.
916    ///
917    /// **Order-independent**: `most_permissive_of(&[a, b, c]) ==
918    /// most_permissive_of(&[c, b, a])` — the fold inherits the
919    /// commutativity AND associativity of the pairwise
920    /// `most_permissive`.
921    ///
922    /// `const fn` so a caller can compose an arbitrary-length preset
923    /// slice at compile time — the N-ary const-fn peer of the pairwise
924    /// const-fn on [`Self::most_permissive`].
925    ///
926    /// Pre-lift, the SAME `.iter().copied().fold(EMPTY,
927    /// most_permissive)` three-primitive cascade appeared verbatim in
928    /// `empty_resource_limits_seeds_most_permissive_fold_over_slice`
929    /// AND at every future consumer that would compose "the loosest
930    /// across this set of presets" — a PRIME DIRECTIVE ≥2 trigger.
931    /// Post-lift the N-ary aggregation binds at ONE typed method with
932    /// the join-identity seed baked in so the consumer cannot pair the
933    /// meet-identity (`UNBOUNDED`) with the join combinator (a silent
934    /// distortion that would inflate every axis to [`usize::MAX`]).
935    ///
936    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
937    /// preserves proofs; the N-ary join of N preset-carried resource
938    /// proofs is itself a `ResourceLimits`. THEORY.md §V.1 — knowable
939    /// platform; the N-ary join becomes a TYPE-level operation on the
940    /// posture algebra rather than an inline fold cascade at every
941    /// consumer that aggregates a slice of presets.
942    #[must_use]
943    pub const fn most_permissive_of(postures: &[Self]) -> Self {
944        let mut acc = EMPTY_RESOURCE_LIMITS;
945        let mut i = 0;
946        while i < postures.len() {
947            acc = acc.most_permissive(postures[i]);
948            i += 1;
949        }
950        acc
951    }
952
953    /// Pointwise projection of `self` into the closed range `[lower, upper]`
954    /// — the bounded-lattice bracket combinator. `a.clamp(lower, upper)`
955    /// returns the posture whose per-axis ceiling is the input's own
956    /// ceiling raised to at least `lower`'s and then lowered to at most
957    /// `upper`'s: on every field the result is `min(max(a, lower), upper)`.
958    ///
959    /// The BRACKET operation on the [`ResourceLimits`] lattice — the
960    /// composition of [`Self::most_permissive`] with `lower` followed by
961    /// [`Self::strictest`] with `upper`. Peer of the (meet, join) pairwise
962    /// combinators one ARITY axis over (2 → 3 inputs) on the lattice-
963    /// algebra surface: where those two combine a `self` with ONE bound,
964    /// this combines a `self` with TWO bounds and closes the (unbounded-
965    /// side, bounded) row on the combinator-arity face. The bounded-lattice
966    /// analog of [`Ord::clamp`] on total orders, and the natural typed
967    /// entry a caller enforces "this posture must sit within the
968    /// `[ORG_MIN, ORG_MAX]` operator bracket" through.
969    ///
970    /// **In-range identity**: if `lower.leq(self) && self.leq(upper)` (the
971    /// input already sits within the bracket), then `a.clamp(lower, upper)
972    /// == a` — the pointwise `max(a, lower)` picks `a` on every axis (since
973    /// `lower ≤ a` per-axis), then pointwise `min(..., upper)` picks `a`
974    /// again (since `a ≤ upper` per-axis). Pinned as
975    /// `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`.
976    ///
977    /// **Below-lower floor**: if `self.leq(lower)` and `lower.leq(upper)`
978    /// (input tighter than the bracket floor, and floor within ceiling),
979    /// then `a.clamp(lower, upper) == lower` — the pointwise `max` lifts
980    /// every axis to `lower`, then the pointwise `min` keeps `lower` (since
981    /// `lower ≤ upper` per-axis). Pinned as
982    /// `resource_limits_clamp_of_posture_below_lower_returns_lower`.
983    ///
984    /// **Above-upper ceiling**: if `upper.leq(self)` and `lower.leq(upper)`
985    /// (input looser than the bracket ceiling), then `a.clamp(lower, upper)
986    /// == upper` — the pointwise `max` keeps `self` on every axis (since
987    /// `lower ≤ self` per-axis via transitivity through `upper`), then the
988    /// pointwise `min` lowers every axis to `upper`. Pinned as
989    /// `resource_limits_clamp_of_posture_above_upper_returns_upper`.
990    ///
991    /// **Bracket-membership** (`lower.leq(a.clamp(lower, upper)) &&
992    /// a.clamp(lower, upper).leq(upper)` for every `a` when `lower.leq(upper)`):
993    /// the result always sits within the `[lower, upper]` bracket
994    /// regardless of the input's position relative to the bracket. This is
995    /// the DEFINING contract of the clamp primitive — every consumer that
996    /// invokes it does so precisely to guarantee the result sits within
997    /// the bracket, and the type system now carries that guarantee at ONE
998    /// named entry. Pinned as
999    /// `resource_limits_clamp_result_sits_within_the_bracket`.
1000    ///
1001    /// **Extrema-bracket identity**: `a.clamp(EMPTY_RESOURCE_LIMITS,
1002    /// UNBOUNDED_RESOURCE_LIMITS) == a` — clamping against the bounded-
1003    /// lattice extrema is the identity function, since every posture
1004    /// already sits within the widest possible bracket. This is the
1005    /// bounded-lattice cross-check with the identity elements previously
1006    /// lifted (`EMPTY_RESOURCE_LIMITS` as the leq-minimum and
1007    /// `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum). Pinned as
1008    /// `resource_limits_clamp_with_lattice_extrema_returns_the_input`.
1009    ///
1010    /// **Degenerate-bracket collapse**: `a.clamp(x, x) == x` for every `a,
1011    /// x` — a zero-width bracket collapses every input to the single point
1012    /// `x`. Pinned as
1013    /// `resource_limits_clamp_with_equal_bounds_returns_the_bound`.
1014    ///
1015    /// **Idempotence**: `a.clamp(lower, upper).clamp(lower, upper) ==
1016    /// a.clamp(lower, upper)` — re-applying the same bracket to an
1017    /// already-clamped posture returns the same result, since the clamped
1018    /// value already sits within the bracket. Pinned as
1019    /// `resource_limits_clamp_is_idempotent`.
1020    ///
1021    /// `const fn` so a caller can pre-clamp a shipped preset against an
1022    /// operator-supplied policy bracket at compile time (`pub const
1023    /// POLICY_CLAMPED: ResourceLimits = SOME_PRESET.clamp(
1024    /// ORG_MIN_PRESET, ORG_MAX_PRESET);`) — the typed bracket-combinator
1025    /// peer of the const-fn composition ability on [`Self::strictest`] /
1026    /// [`Self::most_permissive`].
1027    ///
1028    /// Pre-lift, a caller wanting to bracket a posture within a
1029    /// `[lower, upper]` range composed the two-step cascade at its call
1030    /// site as `self.most_permissive(lower).strictest(upper)` — the same
1031    /// PRIME DIRECTIVE ≥2 pattern the pairwise combinators already lifted
1032    /// one arity down from the six-inline-primitive per-axis cascade.
1033    /// The `resource_limits_clamp_agrees_with_direct_two_step_cascade`
1034    /// pin below composes the pre-lift cascade at ONE explicit assertion
1035    /// site (its sole purpose is to pin the method-vs-cascade equivalence)
1036    /// — every future consumer that enforces a
1037    /// policy bracket (an operator-supplied `[ORG_MIN, ORG_MAX]`, a
1038    /// hostile-input `[EMPTY, HOSTILE_MAX]`, a CI-time `[TEST_MIN,
1039    /// TEST_MAX]`) would compose the same cascade at its call site with
1040    /// no exhaustiveness gate on the composition order. Post-lift the
1041    /// bracket binds at ONE typed method whose signature carries the
1042    /// bounds' roles (`lower` first, `upper` second) into the method
1043    /// rather than the consumer having to remember which side pairs with
1044    /// which combinator at each call site — a copy-paste that swapped the
1045    /// two combinators would clip the input to `min(max(a, upper), lower)`
1046    /// (the WRONG bracket order that returns `lower` for every input
1047    /// looser than `lower` regardless of `upper`'s position), a silent
1048    /// distortion the type system did not gate pre-lift and now does
1049    /// through the method's parameter order.
1050    ///
1051    /// **Non-lattice-bracket behaviour** (`!lower.leq(upper)`): the four
1052    /// documented invariants above are stated for the well-formed bracket
1053    /// case where `lower.leq(upper)`. When the two bounds are pointwise-
1054    /// incomparable OR strictly reversed (`upper.leq(lower) && lower !=
1055    /// upper`), the result is pointwise `min(max(a, lower), upper)` which
1056    /// is well-defined but no longer sits within a "bracket" in the
1057    /// intuitive sense — for a reversed bound the pointwise `max` pushes
1058    /// the value up to `lower`, then the pointwise `min` pushes it down to
1059    /// `upper`, producing `upper` at every axis (since `upper < lower`
1060    /// per-axis makes `min(anything ≥ lower, upper) == upper`). This
1061    /// matches [`Ord::clamp`]'s "min ≤ max is a precondition, otherwise
1062    /// behaviour is unspecified but well-defined" contract on total orders;
1063    /// callers routing preset pairs through `clamp` maintain the
1064    /// `lower.leq(upper)` invariant at their bounds' construction site,
1065    /// not at every clamp call site.
1066    ///
1067    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
1068    /// proofs; the bracket projection of a preset-carried resource proof
1069    /// against two bound preset-carried resource proofs is itself a
1070    /// `ResourceLimits` whose proof content is the input's proof intersected
1071    /// with the ceiling's and unioned with the floor's, and this method is
1072    /// the typed named entry composing them. THEORY.md §V.1 — knowable
1073    /// platform; the bracket combinator becomes a TYPE-level operation on
1074    /// the posture algebra rather than an inline two-step composition at
1075    /// every consumer that enforces a policy range — with the (lower,
1076    /// upper) parameter order baked in so the consumer cannot swap the
1077    /// two combinators (a silent distortion that would clip the input to
1078    /// the wrong bracket order). Frontier inspiration: [`Ord::clamp`] on
1079    /// total orders — the stdlib exposes a first-class named bracket
1080    /// combinator alongside the pairwise `min` / `max`, and the bounded-
1081    /// lattice extension is the same shape one dimension up (per-axis
1082    /// pointwise on a Cartesian product of total orders). Translation
1083    /// through pleme-io primitives is the plain `const fn` composition
1084    /// below, no trait indirection — the pairwise combinators already
1085    /// exist as `const fn` on the algebra so the bracket picks them up
1086    /// structurally.
1087    #[must_use]
1088    pub const fn clamp(self, lower: Self, upper: Self) -> Self {
1089        self.most_permissive(lower).strictest(upper)
1090    }
1091
1092    /// Pointwise bracket-membership predicate — `self` sits within the closed
1093    /// range `[lower, upper]` iff every axis's ceiling satisfies
1094    /// `lower ≤ self ≤ upper`. `a.within(lower, upper)` holds iff
1095    /// `lower.leq(self) && self.leq(upper)`: `self`'s per-axis ceilings all
1096    /// sit at or above `lower`'s AND at or below `upper`'s per-axis ceilings.
1097    ///
1098    /// The boolean PREDICATE peer of the BRACKET COMBINATOR [`Self::clamp`]
1099    /// one PRIMITIVE-KIND axis over on the bracket-primitive surface: where
1100    /// `clamp` PROJECTS an arbitrary input into the `[lower, upper]` bracket,
1101    /// this DECIDES whether the input already sits within it. Peer of
1102    /// [`Self::leq`] one ARITY axis over on the lattice-relation surface:
1103    /// where `leq` is the pairwise partial-order RELATION between two
1104    /// postures, this is the three-input CONTAINMENT RELATION deciding
1105    /// whether a posture sits between two bounds. Together the (leq, within)
1106    /// pair closes the (2-input, 3-input) row on the relation-arity face of
1107    /// the [`ResourceLimits`] lattice-algebra surface — the boolean twin of
1108    /// the (`strictest`+`most_permissive`, `clamp`) combinator-arity closure.
1109    ///
1110    /// **Clamp fixed-point theorem** (`a.within(lower, upper) ⇔
1111    /// a.clamp(lower, upper) == a`): the input sits within the bracket iff
1112    /// the clamp is the identity on it. The forward direction is the
1113    /// in-range identity arm of [`Self::clamp`] (already pinned as
1114    /// `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`);
1115    /// the reverse follows from the bracket-membership contract on `clamp`
1116    /// (the clamp result always sits within the bracket, so if the clamp
1117    /// equals the input, the input also sits within the bracket). Pinned as
1118    /// `resource_limits_within_agrees_with_clamp_fixed_point` — the CANONICAL
1119    /// cross-check axiom binding the predicate to the combinator, analogous
1120    /// to the meet-agreement (`a.leq(b) ⇔ a.strictest(b) == a`) and
1121    /// join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`) axioms binding
1122    /// the `leq` relation to its combinator peers.
1123    ///
1124    /// **In-range identity**: if `lower.leq(self) && self.leq(upper)`
1125    /// (the input already sits within the bracket), then `a.within(lower,
1126    /// upper) == true`. Pinned as
1127    /// `resource_limits_within_of_posture_in_range_is_true`.
1128    ///
1129    /// **Below-lower rejection**: if the input is strictly tighter than the
1130    /// bracket floor on any axis (`!lower.leq(self)`), then
1131    /// `a.within(lower, upper) == false` regardless of `self`'s relation to
1132    /// `upper` — the boolean conjunction rejects the input. Pinned as
1133    /// `resource_limits_within_of_posture_below_lower_is_false`.
1134    ///
1135    /// **Above-upper rejection**: if the input is strictly looser than the
1136    /// bracket ceiling on any axis (`!self.leq(upper)`), then
1137    /// `a.within(lower, upper) == false`. Pinned as
1138    /// `resource_limits_within_of_posture_above_upper_is_false`.
1139    ///
1140    /// **Extrema-bracket identity**: `a.within(EMPTY_RESOURCE_LIMITS,
1141    /// UNBOUNDED_RESOURCE_LIMITS) == true` for every posture, since every
1142    /// posture sits within the widest possible bracket — the bounded-lattice
1143    /// cross-check with the identity elements previously lifted. Pinned as
1144    /// `resource_limits_within_with_lattice_extrema_is_true`.
1145    ///
1146    /// **Degenerate-bracket collapse**: `a.within(x, x) ⇔ a == x` — a
1147    /// zero-width bracket admits only the single point `x`. Follows from
1148    /// antisymmetry of `leq`: `x.leq(a) && a.leq(x) → a == x`. Pinned as
1149    /// `resource_limits_within_of_equal_bounds_iff_equal_to_bound`.
1150    ///
1151    /// **Reflexive-bracket identity**: `a.within(a, a) == true` for every
1152    /// posture — every input trivially sits within the zero-width bracket
1153    /// at itself, since `a.leq(a)` holds by reflexivity of `leq`. Pinned as
1154    /// `resource_limits_within_of_self_is_reflexive`.
1155    ///
1156    /// `const fn` so a caller can pin a preset-bracket-membership identity
1157    /// at compile time (`const _: () = assert!(DEFAULT_RESOURCE_LIMITS
1158    /// .within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS));`) — the
1159    /// typed bracket-predicate peer of the const-fn evaluability on
1160    /// [`Self::leq`] and [`Self::clamp`].
1161    ///
1162    /// Pre-lift, a caller wanting to decide whether a posture sat within a
1163    /// `[lower, upper]` bracket composed the two-primitive scaffolding at its
1164    /// call site as `lower.leq(a) && a.leq(upper)` — the same PRIME DIRECTIVE
1165    /// ≥2 pattern the pairwise `strictest` / `most_permissive` / `leq`
1166    /// combinators already lifted one arity down from six-inline-primitive
1167    /// per-axis cascades. The clamp test cohort exercises this shape at
1168    /// every prerequisite pin (`FLOOR.leq(MID) && MID.leq(CEILING)`) AND at
1169    /// the bracket-membership contract's core assertion (`FLOOR.leq(clamped)
1170    /// && clamped.leq(CEILING)`); post-lift both bindings route through ONE
1171    /// named method whose signature carries the containment relation into the
1172    /// type system rather than the consumer composing the two-primitive
1173    /// conjunction at every call site — a copy-paste that swapped the
1174    /// bounds would test `upper.leq(a) && a.leq(lower)` (the WRONG
1175    /// containment direction returning `true` only for postures that
1176    /// simultaneously exceed the ceiling AND fall below the floor, i.e.
1177    /// only for the impossible `upper.leq(a) && a.leq(lower)` case when
1178    /// `lower.leq(upper)`), a silent distortion the type system did not gate
1179    /// pre-lift and now does through the method's parameter order.
1180    ///
1181    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
1182    /// proofs; the bracket-membership predicate on three preset-carried
1183    /// resource proofs is itself a typed named entry composing the input's
1184    /// containment ordering. THEORY.md §V.1 — knowable platform; the
1185    /// pointwise `≤`-conjunction combinator becomes a TYPE-level operation
1186    /// on the posture algebra rather than an inline two-primitive
1187    /// conjunction at every consumer that decides bracket membership — with
1188    /// the (lower, upper) parameter order baked in so the consumer cannot
1189    /// swap the two bounds. Frontier inspiration: the interval-containment
1190    /// predicate is the natural boolean twin of stdlib's [`Ord::clamp`] —
1191    /// stdlib lacks an `is_in_range(min, max)` method, but the bounded-
1192    /// lattice extension of the pattern is straightforward on the pointwise
1193    /// partial order and closes the (combinator, predicate) row on the
1194    /// bracket-primitive surface. Translation through pleme-io primitives is
1195    /// the plain `const fn` `leq`-conjunction below, no trait indirection —
1196    /// the partial-order relation already exists as a `const fn` on the
1197    /// algebra so the predicate picks it up structurally.
1198    #[must_use]
1199    pub const fn within(self, lower: Self, upper: Self) -> bool {
1200        lower.leq(self) && self.leq(upper)
1201    }
1202
1203    /// Boolean `leq`-conjunction across a slice of postures — `self` sits
1204    /// at-or-below every operand in `postures`. `a.is_lower_bound_of(&[b,
1205    /// c, d])` holds iff `a.leq(b) && a.leq(c) && a.leq(d)`: on every
1206    /// operand `self`'s per-axis ceilings sit at-or-below the operand's,
1207    /// so `self` is a common lower bound for the slice.
1208    ///
1209    /// The N-ary boolean PREDICATE peer of the pairwise partial-order
1210    /// relation [`Self::leq`] one ARITY axis over on the lattice-relation
1211    /// surface: where `leq` decides the two-input partial-order between
1212    /// two postures, this decides the (1 + N)-input containment-from-below
1213    /// between `self` and a slice of postures. Peer of
1214    /// [`Self::strictest_of`] one PRIMITIVE-KIND axis over on the N-ary
1215    /// aggregation surface: where `strictest_of` COMPUTES the meet (the
1216    /// greatest lower bound of the slice), this DECIDES whether `self` is
1217    /// a lower bound of the slice (member of the lower-bound set the meet
1218    /// is the largest element of). Together `strictest_of` and
1219    /// `is_lower_bound_of` close the (combinator, predicate) row on the
1220    /// N-ary-aggregation face, exactly the way (`strictest`,
1221    /// `most_permissive`) ↔ `leq` closes the pairwise row and (`clamp` ↔
1222    /// `within`) closes the bracket row on the (combinator, predicate) ×
1223    /// (pairwise, N-ary, bracket) primitive surface.
1224    ///
1225    /// **Empty-slice vacuous truth**: `a.is_lower_bound_of(&[]) == true`
1226    /// for every posture — the empty conjunction is vacuously true, since
1227    /// every posture is trivially a lower bound of the empty set. Peer of
1228    /// `strictest_of(&[]) == UNBOUNDED_RESOURCE_LIMITS`'s empty-slice
1229    /// identity: the empty slice aggregates to the identity element of
1230    /// the underlying operation (`true` for boolean conjunction,
1231    /// [`UNBOUNDED_RESOURCE_LIMITS`] for pointwise `min`), so the empty
1232    /// aggregate never rejects. Pinned as
1233    /// `resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true`.
1234    ///
1235    /// **Single-element identity**: `a.is_lower_bound_of(&[b]) ==
1236    /// a.leq(b)` — the 1-input predicate reduces to the pairwise
1237    /// relation, the same way `strictest_of(&[a]) == a` reduces the
1238    /// 1-input N-ary combinator to the operand verbatim. Pinned as
1239    /// `resource_limits_is_lower_bound_of_single_element_reduces_to_leq`.
1240    ///
1241    /// **Meet witness**: `strictest_of(&postures).is_lower_bound_of(&
1242    /// postures) == true` for every slice — the N-ary meet is always a
1243    /// lower bound of the slice it aggregates. The definitional link
1244    /// between the N-ary COMBINATOR and the N-ary PREDICATE: the
1245    /// aggregate `strictest_of` produces is a member of the lower-bound
1246    /// set `is_lower_bound_of` characterizes. Pinned as
1247    /// `resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice`.
1248    ///
1249    /// **Universal-bottom witness**: `EMPTY_RESOURCE_LIMITS
1250    /// .is_lower_bound_of(&postures) == true` for every slice — the
1251    /// lattice bottom is a common lower bound of every set, since
1252    /// `EMPTY.leq(a) == true` for every posture by the lattice-bottom
1253    /// axiom (`0 ≤ x` per-axis). Pinned as
1254    /// `resource_limits_empty_is_universal_lower_bound_of_every_slice`.
1255    ///
1256    /// **Any-operand rejection**: if `!self.leq(postures[i])` for any
1257    /// `i`, then `a.is_lower_bound_of(&postures) == false` — the
1258    /// conjunction short-circuits on the first violating operand and
1259    /// rejects. Pinned as
1260    /// `resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self`.
1261    ///
1262    /// **Peer** — [`Self::is_upper_bound_of`] one COMBINATOR-DIRECTION
1263    /// axis over: the two close the (`is_lower_bound_of`,
1264    /// `is_upper_bound_of`) N-ary boolean-conjunction pair the pairwise
1265    /// `leq` extends from 2 to N, itself peer of the (`strictest_of`,
1266    /// `most_permissive_of`) N-ary combinator pair one PRIMITIVE-KIND
1267    /// axis over.
1268    ///
1269    /// `const fn` so a caller can pin an N-ary-bound-membership identity
1270    /// at compile time (`const _: () = assert!(EMPTY_RESOURCE_LIMITS
1271    /// .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS,
1272    /// UNBOUNDED_RESOURCE_LIMITS]));`) — the N-ary predicate peer of the
1273    /// const-fn evaluability on [`Self::leq`] and [`Self::within`].
1274    /// `Copy` on [`ResourceLimits`] lets the const-fn loop index the
1275    /// slice by value without an explicit `.clone()` (which const-fn
1276    /// would not permit anyway).
1277    ///
1278    /// Pre-lift, a caller wanting to decide whether a posture was a
1279    /// common lower bound for a slice composed the
1280    /// `postures.iter().all(|p| self.leq(*p))` two-primitive scaffolding
1281    /// at every call site — the same PRIME DIRECTIVE ≥2 pattern the
1282    /// pairwise `leq` combinator already lifted one arity down from the
1283    /// six-inline-primitive per-axis cascade, and the same shape the
1284    /// pairwise `within` combinator already lifted one arity down from
1285    /// the two-primitive `lower.leq(a) && a.leq(upper)` conjunction.
1286    /// Post-lift the N-ary predicate binds at ONE typed method whose
1287    /// signature carries the containment direction (`self ≤ every
1288    /// operand`) into the type system rather than the consumer composing
1289    /// the `all()`-conjunction at every call site — a copy-paste that
1290    /// flipped the leq direction would test `postures[i].leq(self)` (the
1291    /// dual `is_upper_bound_of` question returning `true` only when
1292    /// `self` DOMINATES rather than UNDERLIES the slice), a silent
1293    /// distortion the method's parameter direction now gates.
1294    ///
1295    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1296    /// preserves proofs; the N-ary containment-from-below predicate on
1297    /// (1 + N) preset-carried resource proofs is itself a typed named
1298    /// entry composing the pairwise partial-order relation. THEORY.md
1299    /// §V.1 — knowable platform; the N-ary `leq`-conjunction becomes a
1300    /// TYPE-level operation on the posture algebra rather than an inline
1301    /// two-primitive conjunction at every consumer that decides
1302    /// N-ary-bound membership. Frontier inspiration: the algebraic-
1303    /// datatypes tradition of a `Foldable` typeclass exposing `all` /
1304    /// `Data.Foldable.all` alongside the pairwise relation — the N-ary
1305    /// traversal of a collection through a boolean-conjunction seed is a
1306    /// first-class named method the typeclass carries. Translation
1307    /// through pleme-io primitives is the plain `const fn` slice-walk
1308    /// below, no typeclass indirection — the partial-order relation
1309    /// already exists as a `const fn` on the algebra so the predicate
1310    /// picks it up structurally.
1311    #[must_use]
1312    pub const fn is_lower_bound_of(self, postures: &[Self]) -> bool {
1313        let mut i = 0;
1314        while i < postures.len() {
1315            if !self.leq(postures[i]) {
1316                return false;
1317            }
1318            i += 1;
1319        }
1320        true
1321    }
1322
1323    /// Boolean `leq`-conjunction across a slice of postures — every operand
1324    /// in `postures` sits at-or-below `self`. `a.is_upper_bound_of(&[b, c,
1325    /// d])` holds iff `b.leq(a) && c.leq(a) && d.leq(a)`: on every operand
1326    /// the operand's per-axis ceilings sit at-or-below `self`'s, so `self`
1327    /// is a common upper bound for the slice.
1328    ///
1329    /// The DUAL N-ary predicate of [`Self::is_lower_bound_of`] one
1330    /// COMBINATOR-DIRECTION axis over: the two close the (lower, upper)
1331    /// N-ary boolean-conjunction pair the pairwise `leq` extends from 2
1332    /// to N, itself peer of the (`strictest_of`, `most_permissive_of`)
1333    /// N-ary combinator pair one PRIMITIVE-KIND axis over on the N-ary-
1334    /// aggregation face of the (combinator, predicate) × (pairwise,
1335    /// N-ary, bracket) primitive surface. Peer of
1336    /// [`Self::most_permissive_of`] one PRIMITIVE-KIND axis over on the
1337    /// N-ary aggregation surface: where `most_permissive_of` COMPUTES
1338    /// the join (the least upper bound of the slice), this DECIDES
1339    /// whether `self` is an upper bound of the slice (member of the
1340    /// upper-bound set the join is the smallest element of).
1341    ///
1342    /// **Empty-slice vacuous truth**: `a.is_upper_bound_of(&[]) == true`
1343    /// for every posture — the empty conjunction is vacuously true. Peer
1344    /// of `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
1345    /// slice identity: the empty aggregate binds structurally to the
1346    /// identity element of the underlying operation. Pinned as
1347    /// `resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true`.
1348    ///
1349    /// **Single-element identity**: `a.is_upper_bound_of(&[b]) ==
1350    /// b.leq(a)` — the 1-input predicate reduces to the pairwise
1351    /// relation with the direction flipped (since `self` sits ABOVE the
1352    /// operand rather than BELOW it). Pinned as
1353    /// `resource_limits_is_upper_bound_of_single_element_reduces_to_leq`.
1354    ///
1355    /// **Join witness**: `most_permissive_of(&postures).is_upper_bound_of(&
1356    /// postures) == true` for every slice — the N-ary join is always an
1357    /// upper bound of the slice it aggregates. The dual definitional link
1358    /// between N-ary COMBINATOR and N-ary PREDICATE: the aggregate
1359    /// `most_permissive_of` produces is a member of the upper-bound set
1360    /// `is_upper_bound_of` characterizes. Pinned as
1361    /// `resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice`.
1362    ///
1363    /// **Universal-top witness**: `UNBOUNDED_RESOURCE_LIMITS
1364    /// .is_upper_bound_of(&postures) == true` for every slice — the
1365    /// lattice top is a common upper bound of every set, since
1366    /// `a.leq(UNBOUNDED) == true` for every posture by the lattice-top
1367    /// axiom (`x ≤ usize::MAX` per-axis). Pinned as
1368    /// `resource_limits_unbounded_is_universal_upper_bound_of_every_slice`.
1369    ///
1370    /// **Any-operand rejection**: if `!postures[i].leq(self)` for any
1371    /// `i`, then `a.is_upper_bound_of(&postures) == false` — the
1372    /// conjunction short-circuits and rejects. Pinned as
1373    /// `resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self`.
1374    ///
1375    /// `const fn` so a caller can pin an N-ary-bound-membership identity
1376    /// at compile time. See [`Self::is_lower_bound_of`] for the shared
1377    /// pre-lift ≥2 pattern (`postures.iter().all(|p| p.leq(*self))`), the
1378    /// theory anchor (THEORY.md §II.1 invariant 5, §V.1), and the
1379    /// `Foldable::all` frontier inspiration — this method carries the
1380    /// dual `postures[i].leq(self)` direction rather than
1381    /// `self.leq(postures[i])`.
1382    #[must_use]
1383    pub const fn is_upper_bound_of(self, postures: &[Self]) -> bool {
1384        let mut i = 0;
1385        while i < postures.len() {
1386            if !postures[i].leq(self) {
1387                return false;
1388            }
1389            i += 1;
1390        }
1391        true
1392    }
1393
1394    /// Strict pointwise partial-order relation across the six ceilings —
1395    /// `self` is STRICTLY tighter than `other` iff `self.leq(other)` AND
1396    /// `self != other`. `a.lt(b)` holds iff every input the `a` posture
1397    /// admits the `b` posture also admits AND there is at least one axis
1398    /// on which `a`'s ceiling is strictly smaller than `b`'s: `a`'s per-
1399    /// axis ceilings all sit at-or-below `b`'s AND at least one field
1400    /// closes the inequality strictly, so `b` strictly extends `a`'s
1401    /// admissible input set on at least one axis.
1402    ///
1403    /// The STRICT peer of [`Self::leq`] one STRICTNESS axis over on the
1404    /// pairwise partial-order surface: where `leq` is the REFLEXIVE
1405    /// non-strict `≤` relation (`a.leq(a) == true`), this is the
1406    /// IRREFLEXIVE strict `<` relation (`a.lt(a) == false`). Together
1407    /// the two close the (non-strict, strict) partial-order pair on the
1408    /// pairwise-relation face of the (combinator, predicate) ×
1409    /// (strictness) primitive surface — itself peer of the
1410    /// (`strictest`, `most_permissive`) meet/join combinator pair one
1411    /// PRIMITIVE-KIND axis over (both bind against the same pointwise
1412    /// `≤` ordering).
1413    ///
1414    /// Encoded as `self.leq(other) && !other.leq(self)` — the two-
1415    /// primitive antisymmetric encoding of strict `<` on any partial
1416    /// order, pointwise or otherwise. Equivalent on the
1417    /// [`ResourceLimits`] pointwise partial order (which is
1418    /// antisymmetric) to `self.leq(other) && self != other`:
1419    /// antisymmetry says `self.leq(other) && other.leq(self) ⇒ self ==
1420    /// other`, so `self.leq(other) && !other.leq(self)` iff
1421    /// `self.leq(other) && self != other`. The antisymmetric-leq
1422    /// encoding is chosen because it does NOT rely on
1423    /// [`PartialEq::eq`] being `const fn` (which the derived impl is
1424    /// not on stable) — both [`Self::leq`] calls are already `const fn`
1425    /// on the algebra.
1426    ///
1427    /// **Irreflexivity**: `a.lt(a) == false` for every posture. A
1428    /// posture is never strictly tighter than itself, since
1429    /// `a.leq(a) && !a.leq(a)` short-circuits `false` on the second
1430    /// conjunct. Pinned as
1431    /// `resource_limits_lt_is_irreflexive`.
1432    ///
1433    /// **Asymmetry**: `a.lt(b) ⇒ !b.lt(a)` for every pair. The two
1434    /// directions of the strict relation cannot both hold, since
1435    /// `a.lt(b)` implies `!b.leq(a)`, and `b.lt(a)` requires
1436    /// `b.leq(a)` — the two are contradictory. Pinned as
1437    /// `resource_limits_lt_is_asymmetric`.
1438    ///
1439    /// **Transitivity**: `a.lt(b) && b.lt(c) ⇒ a.lt(c)` for every
1440    /// triple. Inherits from the transitivity of [`Self::leq`] on both
1441    /// conjuncts, with the strictness carried through by the outer
1442    /// antisymmetric leg (if `c.leq(a)` held, transitivity of `leq`
1443    /// would give `b.leq(a)` contradicting `a.lt(b)`). Pinned as
1444    /// `resource_limits_lt_is_transitive`.
1445    ///
1446    /// **Refines [`Self::leq`]**: `a.lt(b) ⇒ a.leq(b)` for every pair
1447    /// — the strict relation refines the non-strict one. Conversely
1448    /// `a.leq(b) && !a.lt(b) ⇒ a == b` (the reflexive tie is exactly
1449    /// the non-strict-minus-strict gap). Pinned as
1450    /// `resource_limits_lt_refines_leq` AND
1451    /// `resource_limits_lt_agrees_with_leq_minus_equality`.
1452    ///
1453    /// **Bounded-lattice diagonal**:
1454    /// `EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS)` AND
1455    /// `DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS)` AND
1456    /// `EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS)` — the
1457    /// strict chain across the (bottom, middle, top) preset triple on
1458    /// the bounded-lattice diagonal. Every `DEFAULT_MAX_*` module
1459    /// constant is a concrete positive value strictly less than
1460    /// [`usize::MAX`] and strictly greater than `0`, so the two
1461    /// endpoints and the interior are strictly ordered on every axis.
1462    /// Pinned as `resource_limits_lt_of_bottom_diagonal_pinned`.
1463    ///
1464    /// **Incomparable rejection**: on the two hand-authored asymmetric
1465    /// postures (three axes smaller in each direction), both
1466    /// directions of the strict relation fail (`!a.lt(b) && !b.lt(a)`)
1467    /// because neither `leq` direction holds. The predicate does NOT
1468    /// promote incomparable pairs to a comparison verdict. Pinned as
1469    /// `resource_limits_lt_rejects_incomparable_postures`.
1470    ///
1471    /// **Antisymmetric-encoding cross-check**: `a.lt(b) == (a.leq(b)
1472    /// && !b.leq(a))` at every pair by definition, AND `a.lt(b) ==
1473    /// (a.leq(b) && a != b)` at every pair by antisymmetry. Both
1474    /// encodings are pinned equal to the shipped body across the
1475    /// (empty, default, unbounded, mid, other) 5×5 preset matrix.
1476    /// Pinned as
1477    /// `resource_limits_lt_agrees_with_direct_antisymmetric_encoding`.
1478    ///
1479    /// `const fn` so a caller can pin a strict-order identity at
1480    /// compile time (`const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(
1481    /// DEFAULT_RESOURCE_LIMITS));`) rather than deferring the
1482    /// composition to a runtime chain — sibling of the const-fn
1483    /// evaluability pin on [`Self::leq`] one STRICTNESS axis over.
1484    ///
1485    /// Pre-lift, a caller wanting "is this posture STRICTLY tighter
1486    /// than that one?" composed the two-primitive
1487    /// `a.leq(b) && !b.leq(a)` (or equivalently `a.leq(b) && a != b`)
1488    /// conjunction at its call site — a PRIME DIRECTIVE ≥2 pattern
1489    /// once two consumers need it, and one that couples the strictness
1490    /// interpretation to the caller's chosen encoding
1491    /// (antisymmetric-leq vs. leq-and-not-equal, which coincide on
1492    /// pointwise partial orders but diverge on non-antisymmetric
1493    /// preorders). Post-lift the strict relation binds at ONE typed
1494    /// method the algebra exposes, and the encoding is a private
1495    /// implementation detail no caller can drift from.
1496    ///
1497    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1498    /// preserves proofs; the strict-tightening relation between two
1499    /// preset-carried resource proofs is itself a typed named `bool`
1500    /// predicate composing them via the underlying `leq`. THEORY.md
1501    /// §V.1 — knowable platform; the strict `<` relation becomes a
1502    /// TYPE-level primitive on the posture algebra rather than an
1503    /// inline two-primitive conjunction at every consumer that
1504    /// decides "did we strictly tighten?" `tatara-lattice`'s partial
1505    /// order on `Classification` closes the SAME shape one layer
1506    /// down: an entity's classification lattice carries a strict `<`
1507    /// peer of its `leq` for the same reason a resource posture does.
1508    /// Frontier inspiration: [`PartialOrd::lt`] on Rust's partial-
1509    /// order trait; [`Ord::lt`] on the total-order trait — the
1510    /// strict variant of `≤` is a first-class named method the
1511    /// standard library exposes rather than leaving each consumer to
1512    /// compose `partial_cmp(&other) == Some(Less)` at its call site.
1513    /// Translation through pleme-io primitives is the plain `const
1514    /// fn` antisymmetric encoding below, no trait indirection — the
1515    /// non-strict [`Self::leq`] already exists as a `const fn` on the
1516    /// algebra so the strict variant picks it up structurally.
1517    #[must_use]
1518    pub const fn lt(self, other: Self) -> bool {
1519        self.leq(other) && !other.leq(self)
1520    }
1521
1522    /// Strict pointwise partial-order relation dual of [`Self::lt`] —
1523    /// `a.gt(b) == b.lt(a)`. `self` is STRICTLY looser than `other`
1524    /// iff `other.leq(self)` AND `self != other`: on every axis
1525    /// `self`'s ceiling sits at-or-above `other`'s, and at least one
1526    /// axis closes the inequality strictly, so `self` strictly extends
1527    /// `other`'s admissible input set on at least one axis.
1528    ///
1529    /// The DIRECTION peer of [`Self::lt`] one DIRECTION axis over on
1530    /// the strict-pairwise-relation face — where `lt` is the strict
1531    /// "below" relation (`a < b`), this is the strict "above"
1532    /// relation (`a > b`). Together the two close the (below, above)
1533    /// direction pair on the strict pairwise relation, exactly as
1534    /// [`Self::is_lower_bound_of`] / [`Self::is_upper_bound_of`] close
1535    /// the (below, above) direction pair on the non-strict N-ary
1536    /// relation one ARITY axis over.
1537    ///
1538    /// Same laws as [`Self::lt`] with directions flipped:
1539    /// irreflexive (`a.gt(a) == false`), asymmetric
1540    /// (`a.gt(b) ⇒ !b.gt(a)`), transitive
1541    /// (`a.gt(b) && b.gt(c) ⇒ a.gt(c)`); refines a hypothetical `geq`
1542    /// (the reflexive `≥`) exactly as `lt` refines `leq`. Pinned on
1543    /// the shipped preset triangle: `UNBOUNDED_RESOURCE_LIMITS.gt(
1544    /// DEFAULT_RESOURCE_LIMITS) && DEFAULT_RESOURCE_LIMITS.gt(
1545    /// EMPTY_RESOURCE_LIMITS)`.
1546    ///
1547    /// Encoded as `other.lt(self)` — one primitive delegation to
1548    /// [`Self::lt`] so the strict-relation encoding lives at exactly
1549    /// one implementation site, and a future re-derivation of `lt`
1550    /// (e.g. to a different antisymmetry encoding) propagates to `gt`
1551    /// mechanically rather than requiring a per-method fix-up.
1552    ///
1553    /// `const fn` for the same compile-time-pin reasons as
1554    /// [`Self::lt`]. See [`Self::lt`] for the full docstring, the
1555    /// pre-lift `a.leq(b) && !b.leq(a)` two-primitive PRIME DIRECTIVE
1556    /// ≥2 pattern, the theory anchor (THEORY.md §II.1 invariant 5,
1557    /// §V.1), and the [`Ord::gt`] frontier inspiration.
1558    #[must_use]
1559    pub const fn gt(self, other: Self) -> bool {
1560        other.lt(self)
1561    }
1562
1563    /// Non-strict pointwise partial-order relation dual of
1564    /// [`Self::leq`] — `a.geq(b) == b.leq(a)`. `self` is AT-OR-LOOSER-
1565    /// THAN `other` iff on every axis `self`'s ceiling sits at-or-above
1566    /// `other`'s: every input the `other` posture admits the `self`
1567    /// posture also admits, so `self` dominates `other`'s admissible
1568    /// input set pointwise (possibly with equality on some or every
1569    /// axis).
1570    ///
1571    /// The DIRECTION peer of [`Self::leq`] one DIRECTION axis over on
1572    /// the non-strict pairwise-relation face — where `leq` is the
1573    /// non-strict "below" relation (`a ≤ b`), this is the non-strict
1574    /// "above" relation (`a ≥ b`). Together the two close the
1575    /// (below, above) direction pair on the non-strict pairwise
1576    /// relation, exactly as [`Self::lt`] / [`Self::gt`] close the
1577    /// (below, above) direction pair on the STRICT pairwise relation
1578    /// one STRICTNESS axis over. The STRICTNESS peer of [`Self::gt`]
1579    /// one STRICTNESS axis over on the "above" column of the
1580    /// (direction × strictness) 2×2 partial-order face — where `gt` is
1581    /// the IRREFLEXIVE strict `>` relation (`a.gt(a) == false`), this
1582    /// is the REFLEXIVE non-strict `≥` relation (`a.geq(a) == true`).
1583    /// The four methods `(leq, lt, gt, geq)` EXHAUSTIVELY close the
1584    /// (below, above) × (non-strict, strict) 2×2 pairwise partial-order
1585    /// face on [`ResourceLimits`], with each method the single-axis
1586    /// peer of two others (one DIRECTION axis over AND one STRICTNESS
1587    /// axis over).
1588    ///
1589    /// Same laws as [`Self::leq`] with directions flipped:
1590    /// reflexive (`a.geq(a) == true`), antisymmetric
1591    /// (`a.geq(b) && b.geq(a) ⇒ a == b`), transitive
1592    /// (`a.geq(b) && b.geq(c) ⇒ a.geq(c)`); refines [`Self::gt`]
1593    /// (`a.gt(b) ⇒ a.geq(b)`) exactly as `leq` is refined by `lt`.
1594    /// NOT total on incomparable postures: on the two hand-authored
1595    /// asymmetric postures both directions of `geq` fail — the
1596    /// non-strict relation does not promote incomparable pairs to a
1597    /// comparison verdict any more than the strict one does. Pinned on
1598    /// the shipped preset triangle:
1599    /// `UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS) &&
1600    /// DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS)`.
1601    ///
1602    /// Encoded as `other.leq(self)` — one primitive delegation to
1603    /// [`Self::leq`] so the non-strict-relation encoding lives at
1604    /// exactly one implementation site, and a future re-derivation of
1605    /// `leq` (e.g. to a different pointwise conjunction) propagates to
1606    /// `geq` mechanically rather than requiring a per-method fix-up.
1607    /// Mirrors [`Self::gt`]'s `other.lt(self)` delegation one
1608    /// STRICTNESS axis over.
1609    ///
1610    /// **N-ary-predicate link**: `a.geq(b) == a.is_upper_bound_of(&[b])`
1611    /// — the pairwise "above" relation is exactly the 1-input
1612    /// specialization of the N-ary upper-bound predicate. The
1613    /// (`geq`, `is_upper_bound_of`) pair sits on the same ARITY axis
1614    /// as (`leq`, `is_lower_bound_of`) one DIRECTION axis over — every
1615    /// pairwise partial-order relation extends to an N-ary bound-
1616    /// membership predicate the substrate carries as a named entry.
1617    ///
1618    /// `const fn` for the same compile-time-pin reasons as
1619    /// [`Self::leq`] (`const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
1620    /// .geq(DEFAULT_RESOURCE_LIMITS));`). See [`Self::leq`] for the
1621    /// full docstring, the pre-lift six-inline-`>=`-conjunction pattern
1622    /// callers wrote before the pairwise relation was named, the
1623    /// theory anchor (THEORY.md §II.1 invariant 5, §V.1), and the
1624    /// [`PartialOrd::ge`] frontier inspiration.
1625    #[must_use]
1626    pub const fn geq(self, other: Self) -> bool {
1627        other.leq(self)
1628    }
1629
1630    /// Antichain characterization on the [`ResourceLimits`] partial
1631    /// order — `true` iff `self` and `other` are INCOMPARABLE, i.e.,
1632    /// NEITHER `self.leq(other)` NOR `self.geq(other)` holds. The two
1633    /// postures then sit on distinct branches of the lattice: at least
1634    /// one axis has `self` strictly below `other`, and at least one
1635    /// axis has `self` strictly above `other`, so neither pointwise-
1636    /// domination direction closes and neither preset dominates the
1637    /// other on every field.
1638    ///
1639    /// The ANTICHAIN CORNER on the (leq, geq, lt, gt, incomparable)
1640    /// pairwise-relation surface — the corner that the four pairwise
1641    /// partial-order predicates leave open. On a TOTAL order every
1642    /// pair of elements sits at exactly one of `{Less, Equal,
1643    /// Greater}`, so `is_incomparable` would fold to `false`
1644    /// unconditionally; on the [`ResourceLimits`] pointwise partial
1645    /// order the antichain is non-empty (witnessed by
1646    /// `HAND_AUTHORED_MID_POSTURE` + `HAND_AUTHORED_OTHER_POSTURE`,
1647    /// which the just-lifted [`Self::leq`] / [`Self::geq`] / [`Self::lt`]
1648    /// / [`Self::gt`] tests already pin as neither-`leq`-nor-`geq`), so
1649    /// the corner carries a genuine bit distinct from
1650    /// `!self.leq(other) && !self.geq(other)` inlined at the callsite.
1651    /// The (leq, geq, lt, gt) 2×2 pairwise partial-order face the just-
1652    /// lifted [`Self::geq`] EXHAUSTIVELY closes at its (below, above) ×
1653    /// (non-strict, strict) grid pins the COMPARABLE cases; THIS
1654    /// projection pins the DISJOINT antichain complement one COVER
1655    /// axis over — the (comparable, incomparable) two-cell partition of
1656    /// the total (ordered pair × verdict) surface.
1657    ///
1658    /// Symmetric on its argument order: `a.is_incomparable(b) ==
1659    /// b.is_incomparable(a)` — the composition `!self.leq(other) &&
1660    /// !self.geq(other)` folds through `self.leq(other) ==
1661    /// other.geq(self)` (dual identity on [`Self::geq`]) and the
1662    /// conjunction's commutativity, so the projection is
1663    /// order-independent on its two inputs. This SYMMETRY POSTURE
1664    /// distinguishes it from [`Self::leq`] / [`Self::lt`] (both
1665    /// antisymmetric — swapping inputs FLIPS the verdict) and from
1666    /// [`Self::within`] (asymmetric — the first input is the target
1667    /// and the last two are the bracket bounds).
1668    ///
1669    /// Irreflexive on its own argument: `a.is_incomparable(a) ==
1670    /// false` — every posture is comparable to itself via
1671    /// [`Self::leq`]'s reflexivity (`a.leq(a) == true`), so the
1672    /// conjunction's `!self.leq(other)` leg forces the verdict to
1673    /// `false` at every diagonal input. Sibling posture to
1674    /// [`Self::lt`]'s irreflexivity one COVER axis over: `lt` is
1675    /// irreflexive because the strict relation rules out equality;
1676    /// THIS projection is irreflexive because the comparable
1677    /// relation already includes equality.
1678    ///
1679    /// **Bottom-pole absorption**: `a.is_incomparable(
1680    /// EMPTY_RESOURCE_LIMITS) == false` on every posture `a` —
1681    /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so
1682    /// `EMPTY.leq(a)` holds on every axis (`0 <= a.max_*` on `usize`),
1683    /// which gives `a.geq(EMPTY)` through the [`Self::geq`] dual
1684    /// identity, which falsifies the `!self.geq(other)` leg. The
1685    /// bounded-lattice bottom is comparable to every posture. Pinned
1686    /// on the canonical preset roster.
1687    ///
1688    /// **Top-pole absorption**: `a.is_incomparable(
1689    /// UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a` —
1690    /// [`UNBOUNDED_RESOURCE_LIMITS`] is the bounded-lattice top, so
1691    /// `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
1692    /// usize::MAX` unconditionally), which falsifies the
1693    /// `!self.leq(other)` leg. The bounded-lattice top is comparable
1694    /// to every posture. Pinned on the canonical preset roster.
1695    ///
1696    /// **Antichain load-bearing arm**:
1697    /// `HAND_AUTHORED_MID_POSTURE.is_incomparable(
1698    /// HAND_AUTHORED_OTHER_POSTURE) == true` — the two hand-authored
1699    /// asymmetric postures sit on distinct branches (each is smaller
1700    /// on three axes and larger on the other three), so neither
1701    /// pointwise-domination direction closes and the antichain
1702    /// verdict is `true`. Pairs with
1703    /// `resource_limits_geq_rejects_incomparable_postures` +
1704    /// `resource_limits_leq_rejects_incomparable_postures` one COVER
1705    /// axis over: those pin the two comparable-direction predicates
1706    /// FALSIFY on this pair; THIS projection pins the antichain
1707    /// verdict HOLDS on the SAME pair — the two are the negation-
1708    /// paired sides of the (comparable, incomparable) two-cell
1709    /// partition.
1710    ///
1711    /// **De Morgan duality**: `a.is_incomparable(b) == !(a.leq(b) ||
1712    /// a.geq(b))` — the antichain corner is the SET-COMPLEMENT of
1713    /// the union of the two pointwise-domination directions. Pinned
1714    /// by `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
1715    /// which sweeps every ordered pair in the preset roster and
1716    /// confirms the equivalence at every cell.
1717    ///
1718    /// Encoded as `!self.leq(other) && !self.geq(other)` — one
1719    /// primitive delegation each to [`Self::leq`] and [`Self::geq`]
1720    /// (with [`Self::geq`] itself delegating to [`Self::leq`]) so the
1721    /// antichain characterization lives at exactly one implementation
1722    /// site, and a future re-derivation of [`Self::leq`] (e.g. to a
1723    /// different pointwise conjunction) propagates to
1724    /// `is_incomparable` mechanically rather than requiring a per-
1725    /// method fix-up. Mirrors [`Self::gt`]'s `other.lt(self)`
1726    /// delegation and [`Self::geq`]'s `other.leq(self)` delegation one
1727    /// COVER axis over on the pairwise-relation surface.
1728    ///
1729    /// `const fn` for the same compile-time-pin reasons as
1730    /// [`Self::leq`] (`const _: () =
1731    /// assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(
1732    /// EMPTY_RESOURCE_LIMITS));`), so a caller can pin an antichain-
1733    /// disagreement identity at compile time — a build-break rather
1734    /// than a runtime `assert!` on the first execution.
1735    ///
1736    /// Pre-lift, a caller that wanted "these two postures are
1737    /// incomparable on the pointwise partial-order" composed
1738    /// `!a.leq(b) && !b.leq(a)` at the callsite (or the algebraically
1739    /// equivalent `!a.leq(b) && !a.geq(b)` after `geq` landed), a
1740    /// two-primitive composition that appeared verbatim at every
1741    /// prospective antichain-detection site pre-lift — a ≥2 PRIME
1742    /// DIRECTIVE trigger with the (comparable, incomparable) two-cell
1743    /// partition of the ordered pair × verdict surface as its
1744    /// substrate posture. Post-lift the antichain characterization is
1745    /// ONE named primitive on the [`ResourceLimits`] surface,
1746    /// `const fn`-composable into a compile-time bound, and the two-
1747    /// primitive `!leq && !geq` composition lives at ONE
1748    /// implementation site.
1749    ///
1750    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1751    /// preserves proofs; the antichain characterization is the
1752    /// SET-COMPLEMENT of the union of the two comparable-direction
1753    /// predicates, and pinning the projection at ONE typed primitive
1754    /// makes the (comparable, incomparable) two-cell partition of the
1755    /// ordered-pair × verdict surface a substrate-level THEOREM
1756    /// rather than a per-consumer inline `!leq && !geq` sanity check.
1757    /// THEORY.md §V.1 — knowable platform; the antichain-detection
1758    /// corner was an unnamed two-primitive composition recurring at
1759    /// every prospective "these two postures sit on distinct
1760    /// branches?" callsite pre-lift.
1761    ///
1762    /// Frontier inspiration: Haskell's `Data.PartialOrd` typeclass'
1763    /// `compare :: PartialOrd a => a -> a -> Maybe Ordering` returning
1764    /// `Nothing` on incomparable inputs — the antichain corner is
1765    /// exactly the `isNothing` projection of that method; Julia's
1766    /// `Base.Order.NoOrder` sentinel on the same abstract; Rust's own
1767    /// `PartialOrd::partial_cmp` returning `Option<Ordering>` whose
1768    /// `None` arm is the antichain corner; Coq's `PreOrder` +
1769    /// classical LEM-derived `~ (leq a b) /\ ~ (leq b a)` proof
1770    /// obligation on the antichain arm. Translation through pleme-io
1771    /// primitives: the antichain characterization on the closed-set-
1772    /// paired [`ResourceLimits`] posture binds through the just-
1773    /// lifted [`Self::leq`] and [`Self::geq`] primitives (both const-
1774    /// evaluable, both delegating to [`Self::leq`]) with a two-arm
1775    /// conjunction on the negated forms — no new dep, no supertrait
1776    /// bound (`Sized + Copy + 'static`-plus-`Eq` stays untouched), no
1777    /// allocation, `const fn` throughout so the antichain verdict is
1778    /// a compile-time expression at every preset-tuple call site.
1779    #[must_use]
1780    pub const fn is_incomparable(self, other: Self) -> bool {
1781        !self.leq(other) && !self.geq(other)
1782    }
1783
1784    /// Pointwise partial-order COMPARABILITY relation across the six
1785    /// ceilings — `true` iff `self` and `other` sit on the SAME lattice
1786    /// branch, i.e., EITHER `self.leq(other)` OR `self.geq(other)`
1787    /// holds. The two postures then admit a pointwise-domination
1788    /// verdict in at least one direction: every axis has `self` at-or-
1789    /// below `other` (the `leq` branch) or every axis has `self`
1790    /// at-or-above `other` (the `geq` branch), so at least one
1791    /// direction closes and one preset dominates the other on every
1792    /// field.
1793    ///
1794    /// The COMPARABLE CORNER on the (leq, geq, lt, gt, incomparable,
1795    /// comparable) pairwise-relation surface — the DIRECT POSITIVE
1796    /// DUAL of [`Self::is_incomparable`] one COVER-COMPLEMENT axis
1797    /// over on the (comparable, incomparable) two-cell partition of
1798    /// the ordered pair × verdict surface. The just-lifted
1799    /// [`Self::is_incomparable`] projection pins the ANTICHAIN half of
1800    /// that partition (the pairs where NEITHER pointwise-domination
1801    /// direction closes); THIS projection pins the DUAL COMPARABLE
1802    /// half (the pairs where AT LEAST ONE direction closes) —
1803    /// EXHAUSTIVELY CLOSING the (comparable, incomparable) two-cell
1804    /// partition at its FINAL SECOND tile on the pairwise-relation
1805    /// surface. The (leq, geq, lt, gt) 2×2 pairwise partial-order face
1806    /// carries the four DIRECTIONAL comparable predicates one COVER
1807    /// axis over; THIS projection folds them into ONE POSITIVE
1808    /// COMPARABILITY verdict — the SET-UNION of the two pointwise-
1809    /// domination directions.
1810    ///
1811    /// Symmetric on its argument order: `a.is_comparable(b) ==
1812    /// b.is_comparable(a)` — the composition `self.leq(other) ||
1813    /// self.geq(other)` folds through `self.leq(other) ==
1814    /// other.geq(self)` (dual identity on [`Self::geq`]) and the
1815    /// disjunction's commutativity, so the projection is order-
1816    /// independent on its two inputs. This SYMMETRY POSTURE mirrors
1817    /// [`Self::is_incomparable`]'s symmetry one CELL axis over on the
1818    /// (comparable, incomparable) partition: both cells of an
1819    /// EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface must
1820    /// themselves be symmetric — the two verdicts flip in lockstep on
1821    /// argument swap. Distinguishes it from [`Self::leq`] / [`Self::lt`]
1822    /// (both antisymmetric — swapping inputs FLIPS the verdict).
1823    ///
1824    /// Reflexive on its own argument: `a.is_comparable(a) == true` —
1825    /// every posture is comparable to itself via [`Self::leq`]'s
1826    /// reflexivity (`a.leq(a) == true`), so the disjunction's
1827    /// `self.leq(other)` leg holds at every diagonal input. Sibling
1828    /// posture to [`Self::leq`]'s reflexivity one COVER axis over:
1829    /// the diagonal COMPARABLE cell is a substrate-level THEOREM
1830    /// falling out of the partial-order axioms, and THIS projection
1831    /// pins it as ONE positive verdict rather than the caller re-
1832    /// deriving `a.leq(a) || a.geq(a) == true` at every diagonal
1833    /// callsite.
1834    ///
1835    /// **Bottom-pole absorption**: `a.is_comparable(
1836    /// EMPTY_RESOURCE_LIMITS) == true` on every posture `a` —
1837    /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so
1838    /// `EMPTY.leq(a)` holds on every axis (`0 <= a.max_*` on `usize`),
1839    /// which gives `a.geq(EMPTY)` through the [`Self::geq`] dual
1840    /// identity, which satisfies the `self.geq(other)` leg. The
1841    /// bounded-lattice bottom is comparable to every posture. The
1842    /// COMPLEMENT of [`Self::is_incomparable`]'s bottom-pole
1843    /// absorption pin one CELL axis over — where the antichain
1844    /// verdict FOLDS FALSE at the bottom pole, the comparability
1845    /// verdict FOLDS TRUE. Pinned on the canonical preset roster.
1846    ///
1847    /// **Top-pole absorption**: `a.is_comparable(
1848    /// UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a` —
1849    /// [`UNBOUNDED_RESOURCE_LIMITS`] is the bounded-lattice top, so
1850    /// `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
1851    /// usize::MAX` unconditionally), which satisfies the
1852    /// `self.leq(other)` leg. The bounded-lattice top is comparable
1853    /// to every posture. The COMPLEMENT of [`Self::is_incomparable`]'s
1854    /// top-pole absorption pin one CELL axis over; pinned on the
1855    /// canonical preset roster.
1856    ///
1857    /// **Antichain load-bearing arm**:
1858    /// `HAND_AUTHORED_MID_POSTURE.is_comparable(
1859    /// HAND_AUTHORED_OTHER_POSTURE) == false` — the two hand-authored
1860    /// asymmetric postures sit on distinct branches (each is smaller
1861    /// on three axes and larger on the other three), so NEITHER
1862    /// pointwise-domination direction closes and the comparability
1863    /// verdict is `false`. The DIRECT FALSIFICATION of
1864    /// [`Self::is_incomparable`]'s antichain-arm HOLDS pin one CELL
1865    /// axis over on the SAME pair — the two are the negation-paired
1866    /// sides of the (comparable, incomparable) two-cell partition,
1867    /// and pinning BOTH cells' verdicts on the SAME load-bearing
1868    /// antichain pair closes the partition as a substrate-level
1869    /// EXCLUSIVITY THEOREM rather than one direction's assertion.
1870    ///
1871    /// **De Morgan duality**: `a.is_comparable(b) == !a.is_incomparable(b)`
1872    /// — the comparable corner is the SET-COMPLEMENT of the antichain
1873    /// corner. Pinned by
1874    /// `resource_limits_is_comparable_is_de_morgan_dual_of_incomparable`
1875    /// which sweeps every ordered pair in the preset roster and
1876    /// confirms the equivalence at every cell. Together with
1877    /// `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
1878    /// (which pins the OTHER direction on
1879    /// `a.leq(b) || a.geq(b)` composition), the two projections carry
1880    /// the (comparable, incomparable) two-cell partition as an
1881    /// EXHAUSTIVE-AND-DISJOINT decomposition of the ordered pair ×
1882    /// verdict surface.
1883    ///
1884    /// Encoded as `self.leq(other) || self.geq(other)` — one
1885    /// primitive delegation each to [`Self::leq`] and [`Self::geq`]
1886    /// (with [`Self::geq`] itself delegating to [`Self::leq`]) so the
1887    /// comparability characterization lives at exactly one
1888    /// implementation site, and a future re-derivation of [`Self::leq`]
1889    /// (e.g. to a different pointwise conjunction) propagates to
1890    /// `is_comparable` mechanically rather than requiring a per-
1891    /// method fix-up. Mirrors [`Self::is_incomparable`]'s
1892    /// `!self.leq(other) && !self.geq(other)` delegation one COVER
1893    /// axis over on the pairwise-relation surface — the two share
1894    /// the SAME two-primitive substrate and diverge only on the
1895    /// negation posture (`!… && !…` vs `… || …`), which De Morgan's
1896    /// law pins as the SAME projection.
1897    ///
1898    /// `const fn` for the same compile-time-pin reasons as
1899    /// [`Self::is_incomparable`] (`const _: () =
1900    /// assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(
1901    /// EMPTY_RESOURCE_LIMITS));`), so a caller can pin a
1902    /// comparability-agreement identity at compile time — a build-
1903    /// break rather than a runtime `assert!` on the first execution.
1904    ///
1905    /// Pre-lift, a caller that wanted "these two postures are
1906    /// comparable on the pointwise partial-order" composed
1907    /// `a.leq(b) || a.geq(b)` at the callsite — a two-primitive
1908    /// composition that appeared verbatim at every prospective
1909    /// comparability-detection site pre-lift, and the NEGATION of the
1910    /// same two-primitive composition [`Self::is_incomparable`]
1911    /// lifted one COVER axis over — a ≥2 PRIME DIRECTIVE trigger with
1912    /// the (comparable, incomparable) two-cell partition of the
1913    /// ordered pair × verdict surface as its substrate posture. Post-
1914    /// lift the comparability characterization is ONE named primitive
1915    /// on the [`ResourceLimits`] surface, `const fn`-composable into
1916    /// a compile-time bound, and the two-primitive `leq || geq`
1917    /// composition lives at ONE implementation site.
1918    ///
1919    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
1920    /// preserves proofs; the comparability characterization is the
1921    /// SET-UNION of the two pointwise-domination directions, and
1922    /// pinning the projection at ONE typed primitive makes the
1923    /// (comparable, incomparable) two-cell partition of the ordered-
1924    /// pair × verdict surface a substrate-level THEOREM proved on
1925    /// BOTH SIDES rather than one side asserted and the other
1926    /// inferred at consumer sites. THEORY.md §V.1 — knowable
1927    /// platform; the comparability-detection corner was an unnamed
1928    /// two-primitive composition recurring at every prospective
1929    /// "these two postures sit on the same branch?" callsite pre-
1930    /// lift.
1931    ///
1932    /// Frontier inspiration: Haskell's `Data.PartialOrd` typeclass'
1933    /// `compare :: PartialOrd a => a -> a -> Maybe Ordering` — the
1934    /// comparable corner is exactly the `isJust` projection of that
1935    /// method (the DUAL of `is_incomparable`'s `isNothing` projection
1936    /// on the SAME method); Julia's `Base.Order.lt` + `Base.Order.gt`
1937    /// disjunction on the pairwise-comparability check; Rust's own
1938    /// `PartialOrd::partial_cmp` whose `Some(_)` arm is the
1939    /// comparable corner; Coq's `PreOrder` + classical LEM-derived
1940    /// `leq a b \/ leq b a` proof obligation on the comparable arm.
1941    /// Translation through pleme-io primitives: the comparability
1942    /// characterization on the closed-set-paired [`ResourceLimits`]
1943    /// posture binds through the just-lifted [`Self::leq`] and
1944    /// [`Self::geq`] primitives (both const-evaluable, both
1945    /// delegating to [`Self::leq`]) with a two-arm disjunction on the
1946    /// direct forms — no new dep, no supertrait bound (`Sized + Copy + 'static`-plus-`Eq` stays untouched), no allocation, `const fn`
1947    /// throughout so the comparability verdict is a compile-time
1948    /// expression at every preset-tuple call site.
1949    #[must_use]
1950    pub const fn is_comparable(self, other: Self) -> bool {
1951        self.leq(other) || self.geq(other)
1952    }
1953
1954    /// Pointwise partial-order VERDICT across the six ceilings — the
1955    /// `Option<Ordering>` corner on the pairwise-relation surface. Folds
1956    /// the four possible outcomes ([`Ordering::Less`], [`Ordering::Equal`],
1957    /// [`Ordering::Greater`], INCOMPARABLE) into ONE typed value, so a
1958    /// caller that wants "how do these two postures relate?" gets the
1959    /// FULL verdict in a single call rather than composing
1960    /// [`Self::leq`] / [`Self::geq`] / [`Self::lt`] / [`Self::gt`] /
1961    /// `PartialEq::eq` at its callsite.
1962    ///
1963    /// The `Option<Ordering>` PROJECTION on the pairwise-relation surface —
1964    /// the DIRECT LIFT of the frontier inspiration the just-lifted
1965    /// [`Self::is_incomparable`] + [`Self::is_comparable`] pair explicitly
1966    /// named (Rust's `PartialOrd::partial_cmp`, Haskell's
1967    /// `Data.PartialOrd.compare :: a -> a -> Maybe Ordering`, Julia's
1968    /// `Base.Order`, Coq's `PreOrder` decidability obligation). Those two
1969    /// projections pinned ONE ARM each of the surface — the antichain
1970    /// arm ([`Self::is_incomparable`] → `is_none()`) and the comparability
1971    /// arm ([`Self::is_comparable`] → `is_some()`); THIS lift closes the
1972    /// surface at its FULL VERDICT corner by returning the ordering
1973    /// direction on the comparable arm and `None` on the antichain arm —
1974    /// the (Less, Equal, Greater, None) 4-tile partition of the ordered
1975    /// pair × verdict surface at ONE named primitive rather than a
1976    /// four-way conditional composition at every consumer that wanted
1977    /// the full verdict.
1978    ///
1979    /// **Antichain-arm identity**: `a.partial_cmp(b).is_none() ==
1980    /// a.is_incomparable(b)` — the `None` arm of the return coincides
1981    /// EXACTLY with the antichain-corner projection [`Self::is_incomparable`]
1982    /// lifts. On the antichain the two-arm conjunction `!self.leq(other)
1983    /// && !self.geq(other)` holds; neither branch of THIS method's
1984    /// disjunction closes, so the fall-through arm binds. Pinned as a
1985    /// substrate-level identity in the test cohort.
1986    ///
1987    /// **Comparability-arm identity**: `a.partial_cmp(b).is_some() ==
1988    /// a.is_comparable(b)` — the `Some(_)` arm of the return coincides
1989    /// EXACTLY with the comparability-corner projection [`Self::is_comparable`]
1990    /// lifts. On every comparable pair at least one of the two
1991    /// pointwise-domination legs closes, so the method returns a
1992    /// concrete [`Ordering`] variant. Together with the antichain-arm
1993    /// identity, THIS method is the JOINT LIFT of the (comparable,
1994    /// incomparable) two-cell partition into ONE `Option<Ordering>`
1995    /// value.
1996    ///
1997    /// **Equality on the diagonal**: `a.partial_cmp(a) ==
1998    /// Some(Ordering::Equal)` on every posture — the `leq` reflexivity
1999    /// (`a.leq(a) == true`) AND the `geq` reflexivity (`a.geq(a) ==
2000    /// true`) both hold at the diagonal, so both branches of the
2001    /// conjunction close and the equality arm fires. The diagonal cell
2002    /// of the (Less, Equal, Greater, None) 4-tile partition on the
2003    /// ordered pair × verdict surface pins to the Equal tile.
2004    ///
2005    /// **Ordering-direction agreement**: `a.partial_cmp(b) ==
2006    /// Some(Ordering::Less)` iff `a.lt(b)` (strict less); `a.partial_cmp(b)
2007    /// == Some(Ordering::Greater)` iff `a.gt(b)` (strict greater);
2008    /// `a.partial_cmp(b) == Some(Ordering::Equal)` iff `a == b` (via
2009    /// antisymmetry `a.leq(b) && a.geq(b) → a == b`). The four-arm
2010    /// dispatch below binds each verdict tile to its named companion
2011    /// primitive at exactly one implementation site, so a caller that
2012    /// wants the strict-less verdict binds through either `a.lt(b)` OR
2013    /// `matches!(a.partial_cmp(b), Some(Ordering::Less))` and both route
2014    /// to the same substrate.
2015    ///
2016    /// **Antisymmetry under argument swap**: `a.partial_cmp(b) ==
2017    /// b.partial_cmp(a).map(Ordering::reverse)` — swapping the argument
2018    /// pair FLIPS the ordering-direction verdict (`Less <-> Greater`,
2019    /// `Equal` fixed, `None` fixed). Discriminates [`Self::is_incomparable`] /
2020    /// [`Self::is_comparable`] (both SYMMETRIC on argument swap) one
2021    /// POSTURE axis over: the projections carry no directional content
2022    /// and are argument-order-independent; the FULL verdict carries
2023    /// directional content and inverts under swap. Pinned in the test
2024    /// cohort.
2025    ///
2026    /// **Bottom-pole absorption**: `EMPTY_RESOURCE_LIMITS.partial_cmp(a)`
2027    /// returns `Some(Ordering::Less)` for every posture `a` strictly
2028    /// above [`EMPTY_RESOURCE_LIMITS`] on the pointwise partial order,
2029    /// AND `Some(Ordering::Equal)` at `a == EMPTY_RESOURCE_LIMITS`.
2030    /// [`EMPTY_RESOURCE_LIMITS`] is the bounded-lattice bottom, so on
2031    /// every posture the `EMPTY.leq(a)` leg closes; the antisymmetric
2032    /// leg discriminates the strict-less arm (concrete positive posture)
2033    /// from the equality arm (`a == EMPTY`). The DIRECT REFINEMENT of
2034    /// [`Self::is_comparable`]'s bottom-pole absorption pin one
2035    /// DIRECTION-CONTENT axis over — where the comparability projection
2036    /// folds TRUE at the bottom pole across every posture, THIS method
2037    /// discriminates the equality-vs-strict-less refinement on the
2038    /// SAME preset roster.
2039    ///
2040    /// **Top-pole absorption**: symmetric statement — `a.partial_cmp(
2041    /// UNBOUNDED_RESOURCE_LIMITS)` returns `Some(Ordering::Less)` for
2042    /// every posture `a` strictly below the top and
2043    /// `Some(Ordering::Equal)` at `a == UNBOUNDED`. Pinned on the
2044    /// canonical preset roster.
2045    ///
2046    /// **Antichain load-bearing arm**: `HAND_AUTHORED_MID_POSTURE
2047    /// .partial_cmp(HAND_AUTHORED_OTHER_POSTURE) == None` — the two
2048    /// hand-authored asymmetric postures sit on distinct branches
2049    /// (each is smaller on three axes and larger on the other three),
2050    /// so NEITHER pointwise-domination direction closes and the
2051    /// fall-through `None` arm fires. The DIRECT LIFT of
2052    /// [`Self::is_incomparable`]'s antichain-arm HOLDS pin AND
2053    /// [`Self::is_comparable`]'s antichain-arm FALSIFIES pin one
2054    /// PROJECTION axis over on the SAME hand-authored pair — the two
2055    /// projections pinned each ARM's cell verdict; THIS method pins
2056    /// the FULL `Option<Ordering>` return at the antichain corner
2057    /// (`None`).
2058    ///
2059    /// Encoded as `if self.leq(other) && other.leq(self) { Equal } else
2060    /// if self.leq(other) { Less } else if other.leq(self) { Greater }
2061    /// else { None }` — a four-arm dispatch on the (`self.leq(other)`,
2062    /// `other.leq(self)`) 2×2 boolean truth-table, with each arm binding
2063    /// to its named companion primitive at exactly one implementation
2064    /// site so a future re-derivation of [`Self::leq`] propagates to
2065    /// EVERY ordering-direction verdict mechanically rather than
2066    /// requiring a per-arm fix-up. The `self.leq(other) && other.leq(self)`
2067    /// conjunction is the pointwise antisymmetric-equality
2068    /// characterization — pinned as a substrate-level identity by the
2069    /// `resource_limits_leq_is_antisymmetric` test cohort, so the
2070    /// Equal-arm binding above is a substrate-level THEOREM rather than
2071    /// an ad-hoc composition.
2072    ///
2073    /// `const fn` for the same compile-time-pin reasons as [`Self::leq`]
2074    /// / [`Self::geq`] / [`Self::lt`] / [`Self::gt`] / [`Self::is_incomparable`]
2075    /// / [`Self::is_comparable`] one COVER axis over on the pairwise-
2076    /// relation surface (`const _: () = assert!(matches!(
2077    /// EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
2078    /// Some(Ordering::Less)));`) — a caller can pin a full-verdict
2079    /// identity at compile time as a build-break rather than a runtime
2080    /// `assert!` on the first execution. [`Ordering`] itself is
2081    /// `const`-constructible (the three variants are plain enum
2082    /// constructors), so the return-value cascade below stays inside
2083    /// stable const-fn scope with no runtime allocation, no supertrait
2084    /// bound, and no new dep.
2085    ///
2086    /// Pre-lift, a caller that wanted the FULL pointwise ordering
2087    /// verdict either (a) composed four separate calls
2088    /// (`a.lt(b)` / `a == b` / `a.gt(b)` / `a.is_incomparable(b)`) at
2089    /// its callsite and dispatched on the resulting 4-way conditional —
2090    /// a per-consumer FOUR-primitive composition whose exhaustiveness
2091    /// the type system did NOT gate (a dropped arm silently miscategorized
2092    /// the pair) — or (b) invoked [`Self::is_comparable`] to get the
2093    /// `is_some`/`is_none` distinction and THEN chose one of `lt` / `gt`
2094    /// / equality on the `is_some` arm — a two-stage composition that
2095    /// double-evaluates the `leq` / `geq` primitives on every consumer.
2096    /// Post-lift the full verdict is ONE named primitive returning
2097    /// `Option<Ordering>`, `const fn`-composable into a compile-time
2098    /// bound, and the two-primitive `leq(self, other)` + `leq(other, self)`
2099    /// substrate binds at ONE implementation site — a ≥2 PRIME
2100    /// DIRECTIVE trigger, since the four-way ordering dispatch appeared
2101    /// inline at every prospective full-verdict callsite pre-lift.
2102    ///
2103    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
2104    /// preserves proofs; the FULL pointwise partial-order verdict on
2105    /// two preset-carried resource proofs is itself a typed
2106    /// `Option<Ordering>` value whose four arms each carry a distinct
2107    /// substrate-level relation proof (`Less` ⇒ strict-less on every
2108    /// axis; `Equal` ⇒ pointwise equality on every axis; `Greater` ⇒
2109    /// strict-greater on every axis; `None` ⇒ antichain — asymmetric
2110    /// per-axis orderings). Pinning the projection at ONE typed
2111    /// primitive makes the (Less, Equal, Greater, None) 4-tile
2112    /// partition of the ordered pair × verdict surface a substrate-
2113    /// level THEOREM rather than a per-consumer inline four-way
2114    /// dispatch. THEORY.md §V.1 — knowable platform; the full-verdict
2115    /// projection was an unnamed four-primitive composition recurring
2116    /// at every prospective "how do these two postures relate?"
2117    /// callsite pre-lift.
2118    ///
2119    /// Frontier inspiration: Rust's own `PartialOrd::partial_cmp :: fn
2120    /// partial_cmp(&self, &Self) -> Option<Ordering>` — the CANONICAL
2121    /// `Option<Ordering>` corner on any partial-order-carrying type;
2122    /// Haskell's `Data.PartialOrd.compare :: PartialOrd a => a -> a
2123    /// -> Maybe Ordering`; Julia's `Base.Order.NoOrder` sentinel folded
2124    /// into `Option<Ordering>` on the `Base.Order`-compatible partial
2125    /// order; Coq's `PreOrder` + `Decidable` typeclass composition
2126    /// giving `forall a b, {leq a b} + {leq b a} + {~(leq a b) /\
2127    /// ~(leq b a)}` — the three-way decidable sum where the third
2128    /// disjunct is the antichain corner. Translation through pleme-io
2129    /// primitives: the full-verdict projection binds through the just-
2130    /// lifted [`Self::leq`] primitive (invoked twice — once in each
2131    /// argument order — to derive the (`self.leq(other)`,
2132    /// `other.leq(self)`) 2×2 truth-table the four-arm dispatch reads
2133    /// against), no new dep, no supertrait bound (`Sized + Copy +
2134    /// 'static`-plus-`Eq` stays untouched), no allocation, `const fn`
2135    /// throughout. This method does NOT wire an `impl PartialOrd for
2136    /// ResourceLimits` — the derive would give lexicographic ordering
2137    /// on the six fields (WRONG for the pointwise lattice), and manually
2138    /// implementing the trait would expose `<` / `<=` / `>` / `>=`
2139    /// operators that overlap the named `lt` / `leq` / `gt` / `geq`
2140    /// primitives already lifted one COVER axis over. Keeping the
2141    /// `Option<Ordering>` corner at an inherent method preserves the
2142    /// tight name-directed surface without introducing an operator
2143    /// alias for each of the four directional predicates.
2144    #[must_use]
2145    pub const fn partial_cmp(self, other: Self) -> Option<Ordering> {
2146        let self_leq = self.leq(other);
2147        let other_leq = other.leq(self);
2148        if self_leq && other_leq {
2149            Some(Ordering::Equal)
2150        } else if self_leq {
2151            Some(Ordering::Less)
2152        } else if other_leq {
2153            Some(Ordering::Greater)
2154        } else {
2155            None
2156        }
2157    }
2158
2159    /// Boolean `is_comparable`-conjunction across every distinct pair of a
2160    /// slice of postures — `postures` is a CHAIN (a totally-ordered subset of
2161    /// the pointwise partial-order lattice) iff every distinct pair
2162    /// `(postures[i], postures[j])` sits on the same lattice branch
2163    /// (`postures[i].is_comparable(postures[j])`).
2164    ///
2165    /// The SET-LEVEL N-ary peer of [`Self::is_comparable`] one CARDINALITY
2166    /// axis over on the (comparable, incomparable) × (pair-level, set-level)
2167    /// primitive surface. Where [`Self::is_comparable`] decides the pairwise
2168    /// question "do THESE TWO postures sit on the same branch?", THIS
2169    /// projection decides the set-level question "does EVERY PAIR in this
2170    /// slice sit on the same branch?" — the natural lift of the pair-level
2171    /// verdict to the collection level, matching the arity extension the
2172    /// [`Self::is_lower_bound_of`] / [`Self::is_upper_bound_of`] pair carries
2173    /// one PRIMITIVE-KIND axis over on the (leq, comparable) × (pairwise,
2174    /// N-ary) primitive surface.
2175    ///
2176    /// **Empty-slice vacuous truth**: `ResourceLimits::is_chain(&[]) == true`
2177    /// — the empty conjunction is vacuously true; the empty slice contains
2178    /// no distinct pairs to reject. Peer of
2179    /// `is_lower_bound_of(&[]) == true`'s empty-slice identity one
2180    /// PRIMITIVE-KIND axis over. Pinned as
2181    /// `resource_limits_is_chain_empty_slice_is_vacuously_true`.
2182    ///
2183    /// **Singleton vacuous truth**: `ResourceLimits::is_chain(&[a]) == true`
2184    /// for every posture `a` — a one-element slice contains no distinct
2185    /// pairs, so the outer-and-inner-index conjunction never enters the
2186    /// inner loop and returns `true` vacuously. Pinned as
2187    /// `resource_limits_is_chain_singleton_is_vacuously_true`.
2188    ///
2189    /// **Diagonal-duplicate identity**: `ResourceLimits::is_chain(&[a, a])
2190    /// == true` for every posture `a` — [`Self::is_comparable`] is
2191    /// REFLEXIVE (`a.is_comparable(a) == true`), so the single distinct
2192    /// index pair `(0, 1)` binds `a.is_comparable(a) == true` and the
2193    /// conjunction holds. Distinguishes it from [`Self::is_antichain`] one
2194    /// COVER-COMPLEMENT axis over, whose diagonal-duplicate verdict is
2195    /// `false` (via [`Self::is_incomparable`]'s IRREFLEXIVITY). Pinned as
2196    /// `resource_limits_is_chain_of_diagonal_duplicate_is_true`.
2197    ///
2198    /// **Ordered-chain closure**: `ResourceLimits::is_chain(
2199    /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2200    /// UNBOUNDED_RESOURCE_LIMITS]) == true` — the shipped-preset triple on
2201    /// the bounded-lattice diagonal is an ascending chain (`EMPTY <=
2202    /// DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among the
2203    /// three is comparable. Pinned as
2204    /// `resource_limits_is_chain_holds_on_the_shipped_preset_triple`.
2205    ///
2206    /// **Antichain rejection**: `ResourceLimits::is_chain(
2207    /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2208    /// — the two hand-authored asymmetric postures sit on distinct branches
2209    /// (each is smaller on three axes and larger on the other three), so
2210    /// neither pointwise-domination direction closes and the pair is
2211    /// INCOMPARABLE. The chain conjunction rejects at the first
2212    /// incomparable pair. Pinned as
2213    /// `resource_limits_is_chain_rejects_the_hand_authored_antichain_pair`.
2214    ///
2215    /// **Mixed-set rejection**: `ResourceLimits::is_chain(
2216    /// &[DEFAULT_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE,
2217    /// HAND_AUTHORED_OTHER_POSTURE]) == false` — even though the first two
2218    /// AND the first-and-third pairs are comparable in isolation, the
2219    /// second-and-third pair is incomparable, so the whole-set conjunction
2220    /// rejects. The set-level chain verdict is stricter than the union of
2221    /// its pair-level verdicts — one antichain pair anywhere in the slice
2222    /// falsifies the whole projection. Pinned as
2223    /// `resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair`.
2224    ///
2225    /// **`most_permissive_of` closure witness**: on every slice `postures`
2226    /// where `ResourceLimits::is_chain(postures)` holds, the N-ary join
2227    /// `most_permissive_of(postures)` is a MEMBER of the slice (not just a
2228    /// bound above it) — the maximum element of a chain sits inside the
2229    /// chain. The set-level verdict thus underwrites the pointwise-`max`
2230    /// identification of the chain's top element. NOT pinned as a test in
2231    /// this cohort (the general witness needs a slice-membership predicate
2232    /// not yet lifted); recorded here as the future callsite the chain
2233    /// verdict unlocks.
2234    ///
2235    /// Encoded as a doubly-indexed const-`while` walk (`i in 0..n`, `j in
2236    /// i+1..n`) with one [`Self::is_comparable`] delegation per distinct
2237    /// pair. The `j = i + 1` inner starting index skips the diagonal AND
2238    /// the mirror pairs (`(j, i)` pairs the outer loop already visited from
2239    /// the `(i, j)` direction) — [`Self::is_comparable`] is SYMMETRIC on
2240    /// argument swap, so the mirror pair carries no additional information
2241    /// and the (n choose 2) unique-pair count is the tight iteration budget
2242    /// for the set-level verdict. `postures[i].is_comparable(postures[j])`
2243    /// (not `.leq(...) || .geq(...)` directly) so the primitive delegation
2244    /// lives at exactly one implementation site — a future re-derivation of
2245    /// [`Self::is_comparable`] (e.g. to a different comparability
2246    /// characterization) propagates to `is_chain` mechanically rather than
2247    /// requiring a per-callsite fix-up.
2248    ///
2249    /// `const fn` for the same compile-time-pin reasons as
2250    /// [`Self::is_comparable`] one CARDINALITY axis over on the (comparable,
2251    /// incomparable) × (pair-level, set-level) primitive surface (`const _:
2252    /// () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS,
2253    /// DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));`), so a
2254    /// caller can pin a chain-membership identity at compile time — a
2255    /// build-break rather than a runtime `assert!` on the first execution.
2256    /// `Copy` on [`ResourceLimits`] lets the const-fn loop index the slice
2257    /// by value without an explicit `.clone()` (which const-fn would not
2258    /// permit anyway).
2259    ///
2260    /// Pre-lift, a caller wanting "is this slice a chain?" composed the
2261    /// doubly-indexed `postures.iter().enumerate().all(|(i, a)|
2262    /// postures[i + 1..].iter().all(|b| a.is_comparable(*b)))` two-primitive
2263    /// scaffolding at every prospective callsite — a PRIME DIRECTIVE ≥2
2264    /// pattern once two consumers need the set-level chain verdict, and one
2265    /// whose exhaustiveness the type system did NOT gate (a slice iterator
2266    /// that stopped at the first `false` still needed the caller to bind
2267    /// the outer-and-inner-index conjunction correctly, and a copy-paste
2268    /// that swapped `postures[i]` with `postures[j]` would test the SAME
2269    /// pair via [`Self::is_comparable`]'s symmetry — silent no-op — but a
2270    /// copy-paste that inverted the primitive delegation to
2271    /// [`Self::is_incomparable`] would test the DUAL question and silently
2272    /// distort the verdict). Post-lift the chain verdict binds at ONE typed
2273    /// method the algebra exposes, and the doubly-indexed all-pairs
2274    /// scaffolding lives at ONE implementation site.
2275    ///
2276    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2277    /// proofs; the set-level chain verdict on N preset-carried resource
2278    /// proofs is itself a typed named `bool` predicate composing them via
2279    /// the underlying pair-level [`Self::is_comparable`] projection.
2280    /// THEORY.md §V.1 — knowable platform; the doubly-indexed all-pairs
2281    /// comparability conjunction becomes a TYPE-level operation on the
2282    /// posture algebra rather than an inline two-primitive scaffolding at
2283    /// every consumer that decides "is this slice a totally-ordered
2284    /// subset?"
2285    ///
2286    /// Frontier inspiration: Haskell's `Data.List.isSorted`
2287    /// (adjacent-pair-comparability variant on `Ord a`); Coq's `Sorted`
2288    /// inductive on lists over a `TotalOrder`; Idris's `Sorted` predicate
2289    /// over `Ord`-carrying lists; the order-theoretic notion of a CHAIN
2290    /// (a totally-ordered subset of a poset) canonicalized in
2291    /// Birkhoff's *Lattice Theory* and every subsequent poset text.
2292    /// Translation through pleme-io primitives: on a `PartialOrd`-derived
2293    /// carrier the frontier `isSorted` variants reduce to adjacent-pair
2294    /// comparability because total-order transitivity closes the chain; on
2295    /// a PARTIAL order (the pointwise lattice) the adjacent-pair check is
2296    /// INSUFFICIENT (three postures `a, b, c` where `a` and `c` are
2297    /// comparable but `b` is incomparable to at least one of them satisfy
2298    /// adjacent-pair comparability without forming a chain), so this
2299    /// projection walks EVERY distinct pair rather than only the adjacent
2300    /// ones — the strict partial-order generalization of the frontier
2301    /// total-order predicate. No new dep, no supertrait bound, `const fn`
2302    /// throughout.
2303    #[must_use]
2304    pub const fn is_chain(postures: &[Self]) -> bool {
2305        let n = postures.len();
2306        let mut i = 0;
2307        while i < n {
2308            let mut j = i + 1;
2309            while j < n {
2310                if !postures[i].is_comparable(postures[j]) {
2311                    return false;
2312                }
2313                j += 1;
2314            }
2315            i += 1;
2316        }
2317        true
2318    }
2319
2320    /// Boolean `is_incomparable`-conjunction across every distinct pair of a
2321    /// slice of postures — `postures` is an ANTICHAIN (a pairwise-
2322    /// incomparable subset of the pointwise partial-order lattice) iff every
2323    /// distinct pair `(postures[i], postures[j])` sits on distinct branches
2324    /// (`postures[i].is_incomparable(postures[j])`).
2325    ///
2326    /// The SET-LEVEL N-ary peer of [`Self::is_incomparable`] one CARDINALITY
2327    /// axis over on the (comparable, incomparable) × (pair-level, set-level)
2328    /// primitive surface — the DIRECT SET-LEVEL DUAL of [`Self::is_chain`]
2329    /// one COVER-COMPLEMENT axis over on the SAME set-level cardinality
2330    /// slice. Together the two close the (chain, antichain) two-cell face
2331    /// on the set-level pairwise-relation surface, exactly ONE CARDINALITY
2332    /// axis over from the (comparable, incomparable) two-cell partition of
2333    /// the ordered-pair × verdict surface [`Self::is_comparable`] and
2334    /// [`Self::is_incomparable`] closed one arity down.
2335    ///
2336    /// The (chain, antichain) pair is NOT a partition of the set-level
2337    /// verdict surface: a slice of ≥3 postures whose pair-level verdicts
2338    /// mix comparable and incomparable pairs is neither a chain nor an
2339    /// antichain, so the two set-level projections carry a THREE-cell
2340    /// face (chain, antichain, mixed) at the set level whose ends the two
2341    /// projections lifted here pin. Distinguishes it from the pair-level
2342    /// (comparable, incomparable) partition [`Self::is_comparable`] +
2343    /// [`Self::is_incomparable`] EXHAUSTIVELY closed one arity down: at
2344    /// arity 2 every pair is exactly one of comparable-or-incomparable, and
2345    /// the two projections partition the ordered-pair × verdict surface; at
2346    /// arity ≥3 the mixed-set cell opens up between them and the two
2347    /// set-level projections carry the ends of that three-cell face.
2348    ///
2349    /// **Empty-slice vacuous truth**: `ResourceLimits::is_antichain(&[])
2350    /// == true` — the empty conjunction is vacuously true; the empty slice
2351    /// contains no distinct pairs to reject. Peer of [`Self::is_chain`]'s
2352    /// empty-slice identity one COVER-COMPLEMENT axis over: both cells of
2353    /// the (chain, antichain) set-level pair AGREE at the empty slice on
2354    /// the vacuous-truth verdict — the two cells coincide at the empty
2355    /// face of the set-level verdict surface. Pinned as
2356    /// `resource_limits_is_antichain_empty_slice_is_vacuously_true`.
2357    ///
2358    /// **Singleton vacuous truth**: `ResourceLimits::is_antichain(&[a])
2359    /// == true` for every posture `a` — a one-element slice contains no
2360    /// distinct pairs, so the outer-and-inner-index conjunction never
2361    /// enters the inner loop and returns `true` vacuously. Peer of
2362    /// [`Self::is_chain`]'s singleton-vacuous-truth identity: both cells
2363    /// AGREE at singleton slices too — the (chain, antichain) verdict
2364    /// surface coincides at every slice with strictly fewer than two
2365    /// distinct pairs. Pinned as
2366    /// `resource_limits_is_antichain_singleton_is_vacuously_true`.
2367    ///
2368    /// **Diagonal-duplicate rejection**: `ResourceLimits::is_antichain(
2369    /// &[a, a]) == false` for every posture `a` — [`Self::is_incomparable`]
2370    /// is IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
2371    /// distinct index pair `(0, 1)` binds `a.is_incomparable(a) == false`
2372    /// and the conjunction rejects. Distinguishes it from [`Self::is_chain`]
2373    /// one COVER-COMPLEMENT axis over, whose diagonal-duplicate verdict is
2374    /// `true` (via [`Self::is_comparable`]'s REFLEXIVITY). The reflexivity/
2375    /// irreflexivity divergence between the two pair-level projections
2376    /// PROPAGATES to the set-level projections at any slice with a
2377    /// duplicated element — the two set-level cells DIVERGE at every
2378    /// diagonal-duplicate slice, the mirror of their AGREEMENT at every
2379    /// empty-or-singleton slice one CARDINALITY axis over. Pinned as
2380    /// `resource_limits_is_antichain_of_diagonal_duplicate_is_false`.
2381    ///
2382    /// **Hand-authored antichain closure**: `ResourceLimits::is_antichain(
2383    /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == true`
2384    /// — the two hand-authored asymmetric postures sit on distinct
2385    /// branches (each is smaller on three axes and larger on the other
2386    /// three), so neither pointwise-domination direction closes and the
2387    /// pair is INCOMPARABLE. The antichain conjunction accepts every
2388    /// distinct pair of the slice. Pinned as
2389    /// `resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair`.
2390    ///
2391    /// **Chain rejection**: `ResourceLimits::is_antichain(
2392    /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]) == false` — the
2393    /// shipped-preset pair on the bounded-lattice diagonal is a strict
2394    /// chain (`EMPTY.lt(DEFAULT)`), so the single distinct pair is
2395    /// COMPARABLE and the antichain conjunction rejects. The DIRECT
2396    /// FALSIFICATION of [`Self::is_chain`]'s ordered-chain closure one
2397    /// CELL axis over on the SAME slice — the two set-level cells carry
2398    /// the negation-paired verdict at every strictly-chained slice, the
2399    /// mirror of their AGREEMENT at every empty-or-singleton slice one
2400    /// CARDINALITY axis over. Pinned as
2401    /// `resource_limits_is_antichain_rejects_the_shipped_preset_pair`.
2402    ///
2403    /// **Mixed-set rejection**: `ResourceLimits::is_antichain(
2404    /// &[DEFAULT_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE,
2405    /// HAND_AUTHORED_OTHER_POSTURE]) == false` — even though the
2406    /// second-and-third pair is incomparable, the first pair
2407    /// (`DEFAULT`-and-`MID`) is comparable, so the whole-set conjunction
2408    /// rejects. Pins the three-cell (chain, antichain, mixed) set-level
2409    /// face at the mixed-set cell: the same slice both [`Self::is_chain`]
2410    /// AND [`Self::is_antichain`] reject at, and the third cell of the
2411    /// three-cell face the two set-level projections carry the ends of.
2412    /// Pinned as
2413    /// `resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair`.
2414    ///
2415    /// **De Morgan mirror at arity 2**: on any two-element slice `[a, b]`
2416    /// (`a != b`), `ResourceLimits::is_antichain(&[a, b]) ==
2417    /// !ResourceLimits::is_chain(&[a, b])` — at exactly two DISTINCT
2418    /// elements the set-level (chain, antichain) two-cell face reduces to
2419    /// the pair-level (comparable, incomparable) two-cell partition and
2420    /// the two cells become MUTUALLY EXCLUSIVE (the mixed cell requires
2421    /// ≥3 elements to open). The set-level De Morgan mirror of the
2422    /// pair-level identity `a.is_comparable(b) == !a.is_incomparable(b)`
2423    /// [`Self::is_comparable`] pins one CARDINALITY axis over. Pinned as
2424    /// `resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs`.
2425    ///
2426    /// Encoded as a doubly-indexed const-`while` walk (`i in 0..n`, `j in
2427    /// i+1..n`) with one [`Self::is_incomparable`] delegation per distinct
2428    /// pair — the SAME iteration structure as [`Self::is_chain`] one
2429    /// PRIMITIVE-DELEGATION axis over, with only the inner primitive
2430    /// swapped between the two. The two set-level projections share the
2431    /// SAME two-primitive substrate (the doubly-indexed pair walk + one
2432    /// pair-level projection) and diverge only on the pair-level primitive
2433    /// they delegate to (`is_comparable` vs `is_incomparable`), which De
2434    /// Morgan's law pins as the pair-level negation-paired substrate.
2435    ///
2436    /// `const fn` for the same compile-time-pin reasons as
2437    /// [`Self::is_incomparable`] one CARDINALITY axis over on the
2438    /// (comparable, incomparable) × (pair-level, set-level) primitive
2439    /// surface (`const _: () = assert!(ResourceLimits::is_antichain(
2440    /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]));`), so
2441    /// a caller can pin an antichain-membership identity at compile time
2442    /// — a build-break rather than a runtime `assert!` on the first
2443    /// execution.
2444    ///
2445    /// Pre-lift, a caller wanting "is this slice an antichain?" composed
2446    /// the doubly-indexed `postures.iter().enumerate().all(|(i, a)|
2447    /// postures[i + 1..].iter().all(|b| a.is_incomparable(*b)))` two-
2448    /// primitive scaffolding at every prospective callsite — the SAME
2449    /// PRIME DIRECTIVE ≥2 pattern [`Self::is_chain`]'s pre-lift shape
2450    /// carried, with only the pair-level primitive swapped. Post-lift the
2451    /// antichain verdict binds at ONE typed method the algebra exposes,
2452    /// and the doubly-indexed all-pairs scaffolding lives at ONE
2453    /// implementation site — the same site [`Self::is_chain`] delegates
2454    /// to, factored down to a per-projection choice of pair-level
2455    /// primitive.
2456    ///
2457    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2458    /// proofs; the set-level antichain verdict on N preset-carried
2459    /// resource proofs is itself a typed named `bool` predicate composing
2460    /// them via the underlying pair-level [`Self::is_incomparable`]
2461    /// projection. THEORY.md §V.1 — knowable platform; the doubly-indexed
2462    /// all-pairs incomparability conjunction becomes a TYPE-level
2463    /// operation on the posture algebra rather than an inline two-
2464    /// primitive scaffolding at every consumer that decides "is this
2465    /// slice a pairwise-incomparable subset?"
2466    ///
2467    /// Frontier inspiration: the order-theoretic notion of an ANTICHAIN
2468    /// (a pairwise-incomparable subset of a poset) canonicalized in
2469    /// Dilworth's theorem (a poset's width — the maximum antichain size —
2470    /// equals the minimum number of chains covering the poset) and the
2471    /// Robinson–Schensted correspondence's Young-tableau chain/antichain
2472    /// duality; Haskell's `Data.List` idiom of
2473    /// `all (uncurry incomparable) . pairs` for a partial-order carrier;
2474    /// Coq's `Antichain` inductive on lists over a `PartialOrder`.
2475    /// Translation through pleme-io primitives: the doubly-indexed pair
2476    /// walk directly, with [`Self::is_incomparable`] as the pair-level
2477    /// primitive, no new dep, no supertrait bound, `const fn` throughout.
2478    /// The (chain, antichain) set-level pair opens the door to Dilworth's
2479    /// width invariant on the [`ResourceLimits`] lattice at a future
2480    /// callsite — the maximum antichain size is a substrate-level
2481    /// dimension the two set-level projections lift here jointly
2482    /// characterize.
2483    #[must_use]
2484    pub const fn is_antichain(postures: &[Self]) -> bool {
2485        let n = postures.len();
2486        let mut i = 0;
2487        while i < n {
2488            let mut j = i + 1;
2489            while j < n {
2490                if !postures[i].is_incomparable(postures[j]) {
2491                    return false;
2492                }
2493                j += 1;
2494            }
2495            i += 1;
2496        }
2497        true
2498    }
2499
2500    /// The MIDDLE-CELL corner of the (chain, antichain, mixed) three-cell
2501    /// face on the set-level pairwise-relation surface — `postures` is a
2502    /// MIXED slice iff it is NEITHER a chain NOR an antichain, computed
2503    /// as the boolean conjunction of the negations of the just-lifted
2504    /// [`Self::is_chain`] and [`Self::is_antichain`] projections.
2505    ///
2506    /// EXHAUSTIVELY CLOSES the (chain, antichain, mixed) three-cell face
2507    /// at its previously-open middle cell — the two set-level ENDS the
2508    /// just-lifted [`Self::is_chain`] / [`Self::is_antichain`] pair
2509    /// pinned already carried the (chain, antichain) two ends; this
2510    /// projection binds the third cell (F, F) as a NAMED typed `bool`.
2511    /// The three cells now sit at (T, F) — pure chain, (F, T) — pure
2512    /// antichain, and (F, F) — mixed. The fourth cell (T, T) is only
2513    /// achievable at the degenerate empty-or-singleton slice where BOTH
2514    /// projections agree on the vacuous-truth verdict — [`Self::is_mixed`]
2515    /// reports `false` there too, aligning with the intuition that a
2516    /// slice with strictly fewer than two distinct pairs is neither
2517    /// chain-mixed nor antichain-mixed.
2518    ///
2519    /// **Empty-slice contract**: `ResourceLimits::is_mixed(&[]) == false`
2520    /// — [`Self::is_chain`] and [`Self::is_antichain`] BOTH return `true`
2521    /// vacuously, so `!true && !true == false`. The empty slice is NOT
2522    /// mixed. Pinned as
2523    /// `resource_limits_is_mixed_empty_slice_is_false`.
2524    ///
2525    /// **Singleton contract**: `ResourceLimits::is_mixed(&[a]) == false`
2526    /// for every posture `a` — a one-element slice contains no distinct
2527    /// pairs; both [`Self::is_chain`] and [`Self::is_antichain`] return
2528    /// `true` vacuously. Pinned as
2529    /// `resource_limits_is_mixed_singleton_is_false`.
2530    ///
2531    /// **Diagonal-duplicate contract**: `ResourceLimits::is_mixed(&[a, a])
2532    /// == false` for every posture `a` — [`Self::is_chain`] returns
2533    /// `true` via [`Self::is_comparable`]'s REFLEXIVITY;
2534    /// [`Self::is_antichain`] returns `false` via
2535    /// [`Self::is_incomparable`]'s IRREFLEXIVITY; the conjunction is
2536    /// `!true && !false == false`. The diagonal-duplicate slice is a
2537    /// CHAIN, not mixed. Pinned as
2538    /// `resource_limits_is_mixed_of_diagonal_duplicate_is_false`.
2539    ///
2540    /// **Chain rejection**: `ResourceLimits::is_mixed(
2541    /// &[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2542    /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the shipped-preset triple
2543    /// on the bounded-lattice diagonal is an ascending chain,
2544    /// [`Self::is_chain`] returns `true`, and `!true && …` short-
2545    /// circuits to `false`. Pinned as
2546    /// `resource_limits_is_mixed_rejects_the_shipped_preset_triple`.
2547    ///
2548    /// **Antichain rejection**: `ResourceLimits::is_mixed(
2549    /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) ==
2550    /// false` — the hand-authored antichain pair binds
2551    /// [`Self::is_antichain`] `true`, and `!… && !true == false`. Pinned
2552    /// as
2553    /// `resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair`.
2554    ///
2555    /// **Mixed-set closure (LOAD-BEARING `true`-arm catch)**:
2556    /// `ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS,
2557    /// HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == true`
2558    /// — the (DEFAULT, MID) pair is comparable (falsifying
2559    /// [`Self::is_antichain`]) while the (MID, OTHER) pair is
2560    /// incomparable (falsifying [`Self::is_chain`]); the conjunction
2561    /// `!false && !false == true`. This is the SAME fixture the sibling
2562    /// [`Self::is_chain`] and [`Self::is_antichain`] BOTH reject at —
2563    /// pinning the DISCRIMINATING positive-arm catch for the (mixed)
2564    /// cell of the three-cell face. Pinned as
2565    /// `resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair`.
2566    ///
2567    /// **Trichotomy exhaustiveness at arity ≥3 with distinct
2568    /// elements**: for every slice `postures` with `len() >= 2` and
2569    /// pairwise-distinct elements, EXACTLY ONE of [`Self::is_chain`],
2570    /// [`Self::is_antichain`], [`Self::is_mixed`] returns `true` — the
2571    /// three cells partition the set-level verdict surface at every
2572    /// non-degenerate slice. At arity < 2 (empty and singleton) the
2573    /// verdict surface degenerates to the vacuous-truth (T, T, F)
2574    /// cell; at arity ≥2 with any duplicated element the verdict
2575    /// surface degenerates to the chain-only (T, F, F) cell (chain
2576    /// wins via reflexivity, antichain drops via irreflexivity).
2577    /// Pinned as
2578    /// `resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices`.
2579    ///
2580    /// Encoded as the boolean conjunction of the negations of the two
2581    /// just-lifted set-level projections — one implementation site, ZERO
2582    /// new pair-level primitive delegations, `const fn` throughout via
2583    /// the underlying `const fn` bodies. The doubly-indexed all-pairs
2584    /// walk lives at ONE site (the [`Self::is_chain`] +
2585    /// [`Self::is_antichain`] implementations), and this projection
2586    /// factors THROUGH both without reopening it.
2587    ///
2588    /// Pre-lift, a caller wanting "is this slice a mixed subset (neither
2589    /// chain nor antichain)?" composed the doubly-indexed
2590    /// `!postures.iter().enumerate().all(|(i, a)|
2591    /// postures[i + 1..].iter().all(|b| a.is_comparable(*b))) &&
2592    /// !postures.iter().enumerate().all(|(i, a)|
2593    /// postures[i + 1..].iter().all(|b| a.is_incomparable(*b)))`
2594    /// two-scaffolding conjunction at every prospective callsite —
2595    /// the SAME PRIME DIRECTIVE ≥2 pattern the pair of just-lifted
2596    /// projections' pre-lift shapes carried, doubled. Post-lift the
2597    /// mixed-set verdict binds at ONE typed method the algebra exposes.
2598    ///
2599    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition
2600    /// preserves proofs; the set-level mixed-set verdict on N preset-
2601    /// carried resource proofs is itself a typed named `bool` predicate
2602    /// composing them via the underlying set-level [`Self::is_chain`] +
2603    /// [`Self::is_antichain`] projections. THEORY.md §V.1 — knowable
2604    /// platform; the doubly-negated set-level conjunction becomes a
2605    /// TYPE-level operation on the posture algebra rather than an inline
2606    /// negation-composition at every consumer that decides "is this
2607    /// slice a mixed subset?"
2608    ///
2609    /// Frontier inspiration: the order-theoretic notion of a MIXED
2610    /// subset (a subset that is neither a chain nor an antichain) sits
2611    /// at the interior of the (chain, antichain, mixed) trichotomy that
2612    /// canonicalizes the SET-LEVEL verdict surface of a poset — a slice
2613    /// whose Hasse-diagram induced subgraph carries both order-comparable
2614    /// AND order-incomparable edges. Dilworth's width invariant and the
2615    /// dual Mirsky theorem (a poset's height equals the minimum number of
2616    /// antichains covering it) both take mixed sets as their non-trivial
2617    /// inputs — a chain has width 1 and a mixed set of width ≥2 is
2618    /// exactly where the width invariant becomes informative. Haskell's
2619    /// `Data.List` idiom of `not . isChain && not . isAntichain`
2620    /// composition on a partial-order carrier. Translation through
2621    /// pleme-io primitives: the boolean conjunction of the negations of
2622    /// the just-lifted set-level projections, no new dep, no supertrait
2623    /// bound, `const fn` throughout.
2624    #[must_use]
2625    pub const fn is_mixed(postures: &[Self]) -> bool {
2626        !Self::is_chain(postures) && !Self::is_antichain(postures)
2627    }
2628
2629    /// Boolean `leq`-conjunction across every CONSECUTIVE pair of a slice
2630    /// of postures — `postures` is ASCENDING (a leq-monotone sequence on
2631    /// the pointwise partial order) iff every consecutive pair
2632    /// `(postures[i], postures[i + 1])` satisfies
2633    /// `postures[i].leq(postures[i + 1])`.
2634    ///
2635    /// The SEQUENCE-LEVEL N-ary peer of [`Self::leq`] one CARDINALITY axis
2636    /// over on the (pairwise, sequence-level) primitive surface — the
2637    /// DIRECT SEQUENCE-LEVEL LIFT of the pair-level partial-order primitive
2638    /// through a consecutive-index walk. Opens a NEW AXIS one CONSECUTIVE-
2639    /// VS-ALL-PAIRS step over from the just-lifted set-level projections
2640    /// [`Self::is_chain`] / [`Self::is_antichain`] / [`Self::is_mixed`]:
2641    /// where the set-level triple quantifies over EVERY distinct pair
2642    /// `(i, j)` with `j > i`, this projection quantifies over only the
2643    /// `n - 1` CONSECUTIVE pairs `(i, i + 1)`. The two axes lift the same
2644    /// pair-level primitive family through DIFFERENT quantification shapes
2645    /// — the set-level triple carries UNORDERED verdicts about the whole
2646    /// slice's pairwise-relation graph, while this projection carries an
2647    /// ORDER-SENSITIVE verdict about the slice's specific enumeration.
2648    ///
2649    /// **Transitivity theorem — `is_ascending ⇒ is_chain`**: for every
2650    /// slice `postures`, `Self::is_ascending(postures) == true` implies
2651    /// `Self::is_chain(postures) == true`. The pointwise partial order is
2652    /// TRANSITIVE (`a.leq(b) && b.leq(c) ⇒ a.leq(c)` — verified structurally
2653    /// by [`Self::leq`]'s six-axis pointwise `<=` conjunction, since each
2654    /// `usize <=` is transitive), so a consecutive-pair leq-chain closes
2655    /// under transitive composition into the all-pairs leq-chain; every
2656    /// (i, j) pair with j > i decomposes into the composition of
2657    /// consecutive-pair leqs (i, i+1), (i+1, i+2), ..., (j-1, j), which
2658    /// transitivity collapses into `postures[i].leq(postures[j])`, and
2659    /// `leq` implies `is_comparable`. This is a LOAD-BEARING substrate-
2660    /// level theorem: the sequence-level ascending verdict is strictly
2661    /// STRONGER than the set-level chain verdict (a chain admits any
2662    /// permutation of a leq-sorted enumeration; an ascending sequence
2663    /// admits exactly the leq-sorted one). Pinned as
2664    /// `resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice`.
2665    ///
2666    /// **Empty-slice vacuous truth**: `ResourceLimits::is_ascending(&[])
2667    /// == true` — the empty conjunction is vacuously true; the empty
2668    /// slice contains no consecutive pairs to reject. Peer of
2669    /// [`Self::is_chain`]'s empty-slice identity one CONSECUTIVE-VS-ALL-
2670    /// PAIRS axis over: both cells of the (set-level, sequence-level)
2671    /// projection pair AGREE at the empty slice on the vacuous-truth
2672    /// verdict — the two cells coincide at the empty face of the N-ary
2673    /// verdict surface. Pinned as
2674    /// `resource_limits_is_ascending_empty_slice_is_vacuously_true`.
2675    ///
2676    /// **Singleton vacuous truth**: `ResourceLimits::is_ascending(&[a])
2677    /// == true` for every posture `a` — a one-element slice contains no
2678    /// consecutive pairs, so the walk never enters the loop and returns
2679    /// `true` vacuously. Peer of [`Self::is_chain`]'s singleton-vacuous-
2680    /// truth identity: both projections AGREE at singleton slices too.
2681    /// Pinned as
2682    /// `resource_limits_is_ascending_singleton_is_vacuously_true`.
2683    ///
2684    /// **Diagonal-duplicate identity**: `ResourceLimits::is_ascending(
2685    /// &[a, a]) == true` for every posture `a` — [`Self::leq`] is
2686    /// REFLEXIVE (`a.leq(a) == true` via each axis's `<=` reflexivity), so
2687    /// the single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
2688    /// the conjunction holds. AGREES with [`Self::is_chain`]'s diagonal-
2689    /// duplicate verdict at every diagonal-duplicate slice — the two
2690    /// projections diverge only where the ORDER matters, and duplicated
2691    /// elements carry no ordering information to distinguish them.
2692    /// Pinned as `resource_limits_is_ascending_of_diagonal_duplicate_is_true`.
2693    ///
2694    /// **Ascending shipped-preset triple closure**: `ResourceLimits::
2695    /// is_ascending(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2696    /// UNBOUNDED_RESOURCE_LIMITS]) == true` — the shipped-preset triple
2697    /// on the bounded-lattice diagonal is a leq-monotone ascending
2698    /// sequence (`EMPTY.leq(DEFAULT).leq(UNBOUNDED)` pointwise), so every
2699    /// consecutive pair satisfies `leq` and the conjunction closes.
2700    /// Pinned as
2701    /// `resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple`.
2702    ///
2703    /// **Descending shipped-preset triple rejection**: `ResourceLimits::
2704    /// is_ascending(&[UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2705    /// EMPTY_RESOURCE_LIMITS]) == false` — the reversed enumeration of the
2706    /// same shipped-preset chain is a leq-monotone DESCENDING sequence, so
2707    /// the first consecutive pair binds `UNBOUNDED.leq(DEFAULT) == false`
2708    /// and the walk rejects at index 0. The DIRECT ORDER-SENSITIVITY
2709    /// FALSIFICATION of the ascending closure one PERMUTATION axis over on
2710    /// the SAME chain — [`Self::is_chain`] accepts BOTH orderings (its
2711    /// verdict is order-INSENSITIVE), while this projection accepts only
2712    /// the leq-sorted one. Pinned as
2713    /// `resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple`.
2714    ///
2715    /// **Non-monotone chain permutation rejection**: `ResourceLimits::
2716    /// is_ascending(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,
2717    /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the third permutation of
2718    /// the shipped-preset chain has `DEFAULT.leq(EMPTY) == false` at the
2719    /// first consecutive pair, so the walk rejects, EVEN THOUGH the slice
2720    /// is a chain ([`Self::is_chain`] accepts it). Pins the DISCRIMINATING
2721    /// arm between the sequence-level and set-level projections: a chain
2722    /// permutation that is neither ascending nor descending falsifies BOTH
2723    /// sequence-level projections while both set-level projections accept
2724    /// it. Pinned as
2725    /// `resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation`.
2726    ///
2727    /// **Hand-authored antichain rejection**: `ResourceLimits::is_ascending(
2728    /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2729    /// — the two hand-authored asymmetric postures are incomparable
2730    /// (neither `MID.leq(OTHER)` nor `OTHER.leq(MID)`), so the single
2731    /// consecutive pair rejects. Pinned as
2732    /// `resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair`.
2733    ///
2734    /// Encoded as a singly-indexed const-`while` walk (`i in 0..n-1`) with
2735    /// one [`Self::leq`] delegation per consecutive pair — the SAME walk
2736    /// structure the standard-library `<[T]>::is_sorted_by` idiom carries
2737    /// on a totally-ordered carrier, generalized to the partial-order
2738    /// [`Self::leq`] primitive here. `const fn` throughout via the
2739    /// underlying `const fn` [`Self::leq`] body — a caller can pin an
2740    /// ascending-sequence identity at compile time
2741    /// (`const _: () = assert!(ResourceLimits::is_ascending(&[
2742    /// EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2743    /// UNBOUNDED_RESOURCE_LIMITS]));`) — a build-break rather than a
2744    /// runtime `assert!` on the first execution.
2745    ///
2746    /// Pre-lift, a caller wanting "is this slice a leq-monotone ascending
2747    /// sequence?" composed the singly-indexed
2748    /// `postures.windows(2).all(|w| w[0].leq(w[1]))` two-primitive
2749    /// scaffolding at every prospective callsite — the SAME PRIME
2750    /// DIRECTIVE ≥2 pattern the set-level projections' pre-lift shapes
2751    /// carried, one CARDINALITY axis over. Post-lift the ascending
2752    /// verdict binds at ONE typed method the algebra exposes, and the
2753    /// consecutive-pair walk lives at ONE implementation site — the peer
2754    /// [`Self::is_descending`] shares the SAME walk structure and diverges
2755    /// only on the pair-level primitive it delegates to (`geq` in place of
2756    /// `leq`).
2757    ///
2758    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2759    /// proofs; the sequence-level ascending verdict on N preset-carried
2760    /// resource proofs is itself a typed named `bool` predicate composing
2761    /// them via the underlying pair-level [`Self::leq`] projection.
2762    /// THEORY.md §V.1 — knowable platform; the consecutive-pair leq
2763    /// conjunction becomes a TYPE-level operation on the posture algebra
2764    /// rather than an inline windowed scaffolding at every consumer that
2765    /// decides "is this slice a leq-monotone ascending sequence?"
2766    ///
2767    /// Frontier inspiration: the order-theoretic notion of a MONOTONE
2768    /// SEQUENCE on a poset — the standard-library `Iterator::is_sorted`
2769    /// / `<[T]>::is_sorted_by` idioms carry the totally-ordered case;
2770    /// Haskell's `Data.List` idiom of `and . zipWith leq xs . tail xs`
2771    /// on a partial-order carrier; Coq's `Sorted` inductive on lists over
2772    /// a `Relation`. Translation through pleme-io primitives: the singly-
2773    /// indexed consecutive-pair walk directly, with [`Self::leq`] as the
2774    /// pair-level primitive, no new dep, no supertrait bound, `const fn`
2775    /// throughout. The (ascending, descending) sequence-level pair opens
2776    /// the door to `is_strictly_ascending` / `is_strictly_descending` via
2777    /// [`Self::lt`] / [`Self::gt`] one STRICTNESS axis over, and to
2778    /// `is_monotone` (`is_ascending || is_descending`) one DIRECTION-
2779    /// AGNOSTIC axis over, at future callsites.
2780    #[must_use]
2781    pub const fn is_ascending(postures: &[Self]) -> bool {
2782        let n = postures.len();
2783        let mut i = 0;
2784        while i + 1 < n {
2785            if !postures[i].leq(postures[i + 1]) {
2786                return false;
2787            }
2788            i += 1;
2789        }
2790        true
2791    }
2792
2793    /// Boolean `geq`-conjunction across every CONSECUTIVE pair of a slice
2794    /// of postures — `postures` is DESCENDING (a geq-monotone sequence on
2795    /// the pointwise partial order) iff every consecutive pair
2796    /// `(postures[i], postures[i + 1])` satisfies
2797    /// `postures[i].geq(postures[i + 1])`.
2798    ///
2799    /// The DIRECT SEQUENCE-LEVEL DUAL of [`Self::is_ascending`] one PAIR-
2800    /// LEVEL-PRIMITIVE axis over — where `is_ascending` delegates to
2801    /// [`Self::leq`], this projection delegates to [`Self::geq`]. The
2802    /// two together close the (ascending, descending) two-cell face on
2803    /// the sequence-level pairwise-relation surface, exactly one
2804    /// CONSECUTIVE-VS-ALL-PAIRS axis over from the set-level (chain,
2805    /// antichain) two-cell face [`Self::is_chain`] / [`Self::is_antichain`]
2806    /// pin, and exactly one PAIR-LEVEL-PRIMITIVE axis over from the
2807    /// (leq, geq) pair-level two-cell face [`Self::leq`] / [`Self::geq`]
2808    /// pin one CARDINALITY axis down.
2809    ///
2810    /// The (ascending, descending) pair is NOT a partition of the
2811    /// sequence-level verdict surface: a slice of ≥3 postures whose
2812    /// consecutive-pair verdicts mix leq and geq (or contain any
2813    /// incomparable consecutive pair) is neither ascending nor
2814    /// descending, so the two sequence-level projections carry a THREE-
2815    /// cell face (ascending, descending, non-monotone) at the sequence
2816    /// level whose ends the two projections lifted here pin. The
2817    /// (T, T) corner is achievable at every diagonal-duplicate slice
2818    /// AND at every empty-or-singleton slice — [`Self::leq`] and
2819    /// [`Self::geq`] are BOTH REFLEXIVE, so a repeated element passes
2820    /// both consecutive-pair conjunctions.
2821    ///
2822    /// **Transitivity theorem — `is_descending ⇒ is_chain`**: for every
2823    /// slice `postures`, `Self::is_descending(postures) == true` implies
2824    /// `Self::is_chain(postures) == true`, by the SAME transitive
2825    /// composition [`Self::is_ascending`] carries one PAIR-LEVEL-PRIMITIVE
2826    /// axis over — the pointwise partial order is transitive under `geq`
2827    /// (by [`Self::geq`]'s composition `other.leq(self)` and `leq`'s six-
2828    /// axis transitivity), so a consecutive-pair geq-chain closes under
2829    /// transitive composition into an all-pairs geq-chain, and `geq`
2830    /// implies `is_comparable`. Pinned as
2831    /// `resource_limits_is_descending_implies_is_chain_on_every_shipped_slice`.
2832    ///
2833    /// **Empty-slice vacuous truth**: `ResourceLimits::is_descending(&[])
2834    /// == true` — the empty conjunction is vacuously true. Peer of
2835    /// [`Self::is_ascending`]'s empty-slice identity one PAIR-LEVEL-
2836    /// PRIMITIVE axis over: both cells of the (ascending, descending)
2837    /// sequence-level pair AGREE at the empty slice. Pinned as
2838    /// `resource_limits_is_descending_empty_slice_is_vacuously_true`.
2839    ///
2840    /// **Singleton vacuous truth**: `ResourceLimits::is_descending(&[a])
2841    /// == true` for every posture `a`. Pinned as
2842    /// `resource_limits_is_descending_singleton_is_vacuously_true`.
2843    ///
2844    /// **Diagonal-duplicate identity**: `ResourceLimits::is_descending(
2845    /// &[a, a]) == true` for every posture `a` — [`Self::geq`] is
2846    /// REFLEXIVE, so the single consecutive pair binds `a.geq(a) == true`.
2847    /// AGREES with [`Self::is_ascending`]'s diagonal-duplicate verdict —
2848    /// both leq and geq carry reflexivity, so the sequence-level (T, T)
2849    /// corner opens at every diagonal-duplicate slice, distinct from the
2850    /// set-level (T, F) verdict [`Self::is_chain`] / [`Self::is_antichain`]
2851    /// carry on the same slice. Pinned as
2852    /// `resource_limits_is_descending_of_diagonal_duplicate_is_true`.
2853    ///
2854    /// **Descending shipped-preset triple closure**: `ResourceLimits::
2855    /// is_descending(&[UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2856    /// EMPTY_RESOURCE_LIMITS]) == true` — the reversed enumeration of the
2857    /// shipped-preset chain is a geq-monotone descending sequence. The
2858    /// DIRECT MIRROR of [`Self::is_ascending`]'s ascending closure one
2859    /// PERMUTATION axis over — pinning the two closures on the SAME chain
2860    /// in REVERSED orderings closes the (ascending, descending) sequence-
2861    /// level pair on the shipped chain. Pinned as
2862    /// `resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple`.
2863    ///
2864    /// **Ascending shipped-preset triple rejection**: `ResourceLimits::
2865    /// is_descending(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,
2866    /// UNBOUNDED_RESOURCE_LIMITS]) == false` — the ascending-sorted
2867    /// enumeration binds `EMPTY.geq(DEFAULT) == false`. The DIRECT
2868    /// FALSIFICATION of [`Self::is_ascending`]'s ascending closure one
2869    /// CELL axis over on the SAME slice. Pinned as
2870    /// `resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple`.
2871    ///
2872    /// **Hand-authored antichain rejection**: `ResourceLimits::is_descending(
2873    /// &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE]) == false`
2874    /// — the two hand-authored asymmetric postures are incomparable, so
2875    /// the single consecutive pair rejects. Pinned as
2876    /// `resource_limits_is_descending_rejects_the_hand_authored_antichain_pair`.
2877    ///
2878    /// Encoded as a singly-indexed const-`while` walk (`i in 0..n-1`)
2879    /// with one [`Self::geq`] delegation per consecutive pair — the SAME
2880    /// iteration structure as [`Self::is_ascending`] one PAIR-LEVEL-
2881    /// PRIMITIVE axis over, with only the inner primitive swapped between
2882    /// the two. `const fn` throughout via the underlying `const fn`
2883    /// [`Self::geq`] body.
2884    ///
2885    /// Pre-lift, a caller wanting "is this slice a geq-monotone descending
2886    /// sequence?" composed the singly-indexed
2887    /// `postures.windows(2).all(|w| w[0].geq(w[1]))` two-primitive
2888    /// scaffolding at every prospective callsite. Post-lift the
2889    /// descending verdict binds at ONE typed method the algebra exposes.
2890    ///
2891    /// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
2892    /// proofs. THEORY.md §V.1 — knowable platform.
2893    ///
2894    /// Frontier inspiration: `Iterator::is_sorted_by` with a reversed
2895    /// comparator on a totally-ordered carrier; Coq's `StronglySorted`
2896    /// on a `<=` relation applied with the argument order flipped.
2897    /// Translation through pleme-io primitives: the singly-indexed
2898    /// consecutive-pair walk directly, with [`Self::geq`] as the pair-
2899    /// level primitive.
2900    #[must_use]
2901    pub const fn is_descending(postures: &[Self]) -> bool {
2902        let n = postures.len();
2903        let mut i = 0;
2904        while i + 1 < n {
2905            if !postures[i].geq(postures[i + 1]) {
2906                return false;
2907            }
2908            i += 1;
2909        }
2910        true
2911    }
2912}
2913
2914/// Cache key: (macro name, SipHash-2-4 of args). We hash `Sexp` directly via
2915/// its manual `Hash` impl — no serde_json round-trip per cache lookup.
2916type CacheKey = (String, u64);
2917
2918/// A registered macro definition.
2919#[derive(Debug, Clone)]
2920pub struct MacroDef {
2921    pub name: String,
2922    pub params: MacroParams,
2923    /// The template body (usually a Quasiquote).
2924    pub body: Sexp,
2925}
2926
2927impl MacroDef {
2928    /// Project the macro body to its substitution-walked form: the inner of
2929    /// the outer `Sexp::Quasiquote` when `(defmacro NAME (PARAMS) `(...))`
2930    /// authored the body through the canonical quasi-quote affordance, OR
2931    /// `&self.body` verbatim when authored without one. The two expansion
2932    /// strategies — bytecode (`compile_template`) and substitute (`apply`'s
2933    /// substitute fallback) — both walk this projection, never the raw
2934    /// `body`, because the outer quasi-quote is the syntactic "you're
2935    /// inside a template" marker and the substitution semantics operate on
2936    /// what's INSIDE it. Naming the projection lifts the inline
2937    /// `match &def.body { Sexp::Quasiquote(inner) => inner.as_ref(),
2938    /// other => other }` peel that appeared verbatim at BOTH sites — well
2939    /// past the ≥2 PRIME-DIRECTIVE trigger — into ONE function the two
2940    /// strategies share, so a regression that drifts ONE strategy's
2941    /// body-projection from the other (e.g. one path peels twice and the
2942    /// other peels once, or one path treats `Sexp::Quote(...)` as a
2943    /// template marker and the other doesn't) becomes structurally
2944    /// impossible: there is exactly one implementation both strategies
2945    /// call.
2946    ///
2947    /// Single-level peel by design: a nested `` ``form `` body unwraps to
2948    /// `` `form `` (the inner quasi-quote stays as-is), matching the v0
2949    /// "no nested quasi-quotes" scope the module preamble declares. A
2950    /// non-quasi-quote body — `(defmacro NAME (PARAMS) BODY)` where BODY
2951    /// is a plain `Sexp::List` / `Sexp::Atom` — returns `&self.body`
2952    /// verbatim, the "other" arm of the legacy match. The borrow is
2953    /// strictly `&'a Sexp` rooted in `&'a self.body` (no clone, no
2954    /// allocation); both `compile_node` (bytecode path) and `substitute`
2955    /// (substitute path) consume the projection immediately and never
2956    /// outlive the borrow.
2957    ///
2958    /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
2959    /// inline copies of the body-peel match is the ≥2 trigger, and the
2960    /// substrate names the projection ONCE so authoring surfaces and
2961    /// future expansion strategies (a third interpreter? a JIT? a
2962    /// debugger that wants to render the body without the outer
2963    /// quasi-quote marker?) bind to ONE primitive. THEORY.md §II.1
2964    /// invariant 2 — free middle; the two expansion strategies emit
2965    /// IDENTICAL output for the same (macro, args) pair, and sharing one
2966    /// body-projection makes that per-strategy agreement structural at
2967    /// the entry to the walker, not a two-site discipline the
2968    /// `expansion_layers_agree_on_output_and_cache_wins` benchmark only
2969    /// observes after the fact.
2970    #[must_use]
2971    pub fn template_body(&self) -> &Sexp {
2972        match &self.body {
2973            Sexp::Quasiquote(inner) => inner.as_ref(),
2974            other => other,
2975        }
2976    }
2977}
2978
2979/// A macro's parameter list — structurally "zero or more required
2980/// positional params, then zero or more `&optional` params, then an OPTIONAL
2981/// single `&rest` param." This is the canonical Lisp lambda-list ordering
2982/// (Common Lisp `(req* &optional opt* &rest r)`), made a TYPE.
2983///
2984/// This shape promotes the invariants the reader ([`parse_params`])
2985/// previously upheld only by construction — `&rest` is LAST, there is AT MOST
2986/// ONE of it, and (now) `&optional` params sit strictly between the required
2987/// run and the rest — from *unobserved discipline* to *unrepresentable
2988/// state*. The prior representation `Vec<Param>` admitted `[Rest, Required]`
2989/// (a `&rest` in the middle) and `[Rest, Rest]` (two of them); both are
2990/// nonsense the binder cannot honor, yet the type permitted them. The flat
2991/// param INDEX that the bytecode references (`Subst(idx)` / `Splice(idx)`)
2992/// and the positional binder both walk would silently misalign on such a
2993/// `Vec` — a `Rest` at index 0 of `[Rest, Required]` makes the binder grab
2994/// every arg, then fail to bind the trailing `Required`, mapping the
2995/// template's index-1 substitution onto the wrong value. `MacroParams`
2996/// cannot express either shape: `rest` is exactly one `Option<String>`,
2997/// always conceptually after every `required` then every `optional` name,
2998/// and the three kinds live in distinct fields whose order is fixed by the
2999/// struct, not by a discipline the binder trusts a `Vec` to have upheld.
3000///
3001/// `optional` differs from `required` in the binder, not the index contract:
3002/// a required name with no arg at its position is a `MissingMacroArg`
3003/// rejection; an optional name with no arg binds to its declared default form
3004/// — `Sexp::Nil` when none was given, the parsed default literal when one was.
3005/// Both shapes — `&optional x` and `&optional (x 5)` — are now structural in
3006/// the typed [`OptionalParam`] entry rather than smeared across a flat
3007/// `Vec<String>` the binder would have had to discover the default for
3008/// elsewhere.
3009///
3010/// The flat-index contract the template bytecode depends on is preserved by
3011/// [`MacroParams::names`]: index `0..required.len()` are the required names
3012/// in order, the next `optional.len()` indices are the optional names, and
3013/// the final index (if present) is the rest name — the canonical lambda-list
3014/// order. [`MacroParams::bind`] produces the per-index bound values in that
3015/// same order, so the name-keyed (`bind_args` → `substitute`) and
3016/// index-keyed (`apply_compiled`) expansion strategies share ONE binder and
3017/// can never drift.
3018///
3019/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
3020/// unrepresentable"; the lambda-list ordering (required → optional → rest,
3021/// rest-is-last, at-most-one-rest) becomes structural. THEORY.md §VI.1 —
3022/// generation over composition; the positional binding loop (verbatim in
3023/// both `bind_args` and `apply_compiled`, the ≥2 PRIME-DIRECTIVE trigger) is
3024/// lifted to ONE owner, `bind`, which the optional arm extends in one place.
3025#[derive(Debug, Clone, Default, PartialEq)]
3026pub struct MacroParams {
3027    pub required: Vec<String>,
3028    pub optional: Vec<OptionalParam>,
3029    pub rest: Option<String>,
3030}
3031
3032/// One entry in a macro's `&optional` section — a name plus an optional
3033/// default form. The two surface shapes the reader admits collapse into this
3034/// single typed shape:
3035///
3036///   * `&optional x`        ⇒ `OptionalParam { name: "x", default: None }`
3037///   * `&optional (x 5)`    ⇒ `OptionalParam { name: "x", default: Some(Int(5)) }`
3038///
3039/// The `default: Option<Sexp>` slot makes the per-param default-form a
3040/// FIELD on each optional entry, not a discipline a sibling `Vec<Sexp>` would
3041/// have had to maintain in lock-step with `Vec<String>`. Without this shape
3042/// the binder cannot tell "no arg supplied, no default declared → bind nil"
3043/// from "no arg supplied, default `5` declared → bind `5`": both would
3044/// collapse onto `Sexp::Nil`, the precise silent misalignment the typed
3045/// shape exists to forbid.
3046///
3047/// The default is the LITERAL `Sexp` — there is no evaluator in v0, so a
3048/// `(x (foo 1))` spec parks `(foo 1)` verbatim as the bound value when `x`'s
3049/// arg is absent. This is the no-evaluator floor of CL semantics: any
3050/// arbitrary form is admitted at the gate, what it MEANS is the next layer's
3051/// concern. The default is parsed exactly once at `defmacro`/
3052/// `defpoint-template`/`defcheck` time (inside `parse_params`); every call
3053/// to that macro consumes the same parsed `Sexp` via `Clone`, never re-
3054/// reading the source.
3055///
3056/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
3057/// unrepresentable"; the (name, default?) pair is one entry rather than two
3058/// parallel `Vec`s a regression could desynchronize. THEORY.md §VI.1 —
3059/// generation over composition; the binder's optional arm consults
3060/// `param.default` in ONE place, so the substitute and bytecode strategies
3061/// inherit identical default-resolution semantics from the shared `bind`.
3062#[derive(Debug, Clone, PartialEq)]
3063pub struct OptionalParam {
3064    pub name: String,
3065    pub default: Option<Sexp>,
3066}
3067
3068impl OptionalParam {
3069    /// `&optional x` — a bare optional name with no default. An absent
3070    /// argument binds to `Sexp::Nil` (the no-default-form floor).
3071    #[must_use]
3072    pub fn bare(name: impl Into<String>) -> Self {
3073        Self {
3074            name: name.into(),
3075            default: None,
3076        }
3077    }
3078
3079    /// `&optional (x DEFAULT)` — an optional with a default form. An absent
3080    /// argument binds to `default.clone()`.
3081    #[must_use]
3082    pub fn with_default(name: impl Into<String>, default: Sexp) -> Self {
3083        Self {
3084            name: name.into(),
3085            default: Some(default),
3086        }
3087    }
3088
3089    /// The bound value when an absent call leaves this optional slot unfilled:
3090    /// the declared default form (cloned) when one was authored, OR the
3091    /// canonical `Sexp::Nil` floor when none was — the CL `&optional` no-
3092    /// default-form floor. ONE named primitive on the typed [`OptionalParam`]
3093    /// every absent-call binder consults; before this lift the same two-arm
3094    /// fallback `param.default.clone().unwrap_or(Sexp::Nil)` lived inline at
3095    /// [`MacroParams::bind`]'s optional arm, and a future absence-resolver
3096    /// (the kwarg-gate's typed-default fill? a future `&supplied-p` slot's
3097    /// "was this defaulted?" bit?) would have had to re-derive the same
3098    /// two-arm fallback at every site that walks the optional run.
3099    ///
3100    /// The projection IS the structural identity binding the typed
3101    /// `default: Option<Sexp>` slot to its bound-value contract:
3102    ///   * `bare(name).resolved_default()` is `Sexp::Nil` (the no-default
3103    ///     floor — `default.is_none()`).
3104    ///   * `with_default(name, d).resolved_default()` is `d.clone()` (the
3105    ///     declared default — `default = Some(d)` projected through Clone).
3106    ///
3107    /// `resolved_default()` is the typed accessor companion to the
3108    /// `bare` / `with_default` constructors: those two constructors define
3109    /// the ONLY admissible shapes of the typed `default` slot, and this
3110    /// accessor names the BOUND-VALUE projection both shapes yield at the
3111    /// binder's absence arm. Together the three close the `OptionalParam`'s
3112    /// self-contained typed surface — every authored shape lands through ONE
3113    /// of two constructors, and every absent-call binder reads through this
3114    /// ONE accessor.
3115    ///
3116    /// Returns an owned `Sexp` (not `&Sexp`) because the binder pushes the
3117    /// resolved default into a fresh `Vec<Sexp>` slot at every absent call;
3118    /// the `default.clone()` projection is the same allocation the pre-lift
3119    /// inline expression performed, just named at the typed boundary. The
3120    /// `Sexp::Nil` floor is a free per-call construction (a unit variant
3121    /// with no payload), so the no-default path is free of allocation
3122    /// beyond the function return slot.
3123    ///
3124    /// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid
3125    /// states unrepresentable"; the "no-default-form floor" structural
3126    /// concept becomes a NAMED projection on [`OptionalParam`] rather than
3127    /// re-derived `param.default.clone().unwrap_or(Sexp::Nil)` arithmetic
3128    /// at every site that walks the bound optional run. Authoring tools
3129    /// (REPL, LSP, `tatara-check`) that want to render "this optional
3130    /// binds to {default-form|nil} when absent" bind to ONE method on the
3131    /// typed param. THEORY.md §VI.1 — generation over composition; the
3132    /// constructor pair `bare` / `with_default` defines the typed shapes
3133    /// and the `resolved_default` accessor names the symmetric
3134    /// bound-value projection — the typed accessor companion. THEORY.md
3135    /// §II.1 invariant 2 — free middle; both expansion strategies route
3136    /// through the SHARED `MacroParams::bind`, so the new accessor is
3137    /// exposed to the bytecode and substitute paths uniformly via that
3138    /// shared binder.
3139    #[must_use]
3140    pub fn resolved_default(&self) -> Sexp {
3141        self.default.clone().unwrap_or(Sexp::Nil)
3142    }
3143}
3144
3145/// The value carrier a macro call binds its arguments in.
3146///
3147/// [`MacroParams::bind_carrier`] is the single positional binder every
3148/// expansion path routes through; this trait is the only thing that varies
3149/// between those paths. It names the two — and only two — places a binder
3150/// must SYNTHESIZE a value the caller never wrote:
3151///
3152///   * [`Self::lift_default`] — an `&optional` slot the call left unfilled
3153///     binds to the declared default form (or the `Sexp::Nil` floor). The
3154///     default is authored as plain `Sexp` at `defmacro` time, so each
3155///     carrier says how a template literal enters it.
3156///   * [`Self::collect_rest`] — the `&rest` slot collects the surplus args
3157///     into ONE value. `Sexp` wraps them in `Sexp::List`; a span-carrying
3158///     carrier additionally has to decide what span the synthesized list
3159///     wears.
3160///
3161/// Everything else in the binder — the required run, the arity rejections,
3162/// the required→optional→rest ordering — is carrier-independent and lives
3163/// exactly once.
3164///
3165/// `Site` is the per-call synthesis context. It is `()` for `Sexp`, whose
3166/// values carry no position; a span-carrying carrier sets it to the macro
3167/// CALL SITE, because a value the call never supplied has no source position
3168/// of its own and the call site is the honest place to point an error at.
3169///
3170/// Theory anchor: THEORY.md §V.1 — knowable platform; "which lambda-list
3171/// shapes does this expansion path accept?" stops being a per-path answer.
3172/// THEORY.md §VI.1 — generation over composition.
3173pub trait MacroArgCarrier: Clone {
3174    /// Per-call synthesis context — `()` when the carrier needs none.
3175    type Site: Copy;
3176
3177    /// Lift a template-authored default form into this carrier, for an
3178    /// `&optional` slot the call left unfilled.
3179    fn lift_default(default: &Sexp, site: Self::Site) -> Self;
3180
3181    /// Collect the `&rest` surplus into one carrier value.
3182    fn collect_rest(items: Vec<Self>, site: Self::Site) -> Self;
3183}
3184
3185impl MacroArgCarrier for Sexp {
3186    /// `Sexp` carries no position, so there is nothing to thread — the unit
3187    /// site is the structural statement that this carrier synthesizes values
3188    /// without context.
3189    type Site = ();
3190
3191    fn lift_default(default: &Sexp, (): ()) -> Self {
3192        default.clone()
3193    }
3194
3195    fn collect_rest(items: Vec<Self>, (): ()) -> Self {
3196        Sexp::List(items)
3197    }
3198}
3199
3200impl MacroParams {
3201    /// Canonical `&rest` marker — the ONE typed `&'static str` on
3202    /// [`MacroParams`] the parser's rest-slot dispatch specialises on.
3203    /// The Common-Lisp lambda-list `&rest` keyword names the position
3204    /// at which the parser stops collecting `required` / `optional`
3205    /// names and binds every subsequent arg into a `Sexp::List` at the
3206    /// [`Self::rest`] slot. Sibling posture to [`Self::OPTIONAL_MARKER`]
3207    /// (`"&optional"`) on the same lambda-list-keyword algebra layer:
3208    /// the two constants are the closed set of Common-Lisp
3209    /// lambda-list-keyword `&'static str` markers the parser's typed
3210    /// dispatch specialises on, sharing the canonical `char` lead byte
3211    /// [`Self::LAMBDA_LIST_KEYWORD_LEAD`] (`'&'`).
3212    ///
3213    /// Pre-lift the same `"&rest"` bytes lived inline as a single
3214    /// `s == "&rest"` comparison inside [`parse_params`], with the two
3215    /// typed `&'static str` markers the CL lambda-list algebra
3216    /// specialises on smeared across TWO parallel `s == "..."` inline
3217    /// comparisons at the SAME dispatch cascade. Post-lift the
3218    /// (`&rest` rest-slot marker, canonical `&'static str`) pairing
3219    /// binds at ONE constant on the typed [`MacroParams`] algebra that
3220    /// both the parser AND any future authoring / rendering surface
3221    /// route through; a refactor that swaps the marker (e.g. a
3222    /// Clojure-compat port to `&`  ONLY, an Elisp-compat port that
3223    /// keeps `&rest` as-is, a Scheme R7RS-style port to `.` for the
3224    /// dotted-pair rest slot) touches ONE constant rather than the
3225    /// two inline literals scattered across the parser AND the
3226    /// diagnostic message surfaces.
3227    ///
3228    /// Structural round-trip contract:
3229    /// `Self::REST_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3230    /// — the projection law binding the `&'static str` marker to its
3231    /// canonical `char` lead byte on the typed algebra, pinned by
3232    /// `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`.
3233    ///
3234    /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3235    /// (`&rest` rest-slot marker, canonical `&'static str`) pairing
3236    /// binds at ONE `pub const` on the typed [`MacroParams`] algebra
3237    /// regardless of which parser / authoring surface reaches in.
3238    /// THEORY.md §V.1 — knowable platform; the canonical CL lambda-
3239    /// list `&rest` keyword becomes a TYPE-level constant on the
3240    /// typed macro-params algebra rather than an inline `"&rest"`
3241    /// literal at the parser's rest-slot dispatch arm.
3242    pub const REST_MARKER: &'static str = "&rest";
3243
3244    /// Canonical `&optional` marker — the ONE typed `&'static str` on
3245    /// [`MacroParams`] the parser's optional-section dispatch
3246    /// specialises on. The Common-Lisp lambda-list `&optional` keyword
3247    /// names the position at which the parser switches subsequent
3248    /// bare-symbol names from the `required` bin to the `optional`
3249    /// bin, until either the end of the param list OR the
3250    /// [`Self::REST_MARKER`] terminal is reached. Sibling posture to
3251    /// [`Self::REST_MARKER`] (`"&rest"`) on the same lambda-list-
3252    /// keyword algebra layer: the two constants are the closed set of
3253    /// CL lambda-list-keyword `&'static str` markers the parser's
3254    /// typed dispatch specialises on, sharing the canonical `char`
3255    /// lead byte [`Self::LAMBDA_LIST_KEYWORD_LEAD`] (`'&'`).
3256    ///
3257    /// Structural round-trip contract:
3258    /// `Self::OPTIONAL_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3259    /// — pinned by
3260    /// `macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead`.
3261    ///
3262    /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3263    /// (`&optional` section-switch marker, canonical `&'static str`)
3264    /// pairing binds at ONE `pub const` on the typed [`MacroParams`]
3265    /// algebra regardless of which parser / authoring surface reaches
3266    /// in. THEORY.md §V.1 — knowable platform; the canonical CL
3267    /// lambda-list `&optional` keyword becomes a TYPE-level constant
3268    /// on the typed macro-params algebra rather than an inline
3269    /// `"&optional"` literal at the parser's optional-section
3270    /// dispatch arm.
3271    pub const OPTIONAL_MARKER: &'static str = "&optional";
3272
3273    /// Canonical `&` LEAD byte shared across [`Self::REST_MARKER`]
3274    /// (`"&rest"`) and [`Self::OPTIONAL_MARKER`] (`"&optional"`) — the
3275    /// ONE canonical `char` on the [`MacroParams`] algebra that the
3276    /// Common-Lisp lambda-list-keyword disjointness contract binds to.
3277    ///
3278    /// Sibling posture to the closed set of `pub const` per-role
3279    /// canonical bytes on the substrate's other closed-set outer
3280    /// algebras: [`crate::ast::Atom::STR_DELIMITER`] (`'"'`),
3281    /// [`crate::ast::Atom::STR_ESCAPE_LEAD`] (`'\\'`),
3282    /// [`crate::ast::Atom::KEYWORD_MARKER_LEAD`] (`':'`),
3283    /// [`crate::ast::Atom::BOOL_LITERAL_LEAD`] (`'#'`),
3284    /// [`crate::ast::Sexp::LIST_OPEN`] (`'('`),
3285    /// [`crate::ast::Sexp::LIST_CLOSE`] (`')'`),
3286    /// [`crate::ast::Sexp::COMMENT_LEAD`] (`';'`),
3287    /// [`crate::ast::Sexp::COMMENT_TERM`] (`'\n'`),
3288    /// [`crate::ast::QuoteForm::SPLICE_DISCRIMINATOR`] (`'@'`), and
3289    /// every `crate::ast::QuoteForm::lead_char` projection — every
3290    /// canonical per-role byte the substrate's typed algebras
3291    /// specialise on is now a `pub const` on its owning closed-set
3292    /// algebra. This constant closes the CL lambda-list-keyword
3293    /// family lead byte at the SAME algebra as its two `&'static str`
3294    /// marker projections so a delimiter swap (e.g. a Racket-compat
3295    /// port from CL `&rest` / `&optional` to `#!rest` / `#!optional`
3296    /// keyword-args, a Clojure-compat port to `&` ONLY as the rest
3297    /// marker) lands at ONE constant on the algebra.
3298    ///
3299    /// Structural round-trip contract:
3300    /// `Self::REST_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3301    /// AND
3302    /// `Self::OPTIONAL_MARKER.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)`
3303    /// — the projection law binding the two `&'static str` markers to
3304    /// their shared canonical `char` lead byte, pinned by
3305    /// `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
3306    /// and
3307    /// `macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead`.
3308    ///
3309    /// Disjointness contract: `LAMBDA_LIST_KEYWORD_LEAD`'s byte MUST
3310    /// differ from every sibling outer-marker `char` the substrate's
3311    /// other closed-set algebras specialise on. A collision would
3312    /// silently break the reader's outer dispatch: an `&`-prefixed
3313    /// bare atom `&rest` / `&optional` would collide with whichever
3314    /// marker it aliased. Pinned structurally at
3315    /// `macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker`.
3316    ///
3317    /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3318    /// (CL lambda-list-keyword LEAD byte, canonical `'&'`) pairing
3319    /// binds at ONE constant on the typed [`MacroParams`] algebra
3320    /// regardless of which of the two `&'static str` marker consumers
3321    /// reaches in. THEORY.md §V.1 — knowable platform; the canonical
3322    /// CL lambda-list-keyword LEAD byte becomes a TYPE-level constant
3323    /// on the substrate algebra rather than an inline `'&'` glyph at
3324    /// every docstring pinning the family disjointness contract.
3325    pub const LAMBDA_LIST_KEYWORD_LEAD: char = '&';
3326
3327    /// Closed-set forced-arity ALL array over the CL lambda-list-keyword
3328    /// `&'static str` markers the parser's typed dispatch specialises on
3329    /// — the [`Self::REST_MARKER`] (`"&rest"`) rest-slot marker followed
3330    /// by the [`Self::OPTIONAL_MARKER`] (`"&optional"`) optional-section
3331    /// marker, in the canonical CL lambda-list ordering (`&optional`
3332    /// binds AFTER `&required`, `&rest` binds AFTER `&optional`, so the
3333    /// declaration walk fires `REST_MARKER` FIRST as the terminal-arm
3334    /// early return; the ALL array's ordering keys on marker
3335    /// declaration order in this file, not on parse-time ordering).
3336    ///
3337    /// Sibling posture to the closed set of `pub const ALL: [Self; N]`
3338    /// forced-arity arrays on the substrate's other closed-set outer
3339    /// algebras: [`crate::ast::AtomKind::ALL`] (`[Self; 6]`),
3340    /// [`crate::ast::QuoteForm::ALL`] (`[Self; 4]`),
3341    /// [`crate::error::SexpShape::ALL`] (`[Self; 12]`),
3342    /// [`crate::error::UnquoteForm::ALL`] (`[Self; 2]`),
3343    /// [`crate::error::MacroDefHead::ALL`] (`[Self; 3]`) — every
3344    /// closed-set algebra the substrate carries pins its cardinality at
3345    /// the declaration site via a `pub const ALL` array whose forced
3346    /// arity fails compilation if a new variant lands without being
3347    /// added to the set. `LAMBDA_LIST_KEYWORDS` closes the same shape
3348    /// on the CL lambda-list-keyword family: a third marker (`&key`
3349    /// for keyword-args, `&aux` for auxiliaries, `&body` for the
3350    /// docstring-carrying tail of `defmacro` bodies) extends this ONE
3351    /// `pub const` + the matching per-role marker `pub const` above,
3352    /// AND rustc enforces every downstream consumer's closed-set
3353    /// sweep picks up the new marker at the SAME structural site.
3354    ///
3355    /// Pre-lift the two `&'static str` markers were named on the typed
3356    /// algebra but the family they close was NOT — the two structural
3357    /// round-trip pins (`_rest_marker_prefixed_by_lambda_list_keyword_
3358    /// lead` AND `_optional_marker_prefixed_by_lambda_list_keyword_
3359    /// lead`) each named ONE marker inline as a duplicate 3-line
3360    /// `assert!(A.starts_with(LEAD), ...)` shape, and the pairwise-
3361    /// disjointness pin (`_rest_and_optional_markers_pairwise_disjoint`)
3362    /// named the two markers as a hand-rolled `assert_ne!` pair.
3363    /// Post-lift the ALL array binds the family at the typed algebra so
3364    /// every family-wide contract (LEAD-byte round-trip, pairwise
3365    /// disjointness, membership gate) routes through the ONE array and
3366    /// a future `&key` / `&aux` extension automatically extends the
3367    /// sweep without re-deriving per-marker assertions.
3368    ///
3369    /// Structural round-trip contract: every element `m` of
3370    /// `Self::LAMBDA_LIST_KEYWORDS` satisfies
3371    /// `m.starts_with(Self::LAMBDA_LIST_KEYWORD_LEAD)` — pinned by
3372    /// `macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead`.
3373    /// Cardinality contract: `Self::LAMBDA_LIST_KEYWORDS.len() == 2`
3374    /// pins the current family size at the type level; a future third
3375    /// marker extends the array's arity AND updates every downstream
3376    /// pin at ONE structural site. Pairwise-disjointness contract:
3377    /// every distinct index pair `(i, j)` in `Self::LAMBDA_LIST_KEYWORDS`
3378    /// satisfies `Self::LAMBDA_LIST_KEYWORDS[i] !=
3379    /// Self::LAMBDA_LIST_KEYWORDS[j]` — pinned by
3380    /// `macro_params_lambda_list_keywords_pairwise_distinct`.
3381    ///
3382    /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3383    /// CL lambda-list-keyword family closure binds at ONE `pub const`
3384    /// on the typed [`MacroParams`] algebra regardless of which
3385    /// consumer (parser dispatch, family-wide contract sweep,
3386    /// authoring / rendering surface, `is_lambda_list_keyword`
3387    /// membership gate) reaches in. THEORY.md §V.1 — knowable
3388    /// platform; the family's cardinality becomes a TYPE-level
3389    /// constant on the substrate algebra rather than a per-consumer
3390    /// hand-rolled enumeration of the two markers. THEORY.md §VI.1 —
3391    /// generation over composition; the family-wide contract sweeps
3392    /// (LEAD-byte round-trip, pairwise disjointness, membership gate)
3393    /// emerge from the composition of TWO substrate primitives (this
3394    /// `pub const` array + the per-role `pub const REST_MARKER` /
3395    /// `pub const OPTIONAL_MARKER`) rather than as per-marker inline
3396    /// assertions the current pins duplicate structurally.
3397    ///
3398    /// Frontier inspiration: Common Lisp's `LAMBDA-LIST-KEYWORDS`
3399    /// standard variable (CLHS §3.4.1) — the runtime-reflectable list
3400    /// of every reserved lambda-list keyword the implementation
3401    /// recognises; `LAMBDA_LIST_KEYWORDS` is the substrate's typed-Rust
3402    /// peer at compile time, with the closed-set ALL array standing in
3403    /// for CL's runtime-mutable list. Translation through pleme-io
3404    /// primitives: a `pub const [&'static str; N]` on the typed
3405    /// [`MacroParams`] algebra rather than a runtime-mutable list;
3406    /// forced-arity closure at the type layer with the same
3407    /// family-reflective read surface.
3408    pub const LAMBDA_LIST_KEYWORDS: [&'static str; 2] = [Self::REST_MARKER, Self::OPTIONAL_MARKER];
3409
3410    /// Typed membership gate over the closed set
3411    /// [`Self::LAMBDA_LIST_KEYWORDS`] — `true` iff `s` matches some
3412    /// element of the ALL array byte-for-byte, `false` for every other
3413    /// input.
3414    ///
3415    /// The typed "does this symbol name a CL lambda-list keyword?"
3416    /// projection on the [`MacroParams`] algebra. Sibling posture to the
3417    /// closed set of per-algebra membership / classifier gates the
3418    /// substrate carries:
3419    /// [`crate::ast::QuoteForm::from_lead_char`] (per-char quote-family
3420    /// dispatch — Some/None on a closed 3-char set),
3421    /// [`crate::ast::Sexp::is_bare_atom_boundary`] (six-fold
3422    /// bare-atom-boundary sweep), and
3423    /// [`crate::ast::Atom::decode_str_escape`] (Str-escape decode table
3424    /// on a closed 6-arm set) — every family-membership gate the
3425    /// substrate carries binds at ONE method on the closed-set
3426    /// algebra so a family extension lands at ONE place plus the
3427    /// per-role marker without re-deriving per-consumer inline
3428    /// enumerations.
3429    ///
3430    /// Pre-lift the "is this symbol a CL lambda-list keyword?" concept
3431    /// existed structurally but was NOT named on the algebra — the
3432    /// disjointness pin (`_rest_and_optional_markers_pairwise_disjoint`)
3433    /// AND the cross-axis disjointness sweep
3434    /// (`_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker`)
3435    /// each named the concept implicitly as a hand-rolled per-marker
3436    /// `assert_ne!` sweep. Post-lift the membership gate binds at ONE
3437    /// typed method on the algebra a future consumer (a parser-time
3438    /// "this symbol looks like a lambda-list keyword but isn't
3439    /// recognised" hint, an authoring-surface completion bar that
3440    /// suggests `&rest` / `&optional` at param-list positions, a
3441    /// pretty-printer that colors CL lambda-list keywords distinctly)
3442    /// binds against without re-deriving the per-marker enumeration
3443    /// at every consumer site.
3444    ///
3445    /// Contract:
3446    ///   * Every element of [`Self::LAMBDA_LIST_KEYWORDS`] classifies
3447    ///     as `true` — pinned by
3448    ///     `macro_params_is_lambda_list_keyword_accepts_every_marker`.
3449    ///   * The bare CL lambda-list-keyword LEAD byte
3450    ///     ([`Self::LAMBDA_LIST_KEYWORD_LEAD`]) as a 1-char string
3451    ///     classifies as `false` — pinned by
3452    ///     `macro_params_is_lambda_list_keyword_rejects_bare_lead_byte`.
3453    ///   * Symbols starting with the LEAD byte but naming an
3454    ///     unrecognised keyword (`"&key"`, `"&aux"`, `"&body"`)
3455    ///     classify as `false` — pinned by
3456    ///     `macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names`.
3457    ///
3458    /// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the
3459    /// closed-set membership gate binds at ONE typed method on the
3460    /// [`MacroParams`] algebra regardless of which consumer reaches
3461    /// in. THEORY.md §III — the typescape; the "is this a CL
3462    /// lambda-list keyword?" projection becomes a TYPE projection on
3463    /// the substrate algebra rather than per-consumer inline
3464    /// enumeration of the two markers. THEORY.md §V.1 — knowable
3465    /// platform; a future third marker (`&key`, `&aux`, `&body`)
3466    /// extends [`Self::LAMBDA_LIST_KEYWORDS`] ONCE and every
3467    /// membership-gate consumer picks up the new marker for free.
3468    ///
3469    /// Frontier inspiration: Common Lisp's `(find X LAMBDA-LIST-KEYWORDS)`
3470    /// pattern for testing lambda-list keyword membership at runtime;
3471    /// `is_lambda_list_keyword` is the substrate's typed-Rust peer at
3472    /// compile time. Translation through pleme-io primitives: a linear
3473    /// sweep over the [`Self::LAMBDA_LIST_KEYWORDS`] `pub const` array
3474    /// rather than a runtime `find` over a mutable list; ordinary
3475    /// slice `contains` on the `&'static str` array.
3476    #[must_use]
3477    pub fn is_lambda_list_keyword(s: &str) -> bool {
3478        Self::LAMBDA_LIST_KEYWORDS.contains(&s)
3479    }
3480
3481    /// The flat, ordered param-name list the template bytecode indexes into:
3482    /// every `required` name in order, then every `optional` name in order,
3483    /// then the `rest` name if present. `names()[i]` is the param `Subst(i)`
3484    /// / `Splice(i)` reference.
3485    #[must_use]
3486    pub fn names(&self) -> Vec<&str> {
3487        self.required
3488            .iter()
3489            .map(String::as_str)
3490            .chain(self.optional.iter().map(|p| p.name.as_str()))
3491            .chain(self.rest.as_deref())
3492            .collect()
3493    }
3494
3495    /// The rest-less maximum arity of this param list: `required.len() +
3496    /// optional.len()`. Two equivalent readings collapse into ONE primitive
3497    /// on the typed `MacroParams`:
3498    ///
3499    ///   * The **rest-start boundary**: when `self.rest` is `Some`, the
3500    ///     `&rest` slot collects `args[fixed_arity()..]` into a
3501    ///     `Sexp::List` (the empty slice when the call is exactly
3502    ///     saturated). `fixed_arity()` IS that slice's start index.
3503    ///   * The **rest-less maximum arity**: when `self.rest` is `None`,
3504    ///     `args.len() > fixed_arity()` is the surplus-args rejection
3505    ///     boundary [`bind`](Self::bind) checks before raising
3506    ///     `LispError::TooManyMacroArgs` (the call-site mirror of
3507    ///     `RestParamTrailingTokens`'s definition-site rejection).
3508    ///
3509    /// Both readings ARE the same arithmetic; [`bind`](Self::bind)
3510    /// previously inlined the same `self.required.len() +
3511    /// self.optional.len()` expression THREE times — once inside the
3512    /// `Vec::with_capacity(required + optional + rest?)` slot, once at
3513    /// the `rest_start` site (inside `if let Some(_rest_name) =
3514    /// self.rest`), and once at the `expected` site (inside the
3515    /// rest-less `else`). The latter two live in mutually-exclusive
3516    /// branches yet name ONE structural concept; lifting the arithmetic
3517    /// to a single named primitive makes that concept first-class on
3518    /// the typed param list.
3519    ///
3520    /// `fixed_arity()` IGNORES the `rest` slot by construction — a
3521    /// `&rest` param has no maximum and is not part of the fixed run.
3522    /// `names().len() == fixed_arity() + usize::from(self.rest.is_some())`
3523    /// is the structural identity binding this primitive to
3524    /// [`names`](Self::names) and to the `Vec::with_capacity` hint
3525    /// [`bind`](Self::bind) computes for the bound-values vec.
3526    ///
3527    /// Theory anchor: THEORY.md §V.1 — knowable platform; the structural
3528    /// "rest-start boundary / rest-less max arity" concept becomes a
3529    /// named `&MacroParams` projection rather than re-derived arithmetic
3530    /// at every site that walks the bound run. Authoring tools (REPL,
3531    /// LSP, `tatara-check`) that want to render "this macro takes
3532    /// between `required.len()` and `fixed_arity()` args (or unbounded
3533    /// if `rest.is_some()`)" bind to ONE method on the typed param
3534    /// list. THEORY.md §VI.1 — generation over composition; three
3535    /// inline copies of the same arithmetic in one function is past
3536    /// the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
3537    /// named. THEORY.md §II.1 invariant 2 — free middle; both
3538    /// expansion strategies route through the SHARED `bind`, so the
3539    /// new primitive is exposed to the bytecode and substitute paths
3540    /// uniformly — no per-strategy drift in how the boundary is
3541    /// computed.
3542    #[must_use]
3543    pub fn fixed_arity(&self) -> usize {
3544        self.required.len() + self.optional.len()
3545    }
3546
3547    /// The TOTAL lambda-list arity of this param list — every named
3548    /// slot the reader accepted, in the canonical CL lambda-list
3549    /// ordering: `required.len() + optional.len() + rest.is_some() as
3550    /// usize`. Peer to [`Self::fixed_arity`] one REST-SLOT axis over
3551    /// on the `MacroParams` algebra — where [`Self::fixed_arity`]
3552    /// ignores the rest slot (the rest-less maximum-arity /
3553    /// rest-start-boundary reading, unbounded on the call-time surface
3554    /// when `self.rest.is_some()`), this projection INCLUDES the rest
3555    /// slot as one additional named param (the total-slots reading,
3556    /// bounded on the type-time surface regardless of `rest`).
3557    ///
3558    /// `Self::total_arity() == Self::names().len()` — a `Vec`-free
3559    /// projection of the [`Self::names`] flat-index list's length. Two
3560    /// equivalent readings collapse into ONE named primitive on the
3561    /// typed [`MacroParams`] algebra:
3562    ///
3563    ///   * The **`Vec::with_capacity` hint**: [`Self::bind`] previously
3564    ///     inlined `self.fixed_arity() + usize::from(self.rest.is_some())`
3565    ///     as the bound-vec capacity hint. That expression IS this
3566    ///     primitive.
3567    ///   * The **REGISTER-time arity ceiling**: on the arity-bomb
3568    ///     `(defmacro huge (a-1 a-2 … a-N) `,a-1)` [`Expander::register_macro_def`]
3569    ///     compares `def.params.total_arity()` against
3570    ///     [`Expander::max_macro_arity`] BEFORE the body-size gate walks
3571    ///     the body or any insert lands. `total_arity()` IS that gate's
3572    ///     axis.
3573    ///
3574    /// The structural identity
3575    /// `names().len() == fixed_arity() + usize::from(rest.is_some())
3576    ///                 == total_arity()`
3577    /// binds the primitive to [`Self::names`] and [`Self::fixed_arity`]
3578    /// on the [`MacroParams`] algebra — pinned by
3579    /// `macro_params_total_arity_matches_names_len` AND
3580    /// `macro_params_total_arity_equals_fixed_arity_plus_rest_bit`.
3581    ///
3582    /// Theory anchor: THEORY.md §V.1 — knowable platform; the
3583    /// "how many named slots does this macro declare?" projection
3584    /// becomes a NAMED `&MacroParams` accessor rather than re-derived
3585    /// arithmetic at [`Self::bind`]'s capacity hint AND at the
3586    /// [`Expander::register_macro_def`] arity-ceiling gate.
3587    /// THEORY.md §VI.1 — generation over composition; two inline copies
3588    /// of the same three-arm arithmetic across two consumers is the
3589    /// ≥2 PRIME-DIRECTIVE trigger once the structural shape is named.
3590    /// THEORY.md §II.1 invariant 2 — free middle; both the binder AND
3591    /// the arity-guard consult ONE primitive, so a regression that
3592    /// drifts the guard's arity computation from the binder's capacity
3593    /// hint (e.g. one counts `rest` as one slot, the other counts it
3594    /// as zero) becomes structurally impossible.
3595    #[must_use]
3596    pub fn total_arity(&self) -> usize {
3597        self.fixed_arity() + usize::from(self.rest.is_some())
3598    }
3599
3600    /// Bind call args to params positionally, returning the per-index bound
3601    /// values parallel to [`names`](Self::names): each required name takes
3602    /// the arg at its position (a missing one is
3603    /// [`missing_macro_arg`](self::missing_macro_arg)); each optional name
3604    /// takes the arg at its position, or — when the call ran out of args —
3605    /// its declared default form (`Sexp::Nil` when none was declared,
3606    /// matching CL's `&optional` floor); and a present `rest` collects every
3607    /// arg beyond the required+optional run into a `Sexp::List` (the empty
3608    /// list when none remain). Args beyond a REST-LESS param list have
3609    /// nowhere to bind and are rejected
3610    /// ([`too_many_macro_args`](self::too_many_macro_args)) rather than
3611    /// silently truncated. This is the single binding loop every expansion
3612    /// strategy shares — `apply_compiled` consumes the index vec directly,
3613    /// `bind_args` zips it against `names()` into the name-keyed map — and
3614    /// it is the `Sexp` instantiation of the carrier-generic
3615    /// [`Self::bind_carrier`].
3616    fn bind(&self, macro_name: &str, args: &[Sexp]) -> Result<Vec<Sexp>> {
3617        self.bind_carrier(macro_name, args, ())
3618    }
3619
3620    /// The ONE positional binder, generic over the value carrier a macro call
3621    /// binds its arguments in — [`Sexp`] for the plain `Sexp`→`Sexp`
3622    /// expander, `Spanned` for the span-preserving one, and any future
3623    /// carrier a consumer implements [`MacroArgCarrier`] for.
3624    ///
3625    /// Genericising this loop is what makes ONE lambda-list semantics
3626    /// (required run → `&optional` run with per-param defaults → at-most-one
3627    /// `&rest`, plus the too-few / too-many arity rejections) reachable from
3628    /// every expansion path, instead of each path restating the loop against
3629    /// its own value type. Before this lift the span-preserving expander
3630    /// carried its own `bind_spanned_args` that knew only `Required`/`Rest`
3631    /// — so `(defmacro f (a &optional b) …)` parsed on one path and was
3632    /// rejected on the other, and a surplus argument was a hard error on one
3633    /// path and silently dropped on the other. Both divergences are now
3634    /// structurally impossible: there is exactly one loop.
3635    ///
3636    /// `site` is the carrier's synthesis context — `()` for `Sexp` (a
3637    /// default form and a rest list need no context to construct), the
3638    /// macro CALL SITE span for `Spanned` (a value the CALL never supplied
3639    /// has no source position of its own, so it is stamped at the call).
3640    ///
3641    /// Theory anchor: THEORY.md §VI.1 — generation over composition; the
3642    /// binding loop had reached three copies (plain, spanned, and
3643    /// `tatara-lisp-eval`'s macro-time env binder), well past the ≥2
3644    /// PRIME-DIRECTIVE trigger.
3645    pub fn bind_carrier<V: MacroArgCarrier>(
3646        &self,
3647        macro_name: &str,
3648        args: &[V],
3649        site: V::Site,
3650    ) -> Result<Vec<V>> {
3651        let mut out = Vec::with_capacity(self.total_arity());
3652        for (i, name) in self.required.iter().enumerate() {
3653            let arg = args
3654                .get(i)
3655                .cloned()
3656                .ok_or_else(|| missing_macro_arg(macro_name, name))?;
3657            out.push(arg);
3658        }
3659        let opt_start = self.required.len();
3660        for (j, param) in self.optional.iter().enumerate() {
3661            // Absent optional slot binds to the typed `resolved_default()`
3662            // projection on `OptionalParam`: the declared default form when
3663            // one was authored, OR the `Sexp::Nil` no-default floor when
3664            // none was. The two-arm fallback `param.default.clone().
3665            // unwrap_or(Sexp::Nil)` previously inlined here is now ONE named
3666            // accessor on the typed param both expansion strategies share via
3667            // `MacroParams::bind_carrier`.
3668            let arg = match args.get(opt_start + j) {
3669                Some(supplied) => supplied.clone(),
3670                None => V::lift_default(&param.resolved_default(), site),
3671            };
3672            out.push(arg);
3673        }
3674        if let Some(_rest_name) = self.rest.as_ref() {
3675            // The `&rest` slot collects args[fixed_arity()..] (the empty
3676            // slice when the call is exactly saturated); the boundary is
3677            // the typed `fixed_arity()` primitive both branches share.
3678            let rest = args.get(self.fixed_arity()..).unwrap_or(&[]).to_vec();
3679            out.push(V::collect_rest(rest, site));
3680        } else {
3681            // No `&rest` slot — the param list has a FIXED maximum arity
3682            // of `fixed_arity()`. Surplus args have nowhere to bind;
3683            // reject rather than silently truncate. Closes the call-site
3684            // mirror of `RestParamTrailingTokens` (the definition-site
3685            // rejection lifted by the prior-run typed-promotion lineage),
3686            // so the typed-entry macro-call-gate is structurally complete
3687            // in both directions: too-few (`MissingMacroArg`) AND too-many
3688            // (`TooManyMacroArgs`).
3689            let expected = self.fixed_arity();
3690            if args.len() > expected {
3691                return Err(too_many_macro_args(macro_name, expected, args.len()));
3692            }
3693        }
3694        Ok(out)
3695    }
3696}
3697
3698/// Macro environment. Collects `defmacro` forms and rewrites callers.
3699///
3700/// Expansion strategy is tunable per-expander:
3701///   - **Compiled (default)** — every registered macro's template is walked once
3702///     and flattened into a linear `CompiledTemplate` (a tiny bytecode: Literal,
3703///     Subst(index), Splice(index), BeginList, EndList). Expansion of a call
3704///     is then a linear pass with no HashMap lookups and no recursion through
3705///     the template Sexp. Purely-literal subtrees compile to a single
3706///     `Literal(Sexp)` op — huge win for macros where most of the body is fixed.
3707///   - **Substitute-only** — runs the name-keyed `substitute` walker. Slower
3708///     but proves equivalence; used in the benchmark test to measure the
3709///     compiled-vs-substituted speedup.
3710#[derive(Clone, Default)]
3711pub struct Expander {
3712    macros: HashMap<String, MacroDef>,
3713    /// Pre-compiled template bytecodes, populated when `compile_templates`.
3714    templates: HashMap<String, CompiledTemplate>,
3715    /// When true, register a CompiledTemplate alongside each macro and dispatch
3716    /// expansion through the bytecode interpreter.
3717    compile_templates: bool,
3718    /// Memoization of `apply(macro, args)` — repeated calls with identical
3719    /// args skip expansion entirely. Shared across clones so realizations of
3720    /// the same `CompilerSpec` benefit across .compile() invocations.
3721    cache: Arc<Mutex<HashMap<CacheKey, Sexp>>>,
3722    /// Toggle caching. Default on — caching is the actual performance win
3723    /// the bytecode layer enables.
3724    cache_enabled: bool,
3725    /// The six resource ceilings, held as ONE typed [`ResourceLimits`] bundle
3726    /// rather than six sibling `max_*: usize` fields. Peer of the six
3727    /// getter/setter pairs one AGGREGATION axis over — where each pair
3728    /// projects ONE ceiling, this field carries the SIX-fold cross-product
3729    /// so [`Self::resource_limits`] returns `self.limits` verbatim and
3730    /// [`Self::set_resource_limits`] assigns through the bundle in ONE move.
3731    ///
3732    /// Adding a SEVENTH ceiling touches [`ResourceLimits`] + the matching
3733    /// `DEFAULT_MAX_*` module constant + the paired individual getter/setter
3734    /// — the [`Expander`] struct, its constructors, AND
3735    /// [`Self::resource_limits`] / [`Self::set_resource_limits`] stay
3736    /// exactly as they are. That's the compounding win of the
3737    /// consolidation: the (Expander, constructors, bulk getter, bulk
3738    /// setter) four-corner face is field-shape-agnostic — it composes with
3739    /// the bundle by value, not by field-by-field literal.
3740    ///
3741    /// Consumers on the read path (`register_macro_def`'s three REGISTER-time
3742    /// gates, `expand_with_depth`'s depth + output-size gates, `apply`'s
3743    /// cache-entries gate) dereference through `self.limits.max_*`; the
3744    /// projection is a `Copy usize` load — same shape as reading a bare
3745    /// field, no `Deref`, no `Arc`, no lock.
3746    limits: ResourceLimits,
3747}
3748
3749impl Expander {
3750    /// Default-posture expander with an operator-supplied resource-ceiling
3751    /// bundle — the AGGREGATION-axis peer of
3752    /// [`Self::set_resource_limits`] one CONSTRUCTOR-STAGE axis over.
3753    /// Where [`Self::set_resource_limits`] applies the bundled six ceilings
3754    /// POST-construction on an already-built expander, this constructor
3755    /// binds them AT construction so a call-site that wants a fresh
3756    /// expander with a non-default posture composes ONE typed call
3757    /// (`Expander::with_limits(limits)`) rather than the two-step
3758    /// (`let mut e = Expander::new(); e.set_resource_limits(limits);`)
3759    /// dance the post-construction sibling still admits.
3760    ///
3761    /// The (compile_templates, cache_enabled) execution-strategy pair
3762    /// carries the [`Self::new`] default posture (`true, true`) — the
3763    /// bytecode-with-cache strategy — since callers that want the
3764    /// substitute-only strategy still route through
3765    /// [`Self::new_substitute_only`]. Post-lift both convenience
3766    /// constructors delegate their `macros` / `templates` / `cache` field
3767    /// literals through THIS primitive at the resource-posture axis: the
3768    /// four-out-of-six field literals (`HashMap::new()` × 2 +
3769    /// `Arc::new(Mutex::new(HashMap::new()))` + `limits`) lived
3770    /// byte-identical across [`Self::new`] and [`Self::new_substitute_only`]
3771    /// pre-lift; the two divergent flags (`compile_templates`,
3772    /// `cache_enabled`) still differ between the two constructors, and the
3773    /// substitute-only variant flips them AFTER threading through
3774    /// `with_limits` — same shape as [`Self::new_bytecode_no_cache`] does
3775    /// on the `cache_enabled` flag.
3776    ///
3777    /// Round-trip identity: `Expander::with_limits(limits).resource_limits()
3778    /// == limits` for any `ResourceLimits` — pinned as a typed theorem in
3779    /// the test cohort. The at-construction assignment is a direct `Copy`
3780    /// of the bundled field (no per-field cascade), so a mid-construction
3781    /// panic window on the six knobs is structurally impossible. Peer of
3782    /// the `Expander::with_limits(DEFAULT_RESOURCE_LIMITS) ==
3783    /// Expander::new()` delegation identity — pinned on the resource-
3784    /// posture projection so a future refactor that drifts the two
3785    /// constructors on the ceiling axis fails loudly.
3786    ///
3787    /// Frontier inspiration: Cranelift's `settings::Flags::new(builder)` —
3788    /// codegen ceilings (opt level, allocator strategy, feature enables)
3789    /// bind at CONSTRUCTION-time on a typed `Flags` bundle rather than
3790    /// composed through post-construction setters on the `Isa`. Same
3791    /// posture as this lift: the (getter, setter, at-construction)
3792    /// three-corner face on the resource-posture surface now binds at ONE
3793    /// typed bundle across ALL three surfaces.
3794    ///
3795    /// Theory grounding: THEORY.md §II.1 invariants 1 + 2 — typed entry +
3796    /// free middle. Pre-lift the "expander with a chosen resource posture"
3797    /// concept was untyped: consumers composed [`Self::new`] +
3798    /// [`Self::set_resource_limits`] at their call site with no
3799    /// compile-time gate that BOTH steps land atomically. Post-lift the
3800    /// concept has ONE named entry point on the `Expander` surface, and a
3801    /// future consumer (`RealizedCompiler` builder that ships a
3802    /// CI-tightened preset, an LSP session that ships a REPL-permissive
3803    /// preset, a test-harness fixture that ships an `UNBOUNDED`-style
3804    /// preset) binds through ONE typed constructor.
3805    pub fn with_limits(limits: ResourceLimits) -> Self {
3806        Self {
3807            macros: HashMap::new(),
3808            templates: HashMap::new(),
3809            compile_templates: true,
3810            cache: Arc::new(Mutex::new(HashMap::new())),
3811            cache_enabled: true,
3812            limits,
3813        }
3814    }
3815
3816    /// Default expander — compiled bytecode + expansion cache enabled.
3817    /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3818    /// so the (macros, templates, cache, limits) field-shape agreement
3819    /// with every other constructor on this `Expander` surface lives at
3820    /// ONE literal.
3821    pub fn new() -> Self {
3822        Self::with_limits(DEFAULT_RESOURCE_LIMITS)
3823    }
3824
3825    /// Expander using the legacy substitute path (no template compilation,
3826    /// no cache). Kept for benchmarking + equivalence testing.
3827    /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3828    /// and flips the two divergent execution-strategy flags — same
3829    /// composition shape as [`Self::new_bytecode_no_cache`] one flag over.
3830    pub fn new_substitute_only() -> Self {
3831        let mut e = Self::with_limits(DEFAULT_RESOURCE_LIMITS);
3832        e.compile_templates = false;
3833        e.cache_enabled = false;
3834        e
3835    }
3836
3837    /// Expander with bytecode on but expansion cache off — isolates the cache
3838    /// contribution from the bytecode infrastructure. Benchmark baseline.
3839    pub fn new_bytecode_no_cache() -> Self {
3840        let mut e = Self::new();
3841        e.cache_enabled = false;
3842        e
3843    }
3844
3845    /// Toggle the expansion cache at runtime.
3846    pub fn set_cache_enabled(&mut self, enabled: bool) {
3847        self.cache_enabled = enabled;
3848    }
3849
3850    /// How many entries are currently cached.
3851    pub fn cache_size(&self) -> usize {
3852        self.cache.lock().unwrap().len()
3853    }
3854
3855    /// Clear the expansion cache (e.g., after redefining a macro).
3856    pub fn clear_cache(&self) {
3857        self.cache.lock().unwrap().clear();
3858    }
3859
3860    /// The configured ceiling on `expand`'s recursive re-entry into a
3861    /// macro-call form. Defaults to [`DEFAULT_MAX_EXPANSION_DEPTH`].
3862    #[must_use]
3863    pub fn max_expansion_depth(&self) -> usize {
3864        self.limits.max_expansion_depth
3865    }
3866
3867    /// Set the ceiling on `expand`'s recursive re-entry into a macro-call
3868    /// form. Tests bind low values (e.g. `4`) to pin the runaway-rejection
3869    /// shape without paying for a 256-round walk; consumers that compose
3870    /// deep proven macro cascades can raise it. Zero is admitted and
3871    /// makes every first macro-call return
3872    /// [`LispError::ExpansionDepthExceeded`] immediately — useful for
3873    /// contract-tests that assert "this reader path never expands a
3874    /// macro."
3875    pub fn set_max_expansion_depth(&mut self, depth: usize) {
3876        self.limits.max_expansion_depth = depth;
3877    }
3878
3879    /// The configured ceiling on the expansion cache's entry count.
3880    /// Defaults to [`DEFAULT_MAX_CACHE_ENTRIES`]. Peer to
3881    /// [`Self::max_expansion_depth`] one RESOURCE axis over.
3882    #[must_use]
3883    pub fn max_cache_entries(&self) -> usize {
3884        self.limits.max_cache_entries
3885    }
3886
3887    /// Set the ceiling on the expansion cache's entry count. Tests bind
3888    /// low values (e.g. `2`) to pin the bounded-cache contract without
3889    /// paying for an 8K-entry walk; consumers that expand many distinct
3890    /// argument shapes can raise it. Zero is admitted and effectively
3891    /// disables caching (every fresh `(name, args)` pair skips the
3892    /// insert path) — useful for contract-tests that assert "this
3893    /// expander realizes without ever caching." [`usize::MAX`] is the
3894    /// unbounded-cache mode for batch compilation runs where memory is
3895    /// not the concern.
3896    pub fn set_max_cache_entries(&mut self, cap: usize) {
3897        self.limits.max_cache_entries = cap;
3898    }
3899
3900    /// The configured ceiling on a single macro-`apply` output's
3901    /// [`crate::ast::Sexp::node_count`]. Defaults to
3902    /// [`DEFAULT_MAX_EXPANSION_SIZE`]. Peer to
3903    /// [`Self::max_expansion_depth`] and [`Self::max_cache_entries`]
3904    /// one RESOURCE-DIMENSION axis over.
3905    #[must_use]
3906    pub fn max_expansion_size(&self) -> usize {
3907        self.limits.max_expansion_size
3908    }
3909
3910    /// Set the ceiling on a single macro-`apply` output's node count.
3911    /// Tests bind low values (e.g. `8`) to pin the "expansion bomb"
3912    /// rejection shape without materializing a 64K-node blob;
3913    /// consumers that compose deep proven macro cascades can raise it.
3914    /// [`usize::MAX`] admits any lawful output (the ceiling is
3915    /// effectively lifted); operators that want strict "your macros
3916    /// must fit in N nodes" contract-tests set it exactly.
3917    pub fn set_max_expansion_size(&mut self, size: usize) {
3918        self.limits.max_expansion_size = size;
3919    }
3920
3921    /// The configured ceiling on a registered macro's BODY
3922    /// [`crate::ast::Sexp::node_count`]. Defaults to
3923    /// [`DEFAULT_MAX_MACRO_BODY_SIZE`]. Peer to
3924    /// [`Self::max_expansion_depth`], [`Self::max_cache_entries`],
3925    /// and [`Self::max_expansion_size`] one PIPELINE-STAGE axis over
3926    /// (the three prior guards fire at EXPAND time; this one fires
3927    /// at REGISTER time).
3928    #[must_use]
3929    pub fn max_macro_body_size(&self) -> usize {
3930        self.limits.max_macro_body_size
3931    }
3932
3933    /// Set the ceiling on a registered macro's BODY node count.
3934    /// Tests bind low values (e.g. `4`) to pin the authoring-bomb
3935    /// rejection shape without materializing a 16K-node blob;
3936    /// consumers that author deep proven macro-body templates can
3937    /// raise it. [`usize::MAX`] admits any lawful body (the ceiling
3938    /// is effectively lifted). Zero is admitted and rejects every
3939    /// non-empty body immediately (a `Nil` body is 1 node, so the
3940    /// smallest lawful body is still rejected under a zero
3941    /// ceiling) — useful for contract-tests that assert "this
3942    /// expander refuses every registration."
3943    pub fn set_max_macro_body_size(&mut self, size: usize) {
3944        self.limits.max_macro_body_size = size;
3945    }
3946
3947    /// The configured ceiling on the [`Self::macros`] table's entry
3948    /// count. Defaults to [`DEFAULT_MAX_REGISTERED_MACROS`]. Peer to
3949    /// [`Self::max_macro_body_size`] one RESOURCE-DIMENSION axis over
3950    /// on the REGISTER-time surface, and to [`Self::max_cache_entries`]
3951    /// one PIPELINE-STAGE axis over.
3952    #[must_use]
3953    pub fn max_registered_macros(&self) -> usize {
3954        self.limits.max_registered_macros
3955    }
3956
3957    /// Set the ceiling on the [`Self::macros`] table's entry count.
3958    /// Tests bind low values (e.g. `3`) to pin the registration-bomb
3959    /// rejection shape without materializing a 4K-entry macros table;
3960    /// consumers that compose deep proven typescapes can raise it.
3961    /// [`usize::MAX`] admits any lawful cumulative registration count
3962    /// (the ceiling is effectively lifted). Zero is admitted and
3963    /// rejects every FRESH registration immediately — useful for
3964    /// contract-tests that assert "this expander accepts no new
3965    /// macros" — while still admitting overwrites of any macro that
3966    /// was registered before the ceiling was set (a re-registration
3967    /// of an existing key never grows the table).
3968    pub fn set_max_registered_macros(&mut self, cap: usize) {
3969        self.limits.max_registered_macros = cap;
3970    }
3971
3972    /// The configured ceiling on a registered macro's lambda-list
3973    /// arity. Defaults to [`DEFAULT_MAX_MACRO_ARITY`]. Peer to
3974    /// [`Self::max_macro_body_size`] and [`Self::max_registered_macros`]
3975    /// one RESOURCE-DIMENSION axis over on the REGISTER-time surface.
3976    #[must_use]
3977    pub fn max_macro_arity(&self) -> usize {
3978        self.limits.max_macro_arity
3979    }
3980
3981    /// Set the ceiling on a registered macro's lambda-list arity —
3982    /// the [`MacroParams::total_arity`] projection of `def.params`.
3983    /// Tests bind low values (e.g. `2`) to pin the arity-bomb rejection
3984    /// shape without materializing a 128-slot authoring blob;
3985    /// consumers that author high-arity domain macros (a `defschema`
3986    /// with 40 typed slots) can raise it. [`usize::MAX`] admits any
3987    /// lawful arity (the ceiling is effectively lifted). Zero is
3988    /// admitted and rejects every non-nullary macro registration
3989    /// immediately — a `(defmacro nullary () BODY)` is still admitted
3990    /// (its `total_arity()` is `0`, and only strict overrun rejects) —
3991    /// useful for contract-tests that assert "this expander accepts
3992    /// only nullary macros."
3993    pub fn set_max_macro_arity(&mut self, cap: usize) {
3994        self.limits.max_macro_arity = cap;
3995    }
3996
3997    /// Snapshot the six configured resource ceilings as ONE typed
3998    /// [`ResourceLimits`] value — the bundled peer of the six
3999    /// [`Self::max_expansion_depth`] / [`Self::max_cache_entries`] /
4000    /// [`Self::max_expansion_size`] / [`Self::max_macro_body_size`] /
4001    /// [`Self::max_registered_macros`] / [`Self::max_macro_arity`]
4002    /// individual getters. Consumers that want to inspect the full
4003    /// resource posture (a test that compares a preset expander to a
4004    /// reference posture, a diagnostic that renders "the six ceilings"
4005    /// as one line, a caller that clones + selectively overrides one
4006    /// field via a struct-update literal) bind to this projection at
4007    /// ONE call site rather than composing the six individual getters.
4008    ///
4009    /// Post the [`Self::limits`]-field consolidation the projection is
4010    /// a direct `Copy` of the bundled field — a `usize`-sized fixed
4011    /// return with no per-field literal walk. Adding a SEVENTH ceiling
4012    /// extends [`ResourceLimits`] and the paired individual
4013    /// getter/setter; this bulk getter is field-shape-agnostic and
4014    /// stays exactly as it is.
4015    #[must_use]
4016    pub fn resource_limits(&self) -> ResourceLimits {
4017        self.limits
4018    }
4019
4020    /// Bulk-replace the six configured resource ceilings from ONE typed
4021    /// [`ResourceLimits`] value — the bundled peer of the six
4022    /// [`Self::set_max_expansion_depth`] /
4023    /// [`Self::set_max_cache_entries`] /
4024    /// [`Self::set_max_expansion_size`] /
4025    /// [`Self::set_max_macro_body_size`] /
4026    /// [`Self::set_max_registered_macros`] /
4027    /// [`Self::set_max_macro_arity`] individual setters. Presets
4028    /// (a hardened CI expander, a permissive REPL expander, a
4029    /// throttled test harness) compose the six ceilings ONCE at the
4030    /// preset call site and apply them ATOMICALLY here rather than
4031    /// threading six independent setter invocations whose ordering
4032    /// the type system does not gate.
4033    ///
4034    /// Round-trip identity: `let l = e.resource_limits(); e.set_resource_limits(l);`
4035    /// leaves `e` in an equivalent posture — pinned as a typed theorem
4036    /// in the [`ResourceLimits`] test cohort. Post the [`Self::limits`]-
4037    /// field consolidation the assignment is a direct `Copy` of the
4038    /// bundled field, structurally atomic — no per-field destructure +
4039    /// six-way write cascade in which a mid-cascade panic could leave
4040    /// the six knobs in a half-updated posture. Sibling of
4041    /// [`Self::resource_limits`]; the pair closes the (getter, setter)
4042    /// face on the aggregation axis.
4043    pub fn set_resource_limits(&mut self, limits: ResourceLimits) {
4044        self.limits = limits;
4045    }
4046
4047    pub fn with_macros<I: IntoIterator<Item = MacroDef>>(defs: I) -> Result<Self> {
4048        let mut e = Self::new();
4049        for d in defs {
4050            e.register_macro_def(d)?;
4051        }
4052        Ok(e)
4053    }
4054
4055    /// Register a parsed [`MacroDef`] into this expander's macro tables —
4056    /// the single named primitive on the `Expander` surface every
4057    /// macro-registration consumer routes through.
4058    ///
4059    /// The registration discipline is a two-step composition:
4060    ///   1. When [`Self::compile_templates`](Self::new) is on (the
4061    ///      `Self::new` default; flipped off by [`Self::new_substitute_only`]),
4062    ///      [`compile_template`] pre-compiles the macro body to a typed
4063    ///      [`CompiledTemplate`] bytecode and inserts it into `self.templates`
4064    ///      keyed by `def.name`.
4065    ///   2. The `MacroDef` is moved into `self.macros` keyed by `def.name` —
4066    ///      always, regardless of `compile_templates`, because the substitute
4067    ///      strategy reads `self.macros` exclusively while the bytecode
4068    ///      strategy consults `self.templates` first and falls back to
4069    ///      `self.macros` for the body and params.
4070    ///
4071    /// The order is structural: `compile_template` borrows `&def` while
4072    /// `self.macros.insert(def.name.clone(), def)` consumes `def` — the
4073    /// template pre-compile MUST precede the move into `self.macros`, and the
4074    /// `def.name.clone()` projection captures the key for the moved insert.
4075    /// `?`-routing through `compile_template` preserves the structural
4076    /// ordering of the rejection chain end-to-end: a template-compile error
4077    /// (`UnboundTemplateVar` for an unbound `,name`, `NonSymbolUnquoteTarget`
4078    /// for `,5` / `,(nested)`, et al.) short-circuits BEFORE `self.macros`
4079    /// is mutated, so a failed registration leaves both tables exactly as
4080    /// they were — no partial-write window in which `self.macros.has(name)`
4081    /// is true but `self.templates.has(name)` is missing (a regression that
4082    /// would silently coerce the bytecode strategy onto the substitute path
4083    /// for that one macro despite `compile_templates: true`).
4084    ///
4085    /// Before this lift the same two-step block —
4086    ///
4087    /// ```ignore
4088    /// if self.compile_templates {
4089    ///     self.templates.insert(def.name.clone(), compile_template(&def)?);
4090    /// }
4091    /// self.macros.insert(def.name.clone(), def);
4092    /// ```
4093    ///
4094    /// — lived byte-identical (modulo `self`/`e` and `def`/`d` substitutions)
4095    /// at TWO sites: [`Self::with_macros`] (the constructor that
4096    /// bulk-registers an `IntoIterator<Item = MacroDef>`, e.g. a curated
4097    /// preloaded set the caller assembled out-of-band) and
4098    /// [`Self::expand_program`]'s `(defmacro …)`-head arm (the program-level
4099    /// walker that recognizes a `defmacro` / `defpoint-template` / `defcheck`
4100    /// head via [`macro_def_from`] and registers it as a side-effect of
4101    /// walking the program). After this lift the registration block lives in
4102    /// ONE method on the `Expander`; both consumers and any future
4103    /// macro-registration surface bind to ONE primitive instead of
4104    /// re-deriving the two-step discipline at every call site.
4105    ///
4106    /// `pub` so authoring surfaces (an LSP that incrementally registers a
4107    /// `(defmacro …)` head as the user finishes typing it without a full
4108    /// program re-expand, a REPL `:define-macro` command that registers a
4109    /// pre-parsed `MacroDef` directly, a future "library merge" operation
4110    /// that absorbs another expander's macro set MacroDef-by-MacroDef) can
4111    /// register a typed `MacroDef` without round-tripping through source
4112    /// serialization first. Sibling of [`Self::with_macros`] (the
4113    /// bulk-from-iterator constructor — itself the
4114    /// `defs.into_iter().try_fold((), |_, d| self.register_macro_def(d))`
4115    /// shape on a fresh expander) and [`Self::expand_program`] (the
4116    /// source-level walker that recognizes `(defmacro …)` heads via
4117    /// [`macro_def_from`] — itself the program-level fold-over-defmacro-heads
4118    /// of this method). All three end up at this primitive.
4119    ///
4120    /// Returns `Result<()>` so the consumer's rejection chain composes with
4121    /// `?`-routing — `with_macros` short-circuits its bulk loop on the first
4122    /// `compile_template` failure; `expand_program` short-circuits its
4123    /// program walk at the offending `(defmacro …)` form. Infallibility on
4124    /// the `compile_templates: false` path is preserved (`compile_template`
4125    /// is gated behind the conditional), so a substitute-only expander never
4126    /// emits the `compile_template`-side rejection chain.
4127    ///
4128    /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4129    /// byte-identical inline copies of the registration block across
4130    /// `with_macros` and `expand_program` is past the ≥2 PRIME-DIRECTIVE
4131    /// trigger once the structural shape is named. THEORY.md §V.1 — knowable
4132    /// platform; the macro-registration discipline becomes a NAMED primitive
4133    /// on the substrate's `Expander` surface rather than a per-consumer
4134    /// inline duplication that future emitters (an LSP, a REPL, a library-
4135    /// merge operator) would have had to re-derive. THEORY.md §II.1
4136    /// invariant 2 — free middle; both consumers route through the SAME
4137    /// registration primitive, so a regression that drifts ONE consumer's
4138    /// discipline from the other (one path skips the template pre-compile,
4139    /// one path inserts the `self.macros` entry in a different order, a
4140    /// future third side-effect — logging, attestation, metrics — emitted
4141    /// at one site but not the other) cannot reach the substrate's runtime:
4142    /// there is exactly one implementation both consumers route through.
4143    ///
4144    /// Frontier inspiration: Racket's `(define-syntax name template)` at
4145    /// REPL is exactly this — register a typed macro into the live
4146    /// namespace, no source round-trip; the substrate's `register_macro_def`
4147    /// is the Rust-typed peer of that surface, lifted onto the `Expander`'s
4148    /// table-level algebra (`macros: HashMap<String, MacroDef>` +
4149    /// `templates: HashMap<String, CompiledTemplate>`). MLIR's
4150    /// `OpRegistry::registerOp<Op>()` — typed-op registration into a
4151    /// dialect's live table at construction-time AND at JIT-walk-time
4152    /// through ONE registration entry point; `register_macro_def` is the
4153    /// pleme-io peer of that registration entry point on the macro-table
4154    /// algebra.
4155    pub fn register_macro_def(&mut self, def: MacroDef) -> Result<()> {
4156        // Reject a registration-bomb (a code-generator emitting
4157        // unbounded fresh `(defmacro fresh-N (x) `(list ,x))` heads
4158        // whose bodies each sit comfortably under
4159        // `max_macro_body_size` yet collectively saturate process
4160        // memory) at the cheapest gate on the REGISTER-time surface
4161        // — a `HashMap::len` comparison, no body walk. Peer to the
4162        // body-size gate below one RESOURCE-DIMENSION axis over
4163        // (COUNT vs. SIZE) and to the expand-time cache-entries gate
4164        // in `apply` one PIPELINE-STAGE axis over (REGISTER-time
4165        // COUNT vs. EXPAND-time COUNT). Fires BEFORE the body-size
4166        // gate walks `def.body.node_count()` or any insert lands, so
4167        // a failed registration leaves BOTH tables exactly as they
4168        // were. Overwrites (a re-registration of an already-
4169        // registered key) are ADMITTED at any table size — a
4170        // re-`defmacro` never grows the table, so the ceiling cannot
4171        // gate operator redefinitions. Keyed on `>=` (a table
4172        // already at ceiling rejects a fresh key) so
4173        // `max_registered_macros` names the LARGEST admissible
4174        // `self.macros.len()` — same posture the expand-time
4175        // cache-entries gate holds on the memoization cache.
4176        let is_overwrite = self.macros.contains_key(&def.name);
4177        if !is_overwrite && self.macros.len() >= self.limits.max_registered_macros {
4178            return Err(LispError::RegisteredMacrosExceeded {
4179                macro_name: def.name.clone(),
4180                count: self.macros.len(),
4181                limit: self.limits.max_registered_macros,
4182            });
4183        }
4184        // Reject an arity-bomb `(defmacro huge (a-1 a-2 … a-N-million)
4185        // `,a-1)` — a code-generator whose macro body sits at ONE node
4186        // (well under `max_macro_body_size`) yet whose param list
4187        // carries millions of fresh names the per-index binder walks on
4188        // every call. Peer to the body-size gate below one RESOURCE-
4189        // DIMENSION axis over on the REGISTER-time surface (per-body
4190        // ARITY vs. per-body SIZE), AND peer to the table-count gate
4191        // above one RESOURCE-DIMENSION axis over on the REGISTER-time
4192        // surface (per-registration WIDTH vs. cumulative COUNT). Fires
4193        // BEFORE the body-size gate walks `def.body.node_count()` or
4194        // any insert lands, so a failed registration leaves BOTH tables
4195        // exactly as they were. Cheaper than the body-size gate — a
4196        // three-arm addition on `def.params` fields, no AST walk —
4197        // hence positioned BEFORE it on the O(1)-first ordering of the
4198        // REGISTER-time chain. Keyed on `>` (equality is admitted, only
4199        // strict overrun rejects) so `max_macro_arity` names the
4200        // LARGEST admissible `def.params.total_arity()` — same posture
4201        // the body-size gate holds on the body node count.
4202        let arity = def.params.total_arity();
4203        if arity > self.limits.max_macro_arity {
4204            return Err(LispError::MacroArityExceeded {
4205                macro_name: def.name.clone(),
4206                arity,
4207                limit: self.limits.max_macro_arity,
4208            });
4209        }
4210        // Reject an authoring-bomb `(defmacro huge (x) `<template-of-
4211        // N-million-nodes>)` at the REGISTRATION boundary — the
4212        // PIPELINE-STAGE peer of the EXPAND-time output-size gate in
4213        // `expand_with_depth`. Fires BEFORE `compile_template` walks
4214        // the body or `self.macros.insert` lands the entry, so a
4215        // failed registration leaves BOTH tables exactly as they
4216        // were (no partial-write window in which the body-size
4217        // rejection happened but the template pre-compile already
4218        // consumed cycles or the macros table already carries the
4219        // over-ceiling entry). Keyed on `>` rather than `>=` so
4220        // `max_macro_body_size` names the LARGEST admissible body —
4221        // equality is admitted, only strict overrun rejects — same
4222        // posture the expand-time output-size gate holds.
4223        let body_size = def.body.node_count();
4224        if body_size > self.limits.max_macro_body_size {
4225            return Err(LispError::MacroBodySizeExceeded {
4226                macro_name: def.name.clone(),
4227                size: body_size,
4228                limit: self.limits.max_macro_body_size,
4229            });
4230        }
4231        if self.compile_templates {
4232            self.templates
4233                .insert(def.name.clone(), compile_template(&def)?);
4234        }
4235        self.macros.insert(def.name.clone(), def);
4236        Ok(())
4237    }
4238
4239    /// Expand a whole program. Returns the list of top-level forms after
4240    /// `defmacro` definitions are registered and all macro calls expanded.
4241    pub fn expand_program(&mut self, forms: Vec<Sexp>) -> Result<Vec<Sexp>> {
4242        let mut out = Vec::new();
4243        for form in forms {
4244            if let Some(def) = macro_def_from(&form)? {
4245                self.register_macro_def(def)?;
4246                continue;
4247            }
4248            out.push(self.expand(&form)?);
4249        }
4250        Ok(out)
4251    }
4252
4253    /// Read a source string into top-level forms via [`crate::reader::read`],
4254    /// then route the forms through [`expand_program`](Self::expand_program) —
4255    /// the from-source posture of the yield-all-forms-after-expansion primitive,
4256    /// in ONE method on the `Expander` surface.
4257    ///
4258    /// Before this lift the same two-step chain — `let forms = read(src)?;
4259    /// <expander>.expand_program(forms)` — lived inline at two sites in
4260    /// [`crate::compiler_spec`]: [`RealizedCompiler::compile`](crate::compiler_spec::RealizedCompiler::compile)
4261    /// (the public from-source untyped-expansion entry on a realized compiler,
4262    /// returning the expanded `Vec<Sexp>` for untyped consumers like
4263    /// `tatara-check`'s per-form dispatcher) AND [`realize_in_memory`](crate::compiler_spec::realize_in_memory)'s
4264    /// `:macros` library load loop (the per-spec-macro source absorption that
4265    /// builds the preloaded expander's macro library through `expand_program`'s
4266    /// `defmacro` recognition side-effect). After this lift the read-then-expand
4267    /// composition lives in ONE method on the `Expander`; each of the two
4268    /// consumers binds it with the per-site expander posture that fits its call
4269    /// boundary — `self.preloaded.clone()` for `RealizedCompiler::compile`'s
4270    /// per-call clone, `&mut preloaded` for `realize_in_memory`'s shared
4271    /// build-up.
4272    ///
4273    /// Sibling of [`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)
4274    /// — that method stacks the typed-keyword projection on top of the
4275    /// from-source pipeline (`read + expand_program + iter_calls_to(_,
4276    /// keyword) + map + collect`); this method is the bare yield-all-forms
4277    /// from-source primitive (`read + expand_program`) the typed dispatchers
4278    /// stack their keyword projection atop. The two together close the
4279    /// from-source posture of the program-level walk family on the
4280    /// `Expander` surface: bare (this method) vs. typed-keyword-projected
4281    /// (the sibling).
4282    ///
4283    /// Closes the 2×2 program-level walk family on the `Expander` surface:
4284    /// from-forms × yield-all ([`expand_program`](Self::expand_program)),
4285    /// from-forms × keyword-projected ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)),
4286    /// from-source × keyword-projected ([`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)),
4287    /// AND now from-source × yield-all (this method). The four together name
4288    /// the canonical surfaces a dispatcher composes with to extract an
4289    /// expanded program from either a pre-parsed `Vec<Sexp>` (from-forms
4290    /// posture, for callers composing with another `Sexp`-producing surface)
4291    /// or a raw `&str` (from-source posture, for callers consuming
4292    /// authoring-surface source text directly), with or without a
4293    /// typed-keyword filter on the result.
4294    ///
4295    /// `?`-routing through `read` preserves the structural ordering of the
4296    /// rejection chain end-to-end: a reader error (lexer / parser /
4297    /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4298    /// `expand_program` runs; an `expand_program` error
4299    /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4300    /// `RestParamMissingName`, et al.) short-circuits at the offending form.
4301    /// Each consumer's rejection chain remains exactly what it was pre-lift,
4302    /// now sourced from ONE composition point rather than two.
4303    ///
4304    /// `defmacro`-registration side-effects fire on `&mut self` exactly as
4305    /// they do for the from-forms primitive — `realize_in_memory`'s per-spec
4306    /// build-up depends on every `defmacro` in every `:macros` source
4307    /// landing in `self.macros` (and, when `compile_templates` is on, in
4308    /// `self.templates`); `RealizedCompiler::compile`'s per-call clone
4309    /// posture isolates absorption to the cloned expander, so a
4310    /// `defmacro` in the user's source does NOT leak into the persistent
4311    /// realized compiler. Both postures' absorption semantics are
4312    /// preserved by routing through this primitive instead of inlining
4313    /// the two-step chain.
4314    ///
4315    /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4316    /// inline copies of the `let forms = read(src)?; <expander>.expand_program(forms)`
4317    /// two-step chain across `RealizedCompiler::compile` and
4318    /// `realize_in_memory` is past the ≥2 PRIME-DIRECTIVE trigger once the
4319    /// structural shape is named. THEORY.md §V.1 — knowable platform; the
4320    /// read-then-expand composition becomes a NAMED primitive on the
4321    /// substrate's `Expander` surface rather than a re-derived two-step
4322    /// inline pipeline at every consumer. THEORY.md §II.1 invariant 2 —
4323    /// free middle; both consumers route through the SAME composition
4324    /// primitive, so a regression that drifts ONE consumer's pipeline
4325    /// shape from the other cannot reach the substrate's runtime.
4326    ///
4327    /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4328    /// namespace — the from-source-string entry to namespace-level
4329    /// program evaluation is the Racket idiom; the substrate's
4330    /// `expand_source_program` is the Rust-typed peer of that, sourced
4331    /// from `&str` and yielding the post-macroexpansion `Vec<Sexp>`
4332    /// without a typed-keyword filter — exactly the shape an untyped
4333    /// consumer (`tatara-check`'s per-form dispatcher, a REPL's
4334    /// "expand this buffer" command, an LSP's "show me the expanded
4335    /// program" handler) binds to.
4336    pub fn expand_source_program(&mut self, src: &str) -> Result<Vec<Sexp>> {
4337        let forms = crate::reader::read(src)?;
4338        self.expand_program(forms)
4339    }
4340
4341    /// Compose the expander's program-level expansion with the substrate's
4342    /// slice-side typed-keyword projection ([`iter_calls_to`]) and a
4343    /// caller-supplied per-form projection — `expand_program(forms)?` followed
4344    /// by `iter_calls_to(&expanded, keyword).map(project).collect()`, in ONE
4345    /// method on the `Expander` surface. Both [`compile_typed`](crate::compile::compile_typed)
4346    /// and [`compile_named_from_forms`](crate::compile::compile_named_from_forms)
4347    /// route through this primitive — they differ only in the per-form
4348    /// projection `F`: `T::compile_from_args` for the bare-kwargs form
4349    /// (`compile_typed`), and the NAME-then-`T::compile_from_args` split
4350    /// (`compile_named_from_forms`).
4351    ///
4352    /// Before this lift each dispatcher opened the same three-step pipeline
4353    /// inline — `let mut exp = Expander::new(); let expanded =
4354    /// exp.expand_program(forms)?; iter_calls_to(&expanded, T::KEYWORD)
4355    /// .map(<per-site>).collect()` — past the ≥2 PRIME-DIRECTIVE trigger
4356    /// once the structural shape is named. After this lift the pipeline
4357    /// lives in ONE method on the `Expander` surface and the two
4358    /// dispatchers thread their per-site projection through `F`; a
4359    /// regression that drifts ONE dispatcher's pipeline from the other (a
4360    /// future emitter that re-derives `expand_program + iter_calls_to`
4361    /// inline rather than composing through this primitive, a future
4362    /// preloaded-expander consumer that wants the same walk but on its
4363    /// own `Expander` rather than a fresh one) is no longer a silent
4364    /// two-site divergence: the method binds the composition once and
4365    /// every consumer threads the per-site projection through `F`.
4366    ///
4367    /// Method on `Expander` (not a free function) so the primitive
4368    /// composes with the broader expander surface: a preloaded expander
4369    /// (e.g., one that has already absorbed a set of `defmacro` forms
4370    /// via `expand_program` or `with_macros`) can call this method on
4371    /// its own state to walk a follow-up program — the
4372    /// `compiler_spec`'s realize path is the natural future consumer for
4373    /// that shape. `compile_typed` and `compile_named_from_forms`
4374    /// instantiate a fresh `Expander::new()` and dispatch through this
4375    /// method on it; a future preloaded consumer dispatches through it
4376    /// on the preloaded expander directly. ONE primitive, two postures
4377    /// (fresh vs. preloaded), no per-posture duplication of the
4378    /// `expand_program + iter_calls_to + map + collect` pipeline.
4379    ///
4380    /// `F` is `FnMut(&[Sexp]) -> Result<R>` — the per-form projection
4381    /// that fits the consumer's call site. The standard-library
4382    /// `Iterator::map` bound is `FnMut`, so a closure that captures
4383    /// mutable state (a future dispatcher that threads a running index
4384    /// or a borrowed accumulator into the projection) composes
4385    /// naturally. The `Result<R>` projection short-circuits on the
4386    /// first error via `Iterator::collect::<Result<Vec<R>, _>>()`, so
4387    /// the per-form rejection chain (`compile_named_from_forms`'s
4388    /// `NamedFormMissingName` for the missing NAME slot,
4389    /// `NamedFormNonSymbolName` for the non-symbol NAME slot,
4390    /// `T::compile_from_args`'s typed-entry kwargs gate, AND
4391    /// `compile_typed`'s bare-kwargs `T::compile_from_args` rejection)
4392    /// fires in the same order under the new shape.
4393    ///
4394    /// `R` is owned by construction — the iterator's `&[Sexp]` items
4395    /// borrow from the local `expanded: Vec<Sexp>` and that borrow
4396    /// ends when the `.collect()` consumes the iterator, so a `R`
4397    /// that borrowed from `expanded` would fail to compile here. The
4398    /// two production consumers (`Vec<T>`, `Vec<NamedDefinition<T>>`)
4399    /// are both owned-`R` shapes, matching the borrow's structural
4400    /// constraint.
4401    ///
4402    /// Sibling of [`expand_program`](Self::expand_program) — that
4403    /// method names the FIRST step of the pipeline (program-level
4404    /// macroexpansion, yielding `Vec<Sexp>`); this method composes it
4405    /// with the SECOND step (slice-side typed-keyword projection +
4406    /// per-form mapper). The split lets a consumer that wants the
4407    /// expanded forms WITHOUT the keyword filter (`tatara-check`'s
4408    /// per-form dispatcher walks every form, not just matched
4409    /// keywords) bind to `expand_program` directly; a consumer that
4410    /// wants both halves composed binds to this method.
4411    ///
4412    /// Closes the substrate's program-level walk family on the
4413    /// `Expander` surface: `expand_program` (yield-all-forms-after-
4414    /// expansion), `expand` (per-form recursive expansion), `apply`
4415    /// (per-call substitution), AND now `expand_and_collect_calls_to`
4416    /// (typed-keyword projection of the expanded forms with a per-form
4417    /// mapper). Together the four name the canonical surfaces a
4418    /// dispatcher composes with to extract a typed program from a
4419    /// post-expansion form set.
4420    ///
4421    /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4422    /// two inline copies of the `Expander::new() + expand_program +
4423    /// iter_calls_to(_, T::KEYWORD) + map + collect` pipeline across
4424    /// `compile_typed` and `compile_named_from_forms` is past the ≥2
4425    /// PRIME-DIRECTIVE trigger once the structural shape is named.
4426    /// THEORY.md §V.1 — knowable platform; the program-level walk's
4427    /// typed-keyword-projection composition becomes a NAMED primitive
4428    /// on the substrate's `Expander` surface rather than a re-derived
4429    /// three-step inline pipeline at every consumer. Authoring tools
4430    /// (REPL, LSP, `tatara-check`) that want to walk a program by
4431    /// typed keyword bind to ONE method on the `Expander` instead of
4432    /// re-implementing the composition. THEORY.md §II.1 invariant 1 —
4433    /// typed entry; the typed-keyword filter over an expanded program
4434    /// IS the rust-level typed-entry-batch gate, and naming its
4435    /// single shape lifts the gate from two-site duplication to one
4436    /// rust method the substrate's diagnostic promotions hang off of.
4437    /// THEORY.md §II.1 invariant 2 — free middle; both consumers
4438    /// route through the SAME composition primitive, each binding the
4439    /// per-form projection that fits its call site — a regression
4440    /// that drifts ONE dispatcher's pipeline shape from the other
4441    /// cannot reach the substrate's runtime.
4442    ///
4443    /// Frontier inspiration: MLIR's `Region::walk<Op>(callback)` —
4444    /// every typed rewriter that wants "for every Op of kind K in this
4445    /// region, run callback" binds to ONE typed walker that composes
4446    /// the kind filter with the per-op visitor; the substrate's
4447    /// `expand_and_collect_calls_to` is the unstructured-projection
4448    /// peer of that walker, lifted onto the post-expansion `&[Sexp]`
4449    /// algebra with the per-form projection as the visitor. Racket's
4450    /// `syntax-parse` `~seq (keyword args ...) ...` ellipsis-form —
4451    /// the program-level typed-keyword filter with per-match handler
4452    /// is the closed-form sibling of `~seq`'s repeated-pattern
4453    /// matcher, translated through pleme-io primitives as ONE method
4454    /// on `Expander` composing `expand_program` with `iter_calls_to`
4455    /// and a per-form mapper. GHC Core's `everything :: forall b. (b
4456    /// -> b -> b) -> GenericQ b -> GenericQ b` — the typed-IR rewriter's
4457    /// program-level fold over a typed selector is named ONCE and
4458    /// every consumer threads the per-node projection; the substrate's
4459    /// `expand_and_collect_calls_to` is the keyword-projected sibling
4460    /// of that fold on the `&[Sexp]` algebra.
4461    pub fn expand_and_collect_calls_to<R, F>(
4462        &mut self,
4463        forms: Vec<Sexp>,
4464        keyword: &str,
4465        mut project: F,
4466    ) -> Result<Vec<R>>
4467    where
4468        F: FnMut(&[Sexp]) -> Result<R>,
4469    {
4470        // Routes through the typed-decoded classifier sibling
4471        // [`Self::expand_and_collect_calls_to_any`] with a constant-
4472        // classifier decoder — the same constant-classifier composition
4473        // [`crate::ast::iter_calls_to`] uses to route through
4474        // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
4475        // discarded `()` typed witness (`then_some(())`) is consumed by
4476        // the wrapper projection `|(), args| project(args)` so the
4477        // keyword consumer's per-form mapper sees only the args tail,
4478        // matching the pre-lift signature exactly. Both the keyword AND
4479        // classifier expander walks now share ONE pipeline implementation
4480        // (`expand_program + iter_calls_to_any + map + collect`); a
4481        // regression that drifts a future debug-mode logger, span-aware
4482        // borrow walker, or fused-iterator invariant from one expander
4483        // walk to the other becomes structurally impossible.
4484        self.expand_and_collect_calls_to_any(
4485            forms,
4486            |h| (h == keyword).then_some(()),
4487            move |(), args| project(args),
4488        )
4489    }
4490
4491    /// Compose the expander's program-level expansion with the substrate's
4492    /// slice-side typed-decoded classifier projection ([`iter_calls_to_any`])
4493    /// and a caller-supplied per-form projection — `expand_program(forms)?`
4494    /// followed by `iter_calls_to_any(&expanded, decode).map(|(decoded, args)|
4495    /// project(decoded, args)).collect()`, in ONE method on the `Expander`
4496    /// surface. The typed-decoded sibling of
4497    /// [`Self::expand_and_collect_calls_to`] — where that method filters by
4498    /// ONE constant keyword, this method filters AND TYPES by a caller-
4499    /// supplied classifier, yielding the typed witness alongside the per-form
4500    /// projection's args input.
4501    ///
4502    /// Closes the substrate's program-level walk family on the `Expander`
4503    /// surface at the typed-decoded corner the prior runs' slice-side
4504    /// `iter_calls_to_any` lift left open — the (keyword, classifier) 2×2
4505    /// of compose-on-iter projections:
4506    ///
4507    /// |                | per-form algebra            | slice algebra                | expander surface                    |
4508    /// |----------------|-----------------------------|------------------------------|-------------------------------------|
4509    /// | keyword        | [`Sexp::as_call_to`]        | [`iter_calls_to`]            | [`Self::expand_and_collect_calls_to`] |
4510    /// | classifier `F` | [`Sexp::as_call_to_any`]    | [`iter_calls_to_any`]        | `expand_and_collect_calls_to_any` (this) |
4511    ///
4512    /// The keyword corner now ROUTES through the classifier corner at every
4513    /// row: [`Sexp::as_call_to`] = constant-classifier projection of
4514    /// [`Sexp::as_call_to_any`]; [`iter_calls_to`] = constant-classifier
4515    /// projection of [`iter_calls_to_any`]; [`Self::expand_and_collect_calls_to`]
4516    /// = constant-classifier projection of THIS method. The substrate's
4517    /// soft-dispatch family is structurally complete at every algebra
4518    /// level — per-form, slice, AND expander — so a regression that drifts
4519    /// a future debug-mode logger, span-aware borrow walker, or
4520    /// fused-iterator invariant from one walk to its sibling at any algebra
4521    /// level becomes structurally impossible.
4522    ///
4523    /// Two plausible future consumer shapes the typed-decoded expander walk
4524    /// admits with no boilerplate:
4525    ///   * **Closed-set classifier** — `expand_and_collect_calls_to_any(forms,
4526    ///     MacroDefHead::from_keyword, |head, args| dispatch(head, args))`
4527    ///     macroexpands a program and walks each `(defmacro …)` /
4528    ///     `(defpoint-template …)` / `(defcheck …)` form decoded to the typed
4529    ///     `MacroDefHead` enum with its args tail. Future `tatara-check`
4530    ///     consumers that want "for every macro-definition form in this
4531    ///     buffer, dispatch by typed kind" bind to ONE method on the
4532    ///     `Expander` rather than the three-step `expand_program +
4533    ///     iter_calls_to_any + map + collect` pipeline at each consumer site.
4534    ///   * **Live-registry classifier** — `expand_and_collect_calls_to_any(forms,
4535    ///     |h| registry.lookup(h), |handler, args| handler.handle(args))`
4536    ///     macroexpands a program and walks each form whose head matches a
4537    ///     runtime registry, decoded to a handler reference. Future
4538    ///     `tatara-check`-shaped runtime dispatchers (the kind already named
4539    ///     by [`iter_calls_to_any`]'s docstring as "the natural future
4540    ///     consumer") bind to ONE method on the `Expander` rather than the
4541    ///     three-step pipeline.
4542    ///
4543    /// The closed-form composition binding the keyword sibling to this typed-
4544    /// decoded primitive is the structural identity every consumer can pin
4545    /// against:
4546    ///
4547    /// ```ignore
4548    /// expand_and_collect_calls_to(forms, k, p) ==
4549    ///     expand_and_collect_calls_to_any(forms,
4550    ///         |h| (h == k).then_some(()),
4551    ///         |(), args| p(args))
4552    /// ```
4553    ///
4554    /// `D` is `FnMut(&str) -> Option<T>` — the typed-decoded classifier the
4555    /// substrate's per-form algebra already shapes ([`Sexp::as_call_to_any`]
4556    /// uses `FnOnce`, the slice algebra and this method use `FnMut` for the
4557    /// same reason: the walk calls the decoder once per matching form, and a
4558    /// decoder that captures mutable state (a counter, a registry cache, a
4559    /// visited-set) maintains that state across the program-level walk).
4560    /// `F` is `FnMut(T, &[Sexp]) -> Result<R>` — the per-form projection that
4561    /// receives BOTH the typed witness AND the args tail; the keyword sibling
4562    /// discards the (unit) typed witness via `|(), args| project(args)`. The
4563    /// `Result<R>` projection short-circuits on the first error via
4564    /// `Iterator::collect::<Result<Vec<R>, _>>()` so the per-form rejection
4565    /// chain fires in source order at the first failing matched form.
4566    ///
4567    /// `R` is owned by construction — same constraint as the keyword
4568    /// sibling. `T` is owned because the underlying [`iter_calls_to_any`]
4569    /// requires `T: 'a` where `'a` is the post-expansion `Vec<Sexp>` borrow
4570    /// lifetime; consumers projecting to a typed `Copy` enum
4571    /// (`MacroDefHead`, a future `AuthoringDirective`) get the value
4572    /// directly per form, consumers projecting to a borrowed `&'static str`
4573    /// (a closed-set head) project to `&'static str` and inherit the static
4574    /// lifetime through the classifier, consumers projecting to a `&Handler`
4575    /// (a live-registry classifier) bind through the registry's owned
4576    /// borrow with `&self` outliving the walk.
4577    ///
4578    /// Theory anchor: THEORY.md §VI.1 — generation over composition; the
4579    /// keyword sibling [`Self::expand_and_collect_calls_to`] is now a
4580    /// CONSEQUENCE of this typed-decoded primitive + a constant-classifier
4581    /// decoder, parallel to how [`iter_calls_to`] is a consequence of
4582    /// [`iter_calls_to_any`] on the slice algebra. The substrate's
4583    /// program-level walk family is no longer two parallel implementations
4584    /// the type system fails to bind (a future regression that drifts the
4585    /// keyword pipeline's instrumentation from the classifier pipeline's
4586    /// instrumentation cannot reach the runtime — both compose through ONE
4587    /// `expand_program + iter_calls_to_any + map + collect` body, with the
4588    /// keyword filter expressed as a closed-form classifier). THEORY.md
4589    /// §V.1 — knowable platform; the typed-decoded expander walk becomes a
4590    /// NAMED primitive on the substrate's `Expander` surface rather than a
4591    /// future re-derived three-step inline pipeline per consumer.
4592    /// Authoring tools (REPL, LSP, `tatara-check`) that want to walk a
4593    /// program by typed-decoded classifier bind to ONE method on the
4594    /// `Expander` instead of re-implementing the composition. THEORY.md
4595    /// §II.1 invariant 2 — free middle; both expander-surface dispatchers
4596    /// (keyword + classifier) route through the SAME `expand_program +
4597    /// iter_calls_to_any + map + collect` composition primitive.
4598    ///
4599    /// Frontier inspiration: MLIR's `Region::walk<OpInterface,
4600    /// OpInterface2, …>([&](auto op) { … })` — the typed-IR walk over a
4601    /// region yielding ops decoded to their typed interface witness with
4602    /// per-op callback is the MLIR idiom; the substrate's
4603    /// `expand_and_collect_calls_to_any` is the unstructured-Rust peer on
4604    /// the post-expansion `&[Sexp]` algebra, with `decode: FnMut(&str) ->
4605    /// Option<T>` standing in for MLIR's typed-interface dyn-cast bag and
4606    /// `project: FnMut(T, &[Sexp]) -> Result<R>` standing in for the
4607    /// typed-op callback. Racket's `syntax-parse` `~or* (~datum defX)
4608    /// (~datum defY) (head args …)` over an ellipsis-form combined with a
4609    /// `#:with name:id` typed-witness binder — the program-level
4610    /// typed-decoded filter with per-match handler is the closed-form
4611    /// sibling of `syntax-parse`'s typed-choice repeater, translated
4612    /// through pleme-io primitives as ONE method on `Expander`. GHC Core's
4613    /// `everythingBut :: (a -> a -> a) -> GenericQ (a, Bool) -> GenericQ a`
4614    /// — the typed-IR rewriter's program-level fold over a typed
4615    /// kind-with-stop selector is named ONCE and every consumer threads
4616    /// the per-node typed projection; this method is the typed-decoded
4617    /// peer on the `Vec<Sexp>` algebra, with the classifier playing
4618    /// `everythingBut`'s typed-selector role.
4619    pub fn expand_and_collect_calls_to_any<R, F, D, T>(
4620        &mut self,
4621        forms: Vec<Sexp>,
4622        decode: D,
4623        mut project: F,
4624    ) -> Result<Vec<R>>
4625    where
4626        D: FnMut(&str) -> Option<T>,
4627        F: FnMut(T, &[Sexp]) -> Result<R>,
4628    {
4629        let expanded = self.expand_program(forms)?;
4630        crate::ast::iter_calls_to_any(&expanded, decode)
4631            .map(|(decoded, args)| project(decoded, args))
4632            .collect()
4633    }
4634
4635    /// Read a source string into top-level forms via [`crate::reader::read`],
4636    /// then route the forms through
4637    /// [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to) — the
4638    /// from-source posture of the program-level walk, in ONE method on the
4639    /// `Expander` surface.
4640    ///
4641    /// Before this lift the same two-step chain — `let forms = read(src)?;
4642    /// <expander>.expand_and_collect_calls_to(forms, keyword, project)` — lived
4643    /// inline at four sites: the two free-function typed dispatchers
4644    /// ([`compile_typed`](crate::compile::compile_typed) and
4645    /// [`compile_named`](crate::compile::compile_named) via
4646    /// [`compile_named_from_forms`](crate::compile::compile_named_from_forms))
4647    /// AND the two preloaded-expander methods on
4648    /// [`RealizedCompiler`](crate::compiler_spec::RealizedCompiler)
4649    /// (`compile_typed`, `compile_named`). After this lift the read-then-walk
4650    /// composition lives in ONE method on the `Expander`; each of the four
4651    /// dispatchers binds it with the per-site `(expander posture, projection)`
4652    /// pair that fits its call boundary — `Expander::new()` for the
4653    /// fresh-expander dispatchers, `self.preloaded.clone()` for the
4654    /// preloaded-expander dispatchers; `T::compile_from_args` for the
4655    /// bare-kwargs projection, `named_form_projection::<T>` for the
4656    /// NAME-then-kwargs projection.
4657    ///
4658    /// Sibling of [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)
4659    /// — that method takes a pre-parsed `Vec<Sexp>` (the from-forms posture,
4660    /// for callers that have already read or that compose with another
4661    /// `Sexp`-producing surface like a macro-expanded subform); this method
4662    /// takes a `&str` (the from-source posture, for callers consuming
4663    /// authoring-surface source text directly). Both compose with the same
4664    /// `expand_program + iter_calls_to + map + collect` pipeline through ONE
4665    /// from-forms primitive — the from-source posture stacks one read step on
4666    /// top, projecting `crate::reader::read`'s `Result<Vec<Sexp>>` into the
4667    /// from-forms primitive via `?`.
4668    ///
4669    /// `?`-routing through `read` preserves the structural ordering of the
4670    /// rejection chain end-to-end: a reader error (lexer / parser /
4671    /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4672    /// `expand_program` runs; an `expand_program` error
4673    /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4674    /// `RestParamMissingName`, et al.) short-circuits BEFORE the keyword
4675    /// filter walks anything; a per-form `project` error
4676    /// (`NamedFormMissingName`, `NamedFormNonSymbolName`,
4677    /// `T::compile_from_args`'s typed-entry kwargs gate) short-circuits at the
4678    /// first failing matched form via `Iterator::collect::<Result<Vec<R>, _>>()`.
4679    /// Each dispatcher's rejection chain remains exactly what it was pre-lift,
4680    /// now sourced from ONE composition point rather than four.
4681    ///
4682    /// Closes the program-level walk family on the `Expander` surface across
4683    /// BOTH the from-forms posture
4684    /// ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to))
4685    /// AND the from-source posture (this method) — together with
4686    /// `expand_program` (yield-all-forms-after-expansion), `expand` (per-form
4687    /// recursive expansion), and `apply` (per-call substitution), they name
4688    /// the canonical surfaces a dispatcher composes with to extract a typed
4689    /// program from a post-expansion form set. A future dispatcher that wants
4690    /// "read this source, then walk every call to keyword K and project each
4691    /// to R" — a debug-mode REPL command, an LSP "find all typed-domain
4692    /// definitions in this buffer" handler, a `tatara-check` command that
4693    /// dispatches each typed `(defX …)` form in `checks.lisp` to its
4694    /// registered domain — binds to ONE method on the `Expander` instead of
4695    /// re-deriving the two-step `read + expand_and_collect_calls_to` chain.
4696    ///
4697    /// Theory anchor: THEORY.md §VI.1 — generation over composition; four
4698    /// inline copies of the `read(src)? + <expander>.expand_and_collect_calls_to`
4699    /// chain across the two fresh-expander dispatchers and the two
4700    /// preloaded-expander dispatchers is past the ≥2 PRIME-DIRECTIVE trigger
4701    /// once the structural shape is named. THEORY.md §V.1 — knowable
4702    /// platform; the read-then-walk composition becomes a NAMED primitive on
4703    /// the substrate's `Expander` surface rather than a re-derived two-step
4704    /// inline pipeline at every dispatcher. THEORY.md §II.1 invariant 2 —
4705    /// free middle; all four dispatchers route through the SAME composition
4706    /// primitive, so a regression that drifts ONE dispatcher's read-then-walk
4707    /// pipeline from the others cannot reach the substrate's runtime — the
4708    /// type system binds all consumers to the from-source primitive's single
4709    /// emission shape.
4710    ///
4711    /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4712    /// namespace populated with the preloaded compiler's `require`d macros —
4713    /// the from-source-string entry to typed-program evaluation inside a
4714    /// namespace that carries the macro library is the Racket idiom; the
4715    /// substrate's `expand_source_and_collect_calls_to` is the Rust-typed
4716    /// peer of that, with the typed-keyword projection composed in.
4717    ///
4718    /// Post-lift the body routes through the typed-decoded sibling
4719    /// [`Self::expand_source_and_collect_calls_to_any`] with a
4720    /// constant-classifier decoder — the same constant-classifier
4721    /// composition [`Self::expand_and_collect_calls_to`] uses to route
4722    /// through [`Self::expand_and_collect_calls_to_any`] on the
4723    /// from-forms axis, and that [`crate::ast::iter_calls_to`] uses to
4724    /// route through [`crate::ast::iter_calls_to_any`] on the slice
4725    /// algebra. The discarded `()` typed witness (`then_some(())`) is
4726    /// consumed by the wrapper projection `|(), args| project(args)`
4727    /// so the keyword consumer's per-form mapper sees only the args
4728    /// tail. Both the keyword AND classifier from-source walks now
4729    /// share ONE pipeline implementation (`read + expand_program +
4730    /// iter_calls_to_any + map + collect`) — a regression that drifts
4731    /// a future debug-mode logger, span-aware borrow walker, or
4732    /// fused-iterator invariant from one from-source walk to the
4733    /// other becomes structurally impossible.
4734    pub fn expand_source_and_collect_calls_to<R, F>(
4735        &mut self,
4736        src: &str,
4737        keyword: &str,
4738        mut project: F,
4739    ) -> Result<Vec<R>>
4740    where
4741        F: FnMut(&[Sexp]) -> Result<R>,
4742    {
4743        self.expand_source_and_collect_calls_to_any(
4744            src,
4745            |h| (h == keyword).then_some(()),
4746            move |(), args| project(args),
4747        )
4748    }
4749
4750    /// Read a source string into top-level forms via [`crate::reader::read`],
4751    /// then route the forms through
4752    /// [`expand_and_collect_calls_to_any`](Self::expand_and_collect_calls_to_any) —
4753    /// the from-source posture of the typed-decoded classifier walk, in
4754    /// ONE method on the `Expander` surface. The typed-decoded sibling of
4755    /// [`Self::expand_source_and_collect_calls_to`] — where that method
4756    /// filters by ONE constant keyword, this method filters AND TYPES by
4757    /// a caller-supplied classifier, yielding the typed witness alongside
4758    /// the per-form projection's args input. Sourced from `&str` rather
4759    /// than a `Vec<Sexp>`.
4760    ///
4761    /// Closes the substrate's program-level walk family on the `Expander`
4762    /// surface across BOTH input postures × BOTH projection forms — the
4763    /// (from-forms, from-source) × (keyword, classifier) 2×2 of
4764    /// compose-on-iter projections:
4765    ///
4766    /// |                | from-forms (`Vec<Sexp>`)                                      | from-source (`&str`)                                                |
4767    /// |----------------|---------------------------------------------------------------|---------------------------------------------------------------------|
4768    /// | keyword        | [`Self::expand_and_collect_calls_to`]                         | [`Self::expand_source_and_collect_calls_to`]                        |
4769    /// | classifier `F` | [`Self::expand_and_collect_calls_to_any`]                     | `expand_source_and_collect_calls_to_any` (this)                     |
4770    ///
4771    /// Each row's keyword corner now ROUTES through the classifier corner
4772    /// at every column: [`Self::expand_source_and_collect_calls_to`]'s
4773    /// body composes this typed-decoded primitive with a
4774    /// `|h| (h == keyword).then_some(())` decoder and the wrapper
4775    /// projection `|(), args| project(args)`, parallel to how
4776    /// [`Self::expand_and_collect_calls_to`] routes through
4777    /// [`Self::expand_and_collect_calls_to_any`] on the from-forms axis
4778    /// and [`crate::ast::iter_calls_to`] routes through
4779    /// [`crate::ast::iter_calls_to_any`] on the slice algebra. The 2×2
4780    /// reduces to ONE pipeline implementation
4781    /// (`read + expand_program + iter_calls_to_any + map + collect`)
4782    /// every consumer threads its `(decoder, project)` pair through.
4783    ///
4784    /// Future consumer shapes the typed-decoded from-source primitive
4785    /// admits:
4786    ///   * **Closed-set classifier** — `expand_source_and_collect_calls_to_any(
4787    ///     src, MacroDefHead::from_keyword, |head, args| { … })` walks a
4788    ///     source buffer dispatching every `(defmacro …)` / `(defpoint-template …)` /
4789    ///     `(defcheck …)` form to its typed `MacroDefHead` arm — exactly the
4790    ///     shape a future LSP "find every macro-definition form in this
4791    ///     buffer, decode each by typed kind" handler reaches for, sourced
4792    ///     from authoring text directly rather than a pre-parsed
4793    ///     `Vec<Sexp>`.
4794    ///   * **Live-registry classifier** — `expand_source_and_collect_calls_to_any(
4795    ///     src, |h| registry.get(h), |handler, args| handler.compile(args))`
4796    ///     walks a source buffer dispatching every form whose head is
4797    ///     registered to its typed handler — exactly the shape
4798    ///     `tatara-check`'s "macroexpand checks.lisp and dispatch every
4799    ///     `(defX …)` form through the registered domain dispatcher"
4800    ///     walker reaches for, sourced from disk-loaded source text
4801    ///     rather than the round-tripped `Vec<Sexp>` the typed
4802    ///     dispatchers consume.
4803    ///
4804    /// `?`-routing through `read` preserves the structural ordering of
4805    /// the rejection chain end-to-end: a reader error (lexer / parser /
4806    /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4807    /// `expand_program` runs; an `expand_program` error short-circuits
4808    /// BEFORE the classifier filter walks anything; a per-form `project`
4809    /// error short-circuits at the first failing matched form via
4810    /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
4811    /// rejection chain inherits the from-forms typed-decoded primitive's
4812    /// shape verbatim, now sourced from `&str` via ONE composition point.
4813    ///
4814    /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4815    /// the (from-forms, from-source) × (keyword, classifier) 2×2 closes
4816    /// at the from-source classifier corner this method establishes —
4817    /// the prior runs' from-forms classifier sibling
4818    /// (`expand_and_collect_calls_to_any`, run e47d043) AND from-source
4819    /// keyword sibling (`expand_source_and_collect_calls_to`, run
4820    /// b477d81) AND slice-side classifier sibling (`iter_calls_to_any`,
4821    /// run 38625e3) named three of the four corners; this lift names
4822    /// the fourth so the family is structurally complete at every
4823    /// algebra level. THEORY.md §V.1 — knowable platform; the
4824    /// read-then-classifier-walk composition becomes a NAMED primitive
4825    /// on the substrate's `Expander` surface rather than a future
4826    /// re-derived `read(src)? + expand_and_collect_calls_to_any(…)`
4827    /// two-step chain at every consumer that wants to walk source text
4828    /// by typed-decoded classifier. THEORY.md §II.1 invariant 2 — free
4829    /// middle; the from-source keyword filter
4830    /// (`expand_source_and_collect_calls_to`) now routes through the
4831    /// from-source classifier filter via the constant-classifier
4832    /// composition, so a regression that drifts the keyword filter's
4833    /// instrumentation from the classifier filter's instrumentation
4834    /// becomes structurally impossible. The pre-lift inline
4835    /// `read(src)? + expand_and_collect_calls_to(forms, keyword, project)`
4836    /// chain is now a TYPED CONSEQUENCE of the typed-decoded primitive
4837    /// composed with a constant-classifier decoder, not a parallel
4838    /// implementation the type system happens to not catch.
4839    ///
4840    /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4841    /// namespace + typed-syntax-class dispatch — the from-source-string
4842    /// entry to typed-program evaluation inside a namespace that carries
4843    /// the macro library, dispatching by typed syntax-class is the
4844    /// Racket idiom; the substrate's
4845    /// `expand_source_and_collect_calls_to_any` is the Rust-typed peer
4846    /// of that, with the typed-decoded classifier composed in. MLIR's
4847    /// `parseSourceFile<Op>(srcFile, ctx)` then `mod.walk<OpInterface>(
4848    /// callback)` — the parse-then-typed-walk over a source buffer
4849    /// dispatching by typed-interface witness is the MLIR idiom; this
4850    /// method is the substrate's typed `&[Sexp]`-algebra peer.
4851    pub fn expand_source_and_collect_calls_to_any<R, F, D, T>(
4852        &mut self,
4853        src: &str,
4854        decode: D,
4855        project: F,
4856    ) -> Result<Vec<R>>
4857    where
4858        D: FnMut(&str) -> Option<T>,
4859        F: FnMut(T, &[Sexp]) -> Result<R>,
4860    {
4861        let forms = crate::reader::read(src)?;
4862        self.expand_and_collect_calls_to_any(forms, decode, project)
4863    }
4864
4865    /// Compose the expander's program-level expansion with the substrate's
4866    /// typed-decoded classifier walk AND the named-form NAME-shape gate
4867    /// ([`crate::compile::split_name_slot`]) — the from-forms posture of
4868    /// the (named NAME-then-kwargs × typed-decoded classifier) cell of
4869    /// the typed-dispatcher matrix on the `Expander` surface, sibling of
4870    /// [`Self::expand_to_named`] (the constant-`T::KEYWORD` × named cell)
4871    /// and of [`Self::expand_and_collect_calls_to_any`] (the from-forms
4872    /// classifier × bare-kwargs cell).
4873    ///
4874    /// Routes through [`Self::expand_and_collect_calls_to_any`] with a
4875    /// wrapper projection that composes [`crate::compile::split_name_slot`]
4876    /// (the named-form arity + NAME-shape gate on the substrate's
4877    /// `&[Sexp]` algebra) with the caller-supplied per-form projection.
4878    /// The decoder yields `Option<(T, &'static str)>` — the typed witness
4879    /// PAIRED with the canonical static keyword the named-form structural
4880    /// rejection variants ([`crate::error::LispError::NamedFormMissingName`],
4881    /// [`crate::error::LispError::NamedFormNonSymbolName`]) carry as
4882    /// `&'static str` slots. Threading the `&'static` constraint through
4883    /// the decoder's return type pins the same compile-time discipline
4884    /// [`crate::compile::split_name_slot`]'s `keyword: &'static str`
4885    /// parameter pins at the gate boundary — a typo in the canonical
4886    /// keyword can never drift into the diagnostic at runtime, same posture
4887    /// as the constant-`T::KEYWORD` cell where `T::KEYWORD` is
4888    /// `&'static str` by trait construction.
4889    ///
4890    /// Closes the (named × typed-decoded classifier) corner of the
4891    /// typed-dispatcher matrix on the `Expander` surface that the prior
4892    /// run's [`crate::compile::split_name_slot`] lift (dd50801) named as
4893    /// the future change the slice-side gate's named lift enables. The
4894    /// 2×2 of compose-on-iter projections on the named-form axis becomes
4895    /// structurally complete on the `Expander` surface:
4896    ///
4897    /// |                | constant `T::KEYWORD`               | typed-decoded classifier                     |
4898    /// |----------------|-------------------------------------|----------------------------------------------|
4899    /// | from-forms     | [`Self::expand_to_named`]           | `expand_and_collect_named_calls_to_any` (this) |
4900    /// | from-source    | [`Self::expand_source_to_named`]    | [`Self::expand_source_and_collect_named_calls_to_any`] |
4901    ///
4902    /// The constant-`T::KEYWORD` column is the typed CONSEQUENCE of the
4903    /// classifier column: an `expand_to_named::<T>(forms)` call composes
4904    ///
4905    /// ```ignore
4906    /// expand_and_collect_named_calls_to_any(forms,
4907    ///     |h| (h == T::KEYWORD).then_some(((), T::KEYWORD)),
4908    ///     |(), name, spec_args| {
4909    ///         let spec = T::compile_from_args(spec_args)?;
4910    ///         Ok(NamedDefinition { name: name.to_string(), spec })
4911    ///     })
4912    /// ```
4913    ///
4914    /// Both columns route through ONE composition point on the
4915    /// `Expander` surface (the typed-decoded classifier walk inside
4916    /// [`Self::expand_and_collect_calls_to_any`]) and ONE gate body on
4917    /// the `&[Sexp]` algebra ([`crate::compile::split_name_slot`]). A
4918    /// regression that drifts ONE cell's NAME-slot rejection chain from
4919    /// the others is structurally impossible — every cell binds to the
4920    /// SAME `split_name_slot` body, the SAME `expand_program +
4921    /// iter_calls_to_any + map + collect` pipeline. The
4922    /// pre-lift `NamedFormMissingName` / `NamedFormNonSymbolName`
4923    /// rejection chain inside `compile::named_form_projection` (the
4924    /// `pub(crate)` typed-domain SPECIALIZATION that
4925    /// [`Self::expand_to_named`] routes through) fires identically
4926    /// through this classifier primitive — both consumers compose
4927    /// [`crate::compile::split_name_slot`] with their per-site typed
4928    /// continuation.
4929    ///
4930    /// Future consumer shapes the typed-decoded named-form primitive
4931    /// admits with no boilerplate:
4932    ///   * **Closed-set classifier** — a `tatara-check` runner that
4933    ///     dispatches every `(defmonitor NAME …)` / `(defnotify NAME …)`
4934    ///     / `(defalertpolicy NAME …)` form in `checks.lisp` through ONE
4935    ///     classifier (a closed-set enum `Domain::{Monitor, Notify,
4936    ///     AlertPolicy}` with `Domain::from_keyword`) decoding each
4937    ///     head to its typed kind PAIRED with its canonical static
4938    ///     keyword, then dispatches in the per-form projection — binds
4939    ///     to ONE method rather than two-step `expand_and_collect_calls_to_any
4940    ///     + split_name_slot` composition at each consumer site.
4941    ///   * **Live-registry classifier** — a future `tatara-check`-shaped
4942    ///     runtime dispatcher: `expand_and_collect_named_calls_to_any(forms,
4943    ///     |h| registry.get(h).map(|d| (d, d.keyword())), |dispatcher,
4944    ///     name, args| dispatcher.compile(name, args))` walks a program
4945    ///     dispatching every named form whose head is in a runtime
4946    ///     registry, decoded to a handler reference AND its canonical
4947    ///     keyword (sourced from the dispatcher itself, NOT from the
4948    ///     matched head text). The `&'static str` discipline on the
4949    ///     keyword slot is preserved through the dispatcher's own
4950    ///     `keyword() -> &'static str` accessor.
4951    ///   * **`compile_named_any` free function family** — the natural
4952    ///     fresh-expander free-function + preloaded-`RealizedCompiler`
4953    ///     consumer pair, sibling of the existing `compile_typed_any`
4954    ///     / `RealizedCompiler::compile_typed_any` pair. A future run
4955    ///     lands those as 3-line composes on top of this primitive.
4956    ///
4957    /// `D` is `FnMut(&str) -> Option<(T, &'static str)>` — the typed-
4958    /// decoded classifier yields a typed witness `T` AND its canonical
4959    /// static keyword for the gate. `F` is `FnMut(T, &str, &[Sexp]) ->
4960    /// Result<R>` — the per-form projection receives the typed witness
4961    /// `T` ALONGSIDE the NAME slot's BORROWED `&str` projection (sourced
4962    /// from [`Sexp::as_symbol_or_string`], which accepts BOTH symbol and
4963    /// string NAME-author surfaces) AND the spec args tail. Consumers
4964    /// that need owned ownership of the NAME (`NamedDefinition.name:
4965    /// String`, JSON-serialized payloads) `.to_string()` themselves —
4966    /// pushing the clone to the consumer boundary keeps the primitive
4967    /// allocation-free. The `Result<R>` projection short-circuits on
4968    /// the first error via `Iterator::collect::<Result<Vec<R>, _>>()`
4969    /// inside [`Self::expand_and_collect_calls_to_any`].
4970    ///
4971    /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4972    /// the (named × typed-decoded classifier) cell becomes a NAMED
4973    /// primitive on the `Expander` surface composed FROM TWO substrate
4974    /// primitives ([`Self::expand_and_collect_calls_to_any`] +
4975    /// [`crate::compile::split_name_slot`]) rather than re-derived
4976    /// inline at every named-classifier consumer's call site.
4977    /// THEORY.md §II.1 invariant 1 — typed entry; the typed-decoded
4978    /// classifier-filtered + NAME-shape-gated + caller-projected walk
4979    /// IS the typed-entry-batch gate for named-form dispatch at the
4980    /// `Expander` surface, with the `&'static str` keyword discipline
4981    /// preserved through the decoder's return type. THEORY.md §II.1
4982    /// invariant 2 — free middle; ALL four cells of the named-form
4983    /// dispatcher matrix on the `Expander` surface route through ONE
4984    /// composition point — a regression that drifts ONE cell's
4985    /// instrumentation from the others is structurally impossible.
4986    /// THEORY.md §V.1 — knowable platform; the named-form classifier
4987    /// walk becomes a discoverable primitive that LSP / REPL /
4988    /// `tatara-check` consumers bind to ONE method on the `Expander`
4989    /// instead of re-implementing the two-step `expand_and_collect_calls_to_any
4990    /// + split_name_slot` composition.
4991    ///
4992    /// Frontier inspiration: MLIR's `Region::walk<NamedOpInterface>(
4993    /// [&](auto op) { auto name = op.getName(); … })` — the typed-IR
4994    /// walk over a region yielding ops decoded to their typed interface
4995    /// witness with the named-symbol accessor pre-extracted is the
4996    /// MLIR idiom for named typed dispatch; this method is the
4997    /// substrate's unstructured-Rust peer with the typed-decoded
4998    /// classifier composed in and the NAME slot extracted via the
4999    /// substrate's `split_name_slot` gate. Racket's `syntax-parse`
5000    /// `~or* ((~datum defX) name:id arg ...) ((~datum defY) name:id
5001    /// arg ...) ((~datum defZ) name:id arg ...)` over an ellipsis-form
5002    /// — the typed-choice repeater with named-slot binder PAIRED with
5003    /// per-arm dispatch — is the Racket idiom; this primitive is the
5004    /// substrate's Rust-typed peer with the closed-set classifier
5005    /// playing the `~or*` typed-choice role and the NAME slot extracted
5006    /// by the substrate's slice-side gate.
5007    pub fn expand_and_collect_named_calls_to_any<R, F, D, T>(
5008        &mut self,
5009        forms: Vec<Sexp>,
5010        decode: D,
5011        mut project: F,
5012    ) -> Result<Vec<R>>
5013    where
5014        D: FnMut(&str) -> Option<(T, &'static str)>,
5015        F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5016    {
5017        // Routes through the slice-side typed-decoded named projection
5018        // [`crate::ast::iter_named_calls_to_any`] — the same
5019        // `expand_program + iter_*_to_any + map + collect` shape
5020        // [`Self::expand_and_collect_calls_to_any`] uses on the bare-kwargs
5021        // axis. Pre-lift this method routed through the bare expander
5022        // surface and welded [`crate::compile::split_name_slot`] inside
5023        // the projection closure; post-lift the named gate composition
5024        // lives at the slice level (`iter_named_calls_to_any`'s body)
5025        // and the Expander surface inherits it through delegation. Both
5026        // rows of the Expander surface (bare-kwargs, named) now share
5027        // ONE pipeline skeleton on the slice algebra — a regression that
5028        // drifts a future debug-mode logger, span-aware borrow walker,
5029        // or fused-iterator invariant from one row to the other becomes
5030        // structurally impossible at the slice boundary.
5031        let expanded = self.expand_program(forms)?;
5032        crate::ast::iter_named_calls_to_any(&expanded, decode)
5033            .map(|maybe_triple| {
5034                let (decoded, name, spec_args) = maybe_triple?;
5035                project(decoded, name, spec_args)
5036            })
5037            .collect()
5038    }
5039
5040    /// Read a source string into top-level forms via [`crate::reader::read`],
5041    /// then route the forms through
5042    /// [`Self::expand_and_collect_named_calls_to_any`] — the from-source
5043    /// posture of the (named × typed-decoded classifier) cell on the
5044    /// `Expander` surface, sibling of
5045    /// [`Self::expand_source_and_collect_calls_to_any`] (the from-source
5046    /// classifier × bare-kwargs cell).
5047    ///
5048    /// Closes the (from-forms, from-source) × (constant `T::KEYWORD`,
5049    /// typed-decoded classifier) 2×2 on the named-form axis of the
5050    /// `Expander` surface — together with [`Self::expand_to_named`],
5051    /// [`Self::expand_source_to_named`], and
5052    /// [`Self::expand_and_collect_named_calls_to_any`], every cell of the
5053    /// matrix binds to ONE composition point.
5054    ///
5055    /// `?`-routing through `read` preserves the structural ordering of
5056    /// the rejection chain end-to-end: a reader error (lexer / parser /
5057    /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5058    /// `expand_program` runs; an `expand_program` error short-circuits
5059    /// BEFORE the classifier filter walks anything; the named-form gate
5060    /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5061    /// [`crate::compile::split_name_slot`] fires for the first matched
5062    /// form that violates either condition; a per-form `project` error
5063    /// short-circuits at the first failing matched form via
5064    /// `Iterator::collect::<Result<Vec<R>, _>>()`.
5065    ///
5066    /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to_any`].
5067    /// THEORY.md §VI.1 (generation over composition; the from-source
5068    /// posture inherits the named-form gate composition through
5069    /// delegation rather than re-deriving the `read(src)? + walk + gate`
5070    /// chain at every from-source named-classifier consumer's call
5071    /// site), THEORY.md §II.1 invariant 2 (free middle; the from-source
5072    /// posture and the from-forms posture route through the SAME named-
5073    /// form composition).
5074    pub fn expand_source_and_collect_named_calls_to_any<R, F, D, T>(
5075        &mut self,
5076        src: &str,
5077        decode: D,
5078        project: F,
5079    ) -> Result<Vec<R>>
5080    where
5081        D: FnMut(&str) -> Option<(T, &'static str)>,
5082        F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5083    {
5084        let forms = crate::reader::read(src)?;
5085        self.expand_and_collect_named_calls_to_any(forms, decode, project)
5086    }
5087
5088    /// Macroexpand a pre-parsed program through `self` and project every
5089    /// `(keyword NAME :k v …)` form into `R` via a caller-supplied
5090    /// `(name, args) -> Result<R>` projection — the constant-keyword
5091    /// sibling of [`Self::expand_and_collect_named_calls_to_any`] on the
5092    /// named-form axis of the `Expander` surface, parallel to how
5093    /// [`Self::expand_and_collect_calls_to`] is the constant-keyword
5094    /// sibling of [`Self::expand_and_collect_calls_to_any`] on the
5095    /// bare-kwargs axis.
5096    ///
5097    /// Routes through the classifier sibling with a constant-classifier
5098    /// decoder that yields a discarded `()` typed witness paired with
5099    /// the `&'static str` keyword — the same `(T, &'static str)` decoder
5100    /// shape [`crate::compile::split_name_slot`] (composed inside the
5101    /// classifier primitive) pins at the slice-side gate boundary for
5102    /// the named-form structural rejection chain
5103    /// (`NamedFormMissingName.keyword`, `NamedFormNonSymbolName.keyword`).
5104    /// `&'static` is the same lifetime discipline the typed-decoded
5105    /// classifier signature enforces; the named gate's verbatim keyword
5106    /// threading inherits it through the constant-classifier composition.
5107    ///
5108    /// Projection signature `FnMut(&str, &[Sexp]) -> Result<R>` receives
5109    /// the BORROWED NAME slot and the BORROWED spec args tail. Consumers
5110    /// that need owned ownership (`NamedDefinition.name: String`) call
5111    /// `.to_string()` themselves — pushing the clone to the consumer
5112    /// boundary keeps the primitive allocation-free, matching the
5113    /// classifier sibling's projection signature on the NAME slot.
5114    ///
5115    /// Closes the (named NAME-then-kwargs × constant-keyword) cell of
5116    /// the `Expander` typed-walk family at the runtime-keyword × untyped
5117    /// `R` corner — pre-lift the cell was reachable ONLY through
5118    /// [`Self::expand_to_named`] with the `T: TataraDomain` type
5119    /// parameter baking `T::KEYWORD` AND the `T::compile_from_args`-
5120    /// based `crate::compile::named_form_projection<T>` projection
5121    /// into the dispatch through `expand_and_collect_calls_to(forms,
5122    /// T::KEYWORD, named_form_projection::<T>)`. Post-lift the cell
5123    /// surfaces as ONE method that takes the keyword and the
5124    /// `(name, args) -> R` projection as caller-supplied parameters,
5125    /// and [`Self::expand_to_named`] routes through it as the typed
5126    /// `T::KEYWORD`-constant specialization. The split lets a consumer
5127    /// that wants "walk every form whose head is a runtime keyword `kw`
5128    /// and project `(NAME, spec_args) -> R` for an arbitrary `R`" (a
5129    /// future REPL `:walk-named <kw>` command, an LSP "find-named-
5130    /// declarations-of-keyword `kw`" handler, a `tatara-check` runner
5131    /// dispatching on a single typed keyword whose projection isn't
5132    /// `T::compile_from_args`) bind to ONE method on the `Expander`
5133    /// surface rather than re-deriving the
5134    /// `expand_and_collect_named_calls_to_any(forms, |h| (h ==
5135    /// kw).then_some(((), kw)), |(), name, args| project(name, args))`
5136    /// two-line composition inline.
5137    ///
5138    /// Sibling of [`Self::expand_source_and_collect_named_calls_to`]
5139    /// — that method stacks a [`crate::reader::read`] step on top of
5140    /// this one (the from-source posture); this method takes a
5141    /// pre-parsed `Vec<Sexp>` (the from-forms posture). Together with
5142    /// [`Self::expand_and_collect_named_calls_to_any`] /
5143    /// [`Self::expand_source_and_collect_named_calls_to_any`], the four
5144    /// cells of the (named × {constant-keyword, classifier} ×
5145    /// {from-forms, from-source}) sub-matrix close on the `Expander`
5146    /// surface — each cell binds to ONE composition point, with the
5147    /// constant-keyword cells routing through the classifier cells via
5148    /// constant-classifier decoding.
5149    ///
5150    /// Theory anchor: THEORY.md §VI.1 — generation over composition;
5151    /// the `split_name_slot` composition lived at TWO sites pre-lift
5152    /// (`crate::compile::named_form_projection` body, which
5153    /// [`Self::expand_to_named`] routes through, AND
5154    /// [`Self::expand_and_collect_named_calls_to_any`] body, which the
5155    /// typed-decoded classifier consumers route through) — past the
5156    /// ≥2 PRIME-DIRECTIVE trigger once the named-form gate is named.
5157    /// THEORY.md §V.1 — knowable platform; the named constant-keyword
5158    /// walk becomes a NAMED primitive on the `Expander` surface
5159    /// composable by any future consumer (REPL, LSP, `tatara-check`)
5160    /// instead of a re-derived two-line `expand_and_collect_named_
5161    /// calls_to_any(forms, constant-decoder, wrapper-projection)`
5162    /// composition. THEORY.md §II.1 invariant 2 — free middle; both
5163    /// the typed (`expand_to_named<T>`) and untyped (this method)
5164    /// constant-keyword named cells route through the SAME named
5165    /// classifier primitive — a regression that drifts ONE cell's
5166    /// NAME-slot rejection chain (`NamedFormMissingName`,
5167    /// `NamedFormNonSymbolName`) from the other becomes structurally
5168    /// impossible.
5169    ///
5170    /// Frontier inspiration: Racket's `syntax-parse` `((~datum kw)
5171    /// name:id arg ...)` arm — the named NAME-slot binder under a
5172    /// constant `~datum` keyword paired with a per-match handler is
5173    /// the Racket idiom; this method is the substrate's Rust-typed
5174    /// peer with the constant-keyword filter composed at the
5175    /// classifier corner. MLIR's `Region::walk<NamedOpKind>([&](auto
5176    /// op) { auto name = op.getName(); … })` against a single
5177    /// operation kind — the typed-IR walk yielding ops decoded to
5178    /// their typed kind with the named-symbol accessor pre-extracted,
5179    /// specialized on a constant kind; this method is the substrate's
5180    /// unstructured-`R` peer with the constant-classifier composition
5181    /// pinned at the call boundary.
5182    pub fn expand_and_collect_named_calls_to<R, F>(
5183        &mut self,
5184        forms: Vec<Sexp>,
5185        keyword: &'static str,
5186        mut project: F,
5187    ) -> Result<Vec<R>>
5188    where
5189        F: FnMut(&str, &[Sexp]) -> Result<R>,
5190    {
5191        // Routes through the typed-decoded named-classifier sibling
5192        // [`Self::expand_and_collect_named_calls_to_any`] with a
5193        // constant-classifier decoder — the same constant-classifier
5194        // composition [`Self::expand_and_collect_calls_to`] uses to
5195        // route through [`Self::expand_and_collect_calls_to_any`] on
5196        // the bare-kwargs axis, and that
5197        // [`crate::ast::iter_calls_to`] uses to route through
5198        // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
5199        // discarded `()` typed witness (`then_some(((), keyword))`)
5200        // is consumed by the wrapper projection
5201        // `|(), name, args| project(name, args)` so the keyword
5202        // consumer's per-form mapper sees only `(name, spec_args)`,
5203        // matching the bare projection signature.
5204        self.expand_and_collect_named_calls_to_any(
5205            forms,
5206            |h| (h == keyword).then_some(((), keyword)),
5207            move |(), name, args| project(name, args),
5208        )
5209    }
5210
5211    /// Read a source string into top-level forms via [`crate::reader::read`],
5212    /// then route the forms through
5213    /// [`Self::expand_and_collect_named_calls_to`] — the from-source
5214    /// posture of the (named × constant-keyword) cell on the
5215    /// `Expander` surface.
5216    ///
5217    /// Composes [`crate::reader::read`] with
5218    /// [`Self::expand_and_collect_named_calls_to`] — the
5219    /// `(keyword, project)` binding is bound in ONE place (the
5220    /// from-forms row) and this from-source sibling inherits the
5221    /// binding through delegation, mirroring how
5222    /// [`Self::expand_source_and_collect_calls_to`] composes
5223    /// [`crate::reader::read`] with [`Self::expand_and_collect_calls_to`]
5224    /// on the bare-kwargs axis.
5225    ///
5226    /// `?`-routing through `read` preserves the structural ordering of
5227    /// the rejection chain end-to-end: a reader error (lexer / parser /
5228    /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5229    /// `expand_program` runs; an `expand_program` error short-circuits
5230    /// BEFORE the keyword filter walks anything; the named-form gate
5231    /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5232    /// [`crate::compile::split_name_slot`] fires for the first matched
5233    /// form that violates either condition; a per-form `project` error
5234    /// short-circuits at the first failing matched form via
5235    /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
5236    /// rejection chain inherits the constant-keyword named primitive's
5237    /// shape verbatim, now sourced from `&str` via ONE composition
5238    /// point.
5239    ///
5240    /// Sibling of [`Self::expand_and_collect_named_calls_to`] (the
5241    /// from-forms posture) and of [`Self::expand_source_and_collect_calls_to`]
5242    /// (the from-source × bare-kwargs cell on the constant-keyword
5243    /// row). The four cells of (named × {constant, classifier} ×
5244    /// {from-forms, from-source}) close on the `Expander` surface
5245    /// post-lift, each routing through ONE composition point.
5246    ///
5247    /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to`].
5248    /// THEORY.md §VI.1 (generation over composition; the from-source
5249    /// posture inherits the constant-keyword named composition through
5250    /// delegation rather than re-deriving the
5251    /// `read(src)? + expand_and_collect_named_calls_to` chain at every
5252    /// from-source named-keyword consumer's call site),
5253    /// THEORY.md §II.1 invariant 2 (free middle; the from-source
5254    /// posture and the from-forms posture route through the SAME
5255    /// composition).
5256    pub fn expand_source_and_collect_named_calls_to<R, F>(
5257        &mut self,
5258        src: &str,
5259        keyword: &'static str,
5260        mut project: F,
5261    ) -> Result<Vec<R>>
5262    where
5263        F: FnMut(&str, &[Sexp]) -> Result<R>,
5264    {
5265        // From-source = from-forms × constant-classifier specialization
5266        // of the named classifier primitive's from-source sibling. The
5267        // discarded `()` typed witness and the `move |(), name, args|
5268        // project(name, args)` wrapper mirror the from-forms posture's
5269        // composition — both postures route through the SAME named
5270        // classifier primitive on the `Expander` surface, the
5271        // from-source row stacking one `read` step on top.
5272        self.expand_source_and_collect_named_calls_to_any(
5273            src,
5274            |h| (h == keyword).then_some(((), keyword)),
5275            move |(), name, args| project(name, args),
5276        )
5277    }
5278
5279    /// Expand a single form. Top-level macro calls are rewritten; recurses
5280    /// into list children.
5281    ///
5282    /// Routes the macro-call dispatch surface through the substrate's
5283    /// typed-decoded call decomposition: `as_call_to_any(|h|
5284    /// self.macros.get(h))` answers "is this form an invocation of any
5285    /// registered macro, decoded to `(&MacroDef, args)`?" in ONE
5286    /// structural query on the `Sexp` algebra. Pre-lift the same site
5287    /// opened the three-step chain `as_list() + as_call() + self.macros.
5288    /// get(head)` inline — `as_list()` for the children-walk fallthrough,
5289    /// `as_call()` for the (head, args) decomposition (which itself
5290    /// re-derives `as_list()` internally), and `self.macros.get(head)`
5291    /// for the registry lookup; post-lift the call-recognition runs as
5292    /// ONE `as_call_to_any` projection with the HashMap lookup as its
5293    /// classifier, and the `as_list()` fallthrough fires only on the
5294    /// not-a-macro-call path. Sibling consumer to `macro_def_from` — the
5295    /// typed-macro-definition dispatcher that routes through
5296    /// `as_call_to_any(MacroDefHead::from_keyword)` with the closed-set
5297    /// enum classifier. With both in place, BOTH dispatch sites in the
5298    /// macro expander (definition-recognition + call-recognition)
5299    /// project through the SAME family primitive on the `Sexp` algebra,
5300    /// each binding the classifier that fits its candidate set — closed
5301    /// enum for the static head-set, HashMap lookup for the live
5302    /// registry. A regression that drifts ONE site from the other (a
5303    /// future emitter that re-derives `as_list()` + `head.as_symbol()` +
5304    /// `self.macros.get(_)` inline rather than routing through the
5305    /// family) is no longer a silent two-site divergence.
5306    pub fn expand(&self, form: &Sexp) -> Result<Sexp> {
5307        self.expand_with_depth(form, 0)
5308    }
5309
5310    /// The depth-carrying peer of [`Self::expand`]. Each
5311    /// post-apply re-expansion increments `depth`; each tree-child
5312    /// descent passes `depth` through unchanged. The runaway
5313    /// `(defmacro loop (x) `(loop ,x))` accretes depth on the
5314    /// re-expansion path — one unit per round — and hits the ceiling
5315    /// in `max_expansion_depth` rounds; lawful nested macros
5316    /// (`(when1 (when1 x))`) accrete depth in the low single digits
5317    /// because tree traversal doesn't count.
5318    ///
5319    /// The `depth >= limit` check sits INSIDE the macro-call arm
5320    /// (`Some((def, args)) = form.as_call_to_any(…)`) rather than at
5321    /// the top of the function so `def.name.clone()` is available at
5322    /// the rejection site — the operator gets `macro_name` populated
5323    /// from the actual offending call rather than a `<unknown>`
5324    /// fallback. Tree-child descent is not gated on the ceiling
5325    /// because the tree-child arm does not accrete depth; a lawfully
5326    /// deep tree is bounded by the reader's own stack, not by this
5327    /// ceiling.
5328    ///
5329    /// The peer OUTPUT-SIZE gate (`expanded.node_count() >
5330    /// self.limits.max_expansion_size`) sits INSIDE the same macro-call arm,
5331    /// between the `apply` and the re-expansion recursion, so a
5332    /// single macro's `apply` output whose structural size crosses
5333    /// the ceiling is rejected AT the offending `apply` boundary
5334    /// (with `macro_name` populated from `def.name`) before the
5335    /// runaway tree feeds back into further expansion. The gate is
5336    /// keyed on `>` rather than `>=` so `max_expansion_size` names
5337    /// the LARGEST admissible output — the ceiling admits equality,
5338    /// only strict overrun rejects. Together with the depth ceiling
5339    /// (recursion length) and the cache ceiling (memoization width),
5340    /// this closes the expander's RESOURCE surface at three typed
5341    /// dimensions.
5342    fn expand_with_depth(&self, form: &Sexp, depth: usize) -> Result<Sexp> {
5343        if let Some((def, args)) = form.as_call_to_any(|h| self.macros.get(h)) {
5344            if depth >= self.limits.max_expansion_depth {
5345                return Err(LispError::ExpansionDepthExceeded {
5346                    macro_name: def.name.clone(),
5347                    limit: self.limits.max_expansion_depth,
5348                });
5349            }
5350            let expanded = self.apply(def, args)?;
5351            // Reject a single `apply` output whose structural node
5352            // count crosses the OUTPUT-SIZE ceiling — the RESOURCE
5353            // axis peer of the depth (recursion length) and
5354            // cache-entries (memoization width) ceilings. Catches
5355            // the canonical "expansion bomb" where a well-defined
5356            // macro produces a 2^N-node blob from a small input
5357            // while `depth` stays in the low single digits and the
5358            // cache entry count sits under any reasonable ceiling.
5359            let expanded_size = expanded.node_count();
5360            if expanded_size > self.limits.max_expansion_size {
5361                return Err(LispError::ExpansionSizeExceeded {
5362                    macro_name: def.name.clone(),
5363                    size: expanded_size,
5364                    limit: self.limits.max_expansion_size,
5365                });
5366            }
5367            // Recurse — the expansion itself may contain more macro calls.
5368            return self.expand_with_depth(&expanded, depth + 1);
5369        }
5370        // Not a macro call — expand children if this is a list; otherwise
5371        // (atom / Nil / quote-family wrapper) return the form verbatim.
5372        let Some(list) = form.as_list() else {
5373            return Ok(form.clone());
5374        };
5375        let mut out = Vec::with_capacity(list.len());
5376        for item in list {
5377            out.push(self.expand_with_depth(item, depth)?);
5378        }
5379        Ok(Sexp::List(out))
5380    }
5381
5382    /// Apply a macro to its argument list.
5383    ///
5384    /// Three-layer fast path:
5385    ///   1. If `cache_enabled`, hash `(name, args)` and consult the memo table.
5386    ///   2. If a compiled template exists, run the bytecode interpreter.
5387    ///   3. Otherwise fall back to the name-keyed substitute walker.
5388    fn apply(&self, def: &MacroDef, args: &[Sexp]) -> Result<Sexp> {
5389        // Layer 1: expansion cache.
5390        let cache_key = if self.cache_enabled {
5391            args_cache_key(&def.name, args)
5392        } else {
5393            None
5394        };
5395        if let Some(ref key) = cache_key {
5396            if let Some(cached) = self.cache.lock().unwrap().get(key) {
5397                return Ok(cached.clone());
5398            }
5399        }
5400
5401        // Layer 2: compiled bytecode.
5402        let result = if let Some(tmpl) = self.templates.get(&def.name) {
5403            apply_compiled(&def.name, &def.params, tmpl, args)?
5404        } else {
5405            // Layer 3: substitute fallback. Walk the body's substitution
5406            // projection — the inner of the outer quasi-quote when present,
5407            // the body verbatim otherwise — through the shared
5408            // `MacroDef::template_body` primitive both strategies route on.
5409            let bindings = bind_args(&def.name, &def.params, args)?;
5410            substitute(def.template_body(), &bindings)?
5411        };
5412
5413        // Populate cache on miss — capped by `max_cache_entries` so the
5414        // memoization table cannot drift unboundedly across a long-
5415        // running host session. When the cap is reached the freshly-
5416        // computed result is returned verbatim; correctness is
5417        // unaffected because caching is a pure PERFORMANCE optimization
5418        // (the miss path always recomputes the same value the cache
5419        // would have returned).
5420        if let Some(key) = cache_key {
5421            let mut cache = self.cache.lock().unwrap();
5422            if cache.len() < self.limits.max_cache_entries {
5423                cache.insert(key, result.clone());
5424            }
5425        }
5426        Ok(result)
5427    }
5428
5429    pub fn has(&self, name: &str) -> bool {
5430        self.macros.contains_key(name)
5431    }
5432
5433    pub fn len(&self) -> usize {
5434        self.macros.len()
5435    }
5436
5437    pub fn is_empty(&self) -> bool {
5438        self.macros.is_empty()
5439    }
5440}
5441
5442// ── Compiled template bytecode ───────────────────────────────────────
5443
5444/// One op in the template bytecode. Emitted during compilation; consumed at
5445/// expansion to materialize a form without HashMap lookups or recursion.
5446#[derive(Clone, Debug, PartialEq)]
5447pub enum TemplateOp {
5448    /// Push a literal Sexp. Used for atoms and entirely-literal subtrees.
5449    Literal(Sexp),
5450    /// Push the bound arg at the given param index.
5451    Subst(usize),
5452    /// If the bound arg is a list, append its items to the current list; else
5453    /// push it as a single item.
5454    Splice(usize),
5455    /// Begin a new List — pushes a fresh builder onto the expansion stack.
5456    BeginList,
5457    /// End the current List — pops the builder, wraps as `Sexp::List`.
5458    EndList,
5459}
5460
5461/// Pre-compiled template. Built once per macro, interpreted many times.
5462#[derive(Clone, Debug, Default, PartialEq)]
5463pub struct CompiledTemplate {
5464    pub ops: Vec<TemplateOp>,
5465}
5466
5467/// Walk a macro definition's template body and emit linear bytecode.
5468/// Purely-literal subtrees compile to a single `Literal(clone)` op.
5469///
5470/// Compilation can fail if the template references a name that isn't a
5471/// declared parameter — same semantic as the substitute path.
5472///
5473/// Top-level `,@X` bodies (the splice is the entire body, not nested inside
5474/// a `(... ,@xs ...)` list) are rejected here at compile time so the
5475/// bytecode path agrees with the substitute path's emission-time rejection
5476/// (`splice_outside_list`). Without this gate the bytecode interpreter's
5477/// outermost stack frame silently absorbed the splice's items, and the same
5478/// macro emitted different output across paths — `compiled_template_matches
5479/// _substitute_path` only covered well-positioned splice bodies. After this
5480/// gate every `,@-outside-list` body is rejected at registration time on
5481/// both paths with ONE structural variant (`LispError::SpliceOutsideList`).
5482///
5483/// The gate routes through [`Sexp::as_unquote`] — the typed-marker
5484/// projection that pairs `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at
5485/// ONE structural query — matching `Some((UnquoteForm::Splice, inner))`
5486/// rather than the per-arm `Sexp::UnquoteSplice(inner)` literal that
5487/// recurred at three sites pre-lift (`compile_node` Subst/Splice arms,
5488/// `substitute` top-level + list-inner). Sibling shape to `substitute`'s
5489/// list-inner Splice arm — both use the same `Some((UnquoteForm::Splice,
5490/// inner))` shape; `substitute`'s top-level arm uses the wider
5491/// `Some((kind, inner))` and dispatches inside on `kind`. After this lift
5492/// every production-site recognizer of "is this an `,@X` form" routes
5493/// through ONE typed-marker projection rather than re-deriving the
5494/// (Sexp variant, UnquoteForm variant) pair inline.
5495pub fn compile_template(def: &MacroDef) -> Result<CompiledTemplate> {
5496    // Walk the body's substitution projection — the inner of the outer
5497    // quasi-quote when present, the body verbatim otherwise — through the
5498    // shared `MacroDef::template_body` primitive the substitute path also
5499    // routes on. Same projection, both strategies, by construction.
5500    let body = def.template_body();
5501    if let Some((UnquoteForm::Splice, inner)) = body.as_unquote() {
5502        return Err(splice_outside_list(inner));
5503    }
5504    let names = def.params.names();
5505    let mut ops = Vec::new();
5506    compile_node(body, &names, &mut ops)?;
5507    Ok(CompiledTemplate { ops })
5508}
5509
5510fn compile_node(node: &Sexp, params: &[&str], ops: &mut Vec<TemplateOp>) -> Result<()> {
5511    // Fast-path literal: if the subtree has no Unquote/UnquoteSplice, emit a
5512    // single Literal op. This is the big win for macros where most of the
5513    // template is fixed structure.
5514    if !contains_unquote(node) {
5515        ops.push(TemplateOp::Literal(node.clone()));
5516        return Ok(());
5517    }
5518    // Routes the `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice(inner)` arms
5519    // through [`Sexp::as_unquote`] — the typed-marker projection that
5520    // pairs `Sexp::Unquote ↔ UnquoteForm::Unquote` and
5521    // `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at ONE site. The per-form
5522    // `TemplateOp` emission (`Subst` vs `Splice`) keys on the same typed
5523    // `form` value the gate-1+gate-2 composition `resolve_unquote_in_params`
5524    // threads through. Pre-lift the (Sexp variant, UnquoteForm variant)
5525    // pairing was bound per-arm — a future emitter that matched
5526    // `Sexp::Unquote(_)` but threaded `UnquoteForm::Splice` into
5527    // `resolve_unquote_in_params` (or vice versa) would type-check but
5528    // render a misleading diagnostic at the gate-1 / gate-2 rejection.
5529    // Post-lift the pair is bound at ONE projection function and the
5530    // `match form` mechanically lowers it to the bytecode op.
5531    if let Some((form, inner)) = node.as_unquote() {
5532        let idx = resolve_unquote_in_params(inner, params, form)?;
5533        ops.push(match form {
5534            UnquoteForm::Unquote => TemplateOp::Subst(idx),
5535            UnquoteForm::Splice => TemplateOp::Splice(idx),
5536        });
5537        return Ok(());
5538    }
5539    match node {
5540        Sexp::List(items) => {
5541            ops.push(TemplateOp::BeginList);
5542            for item in items {
5543                compile_node(item, params, ops)?;
5544            }
5545            ops.push(TemplateOp::EndList);
5546        }
5547        _ => ops.push(TemplateOp::Literal(node.clone())),
5548    }
5549    Ok(())
5550}
5551
5552fn contains_unquote(node: &Sexp) -> bool {
5553    // Route every quote-family wrapper recognition — both the
5554    // unquote-only subset (Unquote/UnquoteSplice → short-circuit `true`)
5555    // AND the quote-only subset (Quote/Quasiquote → recurse into inner) —
5556    // through [`Sexp::as_quote_form`]'s typed-marker projection. Pre-lift
5557    // the recognizer split into two arms: `as_unquote().is_some()` (the
5558    // substitution subset gate) + the inline
5559    // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm (the remaining
5560    // quote-only subset). Post-lift both arms route through ONE
5561    // projection and the (Sexp variant, QuoteForm variant) pairing binds
5562    // at the closed-set algebra. The `form.as_unquote_form().is_some()`
5563    // gate is the SAME 2-of-4 subset projection [`Sexp::as_unquote`]
5564    // derives from, so recognizing "is this an unquote-family wrapper"
5565    // and "is this a quote-only wrapper" share ONE typed dispatch site;
5566    // rustc enforces that a future `Sexp` wrapper extension carry
5567    // through both `QuoteForm::ALL` AND `QuoteForm::as_unquote_form`'s
5568    // arm. Sibling posture to `Hash for Sexp`'s four-arm
5569    // `hash_discriminator` collapse, `Display for Sexp`'s `prefix`
5570    // collapse, `interop`'s `iac_forge_tag` collapse, and `domain`'s
5571    // `sexp_shape` collapse — every production-site quote-family
5572    // recognizer now routes through ONE projection on the algebra.
5573    if let Some((form, inner)) = node.as_quote_form() {
5574        return form.as_unquote_form().is_some() || contains_unquote(inner);
5575    }
5576    match node {
5577        Sexp::List(items) => items.iter().any(contains_unquote),
5578        _ => false,
5579    }
5580}
5581
5582/// Splice a resolved template value into an in-progress list builder —
5583/// the SHARED coercion both expansion strategies apply once `,@name`'s
5584/// gate-1 (must-be-a-symbol) and gate-2 (must-be-bound-in-scope) have
5585/// resolved the bound value. ONE named primitive the bytecode path
5586/// (`apply_compiled`'s `TemplateOp::Splice` arm) AND the substitute path
5587/// (`substitute`'s list-inner `Sexp::UnquoteSplice` arm) share. Before
5588/// this lift the three-arm coercion —
5589///
5590/// ```ignore
5591/// match value {
5592///     Sexp::List(items) => builder.extend(items.iter().cloned()),
5593///     Sexp::Nil         => {}
5594///     other             => builder.push(other.clone()),
5595/// }
5596/// ```
5597///
5598/// — was inlined at BOTH sites; the splice RESULT semantics (the last
5599/// inline-duplicated piece of the splice path after the prior runs lifted
5600/// gate-1, gate-2, and their composition) lived in two places that MUST
5601/// agree. After this lift the coercion lives in ONE function, so a
5602/// regression that drifts one strategy's splice posture from the other —
5603/// e.g. changing the `Sexp::Nil` arm to push an empty list at the
5604/// bytecode path but not the substitute path, or coercing a non-list
5605/// scalar differently across the two strategies — becomes structurally
5606/// impossible: there is exactly one implementation both strategies call.
5607///
5608/// The coercion's three arms ARE the no-evaluator template language's
5609/// splice contract: a bound LIST flattens its elements into the builder
5610/// (the canonical splice), a bound NIL contributes nothing (splicing the
5611/// empty list), and any other bound value splices as a single element (a
5612/// scalar `,@x` degrades to `,x` rather than erroring — invariant 2's
5613/// "free middle" lets the macro author rely on this without a
5614/// mid-rewrite type check; the typed-exit gate re-validates the
5615/// assembled form). Naming the contract once gives a future gate-3
5616/// (typed-shape enforcement on bound splice targets) ONE site to wrap
5617/// rather than two inline arms to keep in lockstep.
5618///
5619/// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the two
5620/// expansion strategies MUST produce identical output for the same
5621/// (macro, args) pair, and naming the splice coercion once makes that
5622/// per-strategy agreement structural rather than a two-site discipline
5623/// the `expansion_layers_agree_on_output_and_cache_wins` benchmark only
5624/// observes after the fact. THEORY.md §V.1 — knowable platform; the
5625/// splice RESULT semantics becomes a NAMED primitive authoring tools and
5626/// future runs bind to. THEORY.md §VI.1 — generation over composition;
5627/// the two-site coercion is lifted to ONE function, closing the last
5628/// inline-duplicated piece of the splice path the prior runs' gate lifts
5629/// (02173dc gate-1, 68da647 gate-2, b456f1f composition) left behind.
5630fn splice_value_into(builder: &mut Vec<Sexp>, value: &Sexp) {
5631    match value {
5632        Sexp::List(items) => builder.extend(items.iter().cloned()),
5633        Sexp::Nil => {}
5634        other => builder.push(other.clone()),
5635    }
5636}
5637
5638/// Promote the previously `LispError::Compile`-shaped helper into the
5639/// structural `LispError::TemplateInvariant { macro_name, kind }` variant.
5640/// The four reachable bytecode-runtime invariant violations in
5641/// `apply_compiled` — Subst-bad-index, Splice-bad-index, EndList-empty-
5642/// stack, final-no-value — funnel through ONE emission shape keyed on
5643/// the closed-set `TemplateInvariantKind` enum. The index payload of
5644/// the Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
5645/// / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
5646/// kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
5647/// is structurally unrepresentable — the type system encodes "this gate
5648/// has an index, that gate does not."
5649///
5650/// Display matches the legacy `Compile`-shaped diagnostic byte-for-byte
5651/// across all four kinds (`"compile error in {macro_name}: <invariant>"`)
5652/// via the closed-set `TemplateInvariantKind::message()` projection, so
5653/// authoring-tool substring greps (`tatara-check`, REPL) see no drift
5654/// across the lift.
5655///
5656/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
5657/// of bytecode-invariant failure modes becomes a TYPE rather than a
5658/// free-form `message: String` slot. THEORY.md §VI.1 — generation over
5659/// composition; the typed enum lands the structural-completeness floor
5660/// for the bytecode-runtime surface, parallel to how `CompilerSpecIoStage`
5661/// lands the structural-completeness floor for the disk-persistence
5662/// surface (`compiler_spec.rs`, the immediately prior claude-routine
5663/// lift on a sibling file). THEORY.md §II.1 invariant 5 (composition
5664/// preserves proofs): a well-formed bytecode invariant is the proof
5665/// that drives the interpreter; the structural variant makes the
5666/// proof's REJECTION shape first-class — authoring tools (REPL, LSP,
5667/// `tatara-check`) pattern-match on the `kind` slot and bind to the
5668/// gate identity directly instead of substring-parsing the rendered
5669/// diagnostic.
5670fn template_invariant_violation(macro_name: &str, kind: TemplateInvariantKind) -> LispError {
5671    LispError::TemplateInvariant {
5672        macro_name: macro_name.into(),
5673        kind,
5674    }
5675}
5676
5677/// Look up a bound-arg by its template-bytecode index, or raise the
5678/// structural `LispError::TemplateInvariant` rejection with the
5679/// caller-supplied `kind` constructor applied to the bad index. ONE
5680/// named primitive both bytecode-runtime arms that read a bound arg
5681/// by index — [`TemplateOp::Subst`] (single-value push) AND
5682/// [`TemplateOp::Splice`] (list-splicing) — route through.
5683///
5684/// Before this lift the same `args_by_index.get(*idx).ok_or_else(||
5685/// template_invariant_violation(macro_name, KIND(*idx)))?` projection
5686/// appeared at BOTH arms of [`apply_compiled`], differing only in the
5687/// kind constructor: [`TemplateInvariantKind::SubstBadIndex`] at the
5688/// `Subst` arm, [`TemplateInvariantKind::SpliceBadIndex`] at the
5689/// `Splice` arm. The arms also diverged on what they did with the
5690/// returned `&Sexp` — `Subst` cloned and pushed, `Splice` consumed
5691/// the borrow through [`splice_value_into`] — but the lookup-and-
5692/// reject prelude was byte-identical modulo the kind, well past the
5693/// ≥2 PRIME-DIRECTIVE trigger.
5694///
5695/// After this lift the lookup-and-reject shape lives in ONE function;
5696/// the two arms thread the per-call-site kind constructor through the
5697/// helper and apply their respective post-lookup verbs at the call
5698/// site. The `kind: FnOnce(usize) -> TemplateInvariantKind` parameter
5699/// encodes the closed-set bytecode-runtime "this gate has an index"
5700/// surface at the type level — only the two
5701/// [`TemplateInvariantKind`] variants whose payload IS the bad index
5702/// (`SubstBadIndex(usize)` and `SpliceBadIndex(usize)`) construct
5703/// directly through `FnOnce(usize) -> TemplateInvariantKind`; the
5704/// stack-gate variants ([`TemplateInvariantKind::EndListEmptyStack`]
5705/// and [`TemplateInvariantKind::FinalNoValue`]) carry no payload and
5706/// would not type-check at this boundary, so the invalid combination
5707/// "stack-gate kind reached from an op-index lookup" is structurally
5708/// unrepresentable at the helper's call boundary the same way
5709/// [`TemplateInvariantKind`]'s closed-set shape makes it
5710/// unrepresentable in the variant itself.
5711///
5712/// Sibling of [`template_invariant_violation`]: that helper builds the
5713/// typed [`LispError::TemplateInvariant`] variant from a fully-formed
5714/// `kind`; this helper composes the index-keyed lookup with the
5715/// variant-builder, so the kind constructor doesn't have to be evaluated
5716/// eagerly at the call site (lazy via `FnOnce`, only fires on the bad-
5717/// index path). A future fifth bytecode op that reads a bound arg by
5718/// index (a hypothetical [`TemplateOp::Conditional`] that branches on a
5719/// bound boolean, a [`TemplateOp::Project`] that extracts a sub-field
5720/// of a bound `Sexp::List`) extends the family in ONE call to
5721/// `resolve_bound_arg` with the new kind constructor (`KIND(usize) ->
5722/// TemplateInvariantKind`) — the bytecode-runtime's bound-arg-by-index
5723/// projection becomes ONE structural primitive consumers compose with.
5724///
5725/// The returned `&'a Sexp` borrows from `args_by_index` verbatim —
5726/// `Subst`'s arm consumes it through `.clone()` (the consumer pushes
5727/// an owned value into the builder); `Splice`'s arm consumes it
5728/// through [`splice_value_into`] (the consumer borrows for the
5729/// per-arm coercion). The borrow's lifetime `'a` is the unified
5730/// lifetime of `args_by_index`, matching the call site's borrow
5731/// posture.
5732///
5733/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5734/// inline copies of the index-lookup-and-reject prelude across the
5735/// `apply_compiled` body's `Subst` and `Splice` arms is past the ≥2
5736/// PRIME-DIRECTIVE trigger once the structural shape is named.
5737/// THEORY.md §V.1 — knowable platform / "make invalid states
5738/// unrepresentable"; the bytecode-runtime bound-arg-by-index lookup
5739/// becomes a NAMED primitive on the substrate's `&[Sexp]` algebra
5740/// rather than a re-derived `get + ok_or_else + template_invariant_
5741/// violation` chain at every op-arm that reads by index. A future
5742/// authoring tool (REPL, LSP, `tatara-check`) that wants to surface
5743/// "this bytecode op's bound-arg lookup misfired at idx N" binds to
5744/// ONE function. THEORY.md §II.1 invariant 2 — free middle; both
5745/// expansion strategies route through the SHARED `MacroParams::bind`,
5746/// AND the bytecode strategy's op-arms route through this SHARED
5747/// `resolve_bound_arg` lookup — the bytecode-runtime's
5748/// proof-of-well-formedness is now structurally uniform across the
5749/// two reachable index-lookup ops, so a regression that drifts ONE
5750/// arm's posture (e.g. accepts an out-of-range idx at one arm but
5751/// not the other, or swaps the kind constructor at a single arm) is
5752/// no longer a silent two-site divergence.
5753fn resolve_bound_arg<'a>(
5754    args_by_index: &'a [Sexp],
5755    idx: usize,
5756    macro_name: &str,
5757    kind: impl FnOnce(usize) -> TemplateInvariantKind,
5758) -> Result<&'a Sexp> {
5759    args_by_index
5760        .get(idx)
5761        .ok_or_else(|| template_invariant_violation(macro_name, kind(idx)))
5762}
5763
5764/// Project the bytecode-runtime stack to its in-progress builder frame —
5765/// the `&mut Vec<Sexp>` every value-emitting op writes into. ONE named
5766/// primitive both push-emitting arms (`TemplateOp::Literal` /
5767/// `TemplateOp::Subst` / post-`EndList` parent fold) AND the splice-
5768/// emitting arm (`TemplateOp::Splice`) route through.
5769///
5770/// Before this lift the same `stack.last_mut().unwrap()` projection
5771/// appeared at FOUR sites inside [`apply_compiled`]'s op-loop:
5772///
5773///   * `TemplateOp::Literal` — pushes the literal `Sexp` into the
5774///     current builder.
5775///   * `TemplateOp::Subst` — pushes the cloned bound-arg into the
5776///     current builder.
5777///   * `TemplateOp::Splice` — splices the bound-arg into the current
5778///     builder via [`splice_value_into`].
5779///   * `TemplateOp::EndList` — after popping the just-finished list
5780///     frame, pushes the folded `Sexp::List(items)` into the parent
5781///     builder (the new current frame).
5782///
5783/// Four byte-identical re-derivations of the same projection, well past
5784/// the ≥2 PRIME-DIRECTIVE trigger. After this lift the four sites
5785/// collapse to a single `current_builder_mut(&mut stack).{push|extend}`
5786/// call, and the bytecode-runtime invariant the projection rests on
5787/// — "the op-loop always sees at least one stack frame" — lives in ONE
5788/// expect message rather than four silent `.unwrap()` calls.
5789///
5790/// The expect rationale: [`apply_compiled`] seeds the stack with the
5791/// outermost frame at entry (`vec![Vec::with_capacity(1)]`); every
5792/// `TemplateOp::BeginList` pushes a NEW frame and every
5793/// `TemplateOp::EndList` pops it, so the count stays at OR ABOVE 1
5794/// throughout the op-loop. Stack-depleting failure modes are caught
5795/// upstream by their own structural variants:
5796/// [`TemplateInvariantKind::EndListEmptyStack`] fires inside
5797/// [`apply_compiled`]'s `EndList` arm via [`Vec::pop`]'s `Option`
5798/// gate, BEFORE the parent-fold push runs against
5799/// `current_builder_mut`; [`TemplateInvariantKind::FinalNoValue`]
5800/// fires AFTER the op-loop completes, on the outermost `stack.pop()`
5801/// that returns the assembled result. So a reachable
5802/// `current_builder_mut(&mut stack)` always observes a non-empty
5803/// stack, and the `expect` is a structural-invariant marker, not a
5804/// load-bearing rejection path.
5805///
5806/// Sibling of [`resolve_bound_arg`] (the bytecode-runtime bound-arg
5807/// lookup primitive lifted in the prior claude-routine run on this
5808/// module — 492a235) and [`template_invariant_violation`] (the
5809/// structural-variant error builder for the bytecode-runtime's
5810/// closed-set invariant-violation surface). Together the three primitives
5811/// name the bytecode-runtime's substrate-level operations: lookup-a-
5812/// bound-arg ([`resolve_bound_arg`]), build-the-invariant-rejection
5813/// ([`template_invariant_violation`]), and project-to-the-current-
5814/// builder (this lift). A future bytecode op that emits ONE OR MORE
5815/// values into the current builder — a hypothetical
5816/// `TemplateOp::SpliceMany(indices: Vec<usize>)` that splices a batch,
5817/// a `TemplateOp::PushQuoted(form: Sexp)` that wraps before push, a
5818/// span-annotated emit-with-position op — composes with ONE call to
5819/// [`current_builder_mut`] and the per-op post-projection verb
5820/// (`.push(…)`, `.extend(…)`, `splice_value_into(…, _)`); a future
5821/// instrumentation hook that wants to log every op's emit before
5822/// it lands in the builder wraps ONE call boundary, not four inline
5823/// `stack.last_mut().unwrap()` sites.
5824///
5825/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
5826/// inline copies of the top-of-stack projection in one function is
5827/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5828/// named. THEORY.md §V.1 — knowable platform; the bytecode-runtime's
5829/// current-builder projection becomes a NAMED primitive on the
5830/// substrate's `&mut [Vec<Sexp>]` slice algebra rather than a re-derived
5831/// `last_mut + unwrap` chain at every op-arm that emits into the
5832/// builder. The expect message names the invariant
5833/// ("bytecode-runtime invariant: at least one stack frame during
5834/// op-loop") so a regression that drifts the loop's frame management
5835/// surfaces a NAMED panic, not a silent `unwrap` over `None`.
5836/// THEORY.md §II.1 invariant 2 — free middle; both expansion
5837/// strategies route through the SHARED `MacroParams::bind` upstream
5838/// AND the bytecode strategy's op-arms now route through this SHARED
5839/// `current_builder_mut` projection downstream — the bytecode-runtime's
5840/// substrate-level surface (lookup + emit) is named in two
5841/// composable primitives the op-arms compose with.
5842fn current_builder_mut(stack: &mut [Vec<Sexp>]) -> &mut Vec<Sexp> {
5843    stack
5844        .last_mut()
5845        .expect("bytecode-runtime invariant: at least one stack frame during op-loop")
5846}
5847
5848/// Pop the top stack frame off the bytecode-runtime stack, or raise the
5849/// structural [`LispError::TemplateInvariant`] rejection with the
5850/// supplied [`TemplateInvariantKind`] when the stack is empty — ONE
5851/// named primitive both pop-emitting sites in [`apply_compiled`] route
5852/// through.
5853///
5854/// Before this lift two byte-identical
5855/// `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
5856/// kind))?` chains lived inline in [`apply_compiled`]:
5857///
5858///   * `TemplateOp::EndList` arm — pops the just-finished list frame
5859///     before the parent-fold push, with kind
5860///     [`TemplateInvariantKind::EndListEmptyStack`] guarding the
5861///     unreachable empty-stack failure mode.
5862///   * Post-loop final pop — consumes the outermost frame that
5863///     accumulated the template's result, with kind
5864///     [`TemplateInvariantKind::FinalNoValue`] guarding the
5865///     unreachable seed-frame-already-popped failure mode.
5866///
5867/// Two byte-identical re-derivations of the same projection inside one
5868/// function, past the ≥2 PRIME-DIRECTIVE trigger once the structural
5869/// shape is named. After this lift the two sites collapse to a single
5870/// `pop_builder_frame(&mut stack, macro_name, KIND)?` call, and the
5871/// bytecode-runtime invariant the projection rests on — "an empty-stack
5872/// pop is a structural-variant rejection, not a silent `Option::None`"
5873/// — lives in ONE composition point rather than two.
5874///
5875/// Sibling of [`current_builder_mut`] (the bytecode-runtime stack's
5876/// *project-to-top-frame* primitive — the borrow face, never resizes
5877/// the stack) and [`resolve_bound_arg`] (the bound-arg-by-index
5878/// lookup primitive with per-call-site `TemplateInvariantKind`
5879/// constructor). Where `current_builder_mut` borrows the in-progress
5880/// top frame for emission and never panics on a reachable input
5881/// (callers route past it only when the seed frame is present), this
5882/// primitive *consumes* the top frame off the stack and projects its
5883/// absence into a structural `LispError::TemplateInvariant` rejection
5884/// — the failure-on-empty face of the same `&mut Vec<Vec<Sexp>>`
5885/// algebra. The pair —
5886/// [`current_builder_mut`] + [`pop_builder_frame`] — close the
5887/// substrate's bytecode-runtime stack-frame projection algebra at the
5888/// borrow/consume boundary: borrow-the-top-frame for emission (no
5889/// rejection — the invariant rests on the seed-frame contract),
5890/// consume-the-top-frame for finalization (rejection-routed —
5891/// the absence projects through `TemplateInvariantKind` into a
5892/// structural variant of [`LispError`]). Together with
5893/// [`resolve_bound_arg`] (lookup args by index, with per-call-site
5894/// kind constructor) and [`template_invariant_violation`] (the typed
5895/// rejection emitter the three primitives share), the four
5896/// substrate-level operations name the bytecode-runtime's named
5897/// projection surface: lookup-a-bound-arg, project-to-the-current-
5898/// builder, consume-a-finished-builder-frame, build-the-invariant-
5899/// rejection.
5900///
5901/// `kind: TemplateInvariantKind` is the closed-set typed enum whose
5902/// four variants are EXACTLY the four reachable bytecode-runtime
5903/// invariant-violation modes (`SubstBadIndex(usize)` /
5904/// `SpliceBadIndex(usize)` from the index-lookup sibling primitive,
5905/// `EndListEmptyStack` from the `EndList` arm, `FinalNoValue` from
5906/// the post-loop final pop). The Subst / Splice indexed variants
5907/// thread their `usize` payload INSIDE the variant, so the invalid
5908/// combination "stack-gate kind with an op-index" (a hypothetical
5909/// `EndListEmptyStack(99)` carrying a Subst-style payload) is
5910/// structurally unrepresentable at this helper's boundary — the
5911/// caller cannot misroute an indexed kind through `pop_builder_frame`
5912/// at compile time because the kind's data shape is part of its
5913/// variant identity. Same closed-set guarantee
5914/// [`template_invariant_violation`] gives the four kinds; this
5915/// helper composes that guarantee with the `stack.pop()` projection
5916/// at the two pop-emitting sites.
5917///
5918/// The future-run extensions ride this floor: a future bytecode op
5919/// that consumes one or more finished frames — a hypothetical
5920/// `TemplateOp::EndMany(n: usize)` that pops `n` frames into a
5921/// flattened list, a span-aware `TemplateOp::EndListWithSpan(pos)`
5922/// that pops with a position-annotated rejection — composes with
5923/// ONE call (or a fold over N calls) to [`pop_builder_frame`]
5924/// without re-deriving the stack-pop-and-reject shape. A future
5925/// instrumentation hook (a debug-mode logger that records every
5926/// frame consumption, a span-aware pop that threads `Sexp` positions
5927/// through, a multi-frame fold that pops N frames in one step)
5928/// wraps ONE call boundary rather than keeping two inline chains in
5929/// lockstep at the production op-loop sites.
5930///
5931/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5932/// inline copies of the stack-pop-and-reject projection across the
5933/// `apply_compiled` body's `EndList` arm and post-loop final-pop is
5934/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5935/// named — the same threshold [`resolve_bound_arg`] (the index-lookup
5936/// sibling) crossed in the prior claude-routine run on this file
5937/// (492a235) and [`current_builder_mut`] (the top-frame-borrow
5938/// sibling) crossed two runs ago (c6a5a9d). THEORY.md §V.1 — knowable
5939/// platform / "make invalid states unrepresentable"; the bytecode-
5940/// runtime stack-frame consume operation becomes a NAMED primitive
5941/// on the substrate's `&mut Vec<Vec<Sexp>>` algebra rather than a
5942/// re-derived `pop + ok_or_else + template_invariant_violation`
5943/// chain at every op-arm that consumes a frame. THEORY.md §II.1
5944/// invariant 2 — free middle; both pop-emitting sites route through
5945/// the SHARED `pop_builder_frame` projection, so a regression that
5946/// drifts ONE site's posture (e.g. accepts an empty-stack pop at the
5947/// `EndList` arm but not the final pop, or swaps the kind constructor
5948/// at a single site) is no longer a silent two-site divergence — the
5949/// type system binds both sites to ONE composition point.
5950///
5951/// Frontier inspiration: MLIR's `Block::eraseFromParent()` against a
5952/// region's block list — the structured-IR's block-consumption
5953/// operation is a named typed primitive that yields a typed
5954/// `LogicalResult` rejection rather than a silent `nullptr` projection
5955/// when the parent region is empty; the substrate's
5956/// `pop_builder_frame` is the unstructured-projection peer on the
5957/// substrate's `&mut Vec<Vec<Sexp>>` stack-frame algebra, with
5958/// `TemplateInvariantKind` standing in for MLIR's `LogicalResult`'s
5959/// closed-set rejection identity. GHC Core's `popTickish` /
5960/// `stackPop` family — every Core-IR transform that consumes a stack
5961/// frame off the rewriter's working stack binds to one named pop
5962/// primitive that threads a typed `WantedFailure` rejection when the
5963/// stack is empty; translated through pleme-io primitives as ONE
5964/// `pop_builder_frame(stack, macro_name, kind)` call with
5965/// `TemplateInvariantKind` carrying the closed-set rejection
5966/// identity.
5967fn pop_builder_frame(
5968    stack: &mut Vec<Vec<Sexp>>,
5969    macro_name: &str,
5970    kind: TemplateInvariantKind,
5971) -> Result<Vec<Sexp>> {
5972    stack
5973        .pop()
5974        .ok_or_else(|| template_invariant_violation(macro_name, kind))
5975}
5976
5977/// Execute a pre-compiled template against the macro's argument list.
5978fn apply_compiled(
5979    macro_name: &str,
5980    params: &MacroParams,
5981    tmpl: &CompiledTemplate,
5982    args: &[Sexp],
5983) -> Result<Sexp> {
5984    // Resolve args by param index through the shared positional binder —
5985    // identical semantics to the `bind_args` (substitute) path by construction.
5986    let args_by_index = params.bind(macro_name, args)?;
5987
5988    // Run the bytecode against a stack of in-progress list builders. The
5989    // outermost frame accumulates the single result the template yields.
5990    // Each emit-into-builder arm routes through the shared
5991    // `current_builder_mut` projection — the bytecode-runtime invariant
5992    // "at least one stack frame during the op-loop" lives in ONE expect
5993    // message rather than four silent `.unwrap()` calls.
5994    let mut stack: Vec<Vec<Sexp>> = vec![Vec::with_capacity(1)];
5995    for op in &tmpl.ops {
5996        match op {
5997            TemplateOp::Literal(s) => current_builder_mut(&mut stack).push(s.clone()),
5998            TemplateOp::Subst(idx) => {
5999                // Bound-arg-by-index lookup routes through the shared
6000                // `resolve_bound_arg` projection with `SubstBadIndex` as
6001                // the per-call-site kind constructor; the post-lookup
6002                // verb (clone + push into the current builder) is the
6003                // Subst arm's per-op shape.
6004                let v = resolve_bound_arg(
6005                    &args_by_index,
6006                    *idx,
6007                    macro_name,
6008                    TemplateInvariantKind::SubstBadIndex,
6009                )?
6010                .clone();
6011                current_builder_mut(&mut stack).push(v);
6012            }
6013            TemplateOp::Splice(idx) => {
6014                // Sibling lookup through `resolve_bound_arg` with
6015                // `SpliceBadIndex` as the per-call-site kind constructor;
6016                // the post-lookup verb (`splice_value_into` against the
6017                // current builder) consumes the borrow directly without
6018                // an intermediate clone.
6019                let v = resolve_bound_arg(
6020                    &args_by_index,
6021                    *idx,
6022                    macro_name,
6023                    TemplateInvariantKind::SpliceBadIndex,
6024                )?;
6025                splice_value_into(current_builder_mut(&mut stack), v);
6026            }
6027            TemplateOp::BeginList => stack.push(Vec::new()),
6028            TemplateOp::EndList => {
6029                // Pop the just-finished list frame through the shared
6030                // `pop_builder_frame` projection with
6031                // `EndListEmptyStack` as the per-call-site kind
6032                // constructor; the post-pop verb (folded
6033                // `Sexp::List(items)` push into the parent frame via
6034                // `current_builder_mut`) is the EndList arm's per-op
6035                // shape — sibling of the Subst/Splice arms' index-
6036                // lookup-then-emit posture, with the index-lookup
6037                // primitive (`resolve_bound_arg`) and the frame-
6038                // consume primitive (`pop_builder_frame`) BOTH routing
6039                // through the same `TemplateInvariantKind` closed-set
6040                // rejection identity.
6041                let items = pop_builder_frame(
6042                    &mut stack,
6043                    macro_name,
6044                    TemplateInvariantKind::EndListEmptyStack,
6045                )?;
6046                current_builder_mut(&mut stack).push(Sexp::List(items));
6047            }
6048        }
6049    }
6050    // Final pop consumes the outermost (seed) frame via the same
6051    // shared projection — `FinalNoValue` as the per-call-site kind
6052    // constructor pins this site's structural-variant identity, the
6053    // post-pop verb (the `top.len() == 1` arity gate below) is the
6054    // post-loop tail's per-site shape.
6055    let mut top = pop_builder_frame(&mut stack, macro_name, TemplateInvariantKind::FinalNoValue)?;
6056    if top.len() == 1 {
6057        Ok(top.remove(0))
6058    } else {
6059        Ok(Sexp::List(top))
6060    }
6061}
6062
6063/// Hash of `(macro_name, args)` for cache keying — hot path, kept lean.
6064/// Uses `DefaultHasher` (SipHash-2-4) — fast enough that the cache hit rate
6065/// needed to net a win is low even for cheap macros.
6066fn args_cache_key(macro_name: &str, args: &[Sexp]) -> Option<CacheKey> {
6067    let mut h = DefaultHasher::new();
6068    args.len().hash(&mut h);
6069    for a in args {
6070        a.hash(&mut h);
6071    }
6072    Some((macro_name.to_string(), h.finish()))
6073}
6074
6075/// `pub(crate)` so the span-preserving expander recognizes a macro
6076/// definition through THIS function rather than a second copy of the
6077/// `defmacro`-head / name / param-list / body decomposition. A `MacroDef`
6078/// retains no spans (macros are keyed by name), so the spanned path lowers
6079/// its form and lands here — one recognizer, one lambda-list parser, one
6080/// error taxonomy.
6081pub(crate) fn macro_def_from(form: &Sexp) -> Result<Option<MacroDef>> {
6082    // Route the typed-macro-definition dispatch surface through the
6083    // substrate's typed-decoded call decomposition: `as_call_to_any`
6084    // performs the `as_list + head_symbol + MacroDefHead::from_keyword`
6085    // three-step chain in ONE structural query on the `Sexp` algebra.
6086    // The legacy diagnostic anchors on `list.len()` (the FULL form arity
6087    // including the head) — preserved here as `args.len() + 1` so
6088    // `LispError::DefmacroArity.arity` carries the same value across the
6089    // lift.
6090    let Some((head, args)) = form.as_call_to_any(MacroDefHead::from_keyword) else {
6091        return Ok(None);
6092    };
6093    if args.len() < 3 {
6094        return Err(defmacro_arity(head, args.len() + 1));
6095    }
6096    let name = args[0]
6097        .as_symbol()
6098        .ok_or_else(|| defmacro_non_symbol_name(head, &args[0]))?
6099        .to_string();
6100    let param_list = args[1]
6101        .as_list()
6102        .ok_or_else(|| defmacro_non_list_params(head, &args[1]))?;
6103    let params = parse_params(param_list)?;
6104    let body = args[2].clone();
6105    Ok(Some(MacroDef { name, params, body }))
6106}
6107
6108fn parse_params(list: &[Sexp]) -> Result<MacroParams> {
6109    let mut required = Vec::new();
6110    let mut optional: Vec<OptionalParam> = Vec::new();
6111    let mut optional_marker: Option<usize> = None;
6112    let mut i = 0;
6113    while i < list.len() {
6114        // In the optional section a `(name default)` LIST form is a valid spec
6115        // alongside a bare-symbol spec. The list form is only meaningful here,
6116        // so the dispatch fires before the `as_symbol()` gate that would
6117        // otherwise reject it as `NonSymbolParam`.
6118        if optional_marker.is_some() {
6119            if let Sexp::List(items) = &list[i] {
6120                optional.push(parse_optional_list_spec(i, &list[i], items)?);
6121                i += 1;
6122                continue;
6123            }
6124        }
6125        let s = list[i]
6126            .as_symbol()
6127            .ok_or_else(|| non_symbol_param(i, &list[i]))?;
6128        if s == MacroParams::REST_MARKER {
6129            let Some(next) = list.get(i + 1) else {
6130                return Err(rest_param_missing_name(i, None));
6131            };
6132            let Some(name) = next.as_symbol() else {
6133                return Err(rest_param_missing_name(i, Some(next)));
6134            };
6135            let trailing = &list[i + 2..];
6136            if !trailing.is_empty() {
6137                return Err(rest_param_trailing_tokens(i, trailing));
6138            }
6139            return Ok(MacroParams {
6140                required,
6141                optional,
6142                rest: Some(name.to_string()),
6143            });
6144        }
6145        if s == MacroParams::OPTIONAL_MARKER {
6146            if let Some(first) = optional_marker {
6147                return Err(optional_marker_repeated(first, i));
6148            }
6149            optional_marker = Some(i);
6150            i += 1;
6151            continue;
6152        }
6153        if optional_marker.is_some() {
6154            optional.push(OptionalParam::bare(s));
6155        } else {
6156            required.push(s.to_string());
6157        }
6158        i += 1;
6159    }
6160    Ok(MacroParams {
6161        required,
6162        optional,
6163        rest: None,
6164    })
6165}
6166
6167/// Project a `Sexp::List` in the `&optional` section to a typed
6168/// [`OptionalParam`]. The only admissible shape is `(NAME DEFAULT)` — a
6169/// list of exactly TWO elements whose first element is a symbol. Every
6170/// other list shape is the structural rejection
6171/// [`LispError::OptionalParamMalformed`], with a typed `reason`
6172/// ([`OptionalParamMalformedReason`]) naming WHICH way the spec is
6173/// malformed — empty, missing-default, extra-elements, or non-symbol name.
6174///
6175/// `position` is the loop index inside `parse_params`, mirroring the
6176/// `position`/`rest_position`/`first_position` slots on the sibling
6177/// `parse_params` rejection variants. `list_form` is the offending
6178/// `Sexp::List` itself, projected through `crate::domain::sexp_witness` so
6179/// the variant carries BOTH `SexpShape::List` AND the rendered form (for
6180/// LSP / REPL / `tatara-check` consumption). `items` is the list body,
6181/// avoiding a re-`as_list()` at the call boundary.
6182fn parse_optional_list_spec(
6183    position: usize,
6184    list_form: &Sexp,
6185    items: &[Sexp],
6186) -> Result<OptionalParam> {
6187    use crate::error::OptionalParamMalformedReason as R;
6188    // Arity dispatch — threads through the typed classifier on the
6189    // closed-set algebra so the accept-arity binding (arity ==
6190    // `R::OPTIONAL_PARAM_SPEC_ARITY`) is derived from ONE typed const
6191    // rather than the bare literal `2`. Post-classification the arity
6192    // is exactly the accept target; the head-symbol gate is the only
6193    // remaining rejection axis.
6194    if let Some(reason) = R::classify_arity(items.len()) {
6195        return Err(optional_param_malformed(position, list_form, reason));
6196    }
6197    let Some(name) = items[0].as_symbol() else {
6198        return Err(optional_param_malformed(
6199            position,
6200            list_form,
6201            R::NonSymbolName,
6202        ));
6203    };
6204    Ok(OptionalParam::with_default(name, items[1].clone()))
6205}
6206
6207fn bind_args(
6208    macro_name: &str,
6209    params: &MacroParams,
6210    args: &[Sexp],
6211) -> Result<HashMap<String, Sexp>> {
6212    // Zip the shared positional binding (parallel to `names()`) into the
6213    // name-keyed map the `substitute` path looks substitutions up in.
6214    let vals = params.bind(macro_name, args)?;
6215    Ok(params
6216        .names()
6217        .into_iter()
6218        .map(String::from)
6219        .zip(vals)
6220        .collect())
6221}
6222
6223/// Substitute `,name` and `,@name` within a template.
6224/// `,@name` only makes sense inside a List — it splices the bound list into
6225/// the containing list.
6226///
6227/// Routes both unquote-family sites — the top-level `,X` / `,@X`
6228/// recognition AND the list-inner per-item splice recognition — through
6229/// the substrate's typed-marker projection [`Sexp::as_unquote`]. Pre-lift
6230/// each site opened its own `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice
6231/// (inner)` arm paired with a `UnquoteForm::Unquote` / `UnquoteForm::
6232/// Splice` literal; post-lift the (Sexp variant, UnquoteForm variant)
6233/// pairing is bound at ONE projection function the type system threads
6234/// through `(UnquoteForm, &Sexp)`, eliminating the silent two-site
6235/// pairing drift the prior shape allowed.
6236fn substitute(form: &Sexp, bindings: &HashMap<String, Sexp>) -> Result<Sexp> {
6237    if let Some((kind, inner)) = form.as_unquote() {
6238        return match kind {
6239            UnquoteForm::Unquote => resolve_unquote_in_bindings(inner, bindings, kind).cloned(),
6240            UnquoteForm::Splice => Err(splice_outside_list(inner)),
6241        };
6242    }
6243    match form {
6244        Sexp::List(items) => {
6245            let mut out: Vec<Sexp> = Vec::with_capacity(items.len());
6246            for item in items {
6247                if let Some((UnquoteForm::Splice, inner)) = item.as_unquote() {
6248                    let val = resolve_unquote_in_bindings(inner, bindings, UnquoteForm::Splice)?;
6249                    splice_value_into(&mut out, val);
6250                } else {
6251                    out.push(substitute(item, bindings)?);
6252                }
6253            }
6254            Ok(Sexp::List(out))
6255        }
6256        _ => Ok(form.clone()),
6257    }
6258}
6259
6260/// Lift the four inline `LispError::Compile { form: format!("{prefix}{name}"),
6261/// message: "unbound" }` triples (compile_node Unquote/UnquoteSplice +
6262/// substitute Unquote/UnquoteSplice) behind ONE named primitive. Pairs the
6263/// structural variant with `crate::domain::suggest`'s bounded edit-distance
6264/// scan over the candidate set so a typo in `,name` against a macro's params
6265/// (or against a substitution scope's live bindings) surfaces as
6266/// `"compile error in ,xs: unbound; did you mean ,x?"` instead of the bare
6267/// `"unbound"`. The candidate set is per-call — params during compile,
6268/// `bindings.keys()` during substitute — so the operator's hint is always
6269/// drawn from the in-scope name set, never a stale snapshot.
6270///
6271/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6272/// variants are EXACTLY the two reachable syntactic markers
6273/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6274/// boundary (rather than `&'static str`) lands the same compile-time
6275/// closed-set guarantee `defmacro_arity` / `defmacro_non_symbol_name` /
6276/// `defmacro_non_list_params` get from threading `MacroDefHead`: the
6277/// closed set is encoded in the type system, so a regression that drifts
6278/// the marker (e.g. a fourth `prefix: ",,"` call site) becomes a type
6279/// error at the call site, not a runtime substring drift. `name` is the
6280/// offender from source; the hint is `Option<String>` because the matched
6281/// candidate borrows from a transient `Vec<&str>` we built locally —
6282/// copying the matched name into the variant is the cheapest way to keep
6283/// `LispError` lifetime-free.
6284///
6285/// Theory anchor: THEORY.md §VI.1 — generation over composition; four inline
6286/// copies in one module is well past the three-times rule. THEORY.md §V.1 —
6287/// knowable platform; the structural variant exposes `prefix` / `name` /
6288/// `hint` as first-class fields so authoring tools (LSP, REPL,
6289/// `tatara-check`) bind to the data shape instead of substring-parsing the
6290/// rendered diagnostic.
6291fn unbound_template_var(prefix: UnquoteForm, name: &str, candidates: &[&str]) -> LispError {
6292    LispError::UnboundTemplateVar {
6293        prefix,
6294        name: name.to_string(),
6295        hint: crate::domain::suggest(name, candidates).map(str::to_string),
6296    }
6297}
6298
6299/// Lift the four inline `LispError::Compile { form: "unquote" /
6300/// "unquote-splice", message: "only bound symbols may appear after `,` /
6301/// `,@`" }` triples in this module (compile_node Unquote / UnquoteSplice +
6302/// substitute Unquote / UnquoteSplice-inside-list) behind ONE named
6303/// primitive. Sibling of `unbound_template_var`: that helper fires when the
6304/// slot IS a symbol but the symbol isn't bound; this helper fires when the
6305/// slot isn't a symbol at all. Together they close every distinct
6306/// typed-entry template-gate failure mode for the no-evaluator template
6307/// language: each is a structural variant of `LispError`, not a
6308/// `Compile`-shaped substring.
6309///
6310/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6311/// variants are EXACTLY the two reachable syntactic markers
6312/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6313/// boundary (rather than `&'static str`) lands the same compile-time
6314/// closed-set guarantee `unbound_template_var` carries: the closed set is
6315/// encoded in the type system. The inner is the offending `Sexp` routed
6316/// through `crate::domain::sexp_witness` — the typed joint projection
6317/// pairing `SexpShape` (structural shape) with `Sexp::Display`
6318/// (renderable literal) at ONE call boundary. Authoring tools bind to
6319/// BOTH `got.shape` (e.g. `SexpShape::List`) AND `got.display` (e.g.
6320/// `"(list 1 2)"`) jointly — same posture as `splice_outside_list`
6321/// after its prior-run promotion to `SexpWitness`. The two template-
6322/// gate `,X/,@X` rejection variants now share ONE typed witness
6323/// identity at their `got` slot.
6324///
6325/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6326/// inline copies in one module is past the three-times rule. THEORY.md
6327/// §V.1 — knowable platform; the structural variant exposes `prefix` /
6328/// `got` as first-class fields so authoring tools (LSP, REPL,
6329/// `tatara-check`) bind to the data shape instead of substring-parsing
6330/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6331/// a non-symbol unquote target is exactly the failure mode the
6332/// typed-entry gate exists to reject.
6333fn non_symbol_unquote_target(prefix: UnquoteForm, got: &Sexp) -> LispError {
6334    LispError::NonSymbolUnquoteTarget {
6335        prefix,
6336        got: got.witness(),
6337    }
6338}
6339
6340/// Project the inner of a `,X` / `,@X` form to its bound symbol name, or
6341/// raise the structural `LispError::NonSymbolUnquoteTarget` rejection at
6342/// the typed-entry template-gate boundary. ONE named primitive every
6343/// `,X` / `,@X` resolution site in the substrate shares — the inline
6344/// `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(form,
6345/// inner))?` pattern appeared four times across `compile_node`
6346/// (bytecode-path Unquote / UnquoteSplice arms) AND `substitute`
6347/// (substitute-path Unquote / list-inner UnquoteSplice arms), well past
6348/// the three-times-rule trigger. After this lift the four sites collapse
6349/// to a single `unquote_target_symbol(inner, form)?` call, and the
6350/// substrate's understanding of "an unquote target's first gate is `must
6351/// be a symbol`" lives in ONE function — a regression that drifts the
6352/// gate's posture (e.g. accepts non-symbol targets at the bytecode path
6353/// but not the substitute path) becomes a type-level change at this
6354/// helper, not a silent four-site divergence.
6355///
6356/// Sibling of `non_symbol_unquote_target` (the error builder this gate
6357/// calls on failure) and `unbound_template_var` (the typed-entry
6358/// template-gate's SECOND gate — fires once `unquote_target_symbol`
6359/// projects the symbol successfully but the symbol isn't bound in the
6360/// in-scope name set). Together the three close the substrate's
6361/// understanding of the two-step typed-entry template-gate: gate-1 is
6362/// `must-be-a-symbol`, gate-2 is `must-be-bound-in-scope`. With this
6363/// lift, gate-1 lives at ONE call boundary across all four template-
6364/// gate sites — bytecode path AND substitute path AND both `,X` and
6365/// `,@X` forms.
6366///
6367/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6368/// variants are EXACTLY the two reachable syntactic markers
6369/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6370/// helper boundary (rather than `&'static str`) lands the same
6371/// compile-time closed-set guarantee `non_symbol_unquote_target` and
6372/// `unbound_template_var` get from their `UnquoteForm` slots — a
6373/// regression that drifts the marker (e.g. a third pseudo-marker call
6374/// site) becomes a type error at the call site, not a runtime
6375/// substring drift. The returned `&'a str` borrows from `inner` — the
6376/// caller feeds it directly into `params.iter().position(|p| *p ==
6377/// name)` (`compile_node`) or `bindings.get(name)` (`substitute`)
6378/// without an intermediate allocation.
6379///
6380/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6381/// inline copies of the gate-1 projection (`compile_node`
6382/// Unquote/UnquoteSplice + `substitute` Unquote + `substitute`
6383/// list-inner UnquoteSplice) is past the three-times rule. THEORY.md
6384/// §V.1 — knowable platform; the gate's identity becomes a NAMED
6385/// primitive consumer-binding rather than a four-times-inlined
6386/// match-and-reject snippet — authoring surfaces (REPL, LSP,
6387/// `tatara-check`) that want to surface "the typed-entry template-gate
6388/// rejected your form because the unquote target wasn't a symbol" bind
6389/// to ONE function. THEORY.md §II.1 invariant 1 — typed entry; an
6390/// unquote target that isn't a symbol is exactly the failure mode the
6391/// typed-entry template-gate exists to reject. THEORY.md §II.1
6392/// invariant 2 — free middle; both bytecode AND substitute expansion
6393/// paths now project through the SAME gate-1 primitive, so a macro
6394/// that compiles under one strategy compiles under the other (the
6395/// gate's posture is uniform across the two strategies, no
6396/// per-strategy drift can creep in).
6397fn unquote_target_symbol(inner: &Sexp, form: UnquoteForm) -> Result<&str> {
6398    inner
6399        .as_symbol()
6400        .ok_or_else(|| non_symbol_unquote_target(form, inner))
6401}
6402
6403/// Gate-2 for the bytecode-template compile path: resolve a template
6404/// variable name to its index inside the macro's static param list, or
6405/// raise the structural `LispError::UnboundTemplateVar` rejection. ONE
6406/// named primitive that the two `compile_node` sites — `Sexp::Unquote(_)`
6407/// and `Sexp::UnquoteSplice(_)` arms — share. Before this lift the same
6408/// `params.iter().position(|p| *p == name).ok_or_else(|| unbound_template_var(
6409/// FORM, name, params))?` projection was inlined twice in one match
6410/// block; after this lift the two sites collapse to a single
6411/// `resolve_param_index(name, params, form)?` call and the
6412/// `Subst(idx)` / `Splice(idx)` ops push from a uniform projection
6413/// boundary.
6414///
6415/// Sibling of `resolve_binding`: the same gate-2 contract on the
6416/// substitute path. Together the two close the typed-entry template
6417/// gate's gate-2 (must-be-bound-in-scope) primitive across BOTH
6418/// expansion strategies — gate-1 (`unquote_target_symbol`) projects the
6419/// inner to a symbol name; gate-2 looks the name up in the in-scope
6420/// candidate set. The two paths' candidate sets differ structurally
6421/// (compile path: `&[&str]` of macro params, returning `usize`;
6422/// substitute path: `&HashMap<String, Sexp>` of live bindings, returning
6423/// `&Sexp`), so the gate-2 primitive bifurcates by path — but the
6424/// rejection shape (`LispError::UnboundTemplateVar { prefix, name, hint }`
6425/// with `crate::domain::suggest`-driven hint) is identical across both
6426/// paths. A regression that drifts gate-2's posture (e.g., accepts an
6427/// unbound `,name` at the bytecode path but not the substitute path) is
6428/// now a type-level change at this helper, not a silent four-site
6429/// divergence.
6430///
6431/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6432/// variants are EXACTLY the two reachable syntactic markers
6433/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6434/// helper boundary (rather than `&'static str`) lands the same
6435/// compile-time closed-set guarantee `unquote_target_symbol`,
6436/// `unbound_template_var`, and `non_symbol_unquote_target` carry — a
6437/// regression that drifts the marker becomes a type error at the call
6438/// site, not a runtime substring drift.
6439///
6440/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6441/// inline copies of the gate-2 projection in one match block, paired
6442/// with the two substitute-path inline copies, is four copies in two
6443/// functions — past the three-times rule once the structural shape is
6444/// named. THEORY.md §V.1 — knowable platform; the gate's identity
6445/// becomes a NAMED primitive consumer-binding rather than a
6446/// twice-inlined position-and-reject snippet — authoring surfaces
6447/// (REPL, LSP, `tatara-check`) that want to surface "the typed-entry
6448/// template-gate rejected your form because the name isn't bound in
6449/// scope" bind to ONE function per path. THEORY.md §II.1 invariant 1 —
6450/// typed entry; an unbound template variable is exactly the failure
6451/// mode the typed-entry template-gate exists to reject. THEORY.md
6452/// §II.1 invariant 2 — free middle; both expansion strategies'
6453/// gate-2 emit the SAME structural variant, so a macro that compiles
6454/// under one strategy compiles under the other.
6455fn resolve_param_index(name: &str, params: &[&str], form: UnquoteForm) -> Result<usize> {
6456    params
6457        .iter()
6458        .position(|p| *p == name)
6459        .ok_or_else(|| unbound_template_var(form, name, params))
6460}
6461
6462/// Gate-2 for the substitute expansion path: resolve a template
6463/// variable name to its bound `Sexp` value inside the runtime bindings
6464/// map, or raise the structural `LispError::UnboundTemplateVar`
6465/// rejection. ONE named primitive that the two `substitute` sites —
6466/// the top-level `Sexp::Unquote(_)` arm and the list-inner
6467/// `Sexp::UnquoteSplice(_)` arm — share. Before this lift the same
6468/// `bindings.get(sym).<cloned>?.ok_or_else(|| unbound_template_var(
6469/// FORM, sym, &bound_names(bindings)))` projection was inlined twice
6470/// across the substitute walker; after this lift the two sites
6471/// collapse to a single `resolve_binding(bindings, sym, form)?` call
6472/// (with a trailing `.cloned()` at the top-level arm because that arm
6473/// returns an owned `Sexp` while the list-inner arm consumes the
6474/// `&Sexp` borrow directly).
6475///
6476/// Sibling of `resolve_param_index`: the same gate-2 contract on the
6477/// bytecode-template compile path. Together the two close the
6478/// typed-entry template gate's gate-2 (must-be-bound-in-scope)
6479/// primitive across BOTH expansion strategies. The candidate set on
6480/// the substitute path is the live bindings' keys (built fresh per
6481/// call via `bound_names`) — never a stale snapshot, so the
6482/// suggest-driven hint is always drawn from the actual in-scope name
6483/// set the operator sees.
6484///
6485/// The returned `&'a Sexp` borrows from `bindings` — the list-inner
6486/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6487/// splice-expansion match without an intermediate allocation. The
6488/// top-level caller's owned-Sexp obligation is satisfied by the
6489/// `.cloned()` projection at the call site, which is a single typed
6490/// `Sexp::clone` and not a redundant lookup.
6491///
6492/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6493/// `resolve_param_index` and `unquote_target_symbol`. A regression
6494/// that drifts the marker becomes a type error at the call site, not
6495/// a runtime substring drift.
6496///
6497/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6498/// inline copies of the gate-2 projection in the substitute walker,
6499/// paired with the two compile-path inline copies, is four copies in
6500/// two functions — past the three-times rule once the structural
6501/// shape is named. THEORY.md §V.1 — knowable platform; the gate's
6502/// identity becomes a NAMED primitive consumer-binding rather than a
6503/// twice-inlined lookup-and-reject snippet. THEORY.md §II.1
6504/// invariant 1 — typed entry; an unbound template variable is exactly
6505/// the failure mode the typed-entry template-gate exists to reject.
6506/// THEORY.md §II.1 invariant 2 — free middle; both expansion
6507/// strategies' gate-2 emit the SAME structural variant.
6508fn resolve_binding<'a>(
6509    bindings: &'a HashMap<String, Sexp>,
6510    name: &str,
6511    form: UnquoteForm,
6512) -> Result<&'a Sexp> {
6513    bindings
6514        .get(name)
6515        .ok_or_else(|| unbound_template_var(form, name, &bound_names(bindings)))
6516}
6517
6518/// Compose gate-1 + gate-2 for the bytecode-template compile path into ONE
6519/// named primitive: project the unquote `inner` to a symbol name
6520/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6521/// index inside the macro's static param list (gate-2, via
6522/// `resolve_param_index`). Sibling of `resolve_unquote_in_bindings`: the
6523/// same gate-1+gate-2 composition on the substitute expansion path.
6524///
6525/// Before this lift, the two `compile_node` arms (`Sexp::Unquote(_)` and
6526/// `Sexp::UnquoteSplice(_)`) threaded `form: UnquoteForm` through TWO
6527/// helper calls each — once into `unquote_target_symbol(inner, form)?`
6528/// (gate-1) AND once into `resolve_param_index(name, params, form)?`
6529/// (gate-2). The marker's typed identity was re-asserted at the call site
6530/// twice per arm — four `UnquoteForm::Unquote` / `UnquoteForm::Splice`
6531/// literal occurrences across the two arms, for what is structurally ONE
6532/// marker-identity per syntactic-marker arm. After this lift each arm
6533/// threads the marker ONCE through ONE call, and the gate-1-then-gate-2
6534/// sequencing lives in the helper body, not at the call site.
6535///
6536/// The composition is load-bearing: gate-1 (must-be-a-symbol) MUST fire
6537/// before gate-2 (must-be-bound-in-scope) — a non-symbol inner is
6538/// structurally a different failure mode (`LispError::NonSymbolUnquoteTarget`,
6539/// which carries the offending `SexpWitness`) than an unbound symbol
6540/// (`LispError::UnboundTemplateVar`, which carries a `name: String` plus
6541/// a `crate::domain::suggest`-driven hint over the candidate set). A
6542/// regression that reorders or skips gate-1 would emit
6543/// `LispError::UnboundTemplateVar { name: "(list 1 2)", ... }` for a
6544/// non-symbol inner (re-treating the rendered list literal as a bound-
6545/// name lookup key), which is exactly the diagnostic-confusion this
6546/// composition exists to rule out. Naming the composition as one
6547/// primitive makes the sequencing structural — the helper body IS the
6548/// proof that gate-1 ran before gate-2.
6549///
6550/// `form` is `UnquoteForm` — the closed-set typed enum threaded through
6551/// the composition once and passed onward to both gate-1 and gate-2's
6552/// rejection-builders. Same posture as `unquote_target_symbol`,
6553/// `resolve_param_index`, `resolve_binding`, `non_symbol_unquote_target`,
6554/// and `unbound_template_var` — a regression that drifts the marker
6555/// becomes a type error at the helper boundary, not a runtime substring
6556/// drift, AND the marker can no longer drift BETWEEN gate-1 and gate-2
6557/// at a single call site (which the prior pre-lift shape allowed:
6558/// `unquote_target_symbol(inner, UnquoteForm::Unquote)?` followed by
6559/// `resolve_param_index(name, params, UnquoteForm::Splice)?` would
6560/// type-check but render a misleading diagnostic).
6561///
6562/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6563/// gate-1+gate-2 SEQUENCE is itself a named primitive once both halves
6564/// have been named (two prior runs landed the halves; this run lands the
6565/// composition). THEORY.md §V.1 — knowable platform; the gate's
6566/// composition is now load-bearing in the type system — gate-1 cannot be
6567/// silently skipped, gate-2 cannot be silently reordered before gate-1,
6568/// and the marker cannot drift between the two halves. THEORY.md §II.1
6569/// invariant 1 — typed entry; the typed-entry template gate's full
6570/// rejection chain (non-symbol → unbound-symbol) is now ONE primitive.
6571/// THEORY.md §II.1 invariant 2 — free middle; both expansion strategies
6572/// expose the gate's identity as ONE primitive per path, so a macro that
6573/// passes the gate under one strategy passes under the other (no per-
6574/// strategy composition drift can creep in).
6575fn resolve_unquote_in_params(inner: &Sexp, params: &[&str], form: UnquoteForm) -> Result<usize> {
6576    let name = unquote_target_symbol(inner, form)?;
6577    resolve_param_index(name, params, form)
6578}
6579
6580/// Compose gate-1 + gate-2 for the substitute expansion path into ONE
6581/// named primitive: project the unquote `inner` to a symbol name
6582/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6583/// bound `Sexp` value inside the runtime bindings map (gate-2, via
6584/// `resolve_binding`). Sibling of `resolve_unquote_in_params`: the same
6585/// gate-1+gate-2 composition on the bytecode-template compile path.
6586///
6587/// Before this lift, the substitute walker's two unquote sites (the
6588/// top-level `Sexp::Unquote(_)` arm and the list-inner
6589/// `Sexp::UnquoteSplice(_)` arm) threaded `form: UnquoteForm` through
6590/// TWO helper calls each — once into `unquote_target_symbol(inner,
6591/// form)?` (gate-1) AND once into `resolve_binding(bindings, name,
6592/// form)?` (gate-2). After this lift each site threads the marker
6593/// ONCE through ONE call. Same composition contract as
6594/// `resolve_unquote_in_params` — gate-1 fires before gate-2 by the
6595/// helper body's `?`-then-call sequencing, NOT by call-site discipline.
6596///
6597/// The returned `&'a Sexp` borrows from `bindings` so the list-inner
6598/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6599/// splice-expansion match without an intermediate allocation; the
6600/// top-level caller's owned-Sexp obligation is satisfied by a
6601/// `.cloned()` projection at the call site (one typed `Sexp::clone`,
6602/// no redundant lookup).
6603///
6604/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6605/// `resolve_unquote_in_params` and all the helpers it composes. After
6606/// this lift, the marker's identity flows through the substitute path's
6607/// typed-entry template gate via ONE explicit pass per call site, not
6608/// two; the gate's gate-1+gate-2 sequencing is structural across both
6609/// expansion strategies.
6610///
6611/// Theory anchor: same as `resolve_unquote_in_params`. THEORY.md §VI.1
6612/// (generation over composition; named composition of named gates),
6613/// THEORY.md §V.1 (knowable platform; gate composition is type-system
6614/// load-bearing), THEORY.md §II.1 invariant 1 (typed entry; the full
6615/// rejection chain is ONE primitive), THEORY.md §II.1 invariant 2
6616/// (free middle; both strategies share the same composition shape).
6617fn resolve_unquote_in_bindings<'a>(
6618    inner: &Sexp,
6619    bindings: &'a HashMap<String, Sexp>,
6620    form: UnquoteForm,
6621) -> Result<&'a Sexp> {
6622    let name = unquote_target_symbol(inner, form)?;
6623    resolve_binding(bindings, name, form)
6624}
6625
6626/// Lift the lone `LispError::Compile { form: "unquote-splice", message:
6627/// "`,@` may only appear inside a list" }` triple — the substitute path's
6628/// top-level `,@X` rejection — behind ONE named primitive. Sibling of
6629/// `non_symbol_unquote_target` and `unbound_template_var`: those helpers
6630/// fire when the slot inside a `,X` / `,@X` is malformed (non-symbol or
6631/// unbound symbol); this helper fires when the `,@X` form itself is
6632/// ill-positioned (no containing list to flatten into). Together the three
6633/// close every distinct typed-entry template-gate failure mode for the
6634/// no-evaluator template language: each is a structural variant of
6635/// `LispError`, not a `Compile`-shaped substring.
6636///
6637/// `inner` is the offending `Sexp` projected through `Display` so the
6638/// operator sees the literal value they wrote — `xs`, `(list 1 2)`, `5` —
6639/// instead of just the bare "may only appear inside a list" verdict. The
6640/// helper takes `&Sexp` (parallel to `non_symbol_unquote_target`) and
6641/// projects through `to_string()` at the variant boundary; the `prefix:
6642/// &'static str` slot is implicit (always `,@`) and absent from the variant
6643/// itself, parallel to how `OddKwargs { dangling }` names ONE failure mode
6644/// without a syntactic-marker slot.
6645///
6646/// Used by both the substitute path (top-level `,@X` body) AND the bytecode
6647/// path's `compile_template` gate (top-level `,@X` body — closing the prior
6648/// silent-divergence where the bytecode interpreter's outermost stack frame
6649/// absorbed the splice). After this lift `,@-outside-list` is rejected on
6650/// both paths with ONE structural variant — the typed-entry template gate
6651/// is fully structural across both expansion strategies.
6652///
6653/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6654/// emission sites (substitute + compile_template) for one failure mode is
6655/// past the three-times rule once the structural shape is named. THEORY.md
6656/// §V.1 — knowable platform; the structural variant exposes `got` as a
6657/// first-class field so authoring tools (LSP, REPL, `tatara-check`) bind to
6658/// the data shape instead of substring-parsing the rendered diagnostic.
6659/// THEORY.md §II.1 invariant 1 — typed entry; a `,@X` at a position with no
6660/// containing list is exactly the failure mode the typed-entry gate exists
6661/// to reject. THEORY.md §II.1 invariant 2 — free middle; both expansion
6662/// paths now reject the same set of templates, so a macro that registers
6663/// successfully has the same expansion behavior under either strategy.
6664fn splice_outside_list(inner: &Sexp) -> LispError {
6665    LispError::SpliceOutsideList {
6666        got: inner.witness(),
6667    }
6668}
6669
6670/// Lift the two inline `LispError::Compile { form: format!("call to
6671/// {macro_name}"), message: format!("missing required arg: {name}") }`
6672/// triples — `bind_args` (substitute path) AND `apply_compiled` (bytecode
6673/// path) — behind ONE named primitive. Sibling of the typed-entry kwargs
6674/// `MissingKwarg { key }` lift: that variant fires when a `(<head> :key
6675/// value …)` kwargs form omits a required keyword; this variant fires when
6676/// a `(<macroname> a b …)` call omits a required positional param. Together
6677/// they close every distinct typed-entry missing-required surface in the
6678/// substrate — kwargs-gate AND macro-call-gate now share a single
6679/// structural-variant idiom.
6680///
6681/// Same single emission shape across both expansion strategies — before
6682/// this lift the same failure mode emitted byte-identical
6683/// `LispError::Compile { … }` triples at TWO call sites; after this lift
6684/// both sites share ONE structural variant. Two strategies that picked
6685/// different code paths now emit the same structural variant for the same
6686/// failure mode (THEORY.md §II.1 invariant 2 — free middle: which strategy
6687/// you picked must not change which inputs you reject OR how the rejection
6688/// is shaped). Same posture as `splice_outside_list`'s path-uniform
6689/// rejection across substitute + compile_template.
6690///
6691/// `macro_name` and `name` are `&str` borrows from the call-site / param
6692/// list; the variant's owned `String`s are formed at the boundary so
6693/// `LispError` stays lifetime-free.
6694///
6695/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6696/// inline copies of one shape is past the three-times-rule trigger once
6697/// the structural variant is named (the test count gives this the
6698/// fail-before-pass-after edge). THEORY.md §V.1 — knowable platform; the
6699/// structural variant exposes `macro_name` / `param` as first-class
6700/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the data
6701/// shape instead of substring-parsing the rendered diagnostic. THEORY.md
6702/// §II.1 invariant 1 — typed entry; a macro call with too few args is
6703/// exactly the failure mode the typed-entry gate exists to reject.
6704fn missing_macro_arg(macro_name: &str, param: &str) -> LispError {
6705    LispError::MissingMacroArg {
6706        macro_name: macro_name.to_string(),
6707        param: param.to_string(),
6708    }
6709}
6710
6711/// Mirror at the call-site of `missing_macro_arg`: that helper fires when
6712/// the macro CALL supplies TOO FEW args for the required arity (a required
6713/// slot has no arg); this helper fires when the macro CALL supplies TOO
6714/// MANY args for a rest-less param list (the surplus has nowhere to bind).
6715/// Together they close the typed-entry macro-call-gate's positional-arity
6716/// surface in both directions; together with the definition-site
6717/// `RestParamTrailingTokens` (lifted by the prior-run typed-promotion
6718/// lineage at the parse_params boundary), every distinct way a macro
6719/// definition + call pair can MISCOUNT args is now a named structural
6720/// rejection.
6721///
6722/// `expected` is the rest-less binder's fixed maximum arity
6723/// (`required.len() + optional.len()`); `got` is the actual call-site arg
6724/// count. Both are surfaced at the variant boundary so authoring tools
6725/// (REPL, LSP, `tatara-check`) name the "you supplied {got} args but the
6726/// macro takes at most {expected}" quick-fix from one structural projection
6727/// rather than re-deriving either count from the source. `macro_name` is
6728/// `&str` borrowed from the call-site; the variant's owned `String` is
6729/// formed at the boundary so `LispError` stays lifetime-free — same posture
6730/// as `missing_macro_arg`.
6731///
6732/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6733/// rest-less surplus-args gate is a SINGLE-OWNER named rejection, not a
6734/// silent truncation re-asserted at every consumer that walks the bound
6735/// values. THEORY.md §V.1 — knowable platform; the structural variant
6736/// exposes `macro_name` / `expected` / `got` as first-class fields so
6737/// authoring tools bind to the data shape instead of substring-parsing
6738/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a
6739/// macro call with too many args (and no `&rest` slot to absorb them) is
6740/// exactly the failure mode the typed-entry gate exists to reject —
6741/// silently dropping `args[expected..]` is structurally indistinguishable
6742/// from honoring them, the asymmetry this gate closes. THEORY.md §II.1
6743/// invariant 2 — free middle; both expansion strategies route through the
6744/// SHARED `MacroParams::bind`, so the new rejection lands once and the
6745/// substitute + bytecode paths inherit it unable to drift.
6746fn too_many_macro_args(macro_name: &str, expected: usize, got: usize) -> LispError {
6747    LispError::TooManyMacroArgs {
6748        macro_name: macro_name.to_string(),
6749        expected,
6750        got,
6751    }
6752}
6753
6754/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6755/// "expected symbol" }` triple in `parse_params` behind ONE named
6756/// primitive. Sibling of `missing_macro_arg`: that helper fires when the
6757/// macro CALL is malformed (call-site missing a positional arg); this
6758/// helper fires when the macro DEFINITION is malformed (definition-site
6759/// has a non-symbol where a param name should be). Together they open
6760/// the defmacro-syntax-gate / macro-call-gate split — call-site
6761/// rejections vs. definition-site rejections — each as its own
6762/// structural-variant family on `LispError`.
6763///
6764/// `position` is the loop index inside `parse_params`, i.e. the 0-based
6765/// index of the offending element within the param list (`(defmacro f
6766/// (a 5 b) …)` — position 1 is the literal `5`); naming it lets an LSP
6767/// quick-fix point at the exact list element instead of the whole
6768/// param list. `got` is the offending `Sexp` projected through
6769/// `Display` so the operator sees the literal value they wrote
6770/// (`5`, `:foo`, `(nested)`) at the variant boundary; the helper takes
6771/// `&Sexp` (parallel to `non_symbol_unquote_target` and
6772/// `splice_outside_list`) and projects through `to_string()` so the
6773/// variant stays lifetime-free.
6774///
6775/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6776/// inline copy still earns a named primitive once the structural shape
6777/// is named (the test count gives this the fail-before-pass-after edge,
6778/// parallel to how `OddKwargs` was lifted from a single site for the
6779/// structural-completeness payoff). THEORY.md §V.1 — knowable platform;
6780/// the structural variant exposes `position` / `got` as first-class
6781/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the
6782/// data shape instead of substring-parsing the rendered diagnostic.
6783/// THEORY.md §II.1 invariant 1 — typed entry; a non-symbol element
6784/// inside a defmacro param list is exactly the failure mode the
6785/// typed-entry gate exists to reject — and it must reject DEFINITIONS
6786/// as readily as it rejects CALLS.
6787fn non_symbol_param(position: usize, got: &Sexp) -> LispError {
6788    LispError::NonSymbolParam {
6789        position,
6790        got: got.witness(),
6791    }
6792}
6793
6794/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6795/// "&rest needs a name" }` triple in `parse_params` behind ONE named
6796/// primitive. Sibling of `non_symbol_param`: that helper fires when a
6797/// NON-`&rest` element at a param position isn't a symbol; this helper
6798/// fires specifically on the post-`&rest` follower slot, where the
6799/// failure mode bifurcates into "missing entirely" (`got = None`) vs.
6800/// "present but not a symbol" (`got = Some(...)`). Together, the two
6801/// helpers close the `parse_params` walker — every distinct failure
6802/// mode the walker can emit is now a structural variant of `LispError`,
6803/// not a `Compile`-shaped substring.
6804///
6805/// `rest_position` is the loop index inside `parse_params` at which
6806/// the `&rest` marker was matched, i.e. the 0-based index of `&rest`
6807/// within the param list (`(defmacro f (a &rest 5) …)` — rest_position
6808/// 1 is `&rest`, the offender follows at 2); naming the marker
6809/// position lets an LSP quick-fix point at the `&rest` form itself
6810/// rather than at the next list element. `got` is `Option<&Sexp>`
6811/// because the follower slot bifurcates: `None` when the marker was
6812/// the param list's last element (no follower at all), `Some(sexp)`
6813/// when a follower exists but isn't a symbol; the helper projects
6814/// through `to_string()` at the variant boundary so the variant stays
6815/// lifetime-free.
6816///
6817/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6818/// inline copy still earns a named primitive once the structural shape
6819/// is named (the test count gives this the fail-before-pass-after
6820/// edge, parallel to how `non_symbol_param` was lifted from a single
6821/// site for the structural-completeness payoff). THEORY.md §V.1 —
6822/// knowable platform; the structural variant exposes `rest_position` /
6823/// `got` as first-class fields so authoring tools (LSP, REPL,
6824/// `tatara-check`) bind to the data shape instead of substring-parsing
6825/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6826/// a `&rest` marker followed by no name (or by a non-symbol) is
6827/// exactly the failure mode the typed-entry gate exists to reject —
6828/// and the gate must reject DEFINITIONS as readily as it rejects
6829/// CALLS.
6830fn rest_param_missing_name(rest_position: usize, got: Option<&Sexp>) -> LispError {
6831    LispError::RestParamMissingName {
6832        rest_position,
6833        got: got.map(Sexp::witness),
6834    }
6835}
6836
6837/// The third and final `parse_params` definition-site rejection — a
6838/// `&rest <name>` followed by further tokens. Sibling of `non_symbol_param`
6839/// (a param slot that isn't a symbol) and `rest_param_missing_name` (the
6840/// post-`&rest` follower is missing or malformed): this helper fires once
6841/// the rest name is bound and the walker finds the param list does not end
6842/// there. The `&rest` name absorbs every remaining call arg, so it is
6843/// structurally the LAST param a list can name; trailing tokens are
6844/// unrepresentable in `MacroParams` and were previously dropped silently.
6845///
6846/// `rest_position` is the loop index of the `&rest` marker (parallel to
6847/// `rest_param_missing_name`); `trailing` is the non-empty token run after
6848/// the bound rest name — the helper records its length and the typed
6849/// witness of its first element. The caller guarantees `trailing` is
6850/// non-empty (it is only built when `list[i + 2..].first()` is `Some`), so
6851/// `trailing[0]` does not panic.
6852///
6853/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6854/// unrepresentable"; a param list with tokens after `&rest <name>` is
6855/// nonsense `MacroParams` cannot hold, so the gate must REJECT it rather
6856/// than truncate to the representable prefix. THEORY.md §II.1 invariant 1 —
6857/// typed entry; the gate rejects malformed DEFINITIONS as readily as
6858/// malformed calls. THEORY.md §VI.1 — generation over composition; this
6859/// closes the `parse_params` walker's last uncovered failure mode, making
6860/// the sibling docs' "every distinct failure mode is a structural variant"
6861/// claim finally true.
6862fn rest_param_trailing_tokens(rest_position: usize, trailing: &[Sexp]) -> LispError {
6863    LispError::RestParamTrailingTokens {
6864        rest_position,
6865        extra: trailing.len(),
6866        first: trailing[0].witness(),
6867    }
6868}
6869
6870/// A `&optional` marker appeared a SECOND time in one param list —
6871/// `(defmacro f (a &optional b &optional c) …)`. The lambda-list has exactly
6872/// ONE optional section (between the required run and the rest); a second
6873/// `&optional` is nonsense `MacroParams` cannot hold (its `optional` field is
6874/// one flat run, not a sequence of sections). Without this gate the parser
6875/// would otherwise treat the second `&optional` as an optional param literally
6876/// NAMED `&optional`, binding call args to a marker symbol — exactly the kind
6877/// of silent misalignment the typed shape exists to forbid.
6878///
6879/// Sibling of `rest_param_trailing_tokens` (the rest-section ordering gate):
6880/// both reject a param list whose marker structure the canonical lambda-list
6881/// ordering cannot represent. `first_position` is the loop index of the
6882/// first `&optional`, `second_position` the second — naming both lets an LSP
6883/// quick-fix point at the redundant marker to delete.
6884///
6885/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6886/// unrepresentable"; a param list with two `&optional` sections is nonsense
6887/// `MacroParams` cannot hold, so the gate must REJECT rather than bind args
6888/// to a marker symbol. THEORY.md §II.1 invariant 1 — typed entry; the gate
6889/// rejects malformed DEFINITIONS as readily as malformed calls.
6890fn optional_marker_repeated(first_position: usize, second_position: usize) -> LispError {
6891    LispError::OptionalMarkerRepeated {
6892        first_position,
6893        second_position,
6894    }
6895}
6896
6897/// An `&optional` section entry that's a `Sexp::List` did NOT match the only
6898/// admissible shape `(NAME DEFAULT)` — exactly two elements with a symbol
6899/// head. This helper builds the structural rejection from the loop position,
6900/// the offending list form (projected through `crate::domain::sexp_witness`
6901/// to carry both `SexpShape::List` and the literal display), and the typed
6902/// `OptionalParamMalformedReason` naming which of the four malformed shapes
6903/// fired (empty list / missing default / extra elements / non-symbol name).
6904///
6905/// Sibling of `optional_marker_repeated` (the `&optional`-section marker
6906/// gate) and `non_symbol_param` (the bare-symbol gate): the three together
6907/// close every distinct typed-entry rejection the optional section can
6908/// emit. The bare-symbol form `&optional x` is still routed through
6909/// `non_symbol_param`'s sibling acceptance path; the list form `&optional
6910/// (x default)` is admitted iff this gate accepts the spec.
6911///
6912/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6913/// unrepresentable"; an `&optional` list spec of any other shape is
6914/// nonsense `MacroParams` cannot hold, so the gate must REJECT rather than
6915/// bind args to a marker symbol or drop the extras silently. THEORY.md
6916/// §II.1 invariant 1 — typed entry; a malformed default-form spec is
6917/// exactly the failure mode the typed-entry gate exists to reject — and
6918/// the gate must reject DEFINITIONS as readily as it rejects CALLS.
6919fn optional_param_malformed(
6920    position: usize,
6921    got: &Sexp,
6922    reason: crate::error::OptionalParamMalformedReason,
6923) -> LispError {
6924    LispError::OptionalParamMalformed {
6925        position,
6926        got: got.witness(),
6927        reason,
6928    }
6929}
6930
6931/// Lift the lone `LispError::Compile { form: head.to_string(), message:
6932/// "(defmacro name (params) body) required" }` triple in
6933/// `macro_def_from` behind ONE named primitive. Sibling of
6934/// `non_symbol_param` and `rest_param_missing_name`: those helpers
6935/// fire INSIDE `parse_params`, AFTER the arity gate has passed; this
6936/// helper fires AT the arity gate itself, BEFORE name / params / body
6937/// validation can run. Together the three close `macro_def_from`'s
6938/// outermost rejection chain — every distinct failure mode the gate
6939/// can emit at the top level becomes a structural variant of
6940/// `LispError`, not a `Compile`-shaped substring.
6941///
6942/// `head` is `MacroDefHead` (the typed closed-set enum), having been
6943/// projected through `MacroDefHead::from_keyword` at the top of
6944/// `macro_def_from`. The helper threads `head` straight into the
6945/// variant's typed `head: MacroDefHead` slot — no `&'static str`
6946/// projection at the helper boundary; the projection through
6947/// `MacroDefHead::keyword()` happens at Display rendering time via
6948/// `MacroDefHead`'s Display impl inside the variant's `#[error(...)]`
6949/// annotation. Same posture as how
6950/// `compiler_spec.rs::compiler_spec_io_err` threads
6951/// `CompilerSpecIoStage` straight into
6952/// `LispError::CompilerSpecIo.stage`. `arity` is `usize` (the length
6953/// of the form including the head element).
6954///
6955/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6956/// inline copy still earns a named primitive once the structural
6957/// shape is named (the test count gives this the fail-before/pass-
6958/// after edge, parallel to how `non_symbol_param` and
6959/// `rest_param_missing_name` were lifted from a single site for the
6960/// structural-completeness payoff). THEORY.md §V.1 — knowable
6961/// platform; the structural variant exposes `head` / `arity` as
6962/// first-class fields so authoring tools (LSP, REPL, `tatara-check`)
6963/// bind to the data shape instead of substring-parsing the rendered
6964/// diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a defmacro
6965/// form with too few elements is exactly the failure mode the typed-
6966/// entry gate exists to reject — and the gate must reject
6967/// DEFINITIONS as readily as it rejects CALLS. THEORY.md §II.1
6968/// invariant 2 — free middle; the arity gate fires inside
6969/// `macro_def_from` BEFORE either expansion strategy runs, so both
6970/// `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
6971/// (substitute) reject the SAME malformed defmacro at the SAME gate.
6972fn defmacro_arity(head: MacroDefHead, arity: usize) -> LispError {
6973    LispError::DefmacroArity { head, arity }
6974}
6975
6976/// Lift the lone `LispError::Compile { form: head.to_string(), message:
6977/// "expected name symbol" }` triple in `macro_def_from` behind ONE
6978/// named primitive. Sibling of `defmacro_arity`, `non_symbol_param`,
6979/// and `rest_param_missing_name`: those helpers fire at the OUTERMOST
6980/// arity gate (`defmacro_arity`) or INSIDE `parse_params`
6981/// (`non_symbol_param`, `rest_param_missing_name`); this helper fires
6982/// AFTER the arity gate has passed but BEFORE `parse_params` runs —
6983/// at the second of three `macro_def_from` rejection points
6984/// (arity → name-symbol → param-list → parse_params).
6985///
6986/// Walking a malformed `(defmacro …)` from the outside in, the gate
6987/// fires:
6988///   1. `defmacro_arity(head, arity)` if the form has fewer than 4
6989///      elements (`(defmacro)`, `(defmacro f)`).
6990///   2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't a
6991///      symbol (`(defmacro 5 () body)`, `(defmacro :foo () body)`).
6992///   3. The `expected param list` gate (NEXT LIFT) if list[2] isn't a
6993///      list (`(defmacro f x body)`).
6994///   4. Inside `parse_params`: `non_symbol_param` and
6995///      `rest_param_missing_name`.
6996///
6997/// After this lift step 2 is structural; the only remaining
6998/// `Compile`-shaped site in `macro_def_from` is step 3 (`expected
6999/// param list`).
7000///
7001/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7002/// projected through `MacroDefHead::from_keyword` at the top of
7003/// `macro_def_from`. The helper threads `head` straight into the
7004/// variant's typed `head: MacroDefHead` slot — same posture as
7005/// `defmacro_arity` after the typed-slot promotion. `got` is `&Sexp`
7006/// at the call site (a borrow into the form's name slot); the helper
7007/// projects through `crate::domain::sexp_witness` — the typed joint
7008/// projection (`SexpShape` + `Sexp::Display`) — so the variant's
7009/// `got: SexpWitness` slot carries BOTH structural shape AND
7010/// renderable literal across the boundary, parallel to how
7011/// `non_symbol_param` and `non_symbol_unquote_target` project their
7012/// `&Sexp` arguments. The fourth consumer of the typed `SexpWitness`
7013/// primitive on the substrate's Sexp-display-source rejection
7014/// surface.
7015///
7016/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7017/// inline copy still earns a named primitive once the structural
7018/// shape is named (the test count gives this the fail-before/pass-
7019/// after edge, parallel to how `defmacro_arity`, `non_symbol_param`,
7020/// and `rest_param_missing_name` were lifted from a single site for
7021/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7022/// platform; the structural variant exposes `head` / `got` as
7023/// first-class fields so authoring tools (LSP, REPL,
7024/// `tatara-check`) bind to the data shape instead of substring-
7025/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7026/// typed entry; a defmacro form whose name slot isn't a symbol is
7027/// exactly the failure mode the typed-entry gate exists to reject —
7028/// and the gate must reject DEFINITIONS as readily as it rejects
7029/// CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7030/// name-symbol gate fires inside `macro_def_from` BEFORE either
7031/// expansion strategy runs, so both `Expander::new()` (bytecode) and
7032/// `Expander::new_substitute_only()` (substitute) reject the SAME
7033/// malformed defmacro at the SAME gate.
7034fn defmacro_non_symbol_name(head: MacroDefHead, got: &Sexp) -> LispError {
7035    LispError::DefmacroNonSymbolName {
7036        head,
7037        got: got.witness(),
7038    }
7039}
7040
7041/// Lift the lone `LispError::Compile { form: head.to_string(), message:
7042/// "expected param list" }` triple in `macro_def_from` behind ONE
7043/// named primitive. Sibling of `defmacro_arity`,
7044/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7045/// `rest_param_missing_name`: those helpers fire at the OUTERMOST
7046/// arity gate (`defmacro_arity`), at the second `macro_def_from`
7047/// rejection point (`defmacro_non_symbol_name`), or INSIDE
7048/// `parse_params` (`non_symbol_param`, `rest_param_missing_name`);
7049/// this helper fires AFTER both the arity gate AND the name-symbol
7050/// gate have passed but BEFORE `parse_params` runs — at the third
7051/// of three `macro_def_from` rejection points
7052/// (arity → name-symbol → param-list → parse_params).
7053///
7054/// Walking a malformed `(defmacro …)` from the outside in, the gate
7055/// fires:
7056///   1. `defmacro_arity(head, arity)` if the form has fewer than 4
7057///      elements (`(defmacro)`, `(defmacro f)`).
7058///   2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't
7059///      a symbol (`(defmacro 5 () body)`).
7060///   3. `defmacro_non_list_params(head, &list[2])` if list[2] isn't
7061///      a list (`(defmacro f x body)`, `(defmacro f 5 body)`).
7062///   4. Inside `parse_params`: `non_symbol_param` and
7063///      `rest_param_missing_name`.
7064///
7065/// After this lift step 3 is structural; every inline
7066/// `LispError::Compile { … }` triple in `macro_def_from` has been
7067/// lifted to a structural variant — the entire `macro_def_from`
7068/// rejection chain is structurally typed for failure modes.
7069///
7070/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7071/// projected through `MacroDefHead::from_keyword` at the top of
7072/// `macro_def_from`. The helper threads `head` straight into the
7073/// variant's typed `head: MacroDefHead` slot — same posture as
7074/// `defmacro_arity` and `defmacro_non_symbol_name` after the
7075/// typed-slot promotion. `got` is `&Sexp` at the call site (a
7076/// borrow into the form's param-list slot); the helper projects
7077/// through `crate::domain::sexp_witness(_)` — the typed joint
7078/// primitive that pairs the offending `Sexp`'s `SexpShape` with its
7079/// `Sexp::Display` projection in ONE owned `SexpWitness` value, so
7080/// authoring tools bind to both the structural shape AND the rendered
7081/// literal across the variant slot. Same posture as `non_symbol_param`,
7082/// `non_symbol_unquote_target`, `splice_outside_list`, and
7083/// `defmacro_non_symbol_name`'s helpers after the typed-witness
7084/// promotion of their `got` slots.
7085///
7086/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7087/// inline copy still earns a named primitive once the structural
7088/// shape is named (the test count gives this the fail-before/pass-
7089/// after edge, parallel to how `defmacro_arity`,
7090/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7091/// `rest_param_missing_name` were lifted from a single site for
7092/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7093/// platform; the structural variant exposes `head` / `got` as
7094/// first-class fields so authoring tools (LSP, REPL,
7095/// `tatara-check`) bind to the data shape instead of substring-
7096/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7097/// typed entry; a defmacro form whose param-list slot isn't a list
7098/// is exactly the failure mode the typed-entry gate exists to
7099/// reject — and the gate must reject DEFINITIONS as readily as it
7100/// rejects CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7101/// param-list gate fires inside `macro_def_from` BEFORE either
7102/// expansion strategy runs, so both `Expander::new()` (bytecode)
7103/// and `Expander::new_substitute_only()` (substitute) reject the
7104/// SAME malformed defmacro at the SAME gate.
7105fn defmacro_non_list_params(head: MacroDefHead, got: &Sexp) -> LispError {
7106    LispError::DefmacroNonListParams {
7107        head,
7108        got: got.witness(),
7109    }
7110}
7111
7112/// Project a `bindings: &HashMap<String, Sexp>` into the `&[&str]` candidate
7113/// set `crate::domain::suggest` wants. Cold path — only allocated when an
7114/// `,name` / `,@name` substitution misses, i.e. when we're already on the
7115/// diagnostic side of the substitute walker.
7116fn bound_names(bindings: &HashMap<String, Sexp>) -> Vec<&str> {
7117    bindings.keys().map(String::as_str).collect()
7118}
7119
7120#[cfg(test)]
7121mod tests {
7122    use super::*;
7123    use crate::reader::read;
7124
7125    fn parse(src: &str) -> Sexp {
7126        read(src).unwrap().into_iter().next().unwrap()
7127    }
7128
7129    #[test]
7130    fn identity_macro() {
7131        let mut e = Expander::new();
7132        let forms = read("(defmacro id (x) `,x) (id 42)").unwrap();
7133        let out = e.expand_program(forms).unwrap();
7134        assert_eq!(out.len(), 1);
7135        assert_eq!(out[0], Sexp::int(42));
7136    }
7137
7138    #[test]
7139    fn wrap_macro_duplicates_arg() {
7140        let mut e = Expander::new();
7141        let forms = read("(defmacro wrap (x) `(list ,x ,x)) (wrap hello)").unwrap();
7142        let out = e.expand_program(forms).unwrap();
7143        assert_eq!(out[0], parse("(list hello hello)"));
7144    }
7145
7146    #[test]
7147    fn rest_param_splices_with_at() {
7148        let mut e = Expander::new();
7149        let forms = read("(defmacro call (f &rest args) `(,f ,@args)) (call foo a b c)").unwrap();
7150        let out = e.expand_program(forms).unwrap();
7151        assert_eq!(out[0], parse("(foo a b c)"));
7152    }
7153
7154    #[test]
7155    fn nested_macro_expansion() {
7156        let mut e = Expander::new();
7157        let forms = read(
7158            "(defmacro twice (x) `(list ,x ,x))
7159             (defmacro quad (x) `(twice ,x))
7160             (quad hey)",
7161        )
7162        .unwrap();
7163        let out = e.expand_program(forms).unwrap();
7164        assert_eq!(out[0], parse("(list hey hey)"));
7165    }
7166
7167    #[test]
7168    fn unbound_unquote_errors() {
7169        let mut e = Expander::new();
7170        let forms = read("(defmacro bad (x) `(list ,y)) (bad 1)").unwrap();
7171        assert!(e.expand_program(forms).is_err());
7172    }
7173
7174    #[test]
7175    fn missing_required_arg_errors() {
7176        let mut e = Expander::new();
7177        let forms = read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap();
7178        assert!(e.expand_program(forms).is_err());
7179    }
7180
7181    #[test]
7182    fn defpoint_template_treated_as_defmacro() {
7183        let mut e = Expander::new();
7184        let forms = read(
7185            "(defpoint-template obs (name) `(defpoint ,name :class (Gate Observability)))
7186             (obs grafana)",
7187        )
7188        .unwrap();
7189        let out = e.expand_program(forms).unwrap();
7190        assert_eq!(
7191            out[0],
7192            parse("(defpoint grafana :class (Gate Observability))")
7193        );
7194    }
7195
7196    #[test]
7197    fn defcheck_treated_as_defmacro() {
7198        let mut e = Expander::new();
7199        let forms = read(
7200            "(defcheck pair (a b) `(do (yaml-parses ,a) (yaml-parses ,b)))
7201             (pair \"x.yaml\" \"y.yaml\")",
7202        )
7203        .unwrap();
7204        let out = e.expand_program(forms).unwrap();
7205        assert_eq!(
7206            out[0],
7207            parse("(do (yaml-parses \"x.yaml\") (yaml-parses \"y.yaml\"))")
7208        );
7209    }
7210
7211    #[test]
7212    fn empty_rest_splices_nothing() {
7213        let mut e = Expander::new();
7214        let forms = read("(defmacro f (x &rest r) `(list ,x ,@r)) (f 1)").unwrap();
7215        let out = e.expand_program(forms).unwrap();
7216        assert_eq!(out[0], parse("(list 1)"));
7217    }
7218
7219    #[test]
7220    fn macro_expanded_inside_list() {
7221        // A macro call nested in a list position also expands.
7222        let mut e = Expander::new();
7223        let forms = read("(defmacro two () `(list 1 2)) (outer (two))").unwrap();
7224        let out = e.expand_program(forms).unwrap();
7225        assert_eq!(out[0], parse("(outer (list 1 2))"));
7226    }
7227
7228    // ── Compiled-template bytecode equivalence + speedup ──────────────
7229
7230    #[test]
7231    fn compiled_template_matches_substitute_path() {
7232        // Same program, two expanders with different strategies — outputs must agree.
7233        let src = "
7234            (defmacro wrap (x) `(list ,x ,x))
7235            (defmacro call (f &rest args) `(,f ,@args))
7236            (defmacro twice (x) `(list ,x ,x))
7237            (defmacro quad (x) `(twice ,x))
7238            (wrap hello)
7239            (call foo a b c)
7240            (quad hey)
7241            (outer (wrap deep))
7242        ";
7243        let forms = read(src).unwrap();
7244        let mut fast = Expander::new();
7245        let mut slow = Expander::new_substitute_only();
7246        let out_fast = fast.expand_program(forms.clone()).unwrap();
7247        let out_slow = slow.expand_program(forms).unwrap();
7248        assert_eq!(out_fast, out_slow);
7249    }
7250
7251    #[test]
7252    fn literal_subtree_compiles_to_single_literal_op() {
7253        // Macro body where only one leaf is a substitution — the rest of the
7254        // template is literal, so the compiler should prune large chunks to
7255        // a single Literal op.
7256        let def = MacroDef {
7257            name: "label".into(),
7258            params: MacroParams {
7259                required: vec!["x".into()],
7260                optional: Vec::new(),
7261                rest: None,
7262            },
7263            body: Sexp::Quasiquote(Box::new(parse(
7264                "(observed (at timestamp) (in region) (value ,x) (tags (one two three)))",
7265            ))),
7266        };
7267        let compiled = compile_template(&def).expect("compile");
7268        // The template is ONE list. After compile:
7269        //   BeginList,
7270        //     Literal((observed (at timestamp) (in region))), // wait — `observed` is a list too
7271        //     ...
7272        //   EndList
7273        // Point is: many subtrees should be single Literals. We simply count
7274        // that the op stream is SHORTER than the full Sexp size.
7275        let ops_count = compiled.ops.len();
7276        assert!(
7277            ops_count < 15,
7278            "expected pruned op stream, got {ops_count} ops: {:?}",
7279            compiled.ops
7280        );
7281    }
7282
7283    /// The cache-friendly workload the two tests below share: 10 macros, and
7284    /// calls that hit only 10 distinct (macro, args) pairs, so a memo should
7285    /// serve 99.9% of them.
7286    fn cache_friendly_workload(calls: usize) -> String {
7287        let macros = "
7288            (defmacro m1 (a b) `(list ,a ,b))
7289            (defmacro m2 (x) `(if ,x true false))
7290            (defmacro m3 (a b c) `(list ,a ,b ,c ,a ,b ,c))
7291            (defmacro m4 (f &rest args) `(,f ,@args))
7292            (defmacro m5 (x) `(and ,x (not (not ,x))))
7293            (defmacro m6 (a b) `(or ,a ,b (and ,a ,b)))
7294            (defmacro m7 (x) `(debug (at timestamp) (in region) (value ,x)))
7295            (defmacro m8 (x y) `(cond ((= ,x ,y) equal) (#t not-equal)))
7296            (defmacro m9 (x) `(loop (times 10) (eval ,x)))
7297            (defmacro m10 (f g &rest args) `(,f (,g ,@args)))
7298        ";
7299        let mut call_src = String::with_capacity(calls * 12);
7300        for i in 0..calls {
7301            match i % 10 {
7302                0 => call_src.push_str("(m1 a b)\n"),
7303                1 => call_src.push_str("(m2 true)\n"),
7304                2 => call_src.push_str("(m3 x y z)\n"),
7305                3 => call_src.push_str("(m4 f a b c d e)\n"),
7306                4 => call_src.push_str("(m5 y)\n"),
7307                5 => call_src.push_str("(m6 a b)\n"),
7308                6 => call_src.push_str("(m7 answer)\n"),
7309                7 => call_src.push_str("(m8 p q)\n"),
7310                8 => call_src.push_str("(m9 body)\n"),
7311                _ => call_src.push_str("(m10 f g a b c)\n"),
7312            }
7313        }
7314        let all_src = format!("{macros}\n{call_src}");
7315        all_src
7316    }
7317
7318    /// **All three expansion strategies produce identical output.**
7319    ///
7320    /// The load-bearing invariant — THEORY.md §II.1's free middle. Fully
7321    /// deterministic, so it belongs in the ordinary suite. It used to be
7322    /// welded to the timing assertion below, which meant a benchmark losing a
7323    /// core to a neighbouring test reported *this* as failing, and the
7324    /// agreement property is the one you actually want a red build for.
7325    #[test]
7326    fn expansion_layers_agree_on_output() {
7327        let forms = read(&cache_friendly_workload(10_000)).unwrap();
7328
7329        let out_subst = Expander::new_substitute_only()
7330            .expand_program(forms.clone())
7331            .unwrap();
7332        let out_byte = Expander::new_bytecode_no_cache()
7333            .expand_program(forms.clone())
7334            .unwrap();
7335        let mut byte_cache = Expander::new();
7336        let out_cached = byte_cache.expand_program(forms).unwrap();
7337
7338        assert_eq!(out_subst, out_byte);
7339        assert_eq!(out_subst, out_cached);
7340
7341        // Cache captured the 10 unique (macro, args) pairs (plus some inner
7342        // expansions — macros that expand into calls to other macros).
7343        let cache_size = byte_cache.cache_size();
7344        assert!(
7345            (10..=50).contains(&cache_size),
7346            "expected ~10 unique cache entries, got {cache_size}"
7347        );
7348    }
7349
7350    /// **The expansion strategies, measured.**
7351    ///
7352    /// A wall-clock measurement, and therefore fragile in a way the agreement
7353    /// test above is not. Two changes keep it honest rather than merely quiet:
7354    ///
7355    /// - **Best-of-N, not a single sample.** The minimum over several runs is
7356    ///   the least contaminated statistic when 1900 sibling tests compete for
7357    ///   the same cores; a single sample measures contention as much as code.
7358    /// - **Margins that match measurement.** The previous version compared
7359    ///   with a bare `<` while its own comment claimed "a 1.5× threshold so
7360    ///   the test is stable across hosts". The threshold was never in the
7361    ///   code, and the bare comparison flaked in the full suite.
7362    ///
7363    /// # A finding, recorded so it is not re-derived
7364    ///
7365    /// Putting a real threshold on it surfaced something the old assertion hid.
7366    /// On a workload with a **99.9% cache hit rate**, the memo beats
7367    /// bytecode-no-cache by only **~1.17× in release and ~1.04× in debug** —
7368    /// not the "real, visible speedup" the original docstring asserted. The
7369    /// bare `<` had been reporting a 4% difference on one noisy sample as a
7370    /// win.
7371    ///
7372    /// The reading: expansion was never the expensive part of this path.
7373    /// Reading, walking and allocating the output tree dominate, so removing
7374    /// essentially *all* expansion work moves the total by a fifth. That is
7375    /// still worth having and it is not what the memo was sold as. The bytecode
7376    /// strategy is where the real gain is (~1.4× over substitution), and it is
7377    /// there with or without the cache.
7378    ///
7379    /// So the margins differ per axis, deliberately: a real one against
7380    /// substitution, and a non-regression bound against bytecode, because
7381    /// asserting 1.5× there would assert something untrue.
7382    #[test]
7383    fn the_expansion_cache_beats_both_uncached_strategies() {
7384        use std::time::{Duration, Instant};
7385
7386        /// Against substitution the gain is structural — bytecode plus a memo
7387        /// against a naive tree walk. Measured at 1.44–1.54×; gated well below
7388        /// that, since the purpose is to catch the fast path being *disabled*,
7389        /// not to track a ratio.
7390        const MARGIN_VS_SUBSTITUTE: f64 = 1.25;
7391        /// Against bytecode-no-cache the memo is a modest win (see above). The
7392        /// bound that is actually true and worth holding is that the memo must
7393        /// never *cost* more than it saves.
7394        const MAX_OVERHEAD_VS_BYTECODE: f64 = 1.05;
7395        const SAMPLES: usize = 5;
7396
7397        let forms = read(&cache_friendly_workload(10_000)).unwrap();
7398
7399        let best = |mut make: Box<dyn FnMut() -> Expander>| -> Duration {
7400            (0..SAMPLES)
7401                .map(|_| {
7402                    let mut e = make();
7403                    let f = forms.clone();
7404                    let t = Instant::now();
7405                    let out = e.expand_program(f).unwrap();
7406                    let d = t.elapsed();
7407                    std::hint::black_box(out);
7408                    d
7409                })
7410                .min()
7411                .expect("SAMPLES > 0")
7412        };
7413
7414        let t_subst = best(Box::new(Expander::new_substitute_only));
7415        let t_byte = best(Box::new(Expander::new_bytecode_no_cache));
7416        let t_cached = best(Box::new(Expander::new));
7417
7418        let vs_subst = t_subst.as_secs_f64() / t_cached.as_secs_f64();
7419        let vs_byte = t_byte.as_secs_f64() / t_cached.as_secs_f64();
7420        eprintln!(
7421            "\n=== macroexpand: 10k calls × 10 unique (macro, args) pairs, \
7422             best of {SAMPLES} ===\n\
7423             substitute only   : {t_subst:?}\n\
7424             bytecode no cache : {t_byte:?}\n\
7425             bytecode + cache  : {t_cached:?}\n\
7426             speedup           : {vs_subst:.2}× vs substitute, {vs_byte:.2}× vs bytecode\n"
7427        );
7428
7429        assert!(
7430            vs_subst >= MARGIN_VS_SUBSTITUTE,
7431            "the bytecode+cache path should beat substitution by \
7432             {MARGIN_VS_SUBSTITUTE}×, got {vs_subst:.2}× \
7433             ({t_cached:?} vs {t_subst:?})"
7434        );
7435        assert!(
7436            vs_byte >= 1.0 / MAX_OVERHEAD_VS_BYTECODE,
7437            "the memo must not cost more than it saves: {vs_byte:.2}× vs \
7438             bytecode-no-cache ({t_cached:?} vs {t_byte:?}). A ratio below 1 \
7439             means maintaining the cache is now pure overhead on this workload."
7440        );
7441    }
7442
7443    #[test]
7444    fn cache_respects_arg_changes() {
7445        // Cache must not return stale results when args differ.
7446        let src = "
7447            (defmacro wrap (x) `(list ,x ,x))
7448            (wrap a)
7449            (wrap b)
7450            (wrap a)   ;; same as first — cached hit
7451        ";
7452        let mut e = Expander::new();
7453        let out = e.expand_program(read(src).unwrap()).unwrap();
7454        assert_eq!(out.len(), 3);
7455        assert_eq!(out[0], parse("(list a a)"));
7456        assert_eq!(out[1], parse("(list b b)"));
7457        assert_eq!(out[2], parse("(list a a)"));
7458        // Two distinct args → 2 cache entries.
7459        assert_eq!(e.cache_size(), 2);
7460    }
7461
7462    #[test]
7463    fn clear_cache_empties_memo() {
7464        let mut e = Expander::new();
7465        let out = e
7466            .expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
7467            .unwrap();
7468        assert_eq!(out.len(), 2);
7469        assert_eq!(e.cache_size(), 2);
7470        e.clear_cache();
7471        assert_eq!(e.cache_size(), 0);
7472    }
7473
7474    // ── Unbound template-var: structural variant + did-you-mean hint ──
7475
7476    /// Helper for the unbound-template-var tests — pins the variant shape
7477    /// and carries any error context up to the assert site for legibility.
7478    fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
7479        match err {
7480            LispError::UnboundTemplateVar { prefix, name, hint } => {
7481                (*prefix, name.as_str(), hint.as_deref())
7482            }
7483            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7484        }
7485    }
7486
7487    #[test]
7488    fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
7489        // `,xs` against macro params `[x]` — distance 1, bound 1 — hints `,x`.
7490        // Path: compile_node Unquote (the bytecode-template compile, default
7491        // expander).
7492        let mut e = Expander::new();
7493        let err = e
7494            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7495            .expect_err("unbound template var must error");
7496        let (prefix, name, hint) = unbound_var(&err);
7497        assert_eq!(prefix, UnquoteForm::Unquote);
7498        assert_eq!(name, "xs");
7499        assert_eq!(hint, Some("x"));
7500    }
7501
7502    #[test]
7503    fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
7504        // `,@argz` against macro params `[args]` — distance 1, bound 2 —
7505        // hints `,@args`. Path: compile_node UnquoteSplice.
7506        let mut e = Expander::new();
7507        let err = e
7508            .expand_program(
7509                read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7510            )
7511            .expect_err("unbound splice must error");
7512        let (prefix, name, hint) = unbound_var(&err);
7513        assert_eq!(prefix, UnquoteForm::Splice);
7514        assert_eq!(name, "argz");
7515        assert_eq!(hint, Some("args"));
7516    }
7517
7518    #[test]
7519    fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
7520        // Same shape but routed through the substitute-only expander — proves
7521        // the substitute path emits the same variant as the bytecode path.
7522        let mut e = Expander::new_substitute_only();
7523        let err = e
7524            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7525            .expect_err("substitute unbound must error");
7526        let (prefix, name, hint) = unbound_var(&err);
7527        assert_eq!(prefix, UnquoteForm::Unquote);
7528        assert_eq!(name, "xs");
7529        assert_eq!(hint, Some("x"));
7530    }
7531
7532    #[test]
7533    fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
7534        // The substitute path's UnquoteSplice branch fires for splices that
7535        // appear inside a list during the recursive walk. `,@argz` against
7536        // `[args]` hints `,@args`.
7537        let mut e = Expander::new_substitute_only();
7538        let err = e
7539            .expand_program(
7540                read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7541            )
7542            .expect_err("substitute splice unbound must error");
7543        let (prefix, name, hint) = unbound_var(&err);
7544        assert_eq!(prefix, UnquoteForm::Splice);
7545        assert_eq!(name, "argz");
7546        assert_eq!(hint, Some("args"));
7547    }
7548
7549    #[test]
7550    fn unbound_template_var_omits_hint_when_no_close_match() {
7551        // `,wholly-unrelated` against `[x]` — far past the bound, so no
7552        // hint. Negative control: a wrong hint is worse than no hint, so
7553        // the slot must stay empty when the substrate isn't confident.
7554        let mut e = Expander::new();
7555        let err = e
7556            .expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
7557            .expect_err("unrelated unbound must error");
7558        let (prefix, name, hint) = unbound_var(&err);
7559        assert_eq!(prefix, UnquoteForm::Unquote);
7560        assert_eq!(name, "wholly-unrelated");
7561        assert_eq!(hint, None);
7562    }
7563
7564    #[test]
7565    fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
7566        // End-to-end through the Display impl — pins the rendered diagnostic
7567        // a downstream tool sees today (REPL, tatara-check). Hint stays
7568        // additive: the legacy `"unbound"` substring still appears, so any
7569        // assertion that pattern-matches on it keeps passing.
7570        let mut e = Expander::new();
7571        let err = e
7572            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7573            .expect_err("unbound must error");
7574        let msg = format!("{err}");
7575        assert!(
7576            msg.contains("did you mean ,x?"),
7577            "expected hint suffix in message, got: {msg}"
7578        );
7579        assert!(
7580            msg.contains("unbound"),
7581            "expected legacy `unbound` substring in message, got: {msg}"
7582        );
7583        assert!(
7584            msg.contains(",xs"),
7585            "expected the offending form in message, got: {msg}"
7586        );
7587    }
7588
7589    #[test]
7590    fn unbound_template_var_position_is_none_today() {
7591        // Negative control for the future-spans move: until `Sexp` carries
7592        // source positions, `position()` returns `None` for this variant.
7593        let mut e = Expander::new();
7594        let err = e
7595            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7596            .expect_err("unbound must error");
7597        assert_eq!(err.position(), None);
7598    }
7599
7600    // ── Non-symbol unquote target: structural variant ─────────────────
7601
7602    /// Helper for the non-symbol-unquote-target tests — pins the variant
7603    /// shape and carries any error context up to the assert site for
7604    /// legibility. Sibling of `unbound_var` and `splice_outside_list_got`;
7605    /// returns the `display` projection of the typed `SexpWitness` so the
7606    /// existing call sites stay byte-for-byte comparable to the legacy
7607    /// `got: String` shape.
7608    fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
7609        match err {
7610            LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
7611            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7612        }
7613    }
7614
7615    #[test]
7616    fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
7617        // `,(list 1 2)` — the inner is a list, not a symbol. Path:
7618        // compile_node Unquote (the bytecode-template compile, default
7619        // expander). Pins variant identity AND prefix AND the offending
7620        // literal so a regression that re-inlines the legacy
7621        // `LispError::Compile` shape fails-loudly here.
7622        let mut e = Expander::new();
7623        let err = e
7624            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7625            .expect_err("non-symbol unquote target must error");
7626        let (prefix, got) = non_symbol_target(&err);
7627        assert_eq!(prefix, UnquoteForm::Unquote);
7628        assert_eq!(got, "(list 1 2)");
7629    }
7630
7631    #[test]
7632    fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
7633        // `,@5` — the inner is an int atom, not a symbol. Path:
7634        // compile_node UnquoteSplice. The integer literal round-trips
7635        // through the variant's `got` slot via `Sexp::Display`.
7636        let mut e = Expander::new();
7637        let err = e
7638            .expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
7639            .expect_err("non-symbol splice target must error");
7640        let (prefix, got) = non_symbol_target(&err);
7641        assert_eq!(prefix, UnquoteForm::Splice);
7642        assert_eq!(got, "5");
7643    }
7644
7645    #[test]
7646    fn non_symbol_unquote_in_substitute_emits_structural_variant() {
7647        // Same shape as the bytecode path but routed through the
7648        // substitute-only expander — proves the substitute path emits the
7649        // same variant as the compile_node path. Pins that the lift is
7650        // path-uniform.
7651        let mut e = Expander::new_substitute_only();
7652        let err = e
7653            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7654            .expect_err("substitute non-symbol target must error");
7655        let (prefix, got) = non_symbol_target(&err);
7656        assert_eq!(prefix, UnquoteForm::Unquote);
7657        assert_eq!(got, "(list 1 2)");
7658    }
7659
7660    #[test]
7661    fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
7662        // The substitute path's UnquoteSplice-inside-list branch fires for
7663        // splices that appear inside a list during the recursive walk.
7664        // `,@(list 1 2)` inside the body — the inner is a literal list, not
7665        // a symbol — emits the same variant as the compile_node path.
7666        let mut e = Expander::new_substitute_only();
7667        let err = e
7668            .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7669            .expect_err("substitute non-symbol splice must error");
7670        let (prefix, got) = non_symbol_target(&err);
7671        assert_eq!(prefix, UnquoteForm::Splice);
7672        assert_eq!(got, "(list 1 2)");
7673    }
7674
7675    #[test]
7676    fn non_symbol_unquote_target_position_is_none_today() {
7677        // Negative control for the future-spans move: until `Sexp` carries
7678        // source positions, `position()` returns `None` for this variant.
7679        // A future run that gives `Sexp` source spans adds `pos:
7680        // Option<usize>` to ONE place; this test gives that change a
7681        // deliberate fail-before/pass-after delta.
7682        let mut e = Expander::new();
7683        let err = e
7684            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7685            .expect_err("non-symbol target must error");
7686        assert_eq!(err.position(), None);
7687    }
7688
7689    // ── unquote_target_symbol: typed gate-1 primitive for ,X / ,@X ──────
7690    //
7691    // The `unquote_target_symbol(inner, form)?` primitive lifts the
7692    // inline `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(
7693    // form, inner))?` pattern that previously appeared at four call
7694    // sites (`compile_node` Unquote/UnquoteSplice + `substitute` Unquote
7695    // + `substitute` list-inner UnquoteSplice) behind ONE named
7696    // primitive. The tests below pin: (a) the Ok-arm borrows the
7697    // symbol name from `inner` for both UnquoteForm variants; (b) the
7698    // Err-arm routes through `non_symbol_unquote_target` and emits the
7699    // structural `LispError::NonSymbolUnquoteTarget` variant carrying
7700    // the typed `SexpWitness` (joint shape + display identity) for the
7701    // closed set of reachable non-symbol shapes (int / keyword / list /
7702    // nil); (c) the helper is path-uniform — the same Ok / Err
7703    // contracts hold regardless of which call site invokes it. A
7704    // regression that re-inlines the gate-1 projection at any of the
7705    // four call sites can no longer drift independent of the others —
7706    // the helper IS the gate.
7707
7708    #[test]
7709    fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
7710        // Positive control for the Ok-arm: `inner = Sexp::Symbol("xs")`
7711        // under `UnquoteForm::Unquote` projects through `as_symbol()`
7712        // to the borrowed `&str`. The returned slice's lifetime is
7713        // tied to `inner` so the caller can feed it directly into
7714        // `params.iter().position(...)` (`compile_node`) or
7715        // `bindings.get(...)` (`substitute`) without an intermediate
7716        // allocation. Fail-before/pass-after: this assert is meaningless
7717        // pre-lift because the helper does not exist; post-lift it
7718        // pins the typed gate-1 contract at the named primitive.
7719        let inner = Sexp::symbol("xs");
7720        let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7721            .expect("symbol inner must project to Ok");
7722        assert_eq!(name, "xs");
7723    }
7724
7725    #[test]
7726    fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
7727        // Sibling positive control: `UnquoteForm::Splice` shares the
7728        // gate-1 contract with `Unquote`. The helper is path-uniform
7729        // across both syntactic markers — a regression that bifurcates
7730        // the two arms (e.g., accepting non-symbols for `,@X` but not
7731        // `,X`) fails-loudly here. Pins that the closed-set
7732        // `UnquoteForm` enum's two variants share ONE projection
7733        // posture across the gate-1 boundary.
7734        let inner = Sexp::symbol("rest");
7735        let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
7736            .expect("symbol inner must project to Ok under Splice");
7737        assert_eq!(name, "rest");
7738    }
7739
7740    #[test]
7741    fn unquote_target_symbol_rejects_int_inner_under_unquote() {
7742        // Negative control for the Err-arm: `inner = Sexp::Int(5)` is
7743        // NOT a symbol — the gate-1 projection fires and routes through
7744        // `non_symbol_unquote_target` to the structural
7745        // `LispError::NonSymbolUnquoteTarget` variant. Pin the variant
7746        // identity AND the typed `SexpWitness` joint identity (shape +
7747        // display literal): a regression that drops the witness shape
7748        // or display fails-loudly here.
7749        let inner = Sexp::int(5);
7750        let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7751            .expect_err("int inner must error at gate-1");
7752        match err {
7753            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7754                assert_eq!(prefix, UnquoteForm::Unquote);
7755                assert_eq!(got.shape, crate::error::SexpShape::Int);
7756                assert_eq!(got.display, "5");
7757            }
7758            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7759        }
7760    }
7761
7762    #[test]
7763    fn unquote_target_symbol_rejects_list_inner_under_splice() {
7764        // Sibling negative control: `inner = (list 1 2)` is a list, not
7765        // a symbol — the gate-1 projection fires AND routes through
7766        // `non_symbol_unquote_target(UnquoteForm::Splice, inner)`. Pins
7767        // both the variant identity AND the typed witness's joint
7768        // shape (`SexpShape::List`) + display (`"(list 1 2)"`) so a
7769        // future shape drift fails-loudly. Sibling of the Int / Unquote
7770        // pin: closes the gate-1 contract across the closed-set
7771        // product of {Int, List, Keyword, …} × {Unquote, Splice}.
7772        let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
7773        let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
7774            .expect_err("list inner must error at gate-1");
7775        match err {
7776            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7777                assert_eq!(prefix, UnquoteForm::Splice);
7778                assert_eq!(got.shape, crate::error::SexpShape::List);
7779                assert_eq!(got.display, "(list 1 2)");
7780            }
7781            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7782        }
7783    }
7784
7785    #[test]
7786    fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
7787        // Pin a third reachable non-symbol shape: `Sexp::Keyword(":foo")`.
7788        // The gate-1 projection rejects keywords AS WELL as ints and
7789        // lists — closes the closed-set of "non-symbol shapes the gate
7790        // rejects" across one more reachable variant. The typed witness
7791        // carries `SexpShape::Keyword` + display `:foo` jointly so
7792        // authoring tools (REPL, LSP) bind on the structural shape
7793        // directly.
7794        let inner = Sexp::keyword("foo");
7795        let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7796            .expect_err("keyword inner must error at gate-1");
7797        match err {
7798            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7799                assert_eq!(prefix, UnquoteForm::Unquote);
7800                assert_eq!(got.shape, crate::error::SexpShape::Keyword);
7801                assert_eq!(got.display, ":foo");
7802            }
7803            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7804        }
7805    }
7806
7807    #[test]
7808    fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
7809        // Path-uniformity pin: end-to-end through ALL FOUR call sites
7810        // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7811        // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7812        // every non-symbol unquote target now routes through the SAME
7813        // `unquote_target_symbol(inner, form)?` helper. The four
7814        // end-to-end expansions below all reject with the SAME variant
7815        // (`NonSymbolUnquoteTarget`) — pins that the lift preserves the
7816        // path-uniform rejection contract `non_symbol_unquote_target`'s
7817        // prior lift established (and that drove the bytecode-vs-
7818        // substitute reunification in 0e9c… and successors). A
7819        // regression that re-inlines the gate-1 projection at one of
7820        // the four sites can drift the four call sites independent of
7821        // each other — this test would catch that drift.
7822        let cases: &[(&str, UnquoteForm)] = &[
7823            // compile_node Unquote (bytecode-path)
7824            ("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
7825            // compile_node UnquoteSplice (bytecode-path)
7826            ("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
7827        ];
7828        for (src, expected_form) in cases {
7829            let mut e = Expander::new();
7830            let err = e
7831                .expand_program(read(src).unwrap())
7832                .expect_err("non-symbol unquote target must error end-to-end");
7833            match err {
7834                LispError::NonSymbolUnquoteTarget { prefix, .. } => {
7835                    assert_eq!(prefix, *expected_form, "for src: {src}");
7836                }
7837                other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
7838            }
7839        }
7840        // substitute Unquote (substitute-only path) — sibling pin to
7841        // `non_symbol_unquote_in_substitute_emits_structural_variant`.
7842        let mut e_subst = Expander::new_substitute_only();
7843        let err = e_subst
7844            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7845            .expect_err("substitute Unquote must error end-to-end");
7846        assert!(
7847            matches!(
7848                err,
7849                LispError::NonSymbolUnquoteTarget {
7850                    prefix: UnquoteForm::Unquote,
7851                    ..
7852                }
7853            ),
7854            "expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
7855        );
7856        // substitute list-inner UnquoteSplice (substitute-only path) —
7857        // sibling pin to
7858        // `non_symbol_unquote_splice_inside_list_in_substitute_emits_…`.
7859        let mut e_subst2 = Expander::new_substitute_only();
7860        let err = e_subst2
7861            .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7862            .expect_err("substitute UnquoteSplice-in-list must error end-to-end");
7863        assert!(
7864            matches!(
7865                err,
7866                LispError::NonSymbolUnquoteTarget {
7867                    prefix: UnquoteForm::Splice,
7868                    ..
7869                }
7870            ),
7871            "expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
7872        );
7873    }
7874
7875    // ── Gate-2 (must-be-bound-in-scope) typed primitives ──────────────
7876    // Pins the contract of the two gate-2 helpers — `resolve_param_index`
7877    // (bytecode-template compile path) and `resolve_binding`
7878    // (substitute path) — that the four inline `<lookup>.ok_or_else(||
7879    // unbound_template_var(FORM, name, candidates))` projections at
7880    // `compile_node` Unquote/UnquoteSplice AND `substitute` Unquote/
7881    // UnquoteSplice-inside-list collapse behind. Tests pin: (a) Ok-arm
7882    // projection under both `UnquoteForm` variants — the helper returns
7883    // the resolved `usize` (compile path) or `&Sexp` (substitute path)
7884    // for in-scope names; (b) Err-arm projection routes through
7885    // `unbound_template_var` to the typed `LispError::UnboundTemplateVar`
7886    // variant with the correct `prefix` AND the suggest-driven `hint`;
7887    // (c) the helpers are path-uniform — both compile-path arms share
7888    // ONE `resolve_param_index`; both substitute-path arms share ONE
7889    // `resolve_binding`. A regression that re-inlines the gate-2
7890    // projection at any of the four call sites can no longer drift
7891    // independent of the others — the two helpers ARE the gate.
7892
7893    #[test]
7894    fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
7895        // Positive control for the Ok-arm: `name = "x"` against
7896        // `params = ["a", "x", "rest"]` projects through
7897        // `params.iter().position(|p| *p == name)` to `Some(1)`, which
7898        // the helper unwraps to `Ok(1)`. The returned index feeds
7899        // directly into `TemplateOp::Subst(idx)` at the compile site.
7900        let params = ["a", "x", "rest"];
7901        let idx = resolve_param_index("x", &params, UnquoteForm::Unquote)
7902            .expect("bound name must project to Ok at gate-2");
7903        assert_eq!(idx, 1);
7904    }
7905
7906    #[test]
7907    fn resolve_param_index_returns_position_for_bound_name_under_splice() {
7908        // Sibling positive control: `UnquoteForm::Splice` shares the
7909        // gate-2 contract with `Unquote`. The helper is path-uniform
7910        // across both syntactic markers on the compile path — a
7911        // regression that bifurcates the two arms fails-loudly here.
7912        let params = ["a", "x", "rest"];
7913        let idx = resolve_param_index("rest", &params, UnquoteForm::Splice)
7914            .expect("bound name must project to Ok at gate-2 under Splice");
7915        assert_eq!(idx, 2);
7916    }
7917
7918    #[test]
7919    fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
7920        // Negative control for the Err-arm: `name = "xs"` against
7921        // `params = ["x"]` — distance 1, bound 1 — routes through
7922        // `unbound_template_var` to the structural
7923        // `LispError::UnboundTemplateVar` variant with `hint = Some("x")`.
7924        // Pin the variant identity AND the prefix AND the suggest-driven
7925        // hint: a regression that drops the suggestion fails-loudly here.
7926        let params = ["x"];
7927        let err = resolve_param_index("xs", &params, UnquoteForm::Unquote)
7928            .expect_err("unbound name must error at gate-2");
7929        match err {
7930            LispError::UnboundTemplateVar { prefix, name, hint } => {
7931                assert_eq!(prefix, UnquoteForm::Unquote);
7932                assert_eq!(name, "xs");
7933                assert_eq!(hint.as_deref(), Some("x"));
7934            }
7935            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7936        }
7937    }
7938
7939    #[test]
7940    fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
7941        // Sibling negative control: `name = "wholly-unrelated"` against
7942        // `params = ["x"]` — past the bounded edit distance, so no hint.
7943        // Pin that the suggest-driven hint stays empty under Splice when
7944        // the substrate isn't confident — a wrong hint is worse than no
7945        // hint. Closes the closed-set product of {hint, no-hint} ×
7946        // {Unquote, Splice} on the compile-path gate-2.
7947        let params = ["x"];
7948        let err = resolve_param_index("wholly-unrelated", &params, UnquoteForm::Splice)
7949            .expect_err("unrelated unbound must error at gate-2");
7950        match err {
7951            LispError::UnboundTemplateVar { prefix, name, hint } => {
7952                assert_eq!(prefix, UnquoteForm::Splice);
7953                assert_eq!(name, "wholly-unrelated");
7954                assert_eq!(hint, None);
7955            }
7956            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7957        }
7958    }
7959
7960    #[test]
7961    fn resolve_binding_returns_value_for_bound_name_under_unquote() {
7962        // Positive control for the substitute-path Ok-arm: `name = "x"`
7963        // against a bindings map `{x: 42, y: "hi"}` projects through
7964        // `bindings.get(name)` to `Some(&Sexp::Int(42))`, which the
7965        // helper unwraps to `Ok(&Sexp::Int(42))`. The returned
7966        // `&Sexp` borrows from the bindings map — the top-level
7967        // `Sexp::Unquote(_)` substitute caller adds a single
7968        // `.cloned()` to satisfy its owned-`Sexp` return obligation.
7969        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7970        bindings.insert("x".to_string(), Sexp::int(42));
7971        bindings.insert("y".to_string(), Sexp::string("hi"));
7972        let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
7973            .expect("bound name must project to Ok at gate-2 (substitute)");
7974        assert_eq!(val, &Sexp::int(42));
7975    }
7976
7977    #[test]
7978    fn resolve_binding_returns_value_for_bound_name_under_splice() {
7979        // Sibling positive control: `UnquoteForm::Splice` shares the
7980        // gate-2 contract with `Unquote` on the substitute path too.
7981        // The bound value is a `Sexp::List` because the splice arm's
7982        // caller match expression expects `Sexp::List(items)` — but
7983        // the helper itself doesn't inspect the value's shape; it
7984        // just hands back the borrow. A regression that gate-checks
7985        // the value's shape inside `resolve_binding` (instead of at
7986        // the caller match arm) fails-loudly here.
7987        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7988        bindings.insert(
7989            "args".to_string(),
7990            Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
7991        );
7992        let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
7993            .expect("bound name must project to Ok at gate-2 under Splice");
7994        assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
7995    }
7996
7997    #[test]
7998    fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
7999        // Negative control for the substitute-path Err-arm: `name =
8000        // "xs"` against bindings `{x: 1}` — distance 1, bound 1 —
8001        // routes through `unbound_template_var` to the structural
8002        // `LispError::UnboundTemplateVar` variant with `hint =
8003        // Some("x")`. The candidate set is drawn from
8004        // `bound_names(bindings)` — the live bindings' keys, never a
8005        // stale snapshot.
8006        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8007        bindings.insert("x".to_string(), Sexp::int(1));
8008        let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
8009            .expect_err("unbound name must error at gate-2 (substitute)");
8010        match err {
8011            LispError::UnboundTemplateVar { prefix, name, hint } => {
8012                assert_eq!(prefix, UnquoteForm::Unquote);
8013                assert_eq!(name, "xs");
8014                assert_eq!(hint.as_deref(), Some("x"));
8015            }
8016            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8017        }
8018    }
8019
8020    #[test]
8021    fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
8022        // Sibling negative control on the substitute path: past-bound
8023        // distance → no hint. Closes the closed-set product of
8024        // {hint, no-hint} × {Unquote, Splice} on the substitute-path
8025        // gate-2.
8026        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8027        bindings.insert("args".to_string(), Sexp::Nil);
8028        let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
8029            .expect_err("unrelated unbound must error at gate-2");
8030        match err {
8031            LispError::UnboundTemplateVar { prefix, name, hint } => {
8032                assert_eq!(prefix, UnquoteForm::Splice);
8033                assert_eq!(name, "wholly-unrelated");
8034                assert_eq!(hint, None);
8035            }
8036            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8037        }
8038    }
8039
8040    #[test]
8041    fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
8042        // Path-uniformity pin: end-to-end through ALL FOUR call sites
8043        // (`compile_node` Unquote, `compile_node` UnquoteSplice,
8044        // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
8045        // every unbound-template-var rejection now routes through one
8046        // of the TWO `resolve_param_index` / `resolve_binding` helpers
8047        // — `Expander::new()` runs the compile path, so its two arms
8048        // share `resolve_param_index`; `Expander::new_substitute_only()`
8049        // runs the substitute path, so its two arms share
8050        // `resolve_binding`. The four end-to-end expansions below all
8051        // reject with the SAME variant (`UnboundTemplateVar`) with the
8052        // expected `prefix` — pins that the lift preserves the
8053        // path-uniform rejection contract `unbound_template_var`'s
8054        // prior naming established. A regression that re-inlines the
8055        // gate-2 projection at one of the four sites can drift the
8056        // four call sites independent of each other — this test would
8057        // catch that drift.
8058        struct Case {
8059            src: &'static str,
8060            expander: fn() -> Expander,
8061            expected_form: UnquoteForm,
8062        }
8063        let cases: &[Case] = &[
8064            // compile_node Unquote (bytecode path) — uses resolve_param_index
8065            Case {
8066                src: "(defmacro w (x) `(list ,xs)) (w 1)",
8067                expander: Expander::new,
8068                expected_form: UnquoteForm::Unquote,
8069            },
8070            // compile_node UnquoteSplice (bytecode path) — uses resolve_param_index
8071            Case {
8072                src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8073                expander: Expander::new,
8074                expected_form: UnquoteForm::Splice,
8075            },
8076            // substitute Unquote (substitute-only path) — uses resolve_binding
8077            Case {
8078                src: "(defmacro w (x) `(list ,xs)) (w 1)",
8079                expander: Expander::new_substitute_only,
8080                expected_form: UnquoteForm::Unquote,
8081            },
8082            // substitute UnquoteSplice-in-list (substitute-only path) — uses resolve_binding
8083            Case {
8084                src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8085                expander: Expander::new_substitute_only,
8086                expected_form: UnquoteForm::Splice,
8087            },
8088        ];
8089        for case in cases {
8090            let mut e = (case.expander)();
8091            let err = e
8092                .expand_program(read(case.src).unwrap())
8093                .expect_err("unbound template var must error end-to-end");
8094            match err {
8095                LispError::UnboundTemplateVar { prefix, .. } => {
8096                    assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8097                }
8098                other => panic!(
8099                    "expected UnboundTemplateVar for {}, got: {other:?}",
8100                    case.src
8101                ),
8102            }
8103        }
8104    }
8105
8106    // ── resolve_unquote_in_params / _in_bindings: gate-1+gate-2 composition ─
8107
8108    #[test]
8109    fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
8110        // Ok-arm composition under `UnquoteForm::Unquote`: gate-1 projects
8111        // the symbol-inner to "x"; gate-2 looks "x" up in `params` and
8112        // returns its index. The combined helper returns the gate-2
8113        // result directly — pins that gate-1's Ok-arm threads into
8114        // gate-2's input without intermediate state.
8115        let inner = Sexp::symbol("x");
8116        let params = ["x", "y"];
8117        let idx = resolve_unquote_in_params(&inner, &params, UnquoteForm::Unquote)
8118            .expect("symbol-inner bound at index 0 must resolve");
8119        assert_eq!(idx, 0);
8120    }
8121
8122    #[test]
8123    fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
8124        // Sibling Ok-arm under `UnquoteForm::Splice`: pins that the
8125        // marker doesn't change the projection — only the rejection
8126        // path's `prefix` slot.
8127        let inner = Sexp::symbol("args");
8128        let params = ["f", "args"];
8129        let idx = resolve_unquote_in_params(&inner, &params, UnquoteForm::Splice)
8130            .expect("symbol-inner bound at index 1 must resolve");
8131        assert_eq!(idx, 1);
8132    }
8133
8134    #[test]
8135    fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
8136        // Err-arm at gate-1 (must-be-a-symbol): the inner is a list, not
8137        // a symbol, so gate-1 rejects via `non_symbol_unquote_target`
8138        // BEFORE gate-2's param lookup runs. Pins that the composition's
8139        // sequencing is gate-1-then-gate-2: a regression that runs
8140        // gate-2 first would attempt to look up "(list 1 2)" as a param
8141        // name and emit `LispError::UnboundTemplateVar { name: "(list 1
8142        // 2)", ... }` — a confusing diagnostic that would substring-grep
8143        // "unbound" instead of "expected symbol". This test pins the
8144        // structural floor: a non-symbol inner is rejected as a non-
8145        // symbol, never re-treated as a bound-name lookup key.
8146        let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
8147        let params = ["x"];
8148        let err = resolve_unquote_in_params(&inner, &params, UnquoteForm::Unquote)
8149            .expect_err("non-symbol inner must reject at gate-1");
8150        match err {
8151            LispError::NonSymbolUnquoteTarget { prefix, got } => {
8152                assert_eq!(prefix, UnquoteForm::Unquote);
8153                assert_eq!(got.display, "(list 1 2)");
8154            }
8155            other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8156        }
8157    }
8158
8159    #[test]
8160    fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
8161        // Err-arm at gate-2 (must-be-bound-in-scope): the inner IS a
8162        // symbol (gate-1 passes) but the name isn't in `params`, so
8163        // gate-2 rejects via `unbound_template_var`. Pins that gate-1
8164        // forwards its Ok-arm `&str` borrow into gate-2's lookup, and
8165        // that the marker `prefix` is threaded into gate-2's rejection
8166        // unchanged (a regression that hard-codes `UnquoteForm::Unquote`
8167        // at the composition boundary would fail this Splice-marker
8168        // assertion).
8169        let inner = Sexp::symbol("missing");
8170        let params = ["x", "y"];
8171        let err = resolve_unquote_in_params(&inner, &params, UnquoteForm::Splice)
8172            .expect_err("unbound symbol must reject at gate-2");
8173        match err {
8174            LispError::UnboundTemplateVar { prefix, name, .. } => {
8175                assert_eq!(prefix, UnquoteForm::Splice);
8176                assert_eq!(name, "missing");
8177            }
8178            other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8179        }
8180    }
8181
8182    #[test]
8183    fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
8184        // Substitute-path sibling of `resolve_unquote_in_params_returns_
8185        // index_for_symbol_inner_under_unquote`. The combined helper
8186        // composes gate-1 (project inner to symbol) THEN gate-2 (look
8187        // up name in bindings). The returned `&Sexp` borrows from
8188        // `bindings` so the list-inner caller threads it straight into
8189        // the splice-expansion match without an intermediate allocation.
8190        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8191        bindings.insert("v".to_string(), Sexp::int(42));
8192        let inner = Sexp::symbol("v");
8193        let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8194            .expect("symbol-inner bound to 42 must resolve");
8195        assert_eq!(val, &Sexp::int(42));
8196    }
8197
8198    #[test]
8199    fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
8200        // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8201        // non_symbol_inner_at_gate_1`. Pins the gate-1-then-gate-2
8202        // sequencing on the substitute path: a non-symbol inner is
8203        // rejected as a non-symbol BEFORE the bindings map is consulted.
8204        let bindings: HashMap<String, Sexp> = HashMap::new();
8205        let inner = Sexp::int(5);
8206        let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
8207            .expect_err("non-symbol inner must reject at gate-1");
8208        match err {
8209            LispError::NonSymbolUnquoteTarget { prefix, got } => {
8210                assert_eq!(prefix, UnquoteForm::Splice);
8211                assert_eq!(got.display, "5");
8212            }
8213            other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8214        }
8215    }
8216
8217    #[test]
8218    fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
8219        // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8220        // unbound_symbol_at_gate_2`. Pins the gate-2 rejection on the
8221        // substitute path with the marker threaded into the rejection's
8222        // `prefix` slot.
8223        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8224        bindings.insert("known".to_string(), Sexp::Nil);
8225        let inner = Sexp::symbol("missing");
8226        let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8227            .expect_err("unbound symbol must reject at gate-2");
8228        match err {
8229            LispError::UnboundTemplateVar { prefix, name, .. } => {
8230                assert_eq!(prefix, UnquoteForm::Unquote);
8231                assert_eq!(name, "missing");
8232            }
8233            other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8234        }
8235    }
8236
8237    #[test]
8238    fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
8239        // End-to-end pin: all FOUR call sites of the gate-1+gate-2
8240        // composition (compile_node Unquote, compile_node UnquoteSplice,
8241        // substitute Unquote, substitute list-inner UnquoteSplice) now
8242        // share TWO composed primitives — `resolve_unquote_in_params`
8243        // on the bytecode path, `resolve_unquote_in_bindings` on the
8244        // substitute path — and ALL four reject gate-1 failures (non-
8245        // symbol inner) with the SAME `LispError::NonSymbolUnquoteTarget`
8246        // variant carrying the expected `prefix` slot. Before the lift,
8247        // each site threaded `form` twice through two helper calls; this
8248        // test pins that the lift preserves the gate's rejection-shape
8249        // identity across all four sites for a non-symbol inner — i.e.
8250        // gate-1 fires identically across both expansion strategies.
8251        struct Case {
8252            src: &'static str,
8253            expander: fn() -> Expander,
8254            expected_form: UnquoteForm,
8255        }
8256        let cases: &[Case] = &[
8257            Case {
8258                src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8259                expander: Expander::new,
8260                expected_form: UnquoteForm::Unquote,
8261            },
8262            Case {
8263                src: "(defmacro w (x) `(outer ,@5)) (w 1)",
8264                expander: Expander::new,
8265                expected_form: UnquoteForm::Splice,
8266            },
8267            Case {
8268                src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8269                expander: Expander::new_substitute_only,
8270                expected_form: UnquoteForm::Unquote,
8271            },
8272            Case {
8273                src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
8274                expander: Expander::new_substitute_only,
8275                expected_form: UnquoteForm::Splice,
8276            },
8277        ];
8278        for case in cases {
8279            let mut e = (case.expander)();
8280            let err = e
8281                .expand_program(read(case.src).unwrap())
8282                .expect_err("non-symbol inner must error end-to-end");
8283            match err {
8284                LispError::NonSymbolUnquoteTarget { prefix, .. } => {
8285                    assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8286                }
8287                other => panic!(
8288                    "expected NonSymbolUnquoteTarget for {}, got: {other:?}",
8289                    case.src
8290                ),
8291            }
8292        }
8293    }
8294
8295    // ── Splice outside list: structural variant + path-uniform rejection ─
8296
8297    /// Helper for the splice-outside-list tests — pins the variant shape
8298    /// and carries the offending `got` field up to the assert site for
8299    /// legibility. Sibling of `unbound_var` and `non_symbol_target`.
8300    fn splice_outside_list_got(err: &LispError) -> &str {
8301        match err {
8302            LispError::SpliceOutsideList { got } => got.display.as_str(),
8303            other => panic!("expected SpliceOutsideList, got: {other:?}"),
8304        }
8305    }
8306
8307    #[test]
8308    fn splice_outside_list_in_substitute_emits_structural_variant() {
8309        // `,@xs` at the body's top level — there is no containing list to
8310        // splice into. Path: substitute (the `Expander::new_substitute_only`
8311        // path's top-level `Sexp::UnquoteSplice(_)` arm). Pins variant
8312        // identity AND the offending inner so a regression that re-inlines
8313        // the legacy `LispError::Compile` shape fails-loudly here.
8314        let mut e = Expander::new_substitute_only();
8315        let err = e
8316            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8317            .expect_err("splice outside list must error");
8318        assert_eq!(splice_outside_list_got(&err), "xs");
8319    }
8320
8321    #[test]
8322    fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
8323        // `,@(list 1 2)` at the body's top level — the inner is a literal
8324        // list, not a symbol. The structural variant carries the inner's
8325        // Sexp::Display projection so the operator sees the literal value
8326        // they wrote in the parenthetical.
8327        let mut e = Expander::new_substitute_only();
8328        let err = e
8329            .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8330            .expect_err("splice outside list must error");
8331        assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8332    }
8333
8334    #[test]
8335    fn splice_outside_list_in_compile_template_emits_structural_variant() {
8336        // The bytecode path's `compile_template` gate now rejects top-level
8337        // `,@X` bodies BEFORE walking — closing the prior silent-divergence
8338        // where the bytecode interpreter's outermost stack frame absorbed
8339        // the splice. Pins that the bytecode path emits the SAME structural
8340        // variant the substitute path emits — `,@-outside-list` is rejected
8341        // path-uniformly. Path: `Expander::new()` (compile_templates = true)
8342        // → `compile_template` gate.
8343        let mut e = Expander::new();
8344        let err = e
8345            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8346            .expect_err("compile-template splice outside list must error");
8347        assert_eq!(splice_outside_list_got(&err), "xs");
8348    }
8349
8350    #[test]
8351    fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
8352        // Same shape as the substitute test but routed through the bytecode
8353        // path's `compile_template` gate. Proves the gate fires on a
8354        // non-symbol inner too — the slot's contents are irrelevant; only
8355        // the syntactic position matters.
8356        let mut e = Expander::new();
8357        let err = e
8358            .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8359            .expect_err("compile-template splice outside list must error");
8360        assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8361    }
8362
8363    #[test]
8364    fn splice_outside_list_substitute_and_bytecode_paths_agree() {
8365        // Path-uniform rejection: the SAME source emits the SAME structural
8366        // variant (`SpliceOutsideList { got: "xs" }`) under both expansion
8367        // strategies. Before the `compile_template` gate, the bytecode path
8368        // silently produced a list while the substitute path errored —
8369        // expansion strategy was observable. After the gate, the gate is
8370        // strategy-uniform, so a macro that registers under one strategy
8371        // registers under the other.
8372        let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
8373        let mut subst = Expander::new_substitute_only();
8374        let mut bytecode = Expander::new();
8375        let err_subst = subst
8376            .expand_program(read(src).unwrap())
8377            .expect_err("substitute must error");
8378        let err_byte = bytecode
8379            .expand_program(read(src).unwrap())
8380            .expect_err("bytecode must error");
8381        assert_eq!(splice_outside_list_got(&err_subst), "xs");
8382        assert_eq!(splice_outside_list_got(&err_byte), "xs");
8383    }
8384
8385    #[test]
8386    fn splice_outside_list_position_is_none_today() {
8387        // Negative control for the future-spans move: until `Sexp` carries
8388        // source positions, `position()` returns `None` for this variant.
8389        // A future run that gives `Sexp` source spans adds `pos:
8390        // Option<usize>` to ONE place; this test gives that change a
8391        // deliberate fail-before/pass-after delta.
8392        let mut e = Expander::new_substitute_only();
8393        let err = e
8394            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8395            .expect_err("splice outside list must error");
8396        assert_eq!(err.position(), None);
8397    }
8398
8399    #[test]
8400    fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
8401        // End-to-end through the Display impl — pins the rendered diagnostic
8402        // a downstream tool sees today (REPL, tatara-check). The legacy
8403        // substring `"\`,@\` may only appear inside a list"` is preserved
8404        // verbatim AND the parenthetical `(got ,@xs)` names the offending
8405        // form; tools that pattern-match on the variant gain structural
8406        // binding to `got`.
8407        let mut e = Expander::new_substitute_only();
8408        let err = e
8409            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8410            .expect_err("splice outside list must error");
8411        let msg = format!("{err}");
8412        assert_eq!(
8413            msg,
8414            "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
8415        );
8416    }
8417
8418    #[test]
8419    fn splice_inside_list_still_succeeds_under_both_paths() {
8420        // Negative control: a well-positioned splice (`,@xs` INSIDE a list)
8421        // continues to succeed under both paths — the new gate only fires
8422        // when the splice is the entire body. Pins that the gate is scoped
8423        // to top-level only, not all `,@` occurrences. Uses a `&rest`-bound
8424        // list so `xs` is unambiguously a Sexp::List `(1 2)` rather than a
8425        // bare list-literal whose first symbol would also splice through.
8426        let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
8427        let mut subst = Expander::new_substitute_only();
8428        let mut bytecode = Expander::new();
8429        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8430        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8431        assert_eq!(out_subst, out_byte);
8432        assert_eq!(out_subst[0], parse("(outer 1 2)"));
8433    }
8434
8435    // ── splice_value_into: the shared splice-result coercion ──
8436
8437    #[test]
8438    fn splice_value_into_list_flattens_elements_into_builder() {
8439        // The canonical splice arm: a bound LIST contributes its elements
8440        // in order, preserving anything already in the builder.
8441        let mut builder = vec![Sexp::symbol("outer")];
8442        splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8443        assert_eq!(
8444            builder,
8445            vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
8446        );
8447    }
8448
8449    #[test]
8450    fn splice_value_into_nil_is_a_noop() {
8451        // Splicing the empty list (`Sexp::Nil`) contributes nothing —
8452        // the builder is unchanged.
8453        let mut builder = vec![Sexp::symbol("outer")];
8454        splice_value_into(&mut builder, &Sexp::Nil);
8455        assert_eq!(builder, vec![Sexp::symbol("outer")]);
8456    }
8457
8458    #[test]
8459    fn splice_value_into_scalar_pushes_single_element() {
8460        // A non-list, non-nil bound value degrades `,@x` to `,x`: it
8461        // splices as exactly one element. Pins the "free middle" coercion
8462        // every scalar shape (int, keyword, …) shares.
8463        let mut builder = vec![Sexp::symbol("outer")];
8464        splice_value_into(&mut builder, &Sexp::int(5));
8465        assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
8466        let mut other: Vec<Sexp> = vec![];
8467        splice_value_into(&mut other, &Sexp::keyword("k"));
8468        assert_eq!(other, vec![Sexp::keyword("k")]);
8469    }
8470
8471    #[test]
8472    fn splice_of_non_list_value_coerces_identically_under_both_paths() {
8473        // The point of the lift: the NON-list splice arms (scalar → single
8474        // element, nil → nothing) coerce identically under the substitute
8475        // AND bytecode strategies. Before the coercion was lifted to ONE
8476        // primitive these two arms lived inline at two sites; this test
8477        // pins that the two strategies cannot drift on the non-list arms.
8478        let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
8479        let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
8480        for src in [scalar, empty] {
8481            let mut subst = Expander::new_substitute_only();
8482            let mut bytecode = Expander::new();
8483            let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8484            let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8485            assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
8486        }
8487        let mut e = Expander::new();
8488        assert_eq!(
8489            e.expand_program(read(scalar).unwrap()).unwrap()[0],
8490            parse("(outer 5)")
8491        );
8492        let mut e2 = Expander::new();
8493        assert_eq!(
8494            e2.expand_program(read(empty).unwrap()).unwrap()[0],
8495            parse("(outer)")
8496        );
8497    }
8498
8499    // ── Missing macro arg: structural variant + path-uniform rejection ──
8500
8501    /// Helper for the missing-macro-arg tests — pins the variant shape
8502    /// and carries the failing macro's name + un-bound param up to the
8503    /// assert site for legibility. Sibling of `unbound_var`,
8504    /// `non_symbol_target`, and `splice_outside_list_got`.
8505    fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
8506        match err {
8507            LispError::MissingMacroArg { macro_name, param } => {
8508                (macro_name.as_str(), param.as_str())
8509            }
8510            other => panic!("expected MissingMacroArg, got: {other:?}"),
8511        }
8512    }
8513
8514    #[test]
8515    fn missing_macro_arg_in_compile_template_emits_structural_variant() {
8516        // `(need-two 1)` against `(need-two a b)` — `b` has no arg. Path:
8517        // `apply_compiled` (the bytecode-template path, default expander).
8518        // Pins variant identity AND macro_name AND the un-bound param so a
8519        // regression that re-inlines the legacy `LispError::Compile` shape
8520        // fails-loudly here.
8521        let mut e = Expander::new();
8522        let err = e
8523            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8524            .expect_err("missing required macro arg must error");
8525        let (macro_name, param) = missing_macro_arg_fields(&err);
8526        assert_eq!(macro_name, "need-two");
8527        assert_eq!(param, "b");
8528    }
8529
8530    #[test]
8531    fn missing_macro_arg_in_substitute_emits_structural_variant() {
8532        // Same shape as the bytecode test but routed through the
8533        // substitute-only expander → `bind_args` is the failing site.
8534        // Proves the substitute path emits the SAME structural variant the
8535        // bytecode path emits — `missing required arg` rejection is
8536        // path-uniform across both expansion strategies.
8537        let mut e = Expander::new_substitute_only();
8538        let err = e
8539            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8540            .expect_err("missing required macro arg must error");
8541        let (macro_name, param) = missing_macro_arg_fields(&err);
8542        assert_eq!(macro_name, "need-two");
8543        assert_eq!(param, "b");
8544    }
8545
8546    #[test]
8547    fn missing_macro_arg_first_position_is_named() {
8548        // `(f)` against `(f a b)` — `a` (the FIRST required param) has no
8549        // arg. The variant names `a`, not `b` — naming the LEFTMOST
8550        // un-bound param is the shape `bind_args` / `apply_compiled` both
8551        // emit (each iterates positionally and bails on the first missing
8552        // slot). Pins the leftmost-bail contract so a regression that
8553        // names the rightmost (or a surplus) param fails-loudly.
8554        let mut e = Expander::new();
8555        let err = e
8556            .expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
8557            .expect_err("missing first required arg must error");
8558        let (macro_name, param) = missing_macro_arg_fields(&err);
8559        assert_eq!(macro_name, "f");
8560        assert_eq!(param, "a");
8561    }
8562
8563    #[test]
8564    fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
8565        // Path-uniform rejection: the SAME source emits the SAME structural
8566        // variant under both expansion strategies. Negative control for
8567        // the divergence-closing posture: a future refactor that drifts
8568        // either path's rejection shape (or drops one path's rejection
8569        // entirely) fails-loudly here. Sibling of
8570        // `splice_outside_list_substitute_and_bytecode_paths_agree` —
8571        // both close `THEORY.md §II.1 invariant 2 — free middle` for one
8572        // failure mode each.
8573        let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
8574        let mut subst = Expander::new_substitute_only();
8575        let mut bytecode = Expander::new();
8576        let err_subst = subst
8577            .expand_program(read(src).unwrap())
8578            .expect_err("substitute must error");
8579        let err_byte = bytecode
8580            .expand_program(read(src).unwrap())
8581            .expect_err("bytecode must error");
8582        assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
8583        assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
8584    }
8585
8586    #[test]
8587    fn missing_macro_arg_position_is_none_today() {
8588        // Negative control for the future-spans move: until `Sexp` carries
8589        // source positions, `position()` returns `None` for this variant.
8590        // A future run that gives `Sexp` source spans adds `pos:
8591        // Option<usize>` to ONE place; this test gives that change a
8592        // deliberate fail-before/pass-after delta. Parallel to
8593        // `splice_outside_list_position_is_none_today`.
8594        let mut e = Expander::new();
8595        let err = e
8596            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8597            .expect_err("missing required macro arg must error");
8598        assert_eq!(err.position(), None);
8599    }
8600
8601    #[test]
8602    fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
8603        // End-to-end through the Display impl — pins the rendered diagnostic
8604        // a downstream tool sees today (REPL, tatara-check). The legacy
8605        // substring `"missing required arg: {param}"` is preserved verbatim
8606        // AND the head clause names the failing macro via `"call to
8607        // {macro_name}"`; tools that pattern-match on the variant gain
8608        // structural binding to `macro_name` / `param`.
8609        let mut e = Expander::new();
8610        let err = e
8611            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8612            .expect_err("missing required macro arg must error");
8613        assert_eq!(
8614            format!("{err}"),
8615            "compile error in call to need-two: missing required arg: b"
8616        );
8617    }
8618
8619    #[test]
8620    fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
8621        // Both `macro_name` (`wrap-twice`) and `param` (`notify-ref`)
8622        // round-trip through the variant unchanged. Pinning this contract
8623        // means a regression that camelCases or lowercases either side
8624        // fails-loudly here. Parallel to the
8625        // `unknown_kwarg_display_carries_kebab_case_keys_unchanged`
8626        // assertion for the kwarg-gate's symmetric surface.
8627        let mut e = Expander::new();
8628        let err = e
8629            .expand_program(
8630                read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
8631                    .unwrap(),
8632            )
8633            .expect_err("missing required macro arg must error");
8634        let (macro_name, param) = missing_macro_arg_fields(&err);
8635        assert_eq!(macro_name, "wrap-twice");
8636        assert_eq!(param, "body");
8637    }
8638
8639    #[test]
8640    fn rest_param_only_macro_with_no_args_still_succeeds() {
8641        // Negative control: a macro whose only param is `&rest` must NOT
8642        // error when called with zero args — the rest-param binds to the
8643        // empty list. The new structural variant fires only on REQUIRED
8644        // params; the `Param::Rest` arm in both `bind_args` and
8645        // `apply_compiled` continues to bind the empty tail. Pins that the
8646        // helper is scoped to required-param failure, not all
8647        // arity-mismatch shapes.
8648        let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
8649        let mut subst = Expander::new_substitute_only();
8650        let mut bytecode = Expander::new();
8651        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8652        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8653        assert_eq!(out_subst, out_byte);
8654        assert_eq!(out_subst[0], parse("(list)"));
8655    }
8656
8657    // ── TooManyMacroArgs: call-site mirror of RestParamTrailingTokens ──
8658    //
8659    // A rest-less param list has a FIXED maximum arity equal to
8660    // `required.len() + optional.len()`. Surplus call args have nowhere to
8661    // bind. Before this gate the surplus was silently truncated to the
8662    // slice the binder could consume — the typed-entry macro-call-gate
8663    // rejected too-few-args loudly (`MissingMacroArg`) but accepted
8664    // too-many silently, an asymmetry the definition-side `&rest <name>
8665    // extra` rejection (`RestParamTrailingTokens`) had no call-side dual.
8666    // After this gate the call-site arity surface is structurally
8667    // complete in both directions; the substitute + bytecode paths share
8668    // `MacroParams::bind`, so both inherit the rejection without drift.
8669
8670    /// Helper for the too-many-args tests — projects to (macro_name,
8671    /// expected, got) for legibility. Sibling of `missing_macro_arg_fields`.
8672    fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
8673        match err {
8674            LispError::TooManyMacroArgs {
8675                macro_name,
8676                expected,
8677                got,
8678            } => (macro_name.as_str(), *expected, *got),
8679            other => panic!("expected TooManyMacroArgs, got: {other:?}"),
8680        }
8681    }
8682
8683    #[test]
8684    fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
8685        // `(defmacro f (a b) ...)` called as `(f 1 2 3)` — `3` has
8686        // nowhere to bind. The rest-less binder rejects via
8687        // `TooManyMacroArgs { macro_name: "f", expected: 2, got: 3 }`,
8688        // NOT silently drops `3`. Pins both the variant identity AND the
8689        // structural fields the typed gate exposes for authoring-tool
8690        // quick-fixes ("you supplied 3 args; the macro takes at most
8691        // 2").
8692        let mut e = Expander::new();
8693        let err = e
8694            .expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8695            .expect_err("surplus arg on rest-less call must error");
8696        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8697        assert_eq!(macro_name, "f");
8698        assert_eq!(expected, 2);
8699        assert_eq!(got, 3);
8700    }
8701
8702    #[test]
8703    fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
8704        // The rest-less binder's fixed maximum arity is `required.len() +
8705        // optional.len()` — the optional section CONTRIBUTES to capacity.
8706        // `(defmacro f (a &optional b) ...)` accepts 1 OR 2 args; 3
8707        // args rejects with `expected: 2` (required + optional, NOT just
8708        // required). Pins the optional-counts-in-capacity contract so a
8709        // regression that omits optionals from the expected calculation
8710        // (and erroneously rejects 2-arg calls) fails-loudly here.
8711        let mut e = Expander::new();
8712        let err = e
8713            .expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8714            .expect_err("surplus arg beyond required+optional must error");
8715        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8716        assert_eq!(macro_name, "f");
8717        assert_eq!(expected, 2);
8718        assert_eq!(got, 3);
8719    }
8720
8721    #[test]
8722    fn too_many_macro_args_required_plus_two_optionals_arity_three() {
8723        // Larger optional section (capacity 1 + 2 = 3). 4 args rejects
8724        // with `expected: 3`. Pins that the capacity calculation scales
8725        // with optional.len(), not just at-most-one. Mixes a bare
8726        // optional with an optional carrying a default form — both shapes
8727        // contribute identically to capacity (the typed `OptionalParam`
8728        // entry's `default: Option<Sexp>` is irrelevant to the arity gate).
8729        let mut e = Expander::new();
8730        let err = e
8731            .expand_program(
8732                read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
8733            )
8734            .expect_err("surplus arg beyond required+two-optional must error");
8735        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8736        assert_eq!(macro_name, "f");
8737        assert_eq!(expected, 3);
8738        assert_eq!(got, 4);
8739    }
8740
8741    #[test]
8742    fn too_many_macro_args_does_not_fire_when_rest_is_present() {
8743        // Negative control: a rest-PRESENT param list has no maximum
8744        // arity — the `&rest` slot collects every trailing arg into a
8745        // `Sexp::List`. `(defmacro f (a &rest xs) ...)` called as
8746        // `(f 1 2 3 4)` MUST succeed; the new gate fires ONLY when
8747        // `MacroParams.rest` is `None`. Pins the rest-present-path
8748        // remains permissive — a regression that wrongly fires the
8749        // too-many gate for any surplus (including the rest-collecting
8750        // path) would break every `&rest`-using macro.
8751        let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
8752        let mut subst = Expander::new_substitute_only();
8753        let mut bytecode = Expander::new();
8754        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8755        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8756        assert_eq!(out_subst, out_byte);
8757        assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
8758    }
8759
8760    #[test]
8761    fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
8762        // Negative control: the rest-less gate fires STRICTLY when
8763        // `args.len() > expected` — at exact arity the binder accepts.
8764        // `(defmacro f (a &optional b) ...)` called as `(f 1 2)` binds
8765        // a=1, b=2 successfully (the optional takes its supplied arg,
8766        // not the default). Pins the boundary condition so a regression
8767        // that flips the comparison to `>=` (rejecting exact-arity
8768        // calls) fails-loudly here.
8769        let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
8770        let mut e = Expander::new();
8771        let out = e.expand_program(read(src).unwrap()).unwrap();
8772        assert_eq!(out[0], parse("(list 1 2)"));
8773    }
8774
8775    #[test]
8776    fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
8777        // Path-uniform rejection: the SAME source emits the SAME
8778        // structural variant under both expansion strategies. The
8779        // shared `MacroParams::bind` makes the rejection lands once and
8780        // both paths inherit it. Mirror of
8781        // `missing_macro_arg_substitute_and_bytecode_paths_agree` —
8782        // both close `THEORY.md §II.1 invariant 2 — free middle` for
8783        // one failure mode each.
8784        let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
8785        let mut subst = Expander::new_substitute_only();
8786        let mut bytecode = Expander::new();
8787        let err_subst = subst
8788            .expand_program(read(src).unwrap())
8789            .expect_err("substitute must error");
8790        let err_byte = bytecode
8791            .expand_program(read(src).unwrap())
8792            .expect_err("bytecode must error");
8793        assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
8794        assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
8795    }
8796
8797    #[test]
8798    fn too_many_macro_args_fires_after_missing_required_priority_held() {
8799        // Priority discipline: the required walk fires
8800        // `MissingMacroArg` BEFORE the rest-less surplus gate is
8801        // reached. `(defmacro f (a b c) …) (f 1)` is `MissingMacroArg
8802        // { param: "b" }`, NOT `TooManyMacroArgs` (and certainly not a
8803        // collision). The two failure modes are structurally disjoint:
8804        // too-few-required vs. too-many-with-no-rest. Pins the bail-on-
8805        // first-missing-required contract so a regression that swaps
8806        // the two gates' order would emit the wrong variant.
8807        let mut e = Expander::new();
8808        let err = e
8809            .expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
8810            .expect_err("missing required must error");
8811        assert!(
8812            matches!(err, LispError::MissingMacroArg { .. }),
8813            "expected MissingMacroArg (priority), got: {err:?}"
8814        );
8815    }
8816
8817    #[test]
8818    fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
8819        // Degenerate case: a nullary macro `(defmacro f () ...)` has
8820        // capacity 0; ANY supplied arg rejects with `expected: 0`. Pins
8821        // the gate fires even when the rest-less max-arity is zero —
8822        // i.e. the rejection is structural, not conditional on a
8823        // non-empty required+optional.
8824        let mut e = Expander::new();
8825        let err = e
8826            .expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
8827            .expect_err("nullary macro called with arg must error");
8828        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8829        assert_eq!(macro_name, "f");
8830        assert_eq!(expected, 0);
8831        assert_eq!(got, 1);
8832    }
8833
8834    #[test]
8835    fn too_many_macro_args_display_renders_legacy_compile_substring() {
8836        // The rendered Display matches the legacy `Compile`-shaped
8837        // diagnostic style — `"compile error in call to {macro_name}:
8838        // too many args: expected at most {expected}, got {got}"` — so
8839        // the existing `"compile error in call to"` substring authoring
8840        // tools' assertions key on stays unchanged. Pins the byte-level
8841        // rendered shape so a regression that drifts the prefix /
8842        // separator / labels fails-loudly here.
8843        let err = LispError::TooManyMacroArgs {
8844            macro_name: "pair".into(),
8845            expected: 2,
8846            got: 5,
8847        };
8848        assert_eq!(
8849            err.to_string(),
8850            "compile error in call to pair: too many args: expected at most 2, got 5"
8851        );
8852    }
8853
8854    #[test]
8855    fn too_many_macro_args_position_is_none_today() {
8856        // Negative control for the future-spans move: until `Sexp`
8857        // carries source positions, `position()` returns `None` for
8858        // this variant. A future run that gives `Sexp` source spans
8859        // adds `pos: Option<usize>` to ONE place; this test gives that
8860        // change a deliberate fail-before/pass-after delta. Parallel to
8861        // `missing_macro_arg_position_is_none_today`.
8862        let err = LispError::TooManyMacroArgs {
8863            macro_name: "pair".into(),
8864            expected: 2,
8865            got: 3,
8866        };
8867        assert_eq!(err.position(), None);
8868    }
8869
8870    /// Helper for the non-symbol-param tests — pins the variant shape and
8871    /// carries the failing position + offending element up to the assert
8872    /// site for legibility. Sibling of `missing_macro_arg_fields`.
8873    fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
8874        match err {
8875            LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
8876            other => panic!("expected NonSymbolParam, got: {other:?}"),
8877        }
8878    }
8879
8880    #[test]
8881    fn non_symbol_param_at_first_position_emits_structural_variant() {
8882        // `(defmacro f (5) ...)` — the first element of the param list is
8883        // an integer literal, not a symbol. Pins variant identity AND
8884        // that `position` is the loop index inside `parse_params` (0 for
8885        // the first slot) AND that `got` is the offending element via
8886        // `Sexp::Display` (`5`). A regression that re-inlines the legacy
8887        // `LispError::Compile` shape (which named neither the position
8888        // nor the offending element) fails-loudly here.
8889        let mut e = Expander::new();
8890        let err = e
8891            .expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
8892            .expect_err("non-symbol param must error");
8893        let (position, got) = non_symbol_param_fields(&err);
8894        assert_eq!(position, 0);
8895        assert_eq!(got, "5");
8896    }
8897
8898    #[test]
8899    fn non_symbol_param_at_second_position_emits_structural_variant() {
8900        // `(defmacro f (a 5) ...)` — `a` parses fine, `5` at position 1
8901        // misfires. Pins that `position` advances with the loop index, so
8902        // an LSP quick-fix that wants to point at "the second element of
8903        // your param list" gains the index as data, no source re-parse
8904        // required.
8905        let mut e = Expander::new();
8906        let err = e
8907            .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8908            .expect_err("non-symbol param must error");
8909        let (position, got) = non_symbol_param_fields(&err);
8910        assert_eq!(position, 1);
8911        assert_eq!(got, "5");
8912    }
8913
8914    #[test]
8915    fn non_symbol_param_carries_keyword_value_unchanged() {
8916        // `:k` at a param-list position. `Sexp::Display` for
8917        // `Atom::Keyword(s)` writes `:s`; pins that the variant's `got`
8918        // field round-trips the keyword form unchanged so an LSP that
8919        // surfaces "you wrote `:k` where a symbol was expected" gains
8920        // the literal keyword value as data, no re-parsing required.
8921        let mut e = Expander::new();
8922        let err = e
8923            .expand_program(read("(defmacro f (:k) `(list))").unwrap())
8924            .expect_err("non-symbol param must error");
8925        let (position, got) = non_symbol_param_fields(&err);
8926        assert_eq!(position, 0);
8927        assert_eq!(got, ":k");
8928    }
8929
8930    #[test]
8931    fn non_symbol_param_carries_nested_list_value_unchanged() {
8932        // A nested list at a param-list position. `Sexp::Display` for
8933        // `List(xs)` writes `(<x1> <x2> ...)`; pins that the variant's
8934        // `got` field carries the nested form's full Display projection
8935        // unchanged so the operator sees what they wrote.
8936        let mut e = Expander::new();
8937        let err = e
8938            .expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
8939            .expect_err("non-symbol param must error");
8940        let (position, got) = non_symbol_param_fields(&err);
8941        assert_eq!(position, 0);
8942        assert_eq!(got, "(nested)");
8943    }
8944
8945    #[test]
8946    fn non_symbol_param_in_defpoint_template_emits_same_variant() {
8947        // `defpoint-template` shares `parse_params` with `defmacro` (all
8948        // three head keywords route through `macro_def_from`). Pins that
8949        // the lift fires path-uniformly across the three head keywords
8950        // — `defmacro`, `defpoint-template`, `defcheck` — so the
8951        // structural-completeness floor holds for every defmacro-shaped
8952        // form, not just the one with the `defmacro` head literal.
8953        let mut e = Expander::new();
8954        let err = e
8955            .expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
8956            .expect_err("non-symbol param must error");
8957        let (position, got) = non_symbol_param_fields(&err);
8958        assert_eq!(position, 0);
8959        assert_eq!(got, "5");
8960    }
8961
8962    #[test]
8963    fn non_symbol_param_in_defcheck_emits_same_variant() {
8964        // Sibling of the defpoint-template test — `defcheck` is the
8965        // third head keyword `macro_def_from` recognizes. All three
8966        // route through the same `parse_params` and now reject
8967        // non-symbol params with the same structural variant.
8968        let mut e = Expander::new();
8969        let err = e
8970            .expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
8971            .expect_err("non-symbol param must error");
8972        let (position, got) = non_symbol_param_fields(&err);
8973        assert_eq!(position, 1);
8974        assert_eq!(got, "5");
8975    }
8976
8977    #[test]
8978    fn non_symbol_param_position_is_none_today() {
8979        // Negative control for the future-spans move: until `Sexp`
8980        // carries source positions, `position()` on `LispError` returns
8981        // `None` for this variant. A future run that gives `Sexp`
8982        // source spans adds `pos: Option<usize>` to ONE place; this
8983        // test gives that change a deliberate fail-before/pass-after
8984        // delta. Parallel to `missing_macro_arg_position_is_none_today`.
8985        let mut e = Expander::new();
8986        let err = e
8987            .expand_program(read("(defmacro f (5) `(list))").unwrap())
8988            .expect_err("non-symbol param must error");
8989        assert_eq!(err.position(), None);
8990    }
8991
8992    #[test]
8993    fn non_symbol_param_message_renders_legacy_substring_with_position() {
8994        // End-to-end through Display — pins the rendered diagnostic that
8995        // downstream tools (REPL, `tatara-check`) see today. Legacy
8996        // substrings `"defmacro params"` AND `"expected symbol"` are
8997        // preserved verbatim; the appended `at position {position}, got
8998        // {got}` clause is the new structural detail. Tools that
8999        // pattern-match on the variant gain structural binding to
9000        // `position` / `got`.
9001        let mut e = Expander::new();
9002        let err = e
9003            .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
9004            .expect_err("non-symbol param must error");
9005        assert_eq!(
9006            format!("{err}"),
9007            "compile error in defmacro params: \
9008             expected symbol at position 1, got 5"
9009        );
9010    }
9011
9012    #[test]
9013    fn non_symbol_param_substitute_and_bytecode_paths_agree() {
9014        // Path-uniform rejection: the SAME source emits the SAME
9015        // structural variant under both expansion strategies. The
9016        // defmacro-syntax-gate fires inside `macro_def_from` →
9017        // `parse_params`, BEFORE either strategy's expansion path runs;
9018        // so both `Expander::new()` (bytecode) and
9019        // `Expander::new_substitute_only()` (substitute) reject the
9020        // SAME malformed defmacro at the SAME gate. Sibling of
9021        // `missing_macro_arg_substitute_and_bytecode_paths_agree`.
9022        let src = "(defmacro f (a 5) `(,a))";
9023        let mut subst = Expander::new_substitute_only();
9024        let mut bytecode = Expander::new();
9025        let err_subst = subst
9026            .expand_program(read(src).unwrap())
9027            .expect_err("substitute must error");
9028        let err_byte = bytecode
9029            .expand_program(read(src).unwrap())
9030            .expect_err("bytecode must error");
9031        assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
9032        assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
9033    }
9034
9035    /// Helper for the rest-param-missing-name tests — pins the variant
9036    /// shape and carries the marker position + offending follower (or
9037    /// its absence) up to the assert site for legibility. Sibling of
9038    /// `non_symbol_param_fields`.
9039    fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
9040        match err {
9041            LispError::RestParamMissingName { rest_position, got } => {
9042                (*rest_position, got.as_ref().map(|w| w.display.as_str()))
9043            }
9044            other => panic!("expected RestParamMissingName, got: {other:?}"),
9045        }
9046    }
9047
9048    #[test]
9049    fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
9050        // `(defmacro f (&rest))` — the marker is the only param-list
9051        // element; nothing follows. Pins variant identity AND that
9052        // `rest_position == 0` (the first slot) AND that `got == None`
9053        // (no follower exists). A regression that re-inlines the legacy
9054        // `LispError::Compile` shape (which named neither field) fails-
9055        // loudly here.
9056        let mut e = Expander::new();
9057        let err = e
9058            .expand_program(read("(defmacro f (&rest) `(list))").unwrap())
9059            .expect_err("&rest with no follower must error");
9060        let (rest_position, got) = rest_param_missing_name_fields(&err);
9061        assert_eq!(rest_position, 0);
9062        assert_eq!(got, None);
9063    }
9064
9065    #[test]
9066    fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
9067        // `(defmacro f (a &rest))` — `a` parses fine, `&rest` at param-list
9068        // position 1 has no follower at all. Pins that `rest_position`
9069        // advances with the loop index, so an LSP quick-fix that wants to
9070        // point at "your `&rest` at position 1 has no name" gains the
9071        // marker position as data, no source re-parse required.
9072        let mut e = Expander::new();
9073        let err = e
9074            .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9075            .expect_err("&rest with no follower must error");
9076        let (rest_position, got) = rest_param_missing_name_fields(&err);
9077        assert_eq!(rest_position, 1);
9078        assert_eq!(got, None);
9079    }
9080
9081    #[test]
9082    fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
9083        // `(defmacro f (&rest 5))` — `&rest` at position 0 followed by
9084        // `5` (an integer literal, not a symbol). Pins that the variant's
9085        // `got` field is `Some` and carries the offending follower's
9086        // `Sexp::Display` projection; the bifurcation between "missing
9087        // entirely" and "present but non-symbol" is in the renderable
9088        // detail, not in what the gate rejects.
9089        let mut e = Expander::new();
9090        let err = e
9091            .expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
9092            .expect_err("&rest followed by non-symbol must error");
9093        let (rest_position, got) = rest_param_missing_name_fields(&err);
9094        assert_eq!(rest_position, 0);
9095        assert_eq!(got, Some("5"));
9096    }
9097
9098    #[test]
9099    fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
9100        // `(defmacro f (a &rest :foo))` — keyword follower at the rest-name
9101        // slot. `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pins
9102        // that the variant's `got` field round-trips the keyword form
9103        // unchanged so an LSP that surfaces "you wrote `:foo` where a
9104        // rest-name was expected" gains the literal keyword value as
9105        // data, no re-parsing required.
9106        let mut e = Expander::new();
9107        let err = e
9108            .expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
9109            .expect_err("&rest followed by keyword must error");
9110        let (rest_position, got) = rest_param_missing_name_fields(&err);
9111        assert_eq!(rest_position, 1);
9112        assert_eq!(got, Some(":foo"));
9113    }
9114
9115    #[test]
9116    fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
9117        // `(defmacro f (&rest (nested)))` — nested-list follower at the
9118        // rest-name slot. `Sexp::Display` for `List(xs)` writes
9119        // `(<x1> <x2> ...)`; pins that the variant's `got` field carries
9120        // the nested form's full Display projection unchanged so the
9121        // operator sees what they wrote.
9122        let mut e = Expander::new();
9123        let err = e
9124            .expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
9125            .expect_err("&rest followed by list must error");
9126        let (rest_position, got) = rest_param_missing_name_fields(&err);
9127        assert_eq!(rest_position, 0);
9128        assert_eq!(got, Some("(nested)"));
9129    }
9130
9131    #[test]
9132    fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
9133        // `defpoint-template` shares `parse_params` with `defmacro` (all
9134        // three head keywords route through `macro_def_from`). Pins that
9135        // the lift fires path-uniformly across the three head keywords —
9136        // a regression that handles `defpoint-template`'s param list
9137        // differently from `defmacro`'s would fail-loudly here.
9138        let mut e = Expander::new();
9139        let err = e
9140            .expand_program(read("(defpoint-template t (a &rest) `(,a))").unwrap())
9141            .expect_err("&rest with no follower must error");
9142        let (rest_position, got) = rest_param_missing_name_fields(&err);
9143        assert_eq!(rest_position, 1);
9144        assert_eq!(got, None);
9145    }
9146
9147    #[test]
9148    fn rest_param_missing_name_in_defcheck_emits_same_variant() {
9149        // Sibling for the `defcheck` head; rounds out the three-head-
9150        // keyword coverage so the lift is path-uniform across
9151        // `defmacro` / `defpoint-template` / `defcheck`. After this
9152        // test the defmacro-syntax-gate rejects `&rest`-without-name
9153        // identically across all three head keywords — the
9154        // typed-entry surface is single-shape across the cluster.
9155        let mut e = Expander::new();
9156        let err = e
9157            .expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
9158            .expect_err("&rest followed by non-symbol must error");
9159        let (rest_position, got) = rest_param_missing_name_fields(&err);
9160        assert_eq!(rest_position, 0);
9161        assert_eq!(got, Some("5"));
9162    }
9163
9164    #[test]
9165    fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
9166        // Path-uniform rejection: the SAME source emits the SAME
9167        // structural variant under both expansion strategies. The
9168        // defmacro-syntax-gate fires inside `macro_def_from` →
9169        // `parse_params`, BEFORE either strategy's expansion path
9170        // runs; so both `Expander::new()` (bytecode) and
9171        // `Expander::new_substitute_only()` (substitute) reject the
9172        // SAME malformed defmacro at the SAME gate. Sibling of
9173        // `non_symbol_param_substitute_and_bytecode_paths_agree`.
9174        let src = "(defmacro f (a &rest 5) `(,a))";
9175        let mut subst = Expander::new_substitute_only();
9176        let mut bytecode = Expander::new();
9177        let err_subst = subst
9178            .expand_program(read(src).unwrap())
9179            .expect_err("substitute must error");
9180        let err_byte = bytecode
9181            .expand_program(read(src).unwrap())
9182            .expect_err("bytecode must error");
9183        assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
9184        assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
9185    }
9186
9187    #[test]
9188    fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
9189        // End-to-end through Display — pins the rendered diagnostic
9190        // consumers see today (REPL, tatara-check) AND the new `(rest
9191        // marker at position {rest_position}, got {got})` clause. The
9192        // legacy `"&rest needs a name"` substring rides through
9193        // verbatim.
9194        let mut e = Expander::new();
9195        let err = e
9196            .expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
9197            .expect_err("&rest followed by non-symbol must error");
9198        assert_eq!(
9199            format!("{err}"),
9200            "compile error in defmacro params: &rest needs a name \
9201             (rest marker at position 1, got 5)"
9202        );
9203    }
9204
9205    #[test]
9206    fn rest_param_missing_name_message_renders_none_provided_when_follower_absent() {
9207        // Same as the prior test but for the "missing entirely" branch.
9208        // The renderable detail is `(rest marker at position
9209        // {rest_position}, none provided)` — naming the absence
9210        // structurally instead of an empty / partial parenthetical.
9211        let mut e = Expander::new();
9212        let err = e
9213            .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9214            .expect_err("&rest with no follower must error");
9215        assert_eq!(
9216            format!("{err}"),
9217            "compile error in defmacro params: &rest needs a name \
9218             (rest marker at position 1, none provided)"
9219        );
9220    }
9221
9222    #[test]
9223    fn rest_param_missing_name_position_is_none_today() {
9224        // Pins that `position()` returns `None` so the future `pos:
9225        // Option<usize>` add (once `Sexp` carries source spans) lands
9226        // as a deliberate fail-before/pass-after delta rather than a
9227        // silent default. Parallel to
9228        // `non_symbol_param_position_is_none_today` and
9229        // `missing_macro_arg_position_is_none_today`.
9230        let err_missing = LispError::RestParamMissingName {
9231            rest_position: 1,
9232            got: None,
9233        };
9234        assert_eq!(err_missing.position(), None);
9235        let err_got = LispError::RestParamMissingName {
9236            rest_position: 0,
9237            got: Some(crate::error::SexpWitness::new(
9238                crate::error::SexpShape::Int,
9239                "5",
9240            )),
9241        };
9242        assert_eq!(err_got.position(), None);
9243    }
9244
9245    // --- RestParamTrailingTokens: the parse_params gate's third (and
9246    // final) definition-site failure mode ---
9247    //
9248    // A `&rest <name>` absorbs every remaining call arg, so it is the LAST
9249    // thing a param list can name. Before this variant `parse_params`
9250    // returned the moment it bound the rest name, SILENTLY DROPPING any
9251    // trailing tokens — `(a &rest xs extra)` parsed as if `extra` weren't
9252    // there. These tests pin the loud rejection that replaces the silent
9253    // drop; the symbol `RestParamTrailingTokens` exists only after this
9254    // change, so the whole block is fail-before/pass-after by construction
9255    // (compile-time edge) and the end-to-end regression guard below pins
9256    // that the malformed defmacro no longer expands cleanly.
9257
9258    /// Helper mirroring `rest_param_missing_name_fields` — pins the variant
9259    /// shape and lifts the marker position, trailing count, and first
9260    /// offender's display up to the assert site.
9261    fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
9262        match err {
9263            LispError::RestParamTrailingTokens {
9264                rest_position,
9265                extra,
9266                first,
9267            } => (*rest_position, *extra, first.display.as_str()),
9268            other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
9269        }
9270    }
9271
9272    #[test]
9273    fn parse_params_rejects_single_trailing_token_after_rest_name() {
9274        // `(a &rest c extra)` — `&rest c` is well-formed, but `extra`
9275        // follows. The rest name is bound at position 2, the marker at 1;
9276        // the lone trailing token `extra` is reported (extra == 1, first ==
9277        // "extra"). Before this variant `parse_params` returned at the rest
9278        // name and `extra` vanished.
9279        let err = parse_params(&read("a &rest c extra").unwrap())
9280            .expect_err("a trailing token after the rest name must error");
9281        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9282    }
9283
9284    #[test]
9285    fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
9286        // `(&rest c x y z)` — three tokens follow the rest name. `extra`
9287        // counts ALL of them (3), `first` is the first (`x`), and the
9288        // marker is at position 0. A regression that reports only the
9289        // first trailing token's presence (extra hard-coded to 1) fails
9290        // loudly here.
9291        let err = parse_params(&read("&rest c x y z").unwrap())
9292            .expect_err("multiple trailing tokens must error");
9293        assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
9294    }
9295
9296    #[test]
9297    fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
9298        // `(a &rest c 5)` — the rest NAME `c` is a valid symbol, so this is
9299        // NOT a `RestParamMissingName`; the integer `5` is a trailing token
9300        // AFTER a well-formed `&rest c`. Pins that the two sibling failure
9301        // modes don't collide: a malformed rest-name is `RestParamMissingName`,
9302        // a well-formed rest-name followed by junk is
9303        // `RestParamTrailingTokens`. `first` round-trips `5` via the typed
9304        // witness's `Sexp::Display` projection.
9305        let err = parse_params(&read("a &rest c 5").unwrap())
9306            .expect_err("a trailing non-symbol after the rest name must error");
9307        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
9308    }
9309
9310    #[test]
9311    fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
9312        // The fidelity fix, end-to-end through `expand_program`: a defmacro
9313        // whose param list carries a stray token after `&rest <name>` now
9314        // ERRORS at the typed-entry gate instead of expanding as though the
9315        // stray token weren't there. This is the regression guard for the
9316        // silent-drop bug — before this change the same source expanded
9317        // cleanly and `extra` was discarded with no signal.
9318        let mut e = Expander::new();
9319        let err = e
9320            .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9321            .expect_err("trailing token after &rest name must error");
9322        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9323    }
9324
9325    #[test]
9326    fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
9327        // Path-uniform rejection: the gate fires inside `macro_def_from` →
9328        // `parse_params`, BEFORE either expansion strategy runs, so both
9329        // `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
9330        // (substitute) reject the SAME malformed defmacro at the SAME gate.
9331        // Sibling of `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9332        let src = "(defmacro f (a &rest xs extra) `(,a))";
9333        let mut subst = Expander::new_substitute_only();
9334        let mut bytecode = Expander::new();
9335        let err_subst = subst
9336            .expand_program(read(src).unwrap())
9337            .expect_err("substitute must error");
9338        let err_byte = bytecode
9339            .expand_program(read(src).unwrap())
9340            .expect_err("bytecode must error");
9341        assert_eq!(
9342            rest_param_trailing_tokens_fields(&err_subst),
9343            (1, 1, "extra")
9344        );
9345        assert_eq!(
9346            rest_param_trailing_tokens_fields(&err_byte),
9347            (1, 1, "extra")
9348        );
9349    }
9350
9351    #[test]
9352    fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
9353        // End-to-end through Display — pins the rendered diagnostic AND the
9354        // new `(rest marker at position {n}, {extra} trailing after name,
9355        // first: {first})` clause. The `compile error in defmacro params:`
9356        // prefix matches the sibling `&rest needs a name` rendering's shape.
9357        let mut e = Expander::new();
9358        let err = e
9359            .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9360            .expect_err("trailing token after &rest name must error");
9361        assert_eq!(
9362            format!("{err}"),
9363            "compile error in defmacro params: &rest name must be last \
9364             (rest marker at position 1, 1 trailing after name, first: extra)"
9365        );
9366    }
9367
9368    #[test]
9369    fn rest_param_trailing_tokens_position_is_none_today() {
9370        // Pins `position() == None` so the future `pos: Option<usize>` add
9371        // (once `Sexp` carries source spans) lands as a deliberate
9372        // fail-before/pass-after delta. Parallel to
9373        // `rest_param_missing_name_position_is_none_today`.
9374        let err = LispError::RestParamTrailingTokens {
9375            rest_position: 1,
9376            extra: 1,
9377            first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
9378        };
9379        assert_eq!(err.position(), None);
9380    }
9381
9382    // --- MacroDefHead enum (the closed-set lift) ---
9383    //
9384    // The next nine tests pin the typed-enum lift that closes the
9385    // three-times rule on the `head: &str → &'static str` projection
9386    // idiom previously inlined at FOUR sites (the `matches!` gate at
9387    // the top of `macro_def_from` plus the projection match inside
9388    // each of `defmacro_arity`, `defmacro_non_symbol_name`,
9389    // `defmacro_non_list_params`). Every test in this block names
9390    // `MacroDefHead` directly — the symbol exists only after the
9391    // lift, so the entire block is fail-before/pass-after by
9392    // construction (compile-time edge). Theory anchor: THEORY.md
9393    // §VI.1 — three-times rule; THEORY.md §V.1 — the closed set is
9394    // a TYPE rather than a `matches!` literal.
9395
9396    #[test]
9397    fn macro_def_head_from_keyword_recognizes_defmacro() {
9398        // Pins that `MacroDefHead::from_keyword("defmacro")` returns
9399        // `Some(MacroDefHead::Defmacro)` — the first of the three
9400        // canonical macro-definition head keywords. A regression that
9401        // re-inlines a `matches!`-only gate (without the typed-enum
9402        // projection) deletes `from_keyword` and fails-loudly here.
9403        assert_eq!(
9404            MacroDefHead::from_keyword("defmacro"),
9405            Some(MacroDefHead::Defmacro)
9406        );
9407    }
9408
9409    #[test]
9410    fn macro_def_head_from_keyword_recognizes_defpoint_template() {
9411        // Pins that `MacroDefHead::from_keyword("defpoint-template")`
9412        // returns `Some(MacroDefHead::DefpointTemplate)` — the second
9413        // of the three canonical head keywords. The `defpoint-template`
9414        // form is the K8s-as-processes authoring surface (see
9415        // tatara-process); `macro_def_from` must recognize it
9416        // identically to `defmacro` so the `(defpoint-template …)`
9417        // form's macro-style binding works the same way.
9418        assert_eq!(
9419            MacroDefHead::from_keyword("defpoint-template"),
9420            Some(MacroDefHead::DefpointTemplate)
9421        );
9422    }
9423
9424    #[test]
9425    fn macro_def_head_from_keyword_recognizes_defcheck() {
9426        // Pins that `MacroDefHead::from_keyword("defcheck")` returns
9427        // `Some(MacroDefHead::Defcheck)` — the third and final
9428        // canonical head keyword. The `defcheck` form is the
9429        // workspace-coherence authoring surface (see
9430        // tatara-reconciler/checks.lisp); `macro_def_from` must
9431        // recognize it identically to `defmacro` so user-defined
9432        // checks inherit the macro-style binding semantics.
9433        assert_eq!(
9434            MacroDefHead::from_keyword("defcheck"),
9435            Some(MacroDefHead::Defcheck)
9436        );
9437    }
9438
9439    #[test]
9440    fn macro_def_head_from_keyword_rejects_unknown() {
9441        // Pins that `MacroDefHead::from_keyword` returns `None` for
9442        // anything outside the closed set — a non-symbol keyword
9443        // (`"if"`), a near-miss spelling (`"defmacroo"`,
9444        // `"defcheckk"`), and the empty string. `macro_def_from`
9445        // depends on this `None` projection to mean "this form is
9446        // not a defmacro form" and walk past — a regression that
9447        // accidentally accepts a near-miss head (e.g. via a
9448        // lower-cased `EqualFold` match) would route `(defmacroo …)`
9449        // through the arity gate, which is wrong. Pins all four
9450        // canonical near-miss / non-canonical inputs.
9451        assert_eq!(MacroDefHead::from_keyword("if"), None);
9452        assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
9453        assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
9454        assert_eq!(MacroDefHead::from_keyword(""), None);
9455    }
9456
9457    #[test]
9458    fn macro_def_head_keyword_round_trips_each_variant() {
9459        // Pins that `MacroDefHead::keyword` returns the canonical
9460        // `&'static str` literal for each variant. Together with
9461        // `from_keyword` this closes the bidirectional projection:
9462        // for every canonical head keyword `s`, `MacroDefHead::
9463        // from_keyword(s).unwrap().keyword() == s`. The `&'static
9464        // str` lifetime on the return type is load-bearing — it's
9465        // what lets the `LispError::Defmacro*` variants carry
9466        // `head: &'static str` slots without an arbitrary owned
9467        // `String`. Pinning the `: &'static str` binding here
9468        // makes the lifetime requirement load-bearing in the test.
9469        let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
9470        let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
9471        let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
9472        assert_eq!(s_defmacro, "defmacro");
9473        assert_eq!(s_defpoint, "defpoint-template");
9474        assert_eq!(s_defcheck, "defcheck");
9475    }
9476
9477    #[test]
9478    fn macro_def_head_keyword_round_trips_through_from_keyword() {
9479        // Pins that the two halves of the projection compose to the
9480        // identity on the closed set: for every canonical head
9481        // keyword, projecting `&str → MacroDefHead → &'static str`
9482        // returns the original literal. Sibling of
9483        // `macro_def_head_keyword_round_trips_each_variant` —
9484        // together they pin both directions of the bidirection.
9485        for kw in ["defmacro", "defpoint-template", "defcheck"] {
9486            let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
9487            assert_eq!(head.keyword(), kw);
9488        }
9489    }
9490
9491    #[test]
9492    fn macro_def_head_threads_through_defmacro_arity_helper() {
9493        // Pins that `defmacro_arity` accepts a typed `MacroDefHead`
9494        // and threads it through to the variant's typed `head` slot
9495        // unchanged — no `&str` projection at the helper boundary
9496        // (the projection through `MacroDefHead::keyword()` happens at
9497        // Display rendering time inside the `#[error(...)]`
9498        // annotation). A regression that drops the `MacroDefHead`
9499        // parameter type (e.g. by reverting to `head: &str`) breaks
9500        // compilation here. Pinning each of the three variants gives
9501        // the typed-head threading the same path-uniformity edge the
9502        // existing `defmacro_arity_in_*_emits_same_variant` tests pin
9503        // for the call-site path through `macro_def_from`.
9504        for head in [
9505            MacroDefHead::Defmacro,
9506            MacroDefHead::DefpointTemplate,
9507            MacroDefHead::Defcheck,
9508        ] {
9509            let err = defmacro_arity(head, 2);
9510            match err {
9511                LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
9512                other => panic!("expected DefmacroArity, got: {other:?}"),
9513            }
9514        }
9515    }
9516
9517    #[test]
9518    fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
9519        // Sibling of the `defmacro_arity` threading test — pins that
9520        // `defmacro_non_symbol_name` accepts a typed `MacroDefHead`
9521        // and threads it through to the variant's typed `head` slot
9522        // unchanged. The `got: &Sexp` parameter rides through
9523        // `crate::domain::sexp_witness` into the variant's typed
9524        // `got: SexpWitness` slot so BOTH the structural shape AND
9525        // the rendered literal are preserved across the helper
9526        // boundary, parallel to how `non_symbol_param` and
9527        // `non_symbol_unquote_target` project their `&Sexp` arguments
9528        // through the same typed joint primitive.
9529        let got = parse("5");
9530        for head in [
9531            MacroDefHead::Defmacro,
9532            MacroDefHead::DefpointTemplate,
9533            MacroDefHead::Defcheck,
9534        ] {
9535            let err = defmacro_non_symbol_name(head, &got);
9536            match err {
9537                LispError::DefmacroNonSymbolName { head: h, got: g } => {
9538                    assert_eq!(h, head);
9539                    assert_eq!(g.shape, crate::error::SexpShape::Int);
9540                    assert_eq!(g.display, "5");
9541                }
9542                other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9543            }
9544        }
9545    }
9546
9547    #[test]
9548    fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
9549        // Sibling of the `defmacro_arity` and
9550        // `defmacro_non_symbol_name` threading tests — pins that
9551        // `defmacro_non_list_params` accepts a typed `MacroDefHead`
9552        // and threads it through to the variant's typed `head` slot
9553        // unchanged. Together the three threading tests close the
9554        // typed-enum lift across all three error helpers — every
9555        // call site that constructs a `LispError::Defmacro*` variant
9556        // takes its `head` from a `MacroDefHead`, never from a `&str`
9557        // match. The `got: &Sexp` parameter rides through
9558        // `crate::domain::sexp_witness` into the variant's typed
9559        // `got: SexpWitness` slot so BOTH the structural shape AND
9560        // the rendered literal are preserved across the helper
9561        // boundary, parallel to how `defmacro_non_symbol_name`,
9562        // `non_symbol_param`, and `non_symbol_unquote_target` project
9563        // their `&Sexp` arguments through the same typed joint
9564        // primitive.
9565        let got = parse("x");
9566        for head in [
9567            MacroDefHead::Defmacro,
9568            MacroDefHead::DefpointTemplate,
9569            MacroDefHead::Defcheck,
9570        ] {
9571            let err = defmacro_non_list_params(head, &got);
9572            match err {
9573                LispError::DefmacroNonListParams { head: h, got: g } => {
9574                    assert_eq!(h, head);
9575                    assert_eq!(g.shape, crate::error::SexpShape::Symbol);
9576                    assert_eq!(g.display, "x");
9577                }
9578                other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9579            }
9580        }
9581    }
9582
9583    /// Helper for the defmacro-arity tests — pins the variant shape and
9584    /// carries the head / arity up to the assert site for legibility.
9585    /// Sibling of `non_symbol_param_fields` and
9586    /// `rest_param_missing_name_fields`.
9587    fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
9588        match err {
9589            LispError::DefmacroArity { head, arity } => (*head, *arity),
9590            other => panic!("expected DefmacroArity, got: {other:?}"),
9591        }
9592    }
9593
9594    #[test]
9595    fn defmacro_arity_with_head_only_emits_structural_variant() {
9596        // `(defmacro)` — only the head, no name / params / body. Pins
9597        // variant identity AND that `arity == 1` (just the head
9598        // element) AND that `head == "defmacro"`. A regression that
9599        // re-inlines the legacy `LispError::Compile` shape (which
9600        // named neither field) fails-loudly here.
9601        let mut e = Expander::new();
9602        let err = e
9603            .expand_program(read("(defmacro)").unwrap())
9604            .expect_err("defmacro arity gate must error");
9605        let (head, arity) = defmacro_arity_fields(&err);
9606        assert_eq!(head, MacroDefHead::Defmacro);
9607        assert_eq!(arity, 1);
9608    }
9609
9610    #[test]
9611    fn defmacro_arity_with_head_and_name_emits_structural_variant() {
9612        // `(defmacro f)` — head + name, missing params + body. Pins
9613        // that `arity` advances with the actual form length (2 for
9614        // this case) so an LSP quick-fix that wants to surface "you
9615        // wrote 2 elements; need 4" gains the count as data, no
9616        // source re-parse required.
9617        let mut e = Expander::new();
9618        let err = e
9619            .expand_program(read("(defmacro f)").unwrap())
9620            .expect_err("defmacro arity gate must error");
9621        let (head, arity) = defmacro_arity_fields(&err);
9622        assert_eq!(head, MacroDefHead::Defmacro);
9623        assert_eq!(arity, 2);
9624    }
9625
9626    #[test]
9627    fn defmacro_arity_with_head_name_params_emits_structural_variant() {
9628        // `(defmacro f ())` — head + name + params, missing body
9629        // (the most-complete partial defmacro that still trips the
9630        // arity gate). Pins that `arity == 3` exactly so an LSP
9631        // quick-fix that wants to surface "your defmacro is one
9632        // element short — body is missing" gains the count as data.
9633        let mut e = Expander::new();
9634        let err = e
9635            .expand_program(read("(defmacro f ())").unwrap())
9636            .expect_err("defmacro arity gate must error");
9637        let (head, arity) = defmacro_arity_fields(&err);
9638        assert_eq!(head, MacroDefHead::Defmacro);
9639        assert_eq!(arity, 3);
9640    }
9641
9642    #[test]
9643    fn defmacro_arity_in_defpoint_template_emits_same_variant() {
9644        // `defpoint-template` shares `macro_def_from` with `defmacro`
9645        // (all three head keywords route through the same gate). Pins
9646        // that the lift fires path-uniformly across the three head
9647        // keywords AND that the variant's `head` slot carries the
9648        // actual head literal — `defpoint-template`, not `defmacro`
9649        // — so an LSP that wants to point at "your defpoint-template
9650        // form is missing elements" gains the head as data.
9651        let mut e = Expander::new();
9652        let err = e
9653            .expand_program(read("(defpoint-template t)").unwrap())
9654            .expect_err("defpoint-template arity gate must error");
9655        let (head, arity) = defmacro_arity_fields(&err);
9656        assert_eq!(head, MacroDefHead::DefpointTemplate);
9657        assert_eq!(arity, 2);
9658    }
9659
9660    #[test]
9661    fn defmacro_arity_in_defcheck_emits_same_variant() {
9662        // Sibling of the defpoint-template test — `defcheck` is the
9663        // third head keyword `macro_def_from` recognizes. All three
9664        // route through the same arity gate and now reject too-short
9665        // forms with the same structural variant.
9666        let mut e = Expander::new();
9667        let err = e
9668            .expand_program(read("(defcheck)").unwrap())
9669            .expect_err("defcheck arity gate must error");
9670        let (head, arity) = defmacro_arity_fields(&err);
9671        assert_eq!(head, MacroDefHead::Defcheck);
9672        assert_eq!(arity, 1);
9673    }
9674
9675    #[test]
9676    fn defmacro_arity_substitute_and_bytecode_paths_agree() {
9677        // Path-uniform rejection: the SAME source emits the SAME
9678        // structural variant under both expansion strategies. The
9679        // arity gate fires inside `macro_def_from` BEFORE either
9680        // strategy's expansion path runs; so both `Expander::new()`
9681        // (bytecode) and `Expander::new_substitute_only()`
9682        // (substitute) reject the SAME malformed defmacro at the
9683        // SAME gate. Sibling of
9684        // `non_symbol_param_substitute_and_bytecode_paths_agree` and
9685        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9686        let src = "(defmacro f)";
9687        let mut subst = Expander::new_substitute_only();
9688        let mut bytecode = Expander::new();
9689        let err_subst = subst
9690            .expand_program(read(src).unwrap())
9691            .expect_err("substitute must error");
9692        let err_byte = bytecode
9693            .expand_program(read(src).unwrap())
9694            .expect_err("bytecode must error");
9695        assert_eq!(
9696            defmacro_arity_fields(&err_subst),
9697            (MacroDefHead::Defmacro, 2)
9698        );
9699        assert_eq!(
9700            defmacro_arity_fields(&err_byte),
9701            (MacroDefHead::Defmacro, 2)
9702        );
9703    }
9704
9705    #[test]
9706    fn defmacro_arity_message_renders_legacy_substring_with_arity() {
9707        // End-to-end through Display — pins the rendered diagnostic
9708        // consumers see today (REPL, `tatara-check`) AND the new
9709        // `(got {arity} elements, need 4)` clause. The legacy
9710        // `"(defmacro name (params) body) required"` substring
9711        // rides through verbatim. Tools that pattern-match on the
9712        // variant gain structural binding to `head` / `arity`.
9713        let mut e = Expander::new();
9714        let err = e
9715            .expand_program(read("(defmacro f)").unwrap())
9716            .expect_err("defmacro arity gate must error");
9717        assert_eq!(
9718            format!("{err}"),
9719            "compile error in defmacro: (defmacro name (params) body) required \
9720             (got 2 elements, need 4)"
9721        );
9722    }
9723
9724    #[test]
9725    fn defmacro_arity_position_is_none_today() {
9726        // Negative control for the future-spans move: until `Sexp`
9727        // carries source positions, `position()` on `LispError`
9728        // returns `None` for this variant. A future run that gives
9729        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9730        // this test gives that change a deliberate fail-before/pass-
9731        // after delta. Parallel to
9732        // `non_symbol_param_position_is_none_today` and
9733        // `rest_param_missing_name_position_is_none_today`.
9734        let mut e = Expander::new();
9735        let err = e
9736            .expand_program(read("(defmacro)").unwrap())
9737            .expect_err("defmacro arity gate must error");
9738        assert_eq!(err.position(), None);
9739    }
9740
9741    #[test]
9742    fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
9743        // Negative control: a defmacro with exactly 4 elements (head
9744        // + name + params + body) passes the arity gate. Pins that
9745        // the lift is scoped to the arity-deficient case, not to
9746        // every defmacro form. After this test, a regression that
9747        // tightens the arity gate to >= 5 (e.g. spuriously requiring
9748        // a docstring slot) fails-loudly here.
9749        let mut e = Expander::new();
9750        let out = e
9751            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9752            .expect("well-formed defmacro must succeed");
9753        assert_eq!(out[0], Sexp::int(42));
9754    }
9755
9756    /// Helper for the defmacro-non-symbol-name tests — pins variant
9757    /// shape and carries the head / got up to the assert site for
9758    /// legibility. Sibling of `defmacro_arity_fields`,
9759    /// `non_symbol_param_fields`, and `rest_param_missing_name_fields`.
9760    fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
9761        match err {
9762            LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
9763            other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9764        }
9765    }
9766
9767    #[test]
9768    fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
9769        // `(defmacro 5 () body)` — the form passes the arity gate
9770        // (4 elements) but list[1] is `5`, not a symbol. Pins variant
9771        // identity AND that `head == "defmacro"` AND that `got ==
9772        // "5"`. A regression that re-inlines the legacy
9773        // `LispError::Compile { form: "defmacro", message: "expected
9774        // name symbol" }` shape (which named the failure mode but
9775        // not the offending element) fails-loudly here.
9776        let mut e = Expander::new();
9777        let err = e
9778            .expand_program(read("(defmacro 5 () body)").unwrap())
9779            .expect_err("defmacro non-symbol name gate must error");
9780        let (head, got) = defmacro_non_symbol_name_fields(&err);
9781        assert_eq!(head, MacroDefHead::Defmacro);
9782        assert_eq!(got, "5");
9783    }
9784
9785    #[test]
9786    fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
9787        // `(defmacro :foo () body)` — list[1] is the keyword `:foo`,
9788        // not a symbol. Pins that `Sexp::Display` for
9789        // `Atom::Keyword(s)` writes `:s` and the variant's `got` slot
9790        // carries the keyword form unchanged. An LSP that wants to
9791        // surface "you wrote `:foo` where a name symbol was expected"
9792        // gains the literal keyword value as data, no source re-parse
9793        // required.
9794        let mut e = Expander::new();
9795        let err = e
9796            .expand_program(read("(defmacro :foo () body)").unwrap())
9797            .expect_err("defmacro non-symbol name gate must error");
9798        let (head, got) = defmacro_non_symbol_name_fields(&err);
9799        assert_eq!(head, MacroDefHead::Defmacro);
9800        assert_eq!(got, ":foo");
9801    }
9802
9803    #[test]
9804    fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
9805        // `(defmacro "name" () body)` — list[1] is the string
9806        // literal `"name"`, not a symbol. Pins that `Sexp::Display`
9807        // for `Atom::String(s)` writes `"s"` (with quotes) and the
9808        // variant's `got` slot carries the quoted form unchanged.
9809        let mut e = Expander::new();
9810        let err = e
9811            .expand_program(read("(defmacro \"name\" () body)").unwrap())
9812            .expect_err("defmacro non-symbol name gate must error");
9813        let (head, got) = defmacro_non_symbol_name_fields(&err);
9814        assert_eq!(head, MacroDefHead::Defmacro);
9815        assert_eq!(got, "\"name\"");
9816    }
9817
9818    #[test]
9819    fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
9820        // `(defmacro (nested) () body)` — list[1] is a nested list,
9821        // not a symbol. Pins that `Sexp::Display` for a list writes
9822        // `(elements)` and the variant's `got` slot carries the
9823        // parenthesized form unchanged.
9824        let mut e = Expander::new();
9825        let err = e
9826            .expand_program(read("(defmacro (nested) () body)").unwrap())
9827            .expect_err("defmacro non-symbol name gate must error");
9828        let (head, got) = defmacro_non_symbol_name_fields(&err);
9829        assert_eq!(head, MacroDefHead::Defmacro);
9830        assert_eq!(got, "(nested)");
9831    }
9832
9833    #[test]
9834    fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
9835        // `defpoint-template` shares `macro_def_from` with `defmacro`
9836        // (all three head keywords route through the same gate).
9837        // Pins that the lift fires path-uniformly across the three
9838        // head keywords AND that the variant's `head` slot carries
9839        // the actual head literal — `defpoint-template`, not
9840        // `defmacro` — so an LSP that wants to point at "your
9841        // defpoint-template form's name slot isn't a symbol" gains
9842        // the head as data.
9843        let mut e = Expander::new();
9844        let err = e
9845            .expand_program(read("(defpoint-template 7 () body)").unwrap())
9846            .expect_err("defpoint-template non-symbol name gate must error");
9847        let (head, got) = defmacro_non_symbol_name_fields(&err);
9848        assert_eq!(head, MacroDefHead::DefpointTemplate);
9849        assert_eq!(got, "7");
9850    }
9851
9852    #[test]
9853    fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
9854        // Sibling for the `defcheck` head — third head keyword
9855        // `macro_def_from` recognizes. Rounds out the three-head-
9856        // keyword coverage so the lift is path-uniform across
9857        // `defmacro` / `defpoint-template` / `defcheck`.
9858        let mut e = Expander::new();
9859        let err = e
9860            .expand_program(read("(defcheck :k () body)").unwrap())
9861            .expect_err("defcheck non-symbol name gate must error");
9862        let (head, got) = defmacro_non_symbol_name_fields(&err);
9863        assert_eq!(head, MacroDefHead::Defcheck);
9864        assert_eq!(got, ":k");
9865    }
9866
9867    #[test]
9868    fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
9869        // Path-uniform rejection: the SAME source emits the SAME
9870        // structural variant under both expansion strategies. The
9871        // name-symbol gate fires inside `macro_def_from` BEFORE
9872        // either expansion strategy runs, so the gate is naturally
9873        // path-uniform; pinning it gives a regression that drifts
9874        // either strategy's handling of non-symbol-name defmacros (or
9875        // makes one strategy accept what the other rejects) a fail-
9876        // before/pass-after edge. Sibling of
9877        // `defmacro_arity_substitute_and_bytecode_paths_agree`,
9878        // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
9879        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9880        let src = "(defmacro 5 () body)";
9881        let mut subst = Expander::new_substitute_only();
9882        let mut bytecode = Expander::new();
9883        let err_subst = subst
9884            .expand_program(read(src).unwrap())
9885            .expect_err("substitute must error");
9886        let err_byte = bytecode
9887            .expand_program(read(src).unwrap())
9888            .expect_err("bytecode must error");
9889        assert_eq!(
9890            defmacro_non_symbol_name_fields(&err_subst),
9891            (MacroDefHead::Defmacro, "5")
9892        );
9893        assert_eq!(
9894            defmacro_non_symbol_name_fields(&err_byte),
9895            (MacroDefHead::Defmacro, "5")
9896        );
9897    }
9898
9899    #[test]
9900    fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
9901        // End-to-end through Display — pins the rendered diagnostic
9902        // consumers see today (REPL, `tatara-check`) AND the new
9903        // `, got {got}` clause. The legacy `"expected name symbol"`
9904        // substring rides through verbatim; the prefix matches the
9905        // legacy `Compile { form: "defmacro", message: "expected name
9906        // symbol" }` byte-for-byte. Tools that pattern-match on the
9907        // variant gain structural binding to `head` / `got`.
9908        let mut e = Expander::new();
9909        let err = e
9910            .expand_program(read("(defmacro 5 () body)").unwrap())
9911            .expect_err("defmacro non-symbol name gate must error");
9912        assert_eq!(
9913            format!("{err}"),
9914            "compile error in defmacro: expected name symbol, got 5"
9915        );
9916    }
9917
9918    #[test]
9919    fn defmacro_non_symbol_name_position_is_none_today() {
9920        // Negative control for the future-spans move: until `Sexp`
9921        // carries source positions, `position()` on `LispError`
9922        // returns `None` for this variant. A future run that gives
9923        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9924        // this test gives that change a deliberate fail-before/pass-
9925        // after delta. Parallel to
9926        // `defmacro_arity_position_is_none_today`,
9927        // `non_symbol_param_position_is_none_today`, and
9928        // `rest_param_missing_name_position_is_none_today`.
9929        let mut e = Expander::new();
9930        let err = e
9931            .expand_program(read("(defmacro 5 () body)").unwrap())
9932            .expect_err("defmacro non-symbol name gate must error");
9933        assert_eq!(err.position(), None);
9934    }
9935
9936    #[test]
9937    fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
9938        // Negative control: a defmacro whose name slot IS a symbol
9939        // passes the name-symbol gate. Pins that the lift is scoped
9940        // to the non-symbol-name case, not to every defmacro form.
9941        // After this test, a regression that tightens the gate to
9942        // reject e.g. kebab-cased names fails-loudly here.
9943        let mut e = Expander::new();
9944        let out = e
9945            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9946            .expect("well-formed defmacro must succeed");
9947        assert_eq!(out[0], Sexp::int(42));
9948    }
9949
9950    #[test]
9951    fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
9952        // Pins the gate ordering: a 4-element defmacro whose name
9953        // slot is non-symbol fires `DefmacroNonSymbolName`, NOT
9954        // `DefmacroArity`. The arity gate (>= 4 elements) admits
9955        // this form; the name-symbol gate is the next checkpoint.
9956        // A regression that swaps the gate ordering (e.g. checks
9957        // name-symbol before arity, so `(defmacro 5)` would emit
9958        // `DefmacroNonSymbolName` instead of `DefmacroArity`) fails-
9959        // loudly here.
9960        let mut e = Expander::new();
9961        let err = e
9962            .expand_program(read("(defmacro 5 () body)").unwrap())
9963            .expect_err("name-symbol gate must error");
9964        assert!(
9965            matches!(err, LispError::DefmacroNonSymbolName { .. }),
9966            "expected DefmacroNonSymbolName, got: {err:?}"
9967        );
9968
9969        let err_arity = e
9970            .expand_program(read("(defmacro 5)").unwrap())
9971            .expect_err("arity gate must error");
9972        assert!(
9973            matches!(err_arity, LispError::DefmacroArity { .. }),
9974            "expected DefmacroArity (arity < 4 short-circuits before name check), \
9975             got: {err_arity:?}"
9976        );
9977    }
9978
9979    /// Helper for the defmacro-non-list-params tests — pins variant
9980    /// shape and carries the head / got up to the assert site for
9981    /// legibility. Sibling of `defmacro_arity_fields`,
9982    /// `defmacro_non_symbol_name_fields`, `non_symbol_param_fields`,
9983    /// and `rest_param_missing_name_fields`.
9984    fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
9985        match err {
9986            LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
9987            other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9988        }
9989    }
9990
9991    #[test]
9992    fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
9993        // `(defmacro f x body)` — the form passes both the arity gate
9994        // (4 elements) AND the name-symbol gate (`f` is a symbol) but
9995        // list[2] is the symbol `x`, not a list. Pins variant identity
9996        // AND that `head == "defmacro"` AND that `got == "x"`. A
9997        // regression that re-inlines the legacy `LispError::Compile {
9998        // form: "defmacro", message: "expected param list" }` shape
9999        // (which named the failure mode but not the offending element)
10000        // fails-loudly here.
10001        let mut e = Expander::new();
10002        let err = e
10003            .expand_program(read("(defmacro f x body)").unwrap())
10004            .expect_err("defmacro non-list params gate must error");
10005        let (head, got) = defmacro_non_list_params_fields(&err);
10006        assert_eq!(head, MacroDefHead::Defmacro);
10007        assert_eq!(got, "x");
10008    }
10009
10010    #[test]
10011    fn defmacro_non_list_params_with_int_emits_structural_variant() {
10012        // `(defmacro f 5 body)` — list[2] is `5`, not a list. Pins
10013        // that `Sexp::Display` for `Atom::Int(n)` writes `n` and the
10014        // variant's `got` slot carries the integer form unchanged. An
10015        // LSP that surfaces "you wrote `5` where a param list was
10016        // expected" gains the literal value as data, no source
10017        // re-parse required.
10018        let mut e = Expander::new();
10019        let err = e
10020            .expand_program(read("(defmacro f 5 body)").unwrap())
10021            .expect_err("defmacro non-list params gate must error");
10022        let (head, got) = defmacro_non_list_params_fields(&err);
10023        assert_eq!(head, MacroDefHead::Defmacro);
10024        assert_eq!(got, "5");
10025    }
10026
10027    #[test]
10028    fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
10029        // `(defmacro f :foo body)` — list[2] is the keyword `:foo`,
10030        // not a list. Pins that `Sexp::Display` for `Atom::Keyword(s)`
10031        // writes `:s` and the variant's `got` slot carries the
10032        // keyword form unchanged.
10033        let mut e = Expander::new();
10034        let err = e
10035            .expand_program(read("(defmacro f :foo body)").unwrap())
10036            .expect_err("defmacro non-list params gate must error");
10037        let (head, got) = defmacro_non_list_params_fields(&err);
10038        assert_eq!(head, MacroDefHead::Defmacro);
10039        assert_eq!(got, ":foo");
10040    }
10041
10042    #[test]
10043    fn defmacro_non_list_params_with_string_emits_structural_variant() {
10044        // `(defmacro f "params" body)` — list[2] is the string literal
10045        // `"params"`, not a list. Pins that `Sexp::Display` for
10046        // `Atom::String(s)` writes `"s"` (with quotes) and the
10047        // variant's `got` slot carries the quoted form unchanged.
10048        let mut e = Expander::new();
10049        let err = e
10050            .expand_program(read("(defmacro f \"params\" body)").unwrap())
10051            .expect_err("defmacro non-list params gate must error");
10052        let (head, got) = defmacro_non_list_params_fields(&err);
10053        assert_eq!(head, MacroDefHead::Defmacro);
10054        assert_eq!(got, "\"params\"");
10055    }
10056
10057    #[test]
10058    fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
10059        // `defpoint-template` shares `macro_def_from` with `defmacro`
10060        // (all three head keywords route through the same gate).
10061        // Pins that the lift fires path-uniformly across the three
10062        // head keywords AND that the variant's `head` slot carries
10063        // the actual head literal — `defpoint-template`, not
10064        // `defmacro` — so an LSP that wants to point at "your
10065        // defpoint-template form's param-list slot isn't a list"
10066        // gains the head as data.
10067        let mut e = Expander::new();
10068        let err = e
10069            .expand_program(read("(defpoint-template t x body)").unwrap())
10070            .expect_err("defpoint-template non-list params gate must error");
10071        let (head, got) = defmacro_non_list_params_fields(&err);
10072        assert_eq!(head, MacroDefHead::DefpointTemplate);
10073        assert_eq!(got, "x");
10074    }
10075
10076    #[test]
10077    fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
10078        // Sibling for the `defcheck` head — third head keyword
10079        // `macro_def_from` recognizes. Rounds out the three-head-
10080        // keyword coverage so the lift is path-uniform across
10081        // `defmacro` / `defpoint-template` / `defcheck`.
10082        let mut e = Expander::new();
10083        let err = e
10084            .expand_program(read("(defcheck c 7 body)").unwrap())
10085            .expect_err("defcheck non-list params gate must error");
10086        let (head, got) = defmacro_non_list_params_fields(&err);
10087        assert_eq!(head, MacroDefHead::Defcheck);
10088        assert_eq!(got, "7");
10089    }
10090
10091    #[test]
10092    fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
10093        // Path-uniform rejection: the SAME source emits the SAME
10094        // structural variant under both expansion strategies. The
10095        // param-list gate fires inside `macro_def_from` BEFORE either
10096        // expansion strategy runs, so the gate is naturally path-
10097        // uniform; pinning it gives a regression that drifts either
10098        // strategy's handling of non-list-params defmacros (or makes
10099        // one strategy accept what the other rejects) a fail-before/
10100        // pass-after edge. Sibling of
10101        // `defmacro_arity_substitute_and_bytecode_paths_agree`,
10102        // `defmacro_non_symbol_name_substitute_and_bytecode_paths_agree`,
10103        // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
10104        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
10105        let src = "(defmacro f x body)";
10106        let mut subst = Expander::new_substitute_only();
10107        let mut bytecode = Expander::new();
10108        let err_subst = subst
10109            .expand_program(read(src).unwrap())
10110            .expect_err("substitute must error");
10111        let err_byte = bytecode
10112            .expand_program(read(src).unwrap())
10113            .expect_err("bytecode must error");
10114        assert_eq!(
10115            defmacro_non_list_params_fields(&err_subst),
10116            (MacroDefHead::Defmacro, "x")
10117        );
10118        assert_eq!(
10119            defmacro_non_list_params_fields(&err_byte),
10120            (MacroDefHead::Defmacro, "x")
10121        );
10122    }
10123
10124    #[test]
10125    fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
10126        // End-to-end through Display — pins the rendered diagnostic
10127        // consumers see today (REPL, `tatara-check`) AND the new
10128        // `, got {got}` clause. The legacy `"expected param list"`
10129        // substring rides through verbatim; the prefix matches the
10130        // legacy `Compile { form: "defmacro", message: "expected
10131        // param list" }` byte-for-byte. Tools that pattern-match on
10132        // the variant gain structural binding to `head` / `got`.
10133        let mut e = Expander::new();
10134        let err = e
10135            .expand_program(read("(defmacro f x body)").unwrap())
10136            .expect_err("defmacro non-list params gate must error");
10137        assert_eq!(
10138            format!("{err}"),
10139            "compile error in defmacro: expected param list, got x"
10140        );
10141    }
10142
10143    #[test]
10144    fn defmacro_non_list_params_position_is_none_today() {
10145        // Negative control for the future-spans move: until `Sexp`
10146        // carries source positions, `position()` on `LispError`
10147        // returns `None` for this variant. A future run that gives
10148        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
10149        // this test gives that change a deliberate fail-before/pass-
10150        // after delta. Parallel to
10151        // `defmacro_arity_position_is_none_today`,
10152        // `defmacro_non_symbol_name_position_is_none_today`,
10153        // `non_symbol_param_position_is_none_today`, and
10154        // `rest_param_missing_name_position_is_none_today`.
10155        let mut e = Expander::new();
10156        let err = e
10157            .expand_program(read("(defmacro f x body)").unwrap())
10158            .expect_err("defmacro non-list params gate must error");
10159        assert_eq!(err.position(), None);
10160    }
10161
10162    #[test]
10163    fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
10164        // Negative control: a defmacro whose param-list slot IS a
10165        // list passes the param-list gate. Pins that the lift is
10166        // scoped to the non-list-params case, not to every defmacro
10167        // form. After this test, a regression that tightens the gate
10168        // to reject e.g. empty param lists fails-loudly here.
10169        let mut e = Expander::new();
10170        let out = e
10171            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10172            .expect("well-formed defmacro must succeed");
10173        assert_eq!(out[0], Sexp::int(42));
10174    }
10175
10176    #[test]
10177    fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
10178        // Pins the gate ordering: a 4-element defmacro whose name
10179        // slot IS a symbol but whose param-list slot is non-list
10180        // fires `DefmacroNonListParams`, NOT `DefmacroNonSymbolName`.
10181        // The name-symbol gate admits this form; the param-list gate
10182        // is the next checkpoint. A regression that swaps the gate
10183        // ordering (e.g. checks param-list before name-symbol, so
10184        // `(defmacro 5 x body)` would emit `DefmacroNonListParams`
10185        // instead of `DefmacroNonSymbolName`) fails-loudly here.
10186        let mut e = Expander::new();
10187        let err = e
10188            .expand_program(read("(defmacro f x body)").unwrap())
10189            .expect_err("param-list gate must error");
10190        assert!(
10191            matches!(err, LispError::DefmacroNonListParams { .. }),
10192            "expected DefmacroNonListParams, got: {err:?}"
10193        );
10194
10195        let err_name = e
10196            .expand_program(read("(defmacro 5 x body)").unwrap())
10197            .expect_err("name-symbol gate must error");
10198        assert!(
10199            matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
10200            "expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
10201             got: {err_name:?}"
10202        );
10203    }
10204
10205    #[test]
10206    fn defmacro_non_list_params_fires_after_arity_gate_passes() {
10207        // Pins the full gate ordering: a 4-element defmacro whose
10208        // first three slots are head/symbol/non-list fires
10209        // `DefmacroNonListParams`, NOT `DefmacroArity`. The arity
10210        // gate (>= 4 elements) admits this form; the name-symbol
10211        // gate admits the symbol; the param-list gate is the third
10212        // checkpoint. A regression that drifts the gate sequence
10213        // (e.g. fires `DefmacroArity` for a 4-element form) fails-
10214        // loudly here. Parallel to
10215        // `defmacro_non_symbol_name_fires_after_arity_gate_passes`
10216        // — together they pin the full
10217        // arity → name-symbol → param-list ordering inside
10218        // `macro_def_from`.
10219        let mut e = Expander::new();
10220        let err = e
10221            .expand_program(read("(defmacro f x body)").unwrap())
10222            .expect_err("param-list gate must error");
10223        assert!(
10224            matches!(err, LispError::DefmacroNonListParams { .. }),
10225            "expected DefmacroNonListParams, got: {err:?}"
10226        );
10227
10228        let err_arity = e
10229            .expand_program(read("(defmacro f x)").unwrap())
10230            .expect_err("arity gate must error");
10231        assert!(
10232            matches!(err_arity, LispError::DefmacroArity { .. }),
10233            "expected DefmacroArity (arity < 4 short-circuits before param-list check), \
10234             got: {err_arity:?}"
10235        );
10236    }
10237
10238    #[test]
10239    fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
10240        // Negative control: `&rest` is a symbol (`Atom::Symbol("&rest")`)
10241        // at the parser level, so `as_symbol()` succeeds for it. The
10242        // `NonSymbolParam` variant does NOT fire on the `&rest` marker
10243        // itself; the dedicated `&rest needs a name` rejection (a
10244        // separate failure mode in this cluster) handles malformed
10245        // rest-param shapes. Pins that the lift is scoped to
10246        // non-symbol elements at param-list positions, not to
10247        // every malformed-param shape.
10248        let mut e = Expander::new();
10249        let out = e
10250            .expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
10251            .expect("&rest with name must succeed");
10252        assert_eq!(out[0], parse("(list 1 2 3)"));
10253    }
10254
10255    #[test]
10256    fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
10257        // End-to-end through the Display impl — pins the rendered diagnostic
10258        // a downstream tool sees today (REPL, tatara-check). The shape is
10259        // parallel to the existing `TypeMismatch` variant: form, expected
10260        // shape, offending literal — all three slots present.
10261        let mut e = Expander::new();
10262        let err = e
10263            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
10264            .expect_err("non-symbol target must error");
10265        assert_eq!(
10266            format!("{err}"),
10267            "compile error in ,: expected symbol, got (list 1 2)"
10268        );
10269    }
10270
10271    // ── template_invariant_violation: structural lift ───────────────
10272    //
10273    // The four byte-identical inline `LispError::Compile { form:
10274    // macro_name.into(), message: <invariant> }` triples in `apply_compiled`
10275    // (Subst-bad-index, Splice-bad-index, EndList-empty-stack,
10276    // final-no-value gates) were lifted to `template_invariant_violation`,
10277    // and the helper's emission was promoted from `LispError::Compile`-
10278    // shape to the structural `LispError::TemplateInvariant { macro_name,
10279    // kind: TemplateInvariantKind }` variant. The index payload of the
10280    // Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
10281    // / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
10282    // kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
10283    // is structurally unrepresentable. Display matches the legacy
10284    // `Compile`-shaped diagnostic byte-for-byte via the closed-set
10285    // `TemplateInvariantKind::message()` projection so authoring-tool
10286    // substring greps see no drift across the lift.
10287    //
10288    // The tests below pin: (a) the helper produces the structural
10289    // `LispError::TemplateInvariant` variant with `macro_name` and `kind`
10290    // first-class; (b) the Subst / Splice gates thread the bad index
10291    // through the typed variants `SubstBadIndex(usize)` / `SpliceBadIndex(usize)`
10292    // unchanged; (c) the two REACHABLE invariant-violation paths through
10293    // `apply_compiled` — Subst with out-of-bounds idx, Splice with
10294    // out-of-bounds idx — route through the helper end-to-end (the
10295    // EndList / no-value paths are guarded by `last_mut().unwrap()`
10296    // ahead of `pop().ok_or_else()` and are not reachable through any
10297    // single CompiledTemplate; they remain defensive against future
10298    // changes to the stack discipline); (d) the legacy Display
10299    // rendering matches byte-for-byte across the lift; (e) positive
10300    // controls: a well-formed CompiledTemplate routes PAST the helper
10301    // cleanly, and unrelated macro errors (missing-required-arg) do
10302    // NOT route through the helper.
10303
10304    #[test]
10305    fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
10306        // Direct unit test of the helper: a fixed macro_name and a
10307        // `TemplateInvariantKind` produce a `LispError::TemplateInvariant`
10308        // variant with the macro_name in the `macro_name` slot and the
10309        // kind passed through verbatim in the `kind` slot. A regression
10310        // that drifts the variant (e.g., back to `LispError::Compile`)
10311        // or swaps the slot positions fails-loudly here.
10312        let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
10313        match err {
10314            LispError::TemplateInvariant { macro_name, kind } => {
10315                assert_eq!(macro_name, "test-macro");
10316                assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10317            }
10318            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10319        }
10320    }
10321
10322    #[test]
10323    fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
10324        // The Subst gate's `usize` idx lives INSIDE the
10325        // `TemplateInvariantKind::SubstBadIndex(usize)` variant rather
10326        // than being substring-rendered into a free-form `message`
10327        // slot. Pin that the helper threads the bad index through the
10328        // typed variant unchanged; a regression that drops the index
10329        // payload (e.g., via a `usize -> ()` projection) fails here.
10330        let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
10331        match err {
10332            LispError::TemplateInvariant { macro_name, kind } => {
10333                assert_eq!(macro_name, "wrap");
10334                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10335            }
10336            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10337        }
10338    }
10339
10340    #[test]
10341    fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
10342        // Hand-crafted CompiledTemplate with a Subst(99) op against
10343        // an empty params list: `args_by_index` has length 0, so
10344        // `.get(99)` returns None and the `ok_or_else` triggers
10345        // through the helper. Fail-before-pass-after: this same input
10346        // pre-lift went through `LispError::Compile { form: macro_name,
10347        // message: format!("compiled template referenced bad param
10348        // index {idx}") }`; post-lift it routes through
10349        // `template_invariant_violation` and emits the structural
10350        // `TemplateInvariant { macro_name, kind: SubstBadIndex(99) }`
10351        // variant with the bad index threaded through as typed data.
10352        let tmpl = CompiledTemplate {
10353            ops: vec![TemplateOp::Subst(99)],
10354        };
10355        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10356            .expect_err("bad idx must error");
10357        match err {
10358            LispError::TemplateInvariant { macro_name, kind } => {
10359                assert_eq!(macro_name, "test-macro");
10360                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10361            }
10362            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10363        }
10364    }
10365
10366    #[test]
10367    fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
10368        // Hand-crafted CompiledTemplate with a Splice(42) op against
10369        // an empty params list. Sibling of the Subst-bad-idx test;
10370        // pins the Splice gate routes through the helper with the
10371        // typed `SpliceBadIndex(42)` kind carrying the bad index.
10372        let tmpl = CompiledTemplate {
10373            ops: vec![TemplateOp::Splice(42)],
10374        };
10375        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10376            .expect_err("bad splice idx must error");
10377        match err {
10378            LispError::TemplateInvariant { macro_name, kind } => {
10379                assert_eq!(macro_name, "call-macro");
10380                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10381            }
10382            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10383        }
10384    }
10385
10386    #[test]
10387    fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
10388        // End-to-end through the `LispError` Display impl — pins the
10389        // rendered diagnostic byte-for-byte: `"compile error in
10390        // test-macro: compiled template referenced bad param index 99"`.
10391        // Authoring tools that substring-grep the rendered diagnostic
10392        // (`tatara-check`'s diagnostic capture, REPL substring-greps)
10393        // see no drift across the lift. Parallel to how
10394        // `compile_named_non_symbol_name_renders_legacy_compile_shape`
10395        // pins the sibling-file (compile.rs) lift's Display contract.
10396        let tmpl = CompiledTemplate {
10397            ops: vec![TemplateOp::Subst(99)],
10398        };
10399        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10400            .expect_err("bad idx must error");
10401        assert_eq!(
10402            format!("{err}"),
10403            "compile error in test-macro: compiled template referenced bad param index 99"
10404        );
10405    }
10406
10407    #[test]
10408    fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
10409        // Sibling Display test for the Splice gate. Pins the message
10410        // byte-for-byte through the `LispError` Display impl: `"compile
10411        // error in call-macro: compiled template referenced bad splice
10412        // index 42"`.
10413        let tmpl = CompiledTemplate {
10414            ops: vec![TemplateOp::Splice(42)],
10415        };
10416        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10417            .expect_err("bad splice idx must error");
10418        assert_eq!(
10419            format!("{err}"),
10420            "compile error in call-macro: compiled template referenced bad splice index 42"
10421        );
10422    }
10423
10424    #[test]
10425    fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
10426        // Positive control: a CompiledTemplate produced by the
10427        // bytecode compiler (`compile_template`) for a well-formed
10428        // macro never references an out-of-bounds index nor
10429        // unbalances the stack, so `apply_compiled` routes PAST the
10430        // helper cleanly. A regression that fires the helper on
10431        // well-formed bytecode (e.g., off-by-one in the index
10432        // resolution) would fail here. End-to-end through the public
10433        // `Expander` surface so the test exercises the same code
10434        // path users see.
10435        let mut e = Expander::new();
10436        let out = e
10437            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10438            .expect("well-formed macro expansion must not fire template-invariant-violation");
10439        assert_eq!(out.len(), 1);
10440        assert_eq!(out[0], Sexp::int(42));
10441    }
10442
10443    #[test]
10444    fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
10445        // Negative control: the `missing_macro_arg` gate in the shared
10446        // positional binder (`MacroParams::bind`) fires BEFORE the bytecode
10447        // loop runs,
10448        // so a missing required arg routes through
10449        // `LispError::MissingMacroArg`, NOT through
10450        // `template_invariant_violation`. Pins the helper is
10451        // precisely scoped to bytecode-runtime invariant violations
10452        // (Subst / Splice / stack gates), not to macro-call arity
10453        // errors (the latter has its own structural variant). A
10454        // regression that conflates the two gate clusters would
10455        // route this case through `Compile { ... }` instead of
10456        // `MissingMacroArg` and fail-loudly here.
10457        let mut e = Expander::new();
10458        let err = e
10459            .expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
10460            .expect_err("missing required arg must error");
10461        assert!(
10462            matches!(err, LispError::MissingMacroArg { .. }),
10463            "expected MissingMacroArg, got: {err:?}"
10464        );
10465    }
10466
10467    // ── resolve_bound_arg: bytecode-runtime bound-arg-by-index lookup ──
10468    //
10469    // `resolve_bound_arg(args_by_index, idx, macro_name, kind)` lifts the
10470    // `args_by_index.get(*idx).ok_or_else(|| template_invariant_violation(
10471    // macro_name, KIND(*idx)))?` projection that recurred at BOTH the
10472    // `TemplateOp::Subst` and `TemplateOp::Splice` arms inside
10473    // `apply_compiled`. The arms differ in the kind constructor
10474    // (`SubstBadIndex` vs. `SpliceBadIndex`) and in their post-lookup
10475    // verb (clone+push vs. splice-coerce), but the lookup-and-reject
10476    // prelude is byte-identical modulo the constructor. These tests
10477    // pin the lifted helper's contract directly; the existing
10478    // `apply_compiled_*_bad_idx_*` tests are the path-uniformity
10479    // guards proving both production arms route through it without
10480    // behavior drift.
10481
10482    #[test]
10483    fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
10484        // For an in-range index, the helper returns `Ok(&args[idx])`
10485        // borrowed VERBATIM — same pointer as `args_by_index.get(idx)`.
10486        // Pins the borrow-not-clone contract: a regression that drifts
10487        // the helper to clone+return (`Result<Sexp>` instead of
10488        // `Result<&Sexp>`) would allocate per lookup at the production
10489        // `Subst`/`Splice` hot path. The kind constructor must NOT
10490        // fire on the success path (`FnOnce`'s lazy semantics) — pin
10491        // that the test passes a constructor that would panic if
10492        // called, asserting the helper short-circuits before invoking
10493        // it on the in-range arm.
10494        let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10495        let got = resolve_bound_arg(&args, 1, "m", |_| {
10496            panic!("kind constructor must not fire on the in-range path")
10497        })
10498        .expect("in-range lookup must succeed");
10499        assert!(
10500            std::ptr::eq(got, &args[1]),
10501            "resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
10502        );
10503        assert_eq!(*got, Sexp::int(2));
10504    }
10505
10506    #[test]
10507    fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
10508        // For an out-of-range index, the helper raises the structural
10509        // `LispError::TemplateInvariant` variant with the caller-
10510        // supplied `SubstBadIndex` kind constructor applied to the bad
10511        // index. Pins the post-lift emission shape (variant identity
10512        // + the kind constructor threaded with the actual idx); a
10513        // regression that drops the idx payload (e.g., via a `usize ->
10514        // ()` projection) or hard-codes a different kind at the helper
10515        // boundary fails-loudly here. Fail-before-pass-after: this
10516        // assert is contradicted by the pre-lift code path (which
10517        // never called `resolve_bound_arg` because it didn't exist),
10518        // ratifies the post-lift one.
10519        let args: Vec<Sexp> = Vec::new();
10520        let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
10521            .expect_err("out-of-range lookup must error");
10522        match err {
10523            LispError::TemplateInvariant { macro_name, kind } => {
10524                assert_eq!(macro_name, "test-macro");
10525                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10526            }
10527            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10528        }
10529    }
10530
10531    #[test]
10532    fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
10533        // Path-uniformity for the per-call-site kind constructor: the
10534        // SAME out-of-range idx via the `SpliceBadIndex` constructor
10535        // emits `kind: SpliceBadIndex(7)` — distinct from the sibling
10536        // `SubstBadIndex(7)` variant. Pins that the constructor is
10537        // chosen per call site (not hard-coded at the helper boundary),
10538        // closing the structural matrix `{Subst, Splice} × {in-range,
10539        // out-of-range}` the two production arms span across the
10540        // bytecode-runtime's bound-arg-by-index reads. A regression
10541        // that hard-codes a single kind at the helper boundary would
10542        // emit the same variant identity for both call sites and
10543        // fail-loudly here.
10544        let args: Vec<Sexp> = Vec::new();
10545        let err = resolve_bound_arg(
10546            &args,
10547            7,
10548            "test-macro",
10549            TemplateInvariantKind::SpliceBadIndex,
10550        )
10551        .expect_err("out-of-range lookup must error");
10552        match err {
10553            LispError::TemplateInvariant { macro_name, kind } => {
10554                assert_eq!(macro_name, "test-macro");
10555                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
10556            }
10557            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10558        }
10559    }
10560
10561    #[test]
10562    fn resolve_bound_arg_threads_macro_name_verbatim() {
10563        // Path-uniformity for the `macro_name` slot: the helper threads
10564        // the caller's borrow into the variant's owned `String` slot
10565        // verbatim. Pin two distinct macro names route through with no
10566        // mutual interference — a regression that hard-codes a single
10567        // macro_name at the helper boundary or swaps the parameter
10568        // ordering fails-loudly here. Same posture as
10569        // `compiler_spec_io_err_threads_each_stage_through_unchanged`
10570        // pins the typed `stage` slot in the disk-persistence sibling
10571        // lift.
10572        let args: Vec<Sexp> = Vec::new();
10573        for name in ["wrap", "call-macro", "obs"] {
10574            let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
10575                .expect_err("out-of-range lookup must error");
10576            match err {
10577                LispError::TemplateInvariant { macro_name, kind } => {
10578                    assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
10579                    assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10580                }
10581                other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10582            }
10583        }
10584    }
10585
10586    #[test]
10587    fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
10588        // Edge case: idx 0 with a single-element args_by_index returns
10589        // `Ok(&args[0])`. Pins the lower-bound of the in-range surface
10590        // — a regression that off-by-ones the lookup (e.g., `get(idx +
10591        // 1)` or `get(idx).filter(|_| idx > 0)`) would fail here.
10592        // Sibling to the upper-bound `resolve_bound_arg_out_of_range_
10593        // with_subst_kind_emits_typed_invariant` test.
10594        let args = vec![Sexp::int(42)];
10595        let got = resolve_bound_arg(&args, 0, "m", |_| {
10596            panic!("kind constructor must not fire on the in-range path")
10597        })
10598        .expect("idx-0 lookup must succeed");
10599        assert!(std::ptr::eq(got, &args[0]));
10600        assert_eq!(*got, Sexp::int(42));
10601    }
10602
10603    #[test]
10604    fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
10605        // Edge case: idx `len - 1` is the highest valid index. Pin
10606        // that it routes through the success arm (NOT the error arm),
10607        // closing the in-range surface end-to-end with the lower-
10608        // bound sibling. A regression that off-by-ones the upper
10609        // bound (e.g., `get(idx).filter(|_| idx < args.len() - 1)`)
10610        // would fail here.
10611        let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10612        let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
10613            panic!("kind constructor must not fire on the in-range path")
10614        })
10615        .expect("last-element lookup must succeed");
10616        assert!(std::ptr::eq(got, args.last().unwrap()));
10617        assert_eq!(*got, Sexp::int(3));
10618    }
10619
10620    #[test]
10621    fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
10622        // Boundary case: idx EQUAL to `args.len()` is out-of-range
10623        // (since `get` is 0-indexed). Pin that this routes through
10624        // the error arm with the kind constructor applied to the
10625        // EXACT idx that was tried. A regression that off-by-ones
10626        // the boundary (e.g., admits `idx == len`) would fail here.
10627        // This is the canonical off-by-one trap; the helper's
10628        // contract pins it at the variant-construction boundary.
10629        let args = vec![Sexp::int(1)];
10630        let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
10631            .expect_err("idx == len must error");
10632        match err {
10633            LispError::TemplateInvariant { kind, .. } => {
10634                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
10635            }
10636            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10637        }
10638    }
10639
10640    #[test]
10641    fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
10642        // Boundary case: an empty `args_by_index` slice rejects every
10643        // idx (including 0). Pin that the helper's emission shape is
10644        // uniform regardless of which out-of-range idx fires the
10645        // rejection — `SubstBadIndex(0)` for an empty slice is the
10646        // bytecode-runtime mirror of a zero-arity macro template
10647        // referencing the 0-th param.
10648        let args: Vec<Sexp> = Vec::new();
10649        let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
10650            .expect_err("empty slice rejects every idx");
10651        match err {
10652            LispError::TemplateInvariant { macro_name, kind } => {
10653                assert_eq!(macro_name, "zero-arity");
10654                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10655            }
10656            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10657        }
10658    }
10659
10660    #[test]
10661    fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
10662        // End-to-end path-uniformity: a `Subst(99)` op against a
10663        // zero-arity macro routes the bytecode-runtime's bound-arg
10664        // lookup through `resolve_bound_arg` with the
10665        // `SubstBadIndex` constructor, emitting the structural
10666        // variant with `kind: SubstBadIndex(99)`. The pre-lift
10667        // sibling test `apply_compiled_subst_bad_idx_routes_through_
10668        // template_invariant_violation` pins that the same input
10669        // routes through `template_invariant_violation`; this test
10670        // pins that BOTH still hold under the post-lift composition
10671        // — `resolve_bound_arg` calls `template_invariant_violation`
10672        // internally on the rejection arm. A regression that drifts
10673        // ONE arm's projection from the other (e.g., swaps the
10674        // constructor at one call site, or short-circuits the
10675        // composition) would fail here.
10676        let tmpl = CompiledTemplate {
10677            ops: vec![TemplateOp::Subst(99)],
10678        };
10679        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10680            .expect_err("bad idx must error");
10681        match err {
10682            LispError::TemplateInvariant { macro_name, kind } => {
10683                assert_eq!(macro_name, "test-macro");
10684                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10685            }
10686            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10687        }
10688    }
10689
10690    #[test]
10691    fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
10692        // Sibling end-to-end path-uniformity for the `Splice` arm:
10693        // the post-lift composition routes a `Splice(42)` op through
10694        // `resolve_bound_arg` with the `SpliceBadIndex` constructor,
10695        // emitting `kind: SpliceBadIndex(42)`. Together with the
10696        // `Subst` sibling test above, this pins the structural matrix
10697        // `{Subst, Splice} × resolve_bound_arg` end-to-end through
10698        // the public `apply_compiled` surface, so a regression that
10699        // drifts ONE arm's kind constructor (e.g., the `Splice` arm
10700        // accidentally emits `SubstBadIndex` after a copy-paste
10701        // refactor) fails-loudly here.
10702        let tmpl = CompiledTemplate {
10703            ops: vec![TemplateOp::Splice(42)],
10704        };
10705        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10706            .expect_err("bad splice idx must error");
10707        match err {
10708            LispError::TemplateInvariant { macro_name, kind } => {
10709                assert_eq!(macro_name, "call-macro");
10710                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10711            }
10712            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10713        }
10714    }
10715
10716    #[test]
10717    fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
10718        // Positive control: a `Subst(0)` op against a one-arg macro
10719        // routes through `resolve_bound_arg`'s success arm and the
10720        // `Subst` post-lookup verb (clone + push) emits the bound
10721        // value verbatim. Pin the post-lift composition's success
10722        // path: the clone-and-push semantics live at the call site
10723        // (NOT in `resolve_bound_arg`, which only borrows), and a
10724        // regression that drifts the borrow contract (e.g., the
10725        // helper clones internally + the call site clones again)
10726        // would still pass observationally but would regress the
10727        // hot-path allocation count.
10728        let params = MacroParams {
10729            required: vec!["x".into()],
10730            optional: Vec::new(),
10731            rest: None,
10732        };
10733        let tmpl = CompiledTemplate {
10734            ops: vec![TemplateOp::Subst(0)],
10735        };
10736        let out = apply_compiled("id", &params, &tmpl, &[Sexp::int(42)])
10737            .expect("in-range Subst must succeed");
10738        assert_eq!(out, Sexp::int(42));
10739    }
10740
10741    // ── current_builder_mut: the bytecode-runtime top-of-stack projection ──
10742    //
10743    // `current_builder_mut(stack)` lifts the `stack.last_mut().unwrap()`
10744    // projection that appeared at FOUR sites inside `apply_compiled`'s
10745    // op-loop (Literal, Subst, Splice, post-EndList parent-fold) into ONE
10746    // named primitive. The expect message names the bytecode-runtime
10747    // invariant ("at least one stack frame during op-loop") so a
10748    // regression that drifts the loop's frame management (a new op that
10749    // pops without pushing, an early-return that bypasses EndList's
10750    // stack-check) surfaces a NAMED panic rather than a silent unwrap.
10751    // These tests pin the projection's contract directly; the existing
10752    // `apply_compiled_*` tests + the cross-strategy `expansion_layers_
10753    // agree_on_output_and_cache_wins` benchmark are the path-uniformity
10754    // guards proving the four sites still emit the canonical bytecode-
10755    // runtime output across the lift.
10756
10757    #[test]
10758    fn current_builder_mut_returns_the_top_frame_reference() {
10759        // The simplest projection: on a single-frame stack, the helper
10760        // returns a `&mut Vec<Sexp>` pointing at THAT frame. Pin the
10761        // projection's identity end-to-end via a push that mutates
10762        // through the borrow and observe the original frame carries the
10763        // pushed value back.
10764        let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
10765        current_builder_mut(&mut stack).push(Sexp::int(42));
10766        assert_eq!(stack.len(), 1);
10767        assert_eq!(stack[0], vec![Sexp::int(42)]);
10768    }
10769
10770    #[test]
10771    fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
10772        // The projection MUST target the topmost frame, not the bottom
10773        // one — every `TemplateOp::BeginList` pushes a fresh frame the
10774        // subsequent ops emit into, and a regression that flipped the
10775        // projection to `first_mut` (or to a fixed bottom-frame
10776        // reference) would silently smear all op output into the
10777        // outermost result. Pin path-uniformity with the bytecode-
10778        // runtime's mid-list emission posture: with three frames on
10779        // the stack (one outer + two pending lists), the helper
10780        // returns a borrow into the third frame, leaving frames 0 and
10781        // 1 untouched.
10782        let mut stack: Vec<Vec<Sexp>> = vec![
10783            vec![Sexp::symbol("outer")],
10784            vec![Sexp::symbol("inner-a")],
10785            vec![Sexp::symbol("inner-b")],
10786        ];
10787        current_builder_mut(&mut stack).push(Sexp::int(99));
10788        assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10789        assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
10790        assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
10791    }
10792
10793    #[test]
10794    fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
10795        // Structural identity binding the lift to its pre-lift inline
10796        // shape: `current_builder_mut(&mut stack)` IS
10797        // `stack.last_mut().unwrap()` — the same `&mut Vec<Sexp>`,
10798        // pointing at the same allocation. Pin pointer equality via
10799        // `std::ptr::eq` on the projected slice's `as_ptr()` to rule
10800        // out any allocation-shape drift across the lift.
10801        let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
10802        let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
10803        let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
10804        assert!(
10805            std::ptr::eq(via_lift_ptr, via_inline_ptr),
10806            "current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
10807        );
10808    }
10809
10810    #[test]
10811    #[should_panic(
10812        expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
10813    )]
10814    fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
10815        // The bytecode-runtime invariant is encoded in the expect
10816        // message: an empty stack at the projection boundary is
10817        // structurally unreachable inside `apply_compiled`'s op-loop
10818        // (the outermost frame is seeded at entry and every BeginList
10819        // / EndList pair preserves the count >= 1). Pin that the
10820        // NAMED invariant fires on the failure path so a regression
10821        // that drifts the loop's frame management surfaces a
10822        // diagnostic-grade panic rather than a silent unwrap over
10823        // `None`. Authoring tools / future debug-mode hooks can
10824        // pattern-match on the named invariant string instead of
10825        // tracking down an unnamed unwrap site.
10826        let mut empty: Vec<Vec<Sexp>> = Vec::new();
10827        let _ = current_builder_mut(&mut empty);
10828    }
10829
10830    #[test]
10831    fn current_builder_mut_routes_apply_compiled_literal_emit() {
10832        // End-to-end path-uniformity guard: a single-op program
10833        // `TemplateOp::Literal(s)` routes its push through
10834        // `current_builder_mut(&mut stack)` and the literal lands in
10835        // the outermost frame. After the op-loop completes the outer
10836        // `stack.pop().FinalNoValue` gate sees a non-empty top frame
10837        // containing exactly one element, which `apply_compiled`'s
10838        // tail (`top.len() == 1 { top.remove(0) }`) projects back as
10839        // the bound value. Pre-lift the same emission ran through
10840        // `stack.last_mut().unwrap().push(s.clone())`; post-lift it
10841        // runs through `current_builder_mut(&mut stack).push(s.clone())`
10842        // — the byte-identical outcome pins that the Literal arm's
10843        // routing through the new projection preserves the bytecode-
10844        // runtime's emission shape.
10845        let tmpl = CompiledTemplate {
10846            ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
10847        };
10848        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10849            .expect("literal-only template must succeed");
10850        assert_eq!(out, Sexp::symbol("hello"));
10851    }
10852
10853    #[test]
10854    fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
10855        // End-to-end path-uniformity guard for the post-EndList
10856        // parent-fold push: `(BeginList, Literal(a), Literal(b),
10857        // EndList)` builds an inner frame `[a, b]`, pops it on
10858        // EndList, then pushes `Sexp::List([a, b])` into the parent
10859        // (outer) frame via `current_builder_mut`. The outermost
10860        // `stack.pop()` then surfaces that list as the bound result.
10861        // Pre-lift the parent-fold push ran through
10862        // `stack.last_mut().unwrap().push(Sexp::List(items))`; post-
10863        // lift it runs through `current_builder_mut(&mut stack).
10864        // push(Sexp::List(items))` — pin the byte-identical outcome
10865        // so a regression that drifts the parent-fold target (e.g.,
10866        // pushes onto the just-popped frame's pointer instead of the
10867        // new top) fails loudly here.
10868        let tmpl = CompiledTemplate {
10869            ops: vec![
10870                TemplateOp::BeginList,
10871                TemplateOp::Literal(Sexp::symbol("a")),
10872                TemplateOp::Literal(Sexp::symbol("b")),
10873                TemplateOp::EndList,
10874            ],
10875        };
10876        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10877            .expect("BeginList/EndList template must succeed");
10878        assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
10879    }
10880
10881    #[test]
10882    fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
10883        // End-to-end path-uniformity guard for BOTH index-reading
10884        // arms routing through the lifted projection: a one-required +
10885        // one-rest macro `(call f &rest args)` with template
10886        // `(BeginList, Subst(0), Splice(1), EndList)` exercises both
10887        // Subst's clone-and-push AND Splice's splice-value-into
10888        // emit-paths against the current builder via
10889        // `current_builder_mut(&mut stack)`. The composed result is
10890        // `(foo 1 2 3)` — Subst lands the bound `f = foo` and
10891        // Splice flattens `args = (1 2 3)` — and the byte-identical
10892        // outcome pins that BOTH Subst and Splice arms' emits route
10893        // through the SHARED projection. Sibling to
10894        // `apply_compiled_splice_in_range_routes_past_resolve_bound
10895        // _arg_into_splice_value_into` which already exercises this
10896        // shape end-to-end; the addition here is the path-uniformity
10897        // anchor for the `current_builder_mut` lift specifically.
10898        let params = MacroParams {
10899            required: vec!["f".into()],
10900            optional: Vec::new(),
10901            rest: Some("args".into()),
10902        };
10903        let tmpl = CompiledTemplate {
10904            ops: vec![
10905                TemplateOp::BeginList,
10906                TemplateOp::Subst(0),
10907                TemplateOp::Splice(1),
10908                TemplateOp::EndList,
10909            ],
10910        };
10911        let out = apply_compiled(
10912            "call",
10913            &params,
10914            &tmpl,
10915            &[
10916                Sexp::symbol("foo"),
10917                Sexp::int(1),
10918                Sexp::int(2),
10919                Sexp::int(3),
10920            ],
10921        )
10922        .expect("Subst + Splice template must succeed");
10923        assert_eq!(
10924            out,
10925            Sexp::List(vec![
10926                Sexp::symbol("foo"),
10927                Sexp::int(1),
10928                Sexp::int(2),
10929                Sexp::int(3),
10930            ])
10931        );
10932    }
10933
10934    #[test]
10935    fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
10936        // Positive control for the `Splice` arm: a `&rest` macro that
10937        // splices a bound list routes through `resolve_bound_arg`'s
10938        // success arm and the `Splice` post-lookup verb
10939        // (`splice_value_into`) flattens the bound list into the
10940        // builder. Pin the composition's success path end-to-end:
10941        // the bound `Sexp::List([1, 2, 3])` at idx 1 flattens into
10942        // the outer builder's `(call 1 2 3)` shape — the same output
10943        // `rest_param_splices_with_at` pins through the public
10944        // surface, here pinned with the bytecode-runtime composition
10945        // exposed directly.
10946        let params = MacroParams {
10947            required: vec!["f".into()],
10948            optional: Vec::new(),
10949            rest: Some("args".into()),
10950        };
10951        let tmpl = CompiledTemplate {
10952            ops: vec![
10953                TemplateOp::BeginList,
10954                TemplateOp::Subst(0),
10955                TemplateOp::Splice(1),
10956                TemplateOp::EndList,
10957            ],
10958        };
10959        let out = apply_compiled(
10960            "call",
10961            &params,
10962            &tmpl,
10963            &[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
10964        )
10965        .expect("in-range Splice must succeed");
10966        assert_eq!(
10967            out,
10968            Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
10969        );
10970    }
10971
10972    // ── pop_builder_frame: the bytecode-runtime stack-frame consume ─────
10973    //
10974    // `pop_builder_frame(stack, macro_name, kind)` lifts the
10975    // `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
10976    // kind))?` chain that recurred at two sites inside `apply_compiled`
10977    // (the `EndList` arm + the post-loop final pop) into ONE named
10978    // primitive on the bytecode-runtime's stack-frame algebra. Sibling of
10979    // `current_builder_mut` (the top-frame-borrow projection — the same
10980    // `&mut Vec<Vec<Sexp>>` consumed at the borrow face) and
10981    // `resolve_bound_arg` (the bound-arg-by-index lookup primitive that
10982    // also routes through `TemplateInvariantKind` as the per-call-site
10983    // rejection identity). These tests pin the primitive's contract
10984    // directly; the existing `apply_compiled_*` tests are the path-
10985    // uniformity guards proving the two sites route through it without
10986    // behavior drift.
10987
10988    #[test]
10989    fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
10990        // Happy path: a two-frame stack pops the topmost frame off and
10991        // returns it AS-IS while shrinking the stack by exactly one
10992        // element. Pin both the return value (the popped frame's
10993        // contents are byte-identical to what was pushed) AND the
10994        // mutation (`stack.len()` drops from 2 to 1).
10995        let mut stack: Vec<Vec<Sexp>> = vec![
10996            vec![Sexp::symbol("outer")],
10997            vec![Sexp::int(1), Sexp::int(2)],
10998        ];
10999        let popped =
11000            pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
11001                .expect("non-empty stack must pop cleanly");
11002        assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
11003        assert_eq!(stack.len(), 1);
11004        assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
11005    }
11006
11007    #[test]
11008    fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
11009        // Empty-stack rejection: an empty stack flows through the
11010        // `EndListEmptyStack` kind constructor into a structural
11011        // `LispError::TemplateInvariant { macro_name, kind:
11012        // EndListEmptyStack }` variant. Fail-before-pass-after: pre-lift
11013        // the same input would route through the inline
11014        // `stack.pop().ok_or_else(|| template_invariant_violation(_,
11015        // EndListEmptyStack))?` chain at the `EndList` arm; post-lift
11016        // it routes through ONE named primitive both pop-emitting
11017        // sites share, and a regression that drops the kind threading
11018        // (e.g. unifies both kinds into one constant) fails here.
11019        let mut empty: Vec<Vec<Sexp>> = Vec::new();
11020        let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
11021            .expect_err("empty stack must reject");
11022        match err {
11023            LispError::TemplateInvariant { macro_name, kind } => {
11024                assert_eq!(macro_name, "wrap");
11025                assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
11026            }
11027            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11028        }
11029    }
11030
11031    #[test]
11032    fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
11033        // Path-uniformity across the closed-set of pop-emitting kinds:
11034        // the same primitive threads `FinalNoValue` verbatim through
11035        // its `TemplateInvariantKind` slot, with the macro_name
11036        // identity preserved across the call boundary. Sibling of the
11037        // `EndListEmptyStack` test above — together they pin the
11038        // primitive's closed-set posture (BOTH reachable
11039        // pop-emitting kinds route through the same primitive, neither
11040        // is hard-coded), so a future `TemplateInvariantKind` variant
11041        // added for a new pop-emitting op (e.g. a hypothetical
11042        // `EndManyEmptyStack`) extends the primitive's reachability
11043        // mechanically by passing the new kind through the same slot.
11044        let mut empty: Vec<Vec<Sexp>> = Vec::new();
11045        let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
11046            .expect_err("empty stack must reject");
11047        match err {
11048            LispError::TemplateInvariant { macro_name, kind } => {
11049                assert_eq!(macro_name, "id");
11050                assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
11051            }
11052            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11053        }
11054    }
11055
11056    #[test]
11057    fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
11058        // Closed-set posture check: even kinds the production
11059        // `apply_compiled` op-loop ROUTES through `resolve_bound_arg`
11060        // rather than `pop_builder_frame` (the indexed `SubstBadIndex(_)`
11061        // / `SpliceBadIndex(_)` siblings) MUST compose correctly with
11062        // this primitive's typed slot — they are NOT reachable here
11063        // from the production loop, but `TemplateInvariantKind` does
11064        // not distinguish "indexed" kinds from "stack-gate" kinds at
11065        // the helper's signature, so a regression that special-cases
11066        // one kind family would be a silent type-narrowing the closed-
11067        // set typed enum was lifted to prevent. Pin the universal
11068        // routing: ANY kind variant feeds through the primitive's
11069        // `kind` slot identically. Sibling assertion to
11070        // `template_invariant_violation_threads_subst_idx_through_typed_variant`
11071        // — same compose-the-kind-into-the-variant contract, one
11072        // composition step further down the substrate stack.
11073        let mut empty: Vec<Vec<Sexp>> = Vec::new();
11074        let err = pop_builder_frame(
11075            &mut empty,
11076            "compose",
11077            TemplateInvariantKind::SubstBadIndex(42),
11078        )
11079        .expect_err("empty stack must reject regardless of kind family");
11080        match err {
11081            LispError::TemplateInvariant { macro_name, kind } => {
11082                assert_eq!(macro_name, "compose");
11083                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
11084            }
11085            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11086        }
11087    }
11088
11089    #[test]
11090    fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
11091        // Structural-identity binding the lift to its pre-lift inline
11092        // shape: `pop_builder_frame(stack, macro_name, kind)` IS
11093        // `stack.pop().ok_or_else(|| template_invariant_violation(
11094        // macro_name, kind))?` — both reachable arms (success +
11095        // failure) must produce byte-identical outcomes. The success
11096        // arm checks the popped Vec contents AND the post-pop stack
11097        // length match the inline path; the failure arm checks the
11098        // emitted variant's identity AND its `macro_name` / `kind`
11099        // slots match the inline path. A regression that drifts the
11100        // primitive's projection (e.g. a `stack.swap_remove(_)` typo,
11101        // or a kind-rewrite at the helper boundary) fails on at least
11102        // one of the two arms.
11103        // Success arm:
11104        let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11105        let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11106        let via_lift = pop_builder_frame(
11107            &mut stack_lift,
11108            "macro",
11109            TemplateInvariantKind::EndListEmptyStack,
11110        )
11111        .expect("non-empty stack pops cleanly through lift");
11112        let via_inline = stack_inline.pop().ok_or_else(|| {
11113            template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
11114        });
11115        assert_eq!(
11116            via_lift,
11117            via_inline.unwrap(),
11118            "popped frame must be byte-identical across lift vs inline"
11119        );
11120        assert_eq!(
11121            stack_lift.len(),
11122            stack_inline.len(),
11123            "post-pop stack length must be byte-identical across lift vs inline"
11124        );
11125        // Failure arm:
11126        let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
11127        let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
11128        let err_lift = pop_builder_frame(
11129            &mut empty_lift,
11130            "macro",
11131            TemplateInvariantKind::FinalNoValue,
11132        )
11133        .expect_err("empty stack rejects through lift");
11134        let err_inline = empty_inline
11135            .pop()
11136            .ok_or_else(|| {
11137                template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
11138            })
11139            .expect_err("empty stack rejects through inline");
11140        match (err_lift, err_inline) {
11141            (
11142                LispError::TemplateInvariant {
11143                    macro_name: m_lift,
11144                    kind: k_lift,
11145                },
11146                LispError::TemplateInvariant {
11147                    macro_name: m_inline,
11148                    kind: k_inline,
11149                },
11150            ) => {
11151                assert_eq!(m_lift, m_inline);
11152                assert_eq!(k_lift, k_inline);
11153            }
11154            (l, i) => panic!(
11155                "expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
11156            ),
11157        }
11158    }
11159
11160    #[test]
11161    fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
11162        // End-to-end path-uniformity guard for the `EndList` arm: a
11163        // `(BeginList, Literal(x), EndList)` program pushes a child
11164        // frame, populates it with one literal, then routes the child
11165        // frame OUT of the stack via `pop_builder_frame` (kind
11166        // `EndListEmptyStack` — unreachable on this valid input, but
11167        // the kind threads through the primitive identically). The
11168        // post-pop verb (`Sexp::List(items)` push into the parent via
11169        // `current_builder_mut`) yields the same `Sexp::List([x])`
11170        // shape the consumer projected pre-lift. Pin the byte-
11171        // identical outcome so a regression that drifts the EndList
11172        // arm's routing (e.g. swaps the kind constructor, or routes
11173        // through a different stack-mutating primitive) fails loudly
11174        // here. Sibling of
11175        // `current_builder_mut_routes_apply_compiled_end_list_parent_fold`
11176        // — that test pins the parent-fold PUSH; this test pins the
11177        // child-frame POP that immediately precedes it. Together the
11178        // two close the EndList arm's path-uniformity across the
11179        // pop-then-push composition.
11180        let tmpl = CompiledTemplate {
11181            ops: vec![
11182                TemplateOp::BeginList,
11183                TemplateOp::Literal(Sexp::symbol("only")),
11184                TemplateOp::EndList,
11185            ],
11186        };
11187        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11188            .expect("BeginList/EndList one-literal template must succeed");
11189        assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
11190    }
11191
11192    #[test]
11193    fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
11194        // End-to-end path-uniformity guard for the post-loop final
11195        // pop: a single-op `Literal(s)` program emits `s` into the
11196        // outermost (seed) frame, then the post-loop tail routes the
11197        // seed frame OUT via `pop_builder_frame` (kind `FinalNoValue`
11198        // — unreachable on this valid input). The post-pop arity gate
11199        // (`top.len() == 1 { top.remove(0) }`) projects the literal
11200        // back as the bound value. Pre-lift the same emission ran
11201        // through the inline `stack.pop().ok_or_else(|| template_
11202        // invariant_violation(_, FinalNoValue))?` chain at the
11203        // post-loop tail; post-lift it routes through ONE named
11204        // primitive the EndList arm ALSO routes through, and the
11205        // single-literal outcome is byte-identical across both code
11206        // paths. Sibling of
11207        // `current_builder_mut_routes_apply_compiled_literal_emit` —
11208        // that test pins the EMIT into the seed frame; this test pins
11209        // the POP of the same seed frame at the post-loop tail.
11210        // Together the two close the Literal-only program's path-
11211        // uniformity across the emit-then-consume composition.
11212        let tmpl = CompiledTemplate {
11213            ops: vec![TemplateOp::Literal(Sexp::int(123))],
11214        };
11215        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11216            .expect("literal-only template must succeed");
11217        assert_eq!(out, Sexp::int(123));
11218    }
11219
11220    // ── MacroParams: the typed param-list primitive ─────────────────────
11221    //
11222    // `parse_params` now yields a `MacroParams { required, optional, rest }`
11223    // whose shape makes the canonical lambda-list ordering (required →
11224    // optional → rest, "&rest is last + at-most-one", "&optional at most
11225    // once") structural rather than a construction discipline a `Vec<Param>`
11226    // only happened to uphold. These tests pin the parser's mapping into the
11227    // typed shape, the flat-index contract `names()` exposes to the template
11228    // bytecode, and the single positional binder `bind()` both expansion
11229    // strategies now route through. The end-to-end `rest_param_splices_with_at`
11230    // and `compiled_template_matches_substitute_path` tests above are the
11231    // path-uniformity guards proving both strategies still agree.
11232
11233    #[test]
11234    fn parse_params_maps_required_then_rest_into_typed_shape() {
11235        // `(a b &rest c)` — two required, one rest. The rest name lands in
11236        // the `Option`, never in `required`.
11237        let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
11238        assert_eq!(
11239            params,
11240            MacroParams {
11241                required: vec!["a".into(), "b".into()],
11242                optional: Vec::new(),
11243                rest: Some("c".into()),
11244            }
11245        );
11246    }
11247
11248    #[test]
11249    fn parse_params_rest_absent_leaves_none() {
11250        // `(x y)` — no `&rest`, so `rest` is structurally `None`. There is
11251        // no representation in which a rest-less list carries a stray rest.
11252        let params = parse_params(&read("x y").unwrap()).unwrap();
11253        assert_eq!(
11254            params,
11255            MacroParams {
11256                required: vec!["x".into(), "y".into()],
11257                optional: Vec::new(),
11258                rest: None,
11259            }
11260        );
11261    }
11262
11263    #[test]
11264    fn parse_params_maps_optional_section_between_required_and_rest() {
11265        // `(a &optional b c &rest d)` — the canonical lambda-list order. `a`
11266        // is required, `b`/`c` are optional, `d` is rest. The `&optional`
11267        // marker switches collection from `required` to `optional`; `&rest`
11268        // remains terminal.
11269        let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
11270        assert_eq!(
11271            params,
11272            MacroParams {
11273                required: vec!["a".into()],
11274                optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11275                rest: Some("d".into()),
11276            }
11277        );
11278    }
11279
11280    #[test]
11281    fn parse_params_optional_with_no_rest_leaves_rest_none() {
11282        // `(&optional x)` — a leading `&optional` (zero required) with no
11283        // rest. `required` is empty, `x` is the sole optional, `rest` None.
11284        let params = parse_params(&read("&optional x").unwrap()).unwrap();
11285        assert_eq!(
11286            params,
11287            MacroParams {
11288                required: Vec::new(),
11289                optional: vec![OptionalParam::bare("x")],
11290                rest: None,
11291            }
11292        );
11293    }
11294
11295    #[test]
11296    fn parse_params_rejects_repeated_optional_marker() {
11297        // `(a &optional b &optional c)` — a second `&optional` is
11298        // unrepresentable (one flat optional section), so the gate REJECTS
11299        // rather than binding args to a marker symbol named `&optional`. The
11300        // two marker positions (1 and 3) are named.
11301        let err = parse_params(&read("a &optional b &optional c").unwrap())
11302            .expect_err("repeated &optional must error");
11303        assert!(
11304            matches!(
11305                err,
11306                LispError::OptionalMarkerRepeated {
11307                    first_position: 1,
11308                    second_position: 3,
11309                }
11310            ),
11311            "expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
11312        );
11313    }
11314
11315    #[test]
11316    fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
11317        // `(&rest xs &optional y)` — `&rest <name>` is terminal, so the
11318        // `&optional y` tail is REJECTED as trailing tokens (not silently
11319        // dropped, and not a repeated-optional error: the rest gate fires
11320        // first). Pins the interaction the prior run (3627426) signposted.
11321        let err = parse_params(&read("&rest xs &optional y").unwrap())
11322            .expect_err("tokens after &rest <name> must error");
11323        assert!(
11324            matches!(err, LispError::RestParamTrailingTokens { .. }),
11325            "expected RestParamTrailingTokens, got: {err:?}"
11326        );
11327    }
11328
11329    #[test]
11330    fn names_are_required_then_optional_then_rest_in_flat_index_order() {
11331        // The flat-index contract the bytecode `Subst(idx)`/`Splice(idx)`
11332        // depends on: required names at 0.., then optional names, then the
11333        // rest name last.
11334        let params = MacroParams {
11335            required: vec!["a".into(), "b".into()],
11336            optional: vec![OptionalParam::bare("c")],
11337            rest: Some("d".into()),
11338        };
11339        assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
11340        // Optional names occupy the indices immediately after the required run.
11341        assert_eq!(params.names()[params.required.len()], "c");
11342        // The rest name is last, after required + optional — i.e. at the
11343        // structural `fixed_arity()` boundary the typed primitive names.
11344        assert_eq!(params.names()[params.fixed_arity()], "d");
11345    }
11346
11347    // ── `MacroParams::{REST_MARKER, OPTIONAL_MARKER,
11348    // LAMBDA_LIST_KEYWORD_LEAD}` — the typed CL lambda-list-keyword
11349    // marker algebra ────────────────────────────────────────────────
11350    //
11351    // The three `pub const`s close the Common-Lisp lambda-list keyword
11352    // family at the typed [`MacroParams`] algebra: `REST_MARKER` and
11353    // `OPTIONAL_MARKER` are the two `&'static str` markers the parser's
11354    // typed dispatch specialises on; `LAMBDA_LIST_KEYWORD_LEAD` is the
11355    // canonical `'&'` char shared as the LEAD byte of both markers.
11356    // Pre-lift the same two `&'static str` markers lived as two inline
11357    // `s == "..."` comparisons at `parse_params` — post-lift both
11358    // comparisons route through the typed constants so a delimiter swap
11359    // (e.g. a Racket-compat `#!rest` port, a Clojure-compat `&` port)
11360    // lands at ONE constant on the typed algebra.
11361    //
11362    // These pins cover: (a) the exact `&'static str` / `char` values,
11363    // (b) the structural round-trip law binding each `&'static str`
11364    // marker to its shared `char` LEAD byte, (c) the pairwise
11365    // disjointness of the two `&'static str` markers, (d) the cross-
11366    // axis disjointness of the `char` LEAD byte against every sibling
11367    // outer-marker `char` the substrate's other closed-set algebras
11368    // specialise on, and (e) the path-uniformity of `parse_params`
11369    // through the typed constants.
11370
11371    #[test]
11372    fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
11373        // Pins the constant's exact `&'static str` bytes so a typo
11374        // (`"&res"`, `"&Rest"`, `"&rst"`) or an accidental redefinition
11375        // surfaces immediately. Sibling-shape pin to
11376        // `macro_params_optional_marker_projects_canonical_ampersand_optional_str`
11377        // on the peer `&optional` axis.
11378        assert_eq!(
11379            MacroParams::REST_MARKER,
11380            "&rest",
11381            "MacroParams::REST_MARKER drifted from the substrate- \
11382             canonical CL lambda-list `&rest` marker — the parser's \
11383             `parse_params` rest-slot dispatch AND every downstream \
11384             authoring / rendering surface binds to this ONE typed \
11385             constant.",
11386        );
11387    }
11388
11389    #[test]
11390    fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
11391        // Pins the constant's exact `&'static str` bytes so a typo
11392        // (`"&opt"`, `"&Optional"`, `"&option"`) or an accidental
11393        // redefinition surfaces immediately. Sibling-shape pin to
11394        // `macro_params_rest_marker_projects_canonical_ampersand_rest_str`
11395        // on the peer `&rest` axis.
11396        assert_eq!(
11397            MacroParams::OPTIONAL_MARKER,
11398            "&optional",
11399            "MacroParams::OPTIONAL_MARKER drifted from the substrate- \
11400             canonical CL lambda-list `&optional` marker — the parser's \
11401             `parse_params` optional-section dispatch AND every \
11402             downstream authoring / rendering surface binds to this \
11403             ONE typed constant.",
11404        );
11405    }
11406
11407    #[test]
11408    fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
11409        // Pins the constant's exact `char` value so a typo (`'#'`,
11410        // `'@'`, `'!'`) or an accidental redefinition surfaces
11411        // immediately. Sibling-shape pin to
11412        // `atom_keyword_marker_lead_projects_canonical_colon_char`,
11413        // `atom_bool_literal_lead_projects_canonical_hash_char`,
11414        // `atom_str_delimiter_projects_canonical_double_quote_char`
11415        // on the peer per-role LEAD-byte axes across the substrate's
11416        // closed-set outer algebras.
11417        assert_eq!(
11418            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11419            '&',
11420            "LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
11421             canonical `&` LEAD byte — the CL lambda-list-keyword \
11422             family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
11423             typed constant as their common LEAD byte.",
11424        );
11425    }
11426
11427    #[test]
11428    fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
11429        // STRUCTURAL ROUND-TRIP CONTRACT: the `&'static str` marker
11430        // `MacroParams::REST_MARKER` starts with the `char` LEAD byte
11431        // `MacroParams::LAMBDA_LIST_KEYWORD_LEAD` — the projection law
11432        // binding the two typed constants on the [`MacroParams`]
11433        // algebra. A regression that renames the `&'static str` (e.g.
11434        // to Racket's `"#!rest"` keyword-args) OR the `char` (e.g. to
11435        // `'#'` for the `#!` shebang lead) without updating the other
11436        // fails HERE. Sibling-shape pin to
11437        // `atom_keyword_marker_lead_prefixes_keyword_marker` on the
11438        // peer `Atom`-algebra Keyword-prefix LEAD-byte axis.
11439        assert!(
11440            MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11441            "MacroParams::REST_MARKER `{}` does NOT start with \
11442             MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11443             typed constants have drifted apart on the [`MacroParams`] \
11444             algebra; the CL lambda-list-keyword family disjointness \
11445             contract can no longer bind to ONE shared LEAD byte.",
11446            MacroParams::REST_MARKER,
11447            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11448        );
11449    }
11450
11451    #[test]
11452    fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
11453        // STRUCTURAL ROUND-TRIP CONTRACT: peer to
11454        // `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
11455        // on the `&optional` axis. Both `&'static str` markers of the
11456        // CL lambda-list-keyword family MUST share the canonical `char`
11457        // LEAD byte so the typed-marker disjointness contract can bind
11458        // to ONE shared LEAD byte on the [`MacroParams`] algebra.
11459        assert!(
11460            MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11461            "MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
11462             MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11463             typed constants have drifted apart on the [`MacroParams`] \
11464             algebra; the CL lambda-list-keyword family disjointness \
11465             contract can no longer bind to ONE shared LEAD byte.",
11466            MacroParams::OPTIONAL_MARKER,
11467            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11468        );
11469    }
11470
11471    #[test]
11472    fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
11473        // PAIRWISE DISJOINTNESS PIN: the two `&'static str` markers on
11474        // the CL lambda-list-keyword algebra MUST differ so the
11475        // parser's typed dispatch cascade at `parse_params` (which
11476        // tests `REST_MARKER` FIRST, then `OPTIONAL_MARKER`) cannot
11477        // silently route both dispatches through the same arm. A
11478        // regression that aliases the two markers (e.g. both to
11479        // `"&rest"` after a typo) would silently drop the optional
11480        // section's structural distinction — every `&optional` name
11481        // would misclassify as a rest-slot marker.
11482        assert_ne!(
11483            MacroParams::REST_MARKER,
11484            MacroParams::OPTIONAL_MARKER,
11485            "REST_MARKER and OPTIONAL_MARKER collide — the parser's \
11486             typed dispatch cascade at `parse_params` can no longer \
11487             distinguish the rest-slot boundary from the optional- \
11488             section boundary.",
11489        );
11490    }
11491
11492    #[test]
11493    fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
11494        // CROSS-AXIS DISJOINTNESS PIN: `MacroParams::LAMBDA_LIST_KEYWORD_LEAD`
11495        // MUST NOT alias any sibling outer-marker `char` on the
11496        // substrate's other closed-set algebras — the Atom-payload
11497        // markers (`STR_DELIMITER`, `STR_ESCAPE_LEAD`,
11498        // `KEYWORD_MARKER_LEAD`, `BOOL_LITERAL_LEAD`), the paired list
11499        // delimiters (`Sexp::LIST_OPEN` / `Sexp::LIST_CLOSE`), the
11500        // paired line-comment delimiters (`Sexp::COMMENT_LEAD` /
11501        // `Sexp::COMMENT_TERM`), every `QuoteForm::lead_char`
11502        // projection, AND `QuoteForm::SPLICE_DISCRIMINATOR`. A
11503        // collision would silently break the reader's outer dispatch:
11504        // an `&`-prefixed bare atom `&rest` / `&optional` would collide
11505        // with whichever marker it aliased. Sibling-shape pin to
11506        // `atom_keyword_marker_lead_distinct_from_every_other_algebra_marker`
11507        // on the peer `Atom`-algebra Keyword-prefix LEAD-byte axis —
11508        // pins the SAME shape on the CL lambda-list-keyword LEAD-byte
11509        // axis. A future outer-marker extension that collided with
11510        // `'&'` fails HERE at the cross-axis enumeration.
11511        use crate::ast::{Atom, QuoteForm, Sexp};
11512
11513        assert_ne!(
11514            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11515            Atom::STR_DELIMITER,
11516            "LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
11517             bare `&rest` at a param-list position would ambiguously \
11518             begin a lambda-list keyword AND open a string.",
11519        );
11520        assert_ne!(
11521            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11522            Atom::STR_ESCAPE_LEAD,
11523            "LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
11524             the reader's Str-escape lead byte would alias the CL \
11525             lambda-list-keyword LEAD byte.",
11526        );
11527        assert_ne!(
11528            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11529            Atom::KEYWORD_MARKER_LEAD,
11530            "LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
11531             — a bare `&rest` at a param-list position would \
11532             ambiguously begin a lambda-list keyword AND begin an \
11533             `:foo` keyword.",
11534        );
11535        assert_ne!(
11536            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11537            Atom::BOOL_LITERAL_LEAD,
11538            "LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
11539             a bare `&rest` at a param-list position would ambiguously \
11540             begin a lambda-list keyword AND classify as a Bool prefix.",
11541        );
11542        assert_ne!(
11543            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11544            Sexp::LIST_OPEN,
11545            "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
11546             `&rest` at a param-list position would ambiguously begin \
11547             a lambda-list keyword AND open a list.",
11548        );
11549        assert_ne!(
11550            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11551            Sexp::LIST_CLOSE,
11552            "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
11553             `&rest` at a param-list position would ambiguously begin \
11554             a lambda-list keyword AND close a list.",
11555        );
11556        assert_ne!(
11557            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11558            Sexp::COMMENT_LEAD,
11559            "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
11560             bare `&rest` at a param-list position would ambiguously \
11561             begin a lambda-list keyword AND begin a comment.",
11562        );
11563        assert_ne!(
11564            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11565            Sexp::COMMENT_TERM,
11566            "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
11567             reader's line-comment discard loop would terminate on the \
11568             SAME byte the parser's lambda-list-keyword LEAD dispatch \
11569             binds to.",
11570        );
11571        for qf in QuoteForm::ALL {
11572            assert_ne!(
11573                MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11574                qf.lead_char(),
11575                "LAMBDA_LIST_KEYWORD_LEAD collides with \
11576                 QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
11577                 param-list position would ambiguously begin a lambda- \
11578                 list keyword AND begin a quote-family prefix.",
11579            );
11580        }
11581        assert_ne!(
11582            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11583            QuoteForm::SPLICE_DISCRIMINATOR,
11584            "LAMBDA_LIST_KEYWORD_LEAD collides with \
11585             SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
11586             peek byte would alias the CL lambda-list-keyword LEAD \
11587             byte.",
11588        );
11589    }
11590
11591    #[test]
11592    fn parse_params_recognizes_rest_marker_via_typed_constant() {
11593        // PATH-UNIFORMITY PIN: the parser's rest-slot dispatch at
11594        // `parse_params` MUST classify the typed constant
11595        // `MacroParams::REST_MARKER` as the rest-slot boundary —
11596        // authoring surfaces that assemble a param list through the
11597        // typed constant (rather than through an inline string literal
11598        // read from user source) must reach the SAME rest-slot binding
11599        // the reader-driven path does.
11600        //
11601        // The `read(REST_MARKER)` composition here is the load-bearing
11602        // structural link: we build the param-list source through the
11603        // typed constant, then parse it through the reader, then the
11604        // parser MUST bind `xs` at the `rest` slot. A regression that
11605        // drifts the parser's dispatch away from the typed constant
11606        // (e.g. re-inlines a string literal that goes stale against
11607        // the constant) fails HERE.
11608        let src = format!("a {} xs", MacroParams::REST_MARKER);
11609        let params = parse_params(&read(&src).unwrap()).unwrap();
11610        assert_eq!(
11611            params,
11612            MacroParams {
11613                required: vec!["a".into()],
11614                optional: Vec::new(),
11615                rest: Some("xs".into()),
11616            },
11617            "parse_params dispatch drifted away from REST_MARKER — the \
11618             typed constant no longer routes to the rest-slot arm.",
11619        );
11620    }
11621
11622    #[test]
11623    fn parse_params_recognizes_optional_marker_via_typed_constant() {
11624        // PATH-UNIFORMITY PIN: peer to
11625        // `parse_params_recognizes_rest_marker_via_typed_constant` on
11626        // the `&optional` axis. The parser's optional-section dispatch
11627        // at `parse_params` MUST classify the typed constant
11628        // `MacroParams::OPTIONAL_MARKER` as the section-switch
11629        // boundary that reroutes subsequent bare-symbol names from the
11630        // `required` bin to the `optional` bin.
11631        let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
11632        let params = parse_params(&read(&src).unwrap()).unwrap();
11633        assert_eq!(
11634            params,
11635            MacroParams {
11636                required: vec!["a".into()],
11637                optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11638                rest: None,
11639            },
11640            "parse_params dispatch drifted away from OPTIONAL_MARKER — \
11641             the typed constant no longer routes to the optional- \
11642             section arm.",
11643        );
11644    }
11645
11646    // ── `MacroParams::LAMBDA_LIST_KEYWORDS` + `is_lambda_list_keyword` —
11647    // the closed-set forced-arity ALL array + membership gate that closes
11648    // the CL lambda-list-keyword family at the typed [`MacroParams`]
11649    // algebra. Sibling posture to the closed set of `pub const ALL: [Self;
11650    // N]` forced-arity arrays across the substrate's other closed-set
11651    // outer algebras (`AtomKind::ALL`, `QuoteForm::ALL`, `SexpShape::ALL`,
11652    // `UnquoteForm::ALL`, `MacroDefHead::ALL`); a future third marker
11653    // (`&key`, `&aux`, `&body`) extends the array's arity + adds ONE `pub
11654    // const` for the marker and every downstream family-wide contract
11655    // sweep (LEAD-byte round-trip, pairwise disjointness, membership
11656    // gate) picks up the extension at ONE structural site.
11657    //
11658    // These pins cover: (a) the cardinality contract (the ALL array's
11659    // length matches the pre-lift per-role marker count), (b) the
11660    // ordering contract (each ALL entry matches its corresponding per-
11661    // role `pub const` by-index), (c) the family-wide structural round-
11662    // trip (every ALL element starts_with LEAD), (d) the family-wide
11663    // pairwise disjointness (every distinct ALL index pair yields
11664    // distinct markers), (e) the membership-gate acceptance side (every
11665    // ALL element classifies as `true`), and (f) the membership-gate
11666    // rejection side (the bare LEAD byte + unrecognised `&`-prefixed
11667    // names classify as `false`).
11668    //
11669    // Fail-before/pass-after: every test below references
11670    // `MacroParams::LAMBDA_LIST_KEYWORDS` or
11671    // `MacroParams::is_lambda_list_keyword`, which simply did not exist
11672    // on `MacroParams` before this lift — every assertion was a
11673    // compile-time error against the prior surface.
11674
11675    #[test]
11676    fn macro_params_lambda_list_keywords_has_expected_cardinality() {
11677        // CARDINALITY PIN: the ALL array closes the family at exactly TWO
11678        // entries — the two `&'static str` markers the parser's typed
11679        // dispatch specialises on today (`&rest` + `&optional`). A future
11680        // third marker (`&key`, `&aux`, `&body`) extends this arity to
11681        // 3 AND every downstream family-wide contract sweep picks up the
11682        // extension at ONE structural site. A regression that drops a
11683        // marker from the ALL array (silently narrowing the family) OR
11684        // aliases two markers to the same index (silently narrowing the
11685        // typed dispatch) fails HERE at the cardinality contract before
11686        // the structural round-trip / pairwise-disjointness contracts
11687        // even fire.
11688        assert_eq!(
11689            MacroParams::LAMBDA_LIST_KEYWORDS.len(),
11690            2,
11691            "LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
11692             lambda-list-keyword family closure now names a different \
11693             number of markers than the two the parser's typed dispatch \
11694             specialises on.",
11695        );
11696    }
11697
11698    #[test]
11699    fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
11700        // ORDERING PIN: each ALL entry matches its corresponding per-role
11701        // `pub const` by-index. A regression that swaps the ordering
11702        // (`[OPTIONAL_MARKER, REST_MARKER]` after a rebase) would keep
11703        // the cardinality + membership contracts intact but silently
11704        // reorder downstream consumers that iterate `LAMBDA_LIST_KEYWORDS`
11705        // in canonical order (a future authoring surface that renders
11706        // "supported CL lambda-list keywords: &rest, &optional" would
11707        // silently render "supported CL lambda-list keywords: &optional,
11708        // &rest" instead). Pins the array's declaration-order binding
11709        // structurally so a reorder fails HERE at each element rather
11710        // than only at consumer sites downstream.
11711        assert_eq!(
11712            MacroParams::LAMBDA_LIST_KEYWORDS[0],
11713            MacroParams::REST_MARKER,
11714            "LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
11715             array's declaration-order binding to the per-role `pub \
11716             const` broke at the rest-slot marker slot.",
11717        );
11718        assert_eq!(
11719            MacroParams::LAMBDA_LIST_KEYWORDS[1],
11720            MacroParams::OPTIONAL_MARKER,
11721            "LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
11722             ALL array's declaration-order binding to the per-role \
11723             `pub const` broke at the optional-section marker slot.",
11724        );
11725    }
11726
11727    #[test]
11728    fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
11729        // FAMILY-WIDE STRUCTURAL ROUND-TRIP PIN: every element of the ALL
11730        // array MUST start with the canonical LEAD byte. Where the two
11731        // per-marker `_prefixed_by_lambda_list_keyword_lead` pins each
11732        // named ONE marker inline as a duplicate 3-line
11733        // `assert!(A.starts_with(LEAD), ...)` shape, this family-wide
11734        // sweep routes the SAME contract through the closed-set ALL
11735        // array — a future third marker (`&key`, `&aux`, `&body`)
11736        // extends the array AND is automatically covered by this sweep
11737        // without adding a third `_prefixed_by_...` per-marker pin. The
11738        // per-marker pins stay as fine-grained fail-loud sites; this
11739        // pin is the algebra-wide closure the family-extending refactor
11740        // routes through.
11741        for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11742            assert!(
11743                m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11744                "LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
11745                 LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
11746                 list-keyword family's structural round-trip contract \
11747                 no longer binds every marker to its shared LEAD byte.",
11748                lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11749            );
11750        }
11751    }
11752
11753    #[test]
11754    fn macro_params_lambda_list_keywords_pairwise_distinct() {
11755        // FAMILY-WIDE PAIRWISE DISJOINTNESS PIN: every distinct index pair
11756        // `(i, j)` in the ALL array yields distinct markers. Where the
11757        // pre-lift `_rest_and_optional_markers_pairwise_disjoint` pin
11758        // named the TWO markers as a hand-rolled `assert_ne!` pair, this
11759        // family-wide sweep routes the SAME contract through the closed-
11760        // set ALL array so a future third marker automatically extends
11761        // the sweep to `(3 * 2) / 2 == 3` distinct pairs without re-
11762        // deriving per-marker `assert_ne!` calls. The pre-lift pin
11763        // stays as the fine-grained fail-loud site; this pin is the
11764        // family-wide closure the algebra-extending refactor routes
11765        // through.
11766        for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11767            for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11768                if i == j {
11769                    continue;
11770                }
11771                assert_ne!(
11772                    a, b,
11773                    "LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
11774                     LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
11775                     list-keyword family's pairwise disjointness \
11776                     contract no longer binds distinct index pairs \
11777                     to distinct markers.",
11778                );
11779            }
11780        }
11781    }
11782
11783    #[test]
11784    fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
11785        // ACCEPTANCE-SIDE MEMBERSHIP PIN: every element of the ALL array
11786        // MUST classify as `true` through the typed membership gate. The
11787        // gate is defined as `LAMBDA_LIST_KEYWORDS.contains(&s)`, so this
11788        // pin is the structural closure binding the gate's return value
11789        // to the ALL array's element set. A regression that specialises
11790        // the gate to a subset of the array (e.g. a `matches!(s,
11791        // "&rest")` inline that silently drops the `&optional` branch)
11792        // fails HERE at the ALL sweep. Sibling-shape acceptance sweep to
11793        // the closed-set `ClosedSet::parse_label` roundtrip pin — every
11794        // element of `Self::ALL` decodes through the projection back to
11795        // itself.
11796        for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11797            assert!(
11798                MacroParams::is_lambda_list_keyword(m),
11799                "is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
11800                 element `{m}` — the closed-set membership gate's \
11801                 acceptance side drifted from the ALL array.",
11802            );
11803        }
11804    }
11805
11806    #[test]
11807    fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
11808        // REJECTION-SIDE MEMBERSHIP PIN (LEAD byte alone): the bare LEAD
11809        // byte `"&"` MUST classify as `false`. A future Clojure-compat
11810        // port might land a bare `&` marker — but until such an
11811        // extension explicitly lands on the ALL array, the substrate
11812        // MUST NOT silently classify the LEAD byte alone as a
11813        // recognised CL lambda-list keyword. Pins the gate's
11814        // rejection-side contract against a plausible future
11815        // near-miss extension.
11816        let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
11817        assert!(
11818            !MacroParams::is_lambda_list_keyword(&bare_lead),
11819            "is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
11820             the closed-set membership gate silently classifies the bare `&` \
11821             LEAD byte as a recognised CL lambda-list keyword despite the ALL \
11822             array containing only the two suffixed markers.",
11823        );
11824    }
11825
11826    #[test]
11827    fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
11828        // REJECTION-SIDE MEMBERSHIP PIN (unrecognised near-misses): the
11829        // three plausible future CL lambda-list keywords (`&key` for
11830        // keyword-args, `&aux` for auxiliaries, `&body` for the
11831        // docstring-carrying tail of `defmacro` bodies) MUST classify as
11832        // `false` UNTIL they explicitly land on the ALL array. Pins the
11833        // gate's conservative rejection contract — the closed-set
11834        // membership gate is not a "starts with `&`" heuristic; it is a
11835        // typed enumeration over the currently-recognised markers. A
11836        // regression that silently loosens the gate to a `starts_with`
11837        // check (dropping the closed-set discipline) fails HERE.
11838        for candidate in ["&key", "&aux", "&body"] {
11839            assert!(
11840                !MacroParams::is_lambda_list_keyword(candidate),
11841                "is_lambda_list_keyword accepted the unrecognised \
11842                 `&`-prefixed name `{candidate}` — the closed-set \
11843                 membership gate silently loosened its acceptance beyond \
11844                 the two markers the ALL array names today.",
11845            );
11846        }
11847    }
11848
11849    #[test]
11850    fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
11851        // REJECTION-SIDE MEMBERSHIP PIN (bare identifiers + empty): a
11852        // bare identifier (`"a"`, `"xs"`, `"foo"`) and the empty string
11853        // MUST classify as `false`. These are the shapes the parser's
11854        // bare-symbol dispatch cascade sees at `parse_params` when
11855        // walking a param list, so pinning them here binds the
11856        // membership gate's rejection contract to the parser's most
11857        // common non-marker input shapes.
11858        for candidate in ["a", "xs", "foo", ""] {
11859            assert!(
11860                !MacroParams::is_lambda_list_keyword(candidate),
11861                "is_lambda_list_keyword accepted the bare non-marker \
11862                 input `{candidate}` — the closed-set membership gate's \
11863                 rejection side no longer excludes bare identifiers \
11864                 the parser routes through the fall-through cascade.",
11865            );
11866        }
11867    }
11868
11869    // ── fixed_arity: the rest-start / rest-less max-arity primitive ─────
11870    //
11871    // `fixed_arity()` lifts the `self.required.len() + self.optional.len()`
11872    // arithmetic that recurred three times inside `MacroParams::bind` — at
11873    // the `Vec::with_capacity` site (where it adds `usize::from(rest.is_some())`
11874    // to get the bound-values count), at the `rest_start` site (inside the
11875    // `if let Some(rest)` branch), and at the `expected` site (inside the
11876    // rest-less `else`). The latter two sites live in mutually-exclusive
11877    // branches yet name ONE structural concept; lifting them collapses the
11878    // arithmetic to one named primitive. These tests pin the primitive's
11879    // contract directly; the existing `bind_*` tests are the path-uniformity
11880    // guards proving `bind`'s sites route through the same value without
11881    // behavior drift.
11882    //
11883    // Fail-before/pass-after: every test below references
11884    // `params.fixed_arity()`, which simply did not exist on `MacroParams`
11885    // before this lift — every assertion's `expect: ___ == params.fixed_arity()`
11886    // line was a compile-time error against the prior surface.
11887
11888    #[test]
11889    fn fixed_arity_is_zero_for_the_empty_param_list() {
11890        // `()` — a nullary macro has fixed arity 0, the rest-less binder
11891        // boundary at which the FIRST surplus arg already rejects.
11892        let params = MacroParams::default();
11893        assert_eq!(params.fixed_arity(), 0);
11894    }
11895
11896    #[test]
11897    fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
11898        // `(a b c)` — three required, no optional, no rest. fixed_arity is
11899        // exactly the required length.
11900        let params = MacroParams {
11901            required: vec!["a".into(), "b".into(), "c".into()],
11902            optional: Vec::new(),
11903            rest: None,
11904        };
11905        assert_eq!(params.fixed_arity(), 3);
11906    }
11907
11908    #[test]
11909    fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
11910        // `(&optional x y)` — two optional, no required. fixed_arity is the
11911        // optional length; the optional section participates in the fixed
11912        // run because supplied positional args bind to it.
11913        let params = MacroParams {
11914            required: Vec::new(),
11915            optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
11916            rest: None,
11917        };
11918        assert_eq!(params.fixed_arity(), 2);
11919    }
11920
11921    #[test]
11922    fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
11923        // `(a b &optional c d e)` — two required + three optional, no rest.
11924        // fixed_arity is 5: the maximum arity a rest-less call can supply.
11925        let params = MacroParams {
11926            required: vec!["a".into(), "b".into()],
11927            optional: vec![
11928                OptionalParam::bare("c"),
11929                OptionalParam::bare("d"),
11930                OptionalParam::bare("e"),
11931            ],
11932            rest: None,
11933        };
11934        assert_eq!(params.fixed_arity(), 5);
11935    }
11936
11937    #[test]
11938    fn fixed_arity_ignores_rest_slot_by_construction() {
11939        // `(a &optional b &rest r)` and `(a &optional b)` — identical fixed
11940        // arity (2). The `&rest` slot has NO maximum and is structurally
11941        // excluded from `fixed_arity`. Naming this invariant pins that a
11942        // regression that drifts the primitive to "required + optional +
11943        // rest.is_some() as usize" fails loudly here — that drift would
11944        // collapse `fixed_arity` into `names().len()`, losing the rest-start
11945        // vs total-bound-values distinction the typed shape relies on.
11946        let with_rest = MacroParams {
11947            required: vec!["a".into()],
11948            optional: vec![OptionalParam::bare("b")],
11949            rest: Some("r".into()),
11950        };
11951        let without_rest = MacroParams {
11952            required: vec!["a".into()],
11953            optional: vec![OptionalParam::bare("b")],
11954            rest: None,
11955        };
11956        assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
11957        assert_eq!(with_rest.fixed_arity(), 2);
11958    }
11959
11960    #[test]
11961    fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
11962        // When `rest` is `Some`, `names()[fixed_arity()]` IS the rest name
11963        // — the rest-start reading of the primitive. Same arithmetic the
11964        // bytecode index would hit (`Subst(fixed_arity())` resolves to the
11965        // rest-bound `Sexp::List`).
11966        let params = MacroParams {
11967            required: vec!["a".into(), "b".into()],
11968            optional: vec![OptionalParam::bare("c")],
11969            rest: Some("r".into()),
11970        };
11971        assert_eq!(params.fixed_arity(), 3);
11972        assert_eq!(params.names()[params.fixed_arity()], "r");
11973    }
11974
11975    #[test]
11976    fn fixed_arity_equals_names_length_when_rest_is_absent() {
11977        // When `rest` is `None`, `names().len() == fixed_arity()` — there
11978        // is no rest-name slot to extend the flat run past the fixed
11979        // boundary. Pins the structural identity
11980        // `names().len() == fixed_arity() + usize::from(rest.is_some())`
11981        // for the rest-less case; the rest-present case is pinned by the
11982        // sibling test above (where the boundary is the rest-name index,
11983        // i.e. one short of `names().len()`).
11984        let params = MacroParams {
11985            required: vec!["a".into(), "b".into()],
11986            optional: vec![OptionalParam::bare("c")],
11987            rest: None,
11988        };
11989        assert_eq!(params.names().len(), params.fixed_arity());
11990        assert_eq!(params.names().len(), 3);
11991    }
11992
11993    #[test]
11994    fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
11995        // The `expected` field of `TooManyMacroArgs` IS `fixed_arity()` —
11996        // the rest-less binder rejects iff `args.len() > fixed_arity()`.
11997        // This pin is the path-uniformity guard binding the typed primitive
11998        // to the binder's rejection contract: a regression that drifts
11999        // `bind`'s `expected` arithmetic from `fixed_arity()` would silently
12000        // surface a different boundary in the diagnostic without touching
12001        // the primitive — and this assertion fails loudly. Mirror of the
12002        // sibling rest-less surplus pin (`bind_rest_less_params_reject_
12003        // surplus_args`); this test pins WHAT the `expected` slot's value
12004        // structurally IS, that pin checks the variant SHAPE.
12005        let params = MacroParams {
12006            required: vec!["a".into(), "b".into()],
12007            optional: vec![OptionalParam::bare("c")],
12008            rest: None,
12009        };
12010        assert_eq!(params.fixed_arity(), 3);
12011        let err = params
12012            .bind(
12013                "m",
12014                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12015            )
12016            .expect_err("4 args against fixed_arity 3 must reject");
12017        match err {
12018            LispError::TooManyMacroArgs {
12019                expected,
12020                got,
12021                macro_name,
12022            } => {
12023                assert_eq!(expected, params.fixed_arity());
12024                assert_eq!(got, 4);
12025                assert_eq!(macro_name, "m");
12026            }
12027            other => panic!("expected TooManyMacroArgs, got {other:?}"),
12028        }
12029    }
12030
12031    #[test]
12032    fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
12033        // When `rest` is `Some`, `bind` collects `args[fixed_arity()..]`
12034        // into the rest's `Sexp::List`. Pin that the rest list contents
12035        // are exactly the suffix beginning at `fixed_arity()` — the
12036        // primitive's rest-start reading IS the slice index the binder
12037        // consumes. A regression that drifts `bind`'s rest-collection
12038        // slice from `fixed_arity()` would surface as a misaligned rest
12039        // list (off-by-one in either direction) and this assertion fails
12040        // loudly. Sibling of `fixed_arity_is_the_rest_less_surplus_
12041        // rejection_boundary` on the rest-PRESENT branch.
12042        let params = MacroParams {
12043            required: vec!["a".into()],
12044            optional: vec![OptionalParam::bare("b")],
12045            rest: Some("r".into()),
12046        };
12047        assert_eq!(params.fixed_arity(), 2);
12048        let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
12049        let vals = params.bind("m", &args).unwrap();
12050        // Bound vec: [a=1, b=2, r=(3 4)] — the rest list IS args[fixed_arity()..].
12051        let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
12052        assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
12053    }
12054
12055    #[test]
12056    fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
12057        // Exactly-saturated rest-present call: `args.len() == fixed_arity()`.
12058        // The rest slot collects the empty slice; bind succeeds, never
12059        // misaligning to an off-by-one underflow. Pin the boundary on the
12060        // rest-PRESENT path — the rest-less mirror is
12061        // `too_many_macro_args_does_not_fire_at_exact_max_arity` above.
12062        let params = MacroParams {
12063            required: vec!["a".into()],
12064            optional: vec![OptionalParam::bare("b")],
12065            rest: Some("r".into()),
12066        };
12067        assert_eq!(params.fixed_arity(), 2);
12068        let vals = params
12069            .bind("m", &[Sexp::int(1), Sexp::int(2)])
12070            .expect("rest-present at exact fixed_arity must bind cleanly");
12071        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
12072    }
12073
12074    #[test]
12075    fn bind_threads_required_positionally_and_collects_rest_as_list() {
12076        // `(a b &rest c)` bound to `1 2 3 4`: a=1, b=2, c=(3 4). The bound
12077        // vec is parallel to `names()`, so the rest list sits at the rest's
12078        // flat index.
12079        let params = MacroParams {
12080            required: vec!["a".into(), "b".into()],
12081            optional: Vec::new(),
12082            rest: Some("c".into()),
12083        };
12084        let vals = params
12085            .bind(
12086                "m",
12087                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12088            )
12089            .unwrap();
12090        assert_eq!(
12091            vals,
12092            vec![
12093                Sexp::int(1),
12094                Sexp::int(2),
12095                Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12096            ]
12097        );
12098    }
12099
12100    #[test]
12101    fn bind_supplied_optional_takes_its_positional_arg() {
12102        // `(a &optional b)` bound to `1 2`: a=1, b=2. A supplied optional
12103        // behaves exactly like a positional — only its ABSENCE differs.
12104        let params = MacroParams {
12105            required: vec!["a".into()],
12106            optional: vec![OptionalParam::bare("b")],
12107            rest: None,
12108        };
12109        let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
12110        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
12111    }
12112
12113    #[test]
12114    fn bind_unsupplied_optional_defaults_to_nil() {
12115        // `(a &optional b c)` bound to just `1`: a=1, then b and c run out of
12116        // args and bind to `Sexp::Nil` — CL's default for an `&optional` with
12117        // no supplied default-form. The bound vec is still parallel to
12118        // `names()`, so the template's `,b` / `,c` resolve to nil, not a
12119        // missing-arg error.
12120        let params = MacroParams {
12121            required: vec!["a".into()],
12122            optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
12123            rest: None,
12124        };
12125        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12126        assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
12127    }
12128
12129    #[test]
12130    fn bind_rest_collects_args_beyond_required_and_optional() {
12131        // `(a &optional b &rest c)` bound to `1 2 3 4`: a=1, b=2 (supplied),
12132        // c=(3 4). The rest starts AFTER the required+optional run, so the
12133        // optional's supplied arg is not swept into the rest.
12134        let params = MacroParams {
12135            required: vec!["a".into()],
12136            optional: vec![OptionalParam::bare("b")],
12137            rest: Some("c".into()),
12138        };
12139        let vals = params
12140            .bind(
12141                "m",
12142                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12143            )
12144            .unwrap();
12145        assert_eq!(
12146            vals,
12147            vec![
12148                Sexp::int(1),
12149                Sexp::int(2),
12150                Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12151            ]
12152        );
12153    }
12154
12155    #[test]
12156    fn bind_unsupplied_optional_then_empty_rest() {
12157        // `(a &optional b &rest c)` bound to just `1`: a=1, b=nil (absent),
12158        // c=() (nothing left). Both the optional default AND the empty-rest
12159        // contract hold in the same bind.
12160        let params = MacroParams {
12161            required: vec!["a".into()],
12162            optional: vec![OptionalParam::bare("b")],
12163            rest: Some("c".into()),
12164        };
12165        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12166        assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
12167    }
12168
12169    #[test]
12170    fn bind_rest_with_no_remaining_args_is_the_empty_list() {
12171        // Exactly-saturated required args + a rest that captures nothing →
12172        // the rest binds to the empty list, never errors. Mirrors the
12173        // splice contract `,@()` contributes nothing.
12174        let params = MacroParams {
12175            required: vec!["a".into()],
12176            optional: Vec::new(),
12177            rest: Some("c".into()),
12178        };
12179        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12180        assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
12181    }
12182
12183    #[test]
12184    fn bind_missing_required_errors_before_any_rest_collection() {
12185        // A required name with no arg at its position is a
12186        // `MissingMacroArg` — the gate fires during the required walk,
12187        // before the rest is ever collected.
12188        let params = MacroParams {
12189            required: vec!["a".into(), "b".into()],
12190            optional: Vec::new(),
12191            rest: Some("c".into()),
12192        };
12193        let err = params
12194            .bind("m", &[Sexp::int(1)])
12195            .expect_err("missing required `b` must error");
12196        assert!(
12197            matches!(err, LispError::MissingMacroArg { .. }),
12198            "expected MissingMacroArg, got: {err:?}"
12199        );
12200    }
12201
12202    #[test]
12203    fn bind_missing_required_errors_even_with_optional_present() {
12204        // An absent REQUIRED arg errors even when the param list has an
12205        // optional section: the required walk fires `MissingMacroArg` before
12206        // the optional arm (which would otherwise default to nil) is reached.
12207        // Required absence is an error; optional absence is a nil default.
12208        let params = MacroParams {
12209            required: vec!["a".into(), "b".into()],
12210            optional: vec![OptionalParam::bare("c")],
12211            rest: None,
12212        };
12213        let err = params
12214            .bind("m", &[Sexp::int(1)])
12215            .expect_err("missing required `b` must error before optional defaulting");
12216        assert!(
12217            matches!(err, LispError::MissingMacroArg { .. }),
12218            "expected MissingMacroArg, got: {err:?}"
12219        );
12220    }
12221
12222    #[test]
12223    fn bind_rest_less_params_reject_surplus_args() {
12224        // The rest-less binder REJECTS surplus call args via the
12225        // structural `TooManyMacroArgs { macro_name, expected, got }`
12226        // rejection — the call-site mirror of `RestParamTrailingTokens`
12227        // (the definition-site rejection lifted at the parse_params
12228        // boundary). Closes the asymmetry where the typed-entry
12229        // macro-call-gate rejected too-few-args loudly
12230        // (`MissingMacroArg`) but silently truncated too-many. `expected`
12231        // is the rest-less binder's fixed maximum arity
12232        // (`required.len() + optional.len()`); `got` is the actual
12233        // call-site arg count.
12234        let params = MacroParams {
12235            required: vec!["a".into()],
12236            optional: Vec::new(),
12237            rest: None,
12238        };
12239        let err = params
12240            .bind("m", &[Sexp::int(1), Sexp::int(2)])
12241            .expect_err("rest-less surplus must error");
12242        match err {
12243            LispError::TooManyMacroArgs {
12244                macro_name,
12245                expected,
12246                got,
12247            } => {
12248                assert_eq!(macro_name, "m");
12249                assert_eq!(expected, 1);
12250                assert_eq!(got, 2);
12251            }
12252            other => panic!("expected TooManyMacroArgs, got: {other:?}"),
12253        }
12254    }
12255
12256    // ── OptionalParam: per-param default forms — `&optional (x DEFAULT)` ──
12257    //
12258    // The `&optional` section now admits both bare-symbol entries (`x`) AND
12259    // list-form entries (`(x DEFAULT)`). The typed `OptionalParam.default:
12260    // Option<Sexp>` slot makes the per-param default a FIELD on each
12261    // optional entry, not a discipline a sibling `Vec<Sexp>` would have had
12262    // to maintain in lock-step with `Vec<String>`. These tests pin: the
12263    // parser admits both shapes side-by-side; the four malformed list-spec
12264    // shapes (empty / missing-default / extra-elements / non-symbol-name)
12265    // are rejected via `OptionalParamMalformed` with the typed
12266    // `OptionalParamMalformedReason`; the binder consults the default form
12267    // when the arg is absent and ignores it when supplied; and the end-to-
12268    // end expansion agrees between the bytecode and substitute strategies
12269    // (invariant 2 — free middle).
12270
12271    #[test]
12272    fn parse_params_admits_optional_list_spec_with_default() {
12273        // `(a &optional (b 5))` — one bare optional becomes
12274        // `OptionalParam { name: "b", default: Some(Int(5)) }`. The
12275        // surrounding `MacroParams` shape is otherwise identical.
12276        let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
12277        assert_eq!(
12278            params,
12279            MacroParams {
12280                required: vec!["a".into()],
12281                optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12282                rest: None,
12283            }
12284        );
12285    }
12286
12287    #[test]
12288    fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
12289        // `(a &optional b (c "x") d (e 9) &rest r)` — the optional section
12290        // interleaves bare and list-form specs. Each lands in its own
12291        // `OptionalParam` entry; `names()` still yields the flat
12292        // required-then-optional-then-rest order.
12293        let params =
12294            parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
12295        assert_eq!(
12296            params,
12297            MacroParams {
12298                required: vec!["a".into()],
12299                optional: vec![
12300                    OptionalParam::bare("b"),
12301                    OptionalParam::with_default("c", Sexp::string("x")),
12302                    OptionalParam::bare("d"),
12303                    OptionalParam::with_default("e", Sexp::int(9)),
12304                ],
12305                rest: Some("r".into()),
12306            }
12307        );
12308        assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
12309    }
12310
12311    #[test]
12312    fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
12313        // `(&optional (x (list 1 2)))` — the default form is itself a list.
12314        // Without an evaluator, the literal Sexp is parked verbatim into
12315        // `default`; the binder produces it for any absent call.
12316        let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
12317        let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12318        assert_eq!(
12319            params,
12320            MacroParams {
12321                required: Vec::new(),
12322                optional: vec![OptionalParam::with_default("x", want_default)],
12323                rest: None,
12324            }
12325        );
12326    }
12327
12328    #[test]
12329    fn parse_params_rejects_empty_list_optional_spec() {
12330        // `(&optional ())` — a zero-element list is the empty-list rejection.
12331        // Without the gate the loop would `as_symbol()` on a `Sexp::List` and
12332        // fall through to `NonSymbolParam`, which mis-classifies the failure
12333        // (this is a malformed DEFAULT-FORM spec, not a "param must be a
12334        // symbol" rejection).
12335        let err = parse_params(&read("&optional ()").unwrap())
12336            .expect_err("empty list optional spec must error");
12337        assert!(
12338            matches!(
12339                err,
12340                LispError::OptionalParamMalformed {
12341                    position: 1,
12342                    reason: crate::error::OptionalParamMalformedReason::EmptyList,
12343                    ..
12344                }
12345            ),
12346            "expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
12347        );
12348    }
12349
12350    #[test]
12351    fn parse_params_rejects_one_element_optional_list_as_missing_default() {
12352        // `(&optional (x))` — a one-element list. REJECTED with reason
12353        // `MissingDefault` rather than reinterpreted as `&optional x`, because
12354        // a parenthesized single-element spec is structurally ambiguous and
12355        // the bare-symbol form `x` IS the canonical "no default" shape.
12356        let err = parse_params(&read("&optional (x)").unwrap())
12357            .expect_err("one-element list optional spec must error");
12358        assert!(
12359            matches!(
12360                err,
12361                LispError::OptionalParamMalformed {
12362                    position: 1,
12363                    reason: crate::error::OptionalParamMalformedReason::MissingDefault,
12364                    ..
12365                }
12366            ),
12367            "expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
12368        );
12369    }
12370
12371    #[test]
12372    fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
12373        // `(&optional (x 5 6))` — a three-element list. CL's `(name default
12374        // supplied-p)` shape is not yet supported (no evaluator → no
12375        // supplied-p variable binding), so the third element is structurally
12376        // surplus. REJECTED with reason `ExtraElements{length: 3}`.
12377        let err = parse_params(&read("&optional (x 5 6)").unwrap())
12378            .expect_err("three-element list optional spec must error");
12379        assert!(
12380            matches!(
12381                err,
12382                LispError::OptionalParamMalformed {
12383                    position: 1,
12384                    reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
12385                    ..
12386                }
12387            ),
12388            "expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
12389        );
12390    }
12391
12392    #[test]
12393    fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
12394        // `(&optional (5 default))` — the name slot must be a symbol; a
12395        // numeric literal is REJECTED with reason `NonSymbolName`. Without
12396        // this branch the gate would silently populate
12397        // `OptionalParam.name` from a stringified non-symbol value (`"5"`),
12398        // breaking the invariant that param names are symbols.
12399        let err = parse_params(&read("&optional (5 default)").unwrap())
12400            .expect_err("non-symbol-name optional spec must error");
12401        assert!(
12402            matches!(
12403                err,
12404                LispError::OptionalParamMalformed {
12405                    position: 1,
12406                    reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
12407                    ..
12408                }
12409            ),
12410            "expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
12411        );
12412    }
12413
12414    #[test]
12415    fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
12416        // `((a 5))` — a list in the REQUIRED section is NOT a default-form
12417        // spec; default forms are an optional-section affordance. The gate
12418        // must fall through to `NonSymbolParam` (parity with the prior
12419        // behavior on lists in the required section), not silently admit
12420        // the list as a default-form spec.
12421        let err =
12422            parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
12423        assert!(
12424            matches!(err, LispError::NonSymbolParam { position: 0, .. }),
12425            "expected NonSymbolParam{{position: 0}}, got: {err:?}"
12426        );
12427    }
12428
12429    #[test]
12430    fn bind_unsupplied_optional_with_default_takes_the_default() {
12431        // `(a &optional (b 5))` bound to just `1`: a=1, b=5 (the declared
12432        // default), not nil. The default form is consulted ONLY when the
12433        // call ran out of args.
12434        let params = MacroParams {
12435            required: vec!["a".into()],
12436            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12437            rest: None,
12438        };
12439        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12440        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
12441    }
12442
12443    #[test]
12444    fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
12445        // `(&optional (b 5))` bound to `42`: b=42, NOT the default. A
12446        // supplied optional ALWAYS takes its arg; the default is the
12447        // absence-only fallback. Pins that the default form does not
12448        // shadow a supplied call arg.
12449        let params = MacroParams {
12450            required: Vec::new(),
12451            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12452            rest: None,
12453        };
12454        let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12455        assert_eq!(vals, vec![Sexp::int(42)]);
12456    }
12457
12458    #[test]
12459    fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
12460        // `(a &optional (b 5) c (d "z"))` bound to just `1`: a=1, b=5
12461        // (default), c=nil (bare floor), d="z" (default). The three
12462        // absence cases coexist in one bind: per-default fill, nil floor,
12463        // and a tail with a literal-string default.
12464        let params = MacroParams {
12465            required: vec!["a".into()],
12466            optional: vec![
12467                OptionalParam::with_default("b", Sexp::int(5)),
12468                OptionalParam::bare("c"),
12469                OptionalParam::with_default("d", Sexp::string("z")),
12470            ],
12471            rest: None,
12472        };
12473        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12474        assert_eq!(
12475            vals,
12476            vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
12477        );
12478    }
12479
12480    // ── OptionalParam::resolved_default: the absent-call binder accessor ──
12481    //
12482    // `resolved_default` lifts the `param.default.clone().unwrap_or(Sexp::Nil)`
12483    // two-arm fallback that previously inlined at `MacroParams::bind`'s
12484    // optional arm into ONE named accessor on the typed `OptionalParam`.
12485    // The constructor pair `bare` / `with_default` defines the typed
12486    // shapes of the `default` slot; this accessor names the symmetric
12487    // bound-value projection both shapes yield at the absence boundary.
12488    // Tests pin: (a) `bare(name).resolved_default()` is the `Sexp::Nil`
12489    // no-default floor; (b) `with_default(name, d).resolved_default()` is
12490    // `d.clone()`; (c) the projection is `Clone`-stable across repeated
12491    // calls (the typed `default` field is not consumed); (d) path-
12492    // uniformity at the binder — `bind`'s optional arm routes through
12493    // `resolved_default` for both shapes; (e) end-to-end through both
12494    // expansion strategies, the absent-call binding agrees.
12495
12496    #[test]
12497    fn resolved_default_is_nil_for_bare_optional() {
12498        // `OptionalParam::bare(name).default` is `None`, so
12499        // `resolved_default()` projects to `Sexp::Nil` — the CL
12500        // `&optional` no-default-form floor. Fail-before/pass-after: this
12501        // assert is meaningless pre-lift because the helper does not
12502        // exist; post-lift it pins the typed accessor's `Sexp::Nil` arm
12503        // at the named primitive. Sibling of `bare` itself: the
12504        // constructor defines the shape (`default: None`); the accessor
12505        // names the bound-value projection of that shape.
12506        let p = OptionalParam::bare("x");
12507        assert_eq!(p.resolved_default(), Sexp::Nil);
12508    }
12509
12510    #[test]
12511    fn resolved_default_clones_declared_default_for_with_default_optional() {
12512        // `OptionalParam::with_default(name, d).default` is `Some(d)`, so
12513        // `resolved_default()` projects to `d.clone()` — the declared
12514        // default form. Sibling of the bare-floor pin: the closed-set
12515        // `default: Option<Sexp>` slot's two shapes correspond 1:1 with
12516        // the two arms of `resolved_default`. Pins the closed-set
12517        // exhaustive coverage of `Option<Sexp>` × `{Some, None}`.
12518        let p = OptionalParam::with_default("x", Sexp::int(5));
12519        assert_eq!(p.resolved_default(), Sexp::int(5));
12520    }
12521
12522    #[test]
12523    fn resolved_default_clones_arbitrary_sexp_default_form() {
12524        // The declared default can be any `Sexp` — a literal list, a
12525        // keyword, a string, a quasi-quoted form — because v0 has no
12526        // evaluator and the typed slot parks the literal verbatim. Pin
12527        // that `resolved_default()` is faithful to the parked literal
12528        // regardless of shape: a regression that special-cases an arm
12529        // (e.g., projecting `Sexp::List(_)` to `Sexp::Nil`, or "normalizing"
12530        // a `Sexp::Quote`) fails here. The accessor is exactly
12531        // `default.clone()` for the `Some` arm — no shape rewriting.
12532        let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12533        let p = OptionalParam::with_default("x", arbitrary.clone());
12534        assert_eq!(p.resolved_default(), arbitrary);
12535    }
12536
12537    #[test]
12538    fn resolved_default_is_clone_stable_across_repeated_calls() {
12539        // The accessor takes `&self` and projects through `Clone`, so
12540        // repeated calls yield IDENTICAL values — the typed `default`
12541        // field is not consumed. Pins that the accessor is idempotent
12542        // for the same `OptionalParam`, which is the contract the binder
12543        // relies on across multiple `bind` invocations of the same
12544        // macro: every call that leaves the optional unfilled yields
12545        // the SAME bound value, never a partially-consumed shape. A
12546        // regression that converted the accessor to `self.default.take()`
12547        // (consuming the field) would still type-check at the call site
12548        // but would silently desync repeated absent-call bindings; this
12549        // test catches that drift.
12550        let p = OptionalParam::with_default("x", Sexp::string("hi"));
12551        let first = p.resolved_default();
12552        let second = p.resolved_default();
12553        assert_eq!(first, second);
12554        assert_eq!(first, Sexp::string("hi"));
12555    }
12556
12557    #[test]
12558    fn resolved_default_is_the_binders_absent_optional_projection() {
12559        // Path-uniformity pin at the binder boundary: `MacroParams::bind`'s
12560        // optional arm consults `param.resolved_default()` for any
12561        // absent slot. Two-arm coverage: a bare optional (`b`) binds to
12562        // `Sexp::Nil` via the `None` arm; a with-default optional
12563        // (`(c 5)`) binds to `Sexp::int(5)` via the `Some` arm. The
12564        // single bind call exercises both arms in one walk, and the
12565        // bound values vec is parallel to `names()` so position
12566        // checking pins the arm-to-slot mapping. A regression that
12567        // re-inlines the two-arm fallback at the binder, drifting it
12568        // independently from the accessor, would still type-check but
12569        // a future shape change to `resolved_default` (e.g., adding a
12570        // typed `&supplied-p` companion slot) would silently desync
12571        // the binder from the accessor — this test catches that drift.
12572        let params = MacroParams {
12573            required: Vec::new(),
12574            optional: vec![
12575                OptionalParam::bare("b"),
12576                OptionalParam::with_default("c", Sexp::int(5)),
12577            ],
12578            rest: None,
12579        };
12580        // Empty args → both optionals are absent → both arms fire.
12581        let vals = params.bind("m", &[]).unwrap();
12582        assert_eq!(vals.len(), 2);
12583        assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
12584        assert_eq!(
12585            vals[1],
12586            OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
12587        );
12588        // And the absolute identities pin the projection's arms:
12589        assert_eq!(vals[0], Sexp::Nil);
12590        assert_eq!(vals[1], Sexp::int(5));
12591    }
12592
12593    #[test]
12594    fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
12595        // End-to-end path-uniformity: a macro with both an `&optional
12596        // (g "hi")` (with-default) and an `&optional h` (bare) param
12597        // expands the same way under bytecode AND substitute
12598        // strategies, because both strategies route through the SHARED
12599        // `MacroParams::bind` which now consults `resolved_default`
12600        // for absent slots. Pins that the accessor's contract is
12601        // structurally observable at the strategy boundary — a
12602        // regression that bifurcated the accessor's behavior between
12603        // the two paths (impossible, since they share `bind`) would
12604        // surface here.
12605        let src = r#"
12606            (defmacro greet (n &optional (g "hi") h)
12607              `(list ,g ,n ,h))
12608            (greet world)
12609        "#;
12610        let expected = vec![Sexp::List(vec![
12611            Sexp::symbol("list"),
12612            Sexp::string("hi"),
12613            Sexp::symbol("world"),
12614            Sexp::Nil,
12615        ])];
12616        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12617        let substitute = Expander::new_substitute_only()
12618            .expand_program(read(src).unwrap())
12619            .unwrap();
12620        assert_eq!(
12621            bytecode, expected,
12622            "bytecode resolved_default expansion drifted"
12623        );
12624        assert_eq!(
12625            substitute, expected,
12626            "substitute resolved_default expansion drifted"
12627        );
12628        assert_eq!(
12629            bytecode, substitute,
12630            "the two strategies disagree on resolved_default expansion"
12631        );
12632    }
12633
12634    #[test]
12635    fn resolved_default_supplied_optional_does_not_consult_accessor() {
12636        // A SUPPLIED optional binds to its CALL ARG, never to the
12637        // accessor's projection. Pins the contract: `resolved_default`
12638        // is the absence-only fallback; a present arg shadows the
12639        // accessor at the binder. Sibling negative control to
12640        // `resolved_default_is_the_binders_absent_optional_projection`:
12641        // that test exercises the absence arm at every slot; this test
12642        // exercises the presence arm at every slot and proves the
12643        // accessor's `Sexp::int(5)` projection is NOT consulted when
12644        // the optional is supplied with `Sexp::int(42)`. A regression
12645        // that wired the binder to always consult the accessor (the
12646        // wrong direction — the default would shadow supplied args) is
12647        // caught here.
12648        let params = MacroParams {
12649            required: Vec::new(),
12650            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12651            rest: None,
12652        };
12653        let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12654        assert_eq!(vals, vec![Sexp::int(42)]);
12655        // And the accessor's would-be projection IS NOT the bound value:
12656        let p = OptionalParam::with_default("b", Sexp::int(5));
12657        assert_ne!(vals[0], p.resolved_default());
12658    }
12659
12660    #[test]
12661    fn optional_default_macro_expands_end_to_end_under_both_strategies() {
12662        // The end-to-end path: a macro with `&optional (g "hi")` expands to
12663        // the default literal when unsupplied, and to the supplied arg when
12664        // present. Both the bytecode and substitute strategies must agree
12665        // (invariant 2 — free middle); they share `MacroParams::bind`, so
12666        // the default arm lands once in `bind` and both strategies inherit
12667        // it unable to drift. This is the test the prior run (611a682)
12668        // signposted as the next-change-that-benefits.
12669        let src = r#"
12670            (defmacro greet (n &optional (g "hi"))
12671              `(list ,g ,n))
12672            (greet world)
12673            (greet world there)
12674        "#;
12675        let expected = vec![
12676            Sexp::List(vec![
12677                Sexp::symbol("list"),
12678                Sexp::string("hi"),
12679                Sexp::symbol("world"),
12680            ]),
12681            Sexp::List(vec![
12682                Sexp::symbol("list"),
12683                Sexp::symbol("there"),
12684                Sexp::symbol("world"),
12685            ]),
12686        ];
12687        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12688        let substitute = Expander::new_substitute_only()
12689            .expand_program(read(src).unwrap())
12690            .unwrap();
12691        assert_eq!(
12692            bytecode, expected,
12693            "bytecode optional-default expansion drifted"
12694        );
12695        assert_eq!(
12696            substitute, expected,
12697            "substitute optional-default expansion drifted"
12698        );
12699        assert_eq!(
12700            bytecode, substitute,
12701            "the two strategies disagree on optional-default expansion"
12702        );
12703    }
12704
12705    #[test]
12706    fn optional_macro_expands_end_to_end_under_both_strategies() {
12707        // The end-to-end path: a macro with an `&optional` param expands
12708        // correctly whether the optional is supplied or defaulted, and the
12709        // bytecode and substitute strategies agree (invariant 2 — free
12710        // middle). `,b` resolves to the supplied arg when present, to
12711        // `Sexp::Nil` when absent (CL's `&optional` default).
12712        let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
12713        // (cons 1 2) — optional supplied; (cons 3 <Nil>) — optional defaulted.
12714        // The defaulted slot is the canonical `Sexp::Nil`, distinct in the AST
12715        // from a reader-produced empty list `()` even though both Display as
12716        // `()`.
12717        let expected = vec![
12718            Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
12719            Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
12720        ];
12721        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12722        let substitute = Expander::new_substitute_only()
12723            .expand_program(read(src).unwrap())
12724            .unwrap();
12725        assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
12726        assert_eq!(
12727            substitute, expected,
12728            "substitute optional expansion drifted"
12729        );
12730        assert_eq!(
12731            bytecode, substitute,
12732            "the two strategies disagree on optional expansion"
12733        );
12734    }
12735
12736    // ── MacroDef::template_body: the shared body-projection primitive ──
12737    //
12738    // `template_body` lifts the `match &def.body { Sexp::Quasiquote(inner)
12739    // => inner.as_ref(), other => other }` inline peel — present
12740    // byte-identically at the bytecode (`compile_template`) AND substitute
12741    // (`apply`'s fallback) path entries — into ONE named projection both
12742    // strategies share. The existing `compiled_template_matches_substitute
12743    // _path` and `expansion_layers_agree_on_output_and_cache_wins` tests
12744    // are the path-uniformity guards covering the SHARED-PROJECTION shape;
12745    // the four tests below pin the projection's contract DIRECTLY:
12746    // (a) a quasi-quoted body unwraps to the inner; (b) a non-quasi-quoted
12747    // body returns the body verbatim; (c) the borrow is rooted in the
12748    // body field (single-level peel); (d) the projection is the same
12749    // `&Sexp` both strategies route on (so a regression that drifts the
12750    // body-peel from one strategy to the other becomes a type-level
12751    // change at this helper, not a silent two-site divergence).
12752
12753    #[test]
12754    fn template_body_unwraps_outer_quasiquote_to_inner() {
12755        // The canonical authoring shape: `(defmacro f (a) `(list ,a))` —
12756        // the reader wraps the `` ` `` form into `Sexp::Quasiquote(inner)`,
12757        // and `template_body` peels the outer marker. The returned `&Sexp`
12758        // is the inner walker-payload both expansion strategies consume.
12759        let inner = Sexp::List(vec![
12760            Sexp::symbol("list"),
12761            Sexp::Unquote(Box::new(Sexp::symbol("a"))),
12762        ]);
12763        let def = MacroDef {
12764            name: "f".into(),
12765            params: MacroParams::default(),
12766            body: Sexp::Quasiquote(Box::new(inner.clone())),
12767        };
12768        assert_eq!(def.template_body(), &inner);
12769    }
12770
12771    #[test]
12772    fn template_body_returns_non_quasiquote_body_verbatim() {
12773        // A body authored WITHOUT the outer `` ` `` affordance — a bare
12774        // `Sexp::List` body — returns verbatim. The "other" arm of the
12775        // legacy match. Pin parity with the pre-lift code path so a
12776        // regression that drifts the body-peel into "always peel
12777        // something" (which would break literal-body macros) fails here.
12778        let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
12779        let def = MacroDef {
12780            name: "f".into(),
12781            params: MacroParams::default(),
12782            body: body.clone(),
12783        };
12784        assert_eq!(def.template_body(), &body);
12785        // Atom bodies too — the projection is a single-arm match, not a
12786        // recursive descent. A `Sexp::Atom` body is its own template payload.
12787        let atom_def = MacroDef {
12788            name: "g".into(),
12789            params: MacroParams::default(),
12790            body: Sexp::symbol("nil-template"),
12791        };
12792        assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
12793    }
12794
12795    #[test]
12796    fn template_body_peels_single_level_only() {
12797        // A nested `` ``form `` body — `Sexp::Quasiquote(Box::new(
12798        // Sexp::Quasiquote(...)))` — unwraps ONE outer quasi-quote and
12799        // returns the inner `Sexp::Quasiquote(...)` as-is. The v0 module
12800        // preamble declares "Nested quasi-quotes: Not yet supported"; the
12801        // single-level peel matches the legacy inline match's posture
12802        // (which only matched ONE outer `Sexp::Quasiquote(_)` arm, not a
12803        // recursive loop). A regression that drifts to a recursive peel
12804        // would project too far and the inner `Sexp::Quasiquote` marker —
12805        // which the substitute walker treats as an atomic leaf returned
12806        // verbatim (line ~830, `Sexp::Quote(_) | Sexp::Quasiquote(_)
12807        // => Ok(form.clone())`) — would silently disappear from the
12808        // expansion's emitted form. Pin the single-level contract here.
12809        let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
12810        let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
12811        let def = MacroDef {
12812            name: "nested".into(),
12813            params: MacroParams::default(),
12814            body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
12815        };
12816        // Outer peel returns the INNER quasi-quote, NOT its inner payload.
12817        assert_eq!(def.template_body(), &inner_qq);
12818        assert_ne!(def.template_body(), &inner_payload);
12819    }
12820
12821    #[test]
12822    fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
12823        // A `Sexp::Quote(_)` body — not a `Sexp::Quasiquote(_)` — returns
12824        // verbatim through the "other" arm. The two close-cousin shapes
12825        // share an outer-marker character (`'` vs `` ` ``) at the reader
12826        // boundary but differ semantically: a `Quote` body is a literal
12827        // template (no substitution semantics), a `Quasiquote` body is a
12828        // substitution-walker entry. A regression that conflated the two
12829        // — peeling Quote as if it were Quasiquote — would silently turn
12830        // every quoted-body macro into a template-walked macro. Pin the
12831        // discrimination.
12832        let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
12833        let body = Sexp::Quote(Box::new(inner.clone()));
12834        let def = MacroDef {
12835            name: "quoted".into(),
12836            params: MacroParams::default(),
12837            body: body.clone(),
12838        };
12839        assert_eq!(def.template_body(), &body);
12840        // The inner is NOT what comes back — only Quasiquote-bodied macros
12841        // would peel to the inner.
12842        assert_ne!(def.template_body(), &inner);
12843    }
12844
12845    #[test]
12846    fn template_body_is_the_shared_projection_both_strategies_walk() {
12847        // End-to-end path-uniformity at the projection boundary: a macro
12848        // authored with the canonical quasi-quoted body expands
12849        // IDENTICALLY under bytecode and substitute strategies because
12850        // both route their walker's body through `template_body()` — the
12851        // SAME `&Sexp` projection. Sibling of
12852        // `compiled_template_matches_substitute_path` (which observes
12853        // agreement on the EMITTED form); this test pins agreement on
12854        // the projection ENTRY: `compile_template` and the substitute
12855        // fallback now consume the same `&Sexp` (`def.template_body()`),
12856        // never a divergent inline match the two paths could regress
12857        // independently.
12858        let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
12859        let expected = vec![Sexp::List(vec![
12860            Sexp::symbol("list"),
12861            Sexp::int(5),
12862            Sexp::int(5),
12863        ])];
12864        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12865        let substitute = Expander::new_substitute_only()
12866            .expand_program(read(src).unwrap())
12867            .unwrap();
12868        assert_eq!(bytecode, expected, "bytecode body-projection drifted");
12869        assert_eq!(substitute, expected, "substitute body-projection drifted");
12870        assert_eq!(
12871            bytecode, substitute,
12872            "the two strategies disagree on the body-projection's emission"
12873        );
12874    }
12875
12876    // ── Expander::expand: macro-call dispatch routes through `as_call_to_any` ──
12877    //
12878    // `expand` lifts its macro-call recognition to route through the
12879    // substrate's typed-decoded call decomposition: `as_call_to_any(|h|
12880    // self.macros.get(h))` answers "is this form an invocation of any
12881    // registered macro?" in ONE structural query on the Sexp algebra,
12882    // and a HashMap-backed lookup as its classifier. Sibling consumer to
12883    // `macro_def_from` (the typed-macro-definition dispatcher already
12884    // routing through `as_call_to_any(MacroDefHead::from_keyword)` with
12885    // a closed-set enum classifier). With both in place, BOTH dispatch
12886    // sites in the macro expander project through the SAME family
12887    // primitive — each binding the classifier that fits its candidate
12888    // set. The tests below pin the consumer's path-uniformity contract
12889    // at the new boundary: a hand-rolled `as_call_to_any(|h| macros.get
12890    // (h))` dispatch observes the SAME `(def, args)` decomposition the
12891    // `Expander::expand` consumer routes through.
12892
12893    #[test]
12894    fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
12895        // Structural identity: on a registered-macro call, the consumer's
12896        // expansion is observably equivalent to: classify the form via
12897        // `as_call_to_any(|h| macros.get(h))` → some `(def, args)` →
12898        // apply the def to args. Pin path-uniformity: a hand-rolled
12899        // `as_call_to_any` lookup against the same registry the expander
12900        // walks produces the SAME `MacroDef` reference for the SAME
12901        // input form. A regression that drifts the consumer back to an
12902        // inline `as_list + as_call + macros.get` chain (which would
12903        // fragment the family adoption) is caught structurally — the
12904        // hand-rolled `as_call_to_any` and the consumer's dispatch must
12905        // observe the same decomposition.
12906        let mut e = Expander::new();
12907        e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12908            .unwrap();
12909        let call_form = parse("(wrap 42)");
12910
12911        // Hand-rolled family-primitive lookup mirrors the lifted consumer.
12912        let (def_via_family, args_via_family) = call_form
12913            .as_call_to_any(|h| e.macros.get(h))
12914            .expect("registered macro call must decompose via as_call_to_any");
12915        assert_eq!(def_via_family.name, "wrap");
12916        assert_eq!(args_via_family, &[Sexp::int(42)]);
12917
12918        // Consumer's expand observes the SAME decomposition: the expanded
12919        // form is `(list 42 42)`, derived from the SAME def + args the
12920        // hand-rolled lookup found. Path-uniform with the family
12921        // primitive at the dispatch boundary.
12922        let expanded = e.expand(&call_form).unwrap();
12923        assert_eq!(
12924            expanded,
12925            Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
12926        );
12927    }
12928
12929    #[test]
12930    fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
12931        // Path-uniformity for the non-registered-head path: `as_call_to_any
12932        // (|h| macros.get(h))` returns `None` for a call whose head ISN'T
12933        // a registered macro, and the consumer falls through to the
12934        // children-walk (which expands any nested macro calls). Pin
12935        // both halves: the hand-rolled lookup returns `None`, AND the
12936        // consumer's expand walks into the children. A regression that
12937        // accidentally short-circuits the children-walk for non-macro
12938        // calls (e.g. by treating `as_call_to_any` = `None` as
12939        // "non-expandable" globally) would fail here.
12940        let mut e = Expander::new();
12941        e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12942            .unwrap();
12943        let outer = parse("(foo (wrap 5))");
12944
12945        // Hand-rolled family-primitive lookup rejects the outer head.
12946        assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
12947
12948        // Consumer walks children — the inner `(wrap 5)` IS a macro call
12949        // and expands to `(list 5 5)`; the outer `foo` head is preserved.
12950        let expanded = e.expand(&outer).unwrap();
12951        assert_eq!(
12952            expanded,
12953            Sexp::List(vec![
12954                Sexp::symbol("foo"),
12955                Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
12956            ])
12957        );
12958    }
12959
12960    #[test]
12961    fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
12962        // Path-uniformity for the non-call path: every shape `as_call`
12963        // rejects (atoms across all 6 kinds, Nil, Quote-family wrappers)
12964        // ALSO routes past `as_call_to_any` into the `as_list()`
12965        // fallthrough, where the not-a-list arm returns `form.clone()`
12966        // verbatim. Pin both halves: the hand-rolled lookup rejects
12967        // every non-call shape regardless of decoder, AND the consumer
12968        // preserves each shape unchanged. A regression that drifts the
12969        // consumer's dispatch order (e.g. checking `as_list()` BEFORE
12970        // `as_call_to_any` in a way that mis-handles Quote-family
12971        // wrappers) would fail here.
12972        let e = Expander::new();
12973        let shapes = [
12974            Sexp::symbol("foo"),
12975            Sexp::int(5),
12976            Sexp::keyword("k"),
12977            Sexp::string("s"),
12978            Sexp::boolean(true),
12979            Sexp::float(1.5),
12980            Sexp::Nil,
12981            Sexp::Quote(Box::new(Sexp::symbol("x"))),
12982            Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
12983            Sexp::Unquote(Box::new(Sexp::symbol("x"))),
12984            Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
12985        ];
12986        for s in &shapes {
12987            // Hand-rolled family-primitive lookup rejects non-call shapes
12988            // even for a promiscuous decoder — the call-shape gate fires
12989            // BEFORE the decoder runs.
12990            assert!(
12991                s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
12992                "non-call shape must yield None for as_call_to_any: {s}"
12993            );
12994            // Consumer preserves the shape verbatim — the not-a-list
12995            // arm at the fallthrough returns `form.clone()`.
12996            assert_eq!(
12997                e.expand(s).unwrap(),
12998                s.clone(),
12999                "non-call shape must round-trip unchanged through expand: {s}"
13000            );
13001        }
13002    }
13003
13004    #[test]
13005    fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
13006        // The empty list `()` has no operator and no children. Pin that
13007        // `as_call_to_any` rejects it (no head to feed the decoder), the
13008        // consumer falls through to `as_list()` which returns `Some(&[])`,
13009        // and the children-walk emits `Sexp::List(vec![])` (an empty
13010        // list, not `form.clone()` of `Sexp::List(vec![])` — both happen
13011        // to be observationally identical, but the path is the
13012        // children-walk arm, NOT the not-a-list arm). Path-uniformity
13013        // gate for the singleton-list edge case the `compile_named_from_
13014        // forms` rejection chain relies on `as_call_to(KEYWORD)` to yield
13015        // `Some(&[])` for — same posture, different family member.
13016        let e = Expander::new();
13017        let empty = Sexp::List(vec![]);
13018
13019        // Hand-rolled family-primitive lookup rejects the empty list.
13020        assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
13021
13022        // Consumer walks children (zero of them) — output is the empty
13023        // list, same as input.
13024        assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
13025    }
13026
13027    // ── Expander::expand_and_collect_calls_to: program-level walk ───────
13028    //
13029    // `expand_and_collect_calls_to(forms, keyword, project)` composes the
13030    // expander's program-level expansion (`expand_program`) with the
13031    // substrate's slice-side typed-keyword projection (`iter_calls_to`)
13032    // and a caller-supplied per-form mapper into ONE method on the
13033    // `Expander` surface. Both `compile_typed::<T>` and
13034    // `compile_named_from_forms::<T>` (compile.rs) route through it; the
13035    // tests below pin its contract directly. The existing compile.rs
13036    // dispatch tests are the path-uniformity guards proving the two
13037    // production sites route through it without behavior drift.
13038
13039    #[test]
13040    fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
13041        // Three forms — two match `defmonitor`, one matches `defalert`.
13042        // The mapper records `args.len()` from each matching form's tail.
13043        // Pin: only the two `defmonitor` matches flow through `project`,
13044        // in source order (so the recorded lengths are 2, 1).
13045        let forms = vec![
13046            Sexp::List(vec![
13047                Sexp::symbol("defmonitor"),
13048                Sexp::keyword("name"),
13049                Sexp::string("first"),
13050            ]),
13051            Sexp::List(vec![
13052                Sexp::symbol("defalert"),
13053                Sexp::keyword("name"),
13054                Sexp::string("not-a-match"),
13055            ]),
13056            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
13057        ];
13058        let mut e = Expander::new();
13059        let lengths: Vec<usize> = e
13060            .expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
13061            .expect("matching forms must compose");
13062        assert_eq!(lengths, vec![2, 1]);
13063    }
13064
13065    #[test]
13066    fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13067        // Path-uniformity for the soft-projection posture inherited from
13068        // `iter_calls_to`: non-matching forms are skipped silently, and
13069        // `project` is never invoked on them. Pin via a counter the
13070        // closure increments per invocation — the counter is the
13071        // observable that distinguishes "skipped silently" from
13072        // "projected to a no-op".
13073        let forms = vec![
13074            Sexp::symbol("bare-atom"),
13075            Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
13076            Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
13077            Sexp::List(vec![]),
13078            Sexp::Nil,
13079        ];
13080        let mut count = 0usize;
13081        let mut e = Expander::new();
13082        let out: Vec<()> = e
13083            .expand_and_collect_calls_to(forms, "defmonitor", |_args| {
13084                count += 1;
13085                Ok(())
13086            })
13087            .expect("non-matching-only slice must collect to empty Vec");
13088        assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13089        assert_eq!(count, 0, "project must never run for non-matching forms");
13090    }
13091
13092    #[test]
13093    fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13094        // Three matching forms; the closure errors on the SECOND. Pin: the
13095        // collect's `Result<Vec<_>, _>` short-circuit fires at the second
13096        // form's error, the third form's `project` is NEVER invoked, and
13097        // the returned error is exactly the one the closure raised. This
13098        // mirrors `compile_named_from_forms`'s short-circuit on the first
13099        // `NamedFormMissingName` / `NamedFormNonSymbolName` / typed-entry
13100        // kwargs rejection.
13101        let forms = vec![
13102            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13103            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
13104            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13105        ];
13106        let mut seen = Vec::new();
13107        let mut e = Expander::new();
13108        let err = e
13109            .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
13110                let n = args[0].as_int().expect("test args are ints");
13111                seen.push(n);
13112                if n == 2 {
13113                    Err(LispError::Compile {
13114                        form: "test".to_string(),
13115                        message: "stop at two".to_string(),
13116                    })
13117                } else {
13118                    Ok(())
13119                }
13120            })
13121            .expect_err("project's error must short-circuit collect");
13122        // Only the first two forms were ever projected — the third
13123        // never reached `project`.
13124        assert_eq!(seen, vec![1, 2]);
13125        assert!(
13126            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13127            "short-circuit must propagate the project's error verbatim, got {err:?}"
13128        );
13129    }
13130
13131    #[test]
13132    fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
13133        // The `expand_program` step runs BEFORE `iter_calls_to` walks
13134        // anything — so an `expand_program` error (e.g., a malformed
13135        // `defmacro` head) must short-circuit the entire composition
13136        // and `project` must NEVER run. Pin via the closure-running
13137        // counter: an error from the FIRST step never invokes `project`.
13138        // `(defmacro 5 (x) `,x)` — the macro NAME slot is an int, not a
13139        // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13140        // fires during `expand_program`.
13141        let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
13142        let mut count = 0usize;
13143        let mut e = Expander::new();
13144        let err = e
13145            .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
13146                count += 1;
13147                Ok(())
13148            })
13149            .expect_err("expand_program error must short-circuit before project");
13150        assert_eq!(
13151            count, 0,
13152            "project must never run when expand_program errors"
13153        );
13154        // Sanity: the error IS the expand_program-stage rejection, NOT
13155        // a project-stage error (path-uniformity for the ordering).
13156        let rendered = format!("{err}");
13157        assert!(
13158            rendered.contains("NAME") || rendered.contains("symbol"),
13159            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13160        );
13161    }
13162
13163    #[test]
13164    fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
13165        // Boundary: empty forms slice yields zero items regardless of
13166        // keyword. Pin the degenerate boundary — empty in, empty out —
13167        // and the closure is never invoked. Sibling of
13168        // `iter_calls_to_yields_nothing_for_empty_slice` in ast.rs,
13169        // one level up the composition: the program-level walk is
13170        // fused-empty when `expand_program` yields an empty `Vec<Sexp>`.
13171        let mut count = 0usize;
13172        let mut e = Expander::new();
13173        let out: Vec<()> = e
13174            .expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
13175                count += 1;
13176                Ok(())
13177            })
13178            .expect("empty forms is not an error");
13179        assert!(out.is_empty());
13180        assert_eq!(count, 0);
13181    }
13182
13183    #[test]
13184    fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13185        // Critical ordering: `expand_program` runs BEFORE the keyword
13186        // filter, so a `defmacro` whose expansion produces a form with
13187        // a matching head IS visible to `project`. Pin the pipeline
13188        // order: `(defmacro emit-monitor (n) `(defmonitor :name ,n))
13189        //         (emit-monitor "alpha") (defmonitor :name "beta")`
13190        // expands the macro call to `(defmonitor :name "alpha")` and
13191        // the keyword walk then sees TWO `defmonitor` forms (the
13192        // macro-emitted one AND the directly-authored one). A
13193        // pre-expansion filter would miss the macro-emitted form.
13194        let forms = read(
13195            "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13196             (emit-monitor \"alpha\")
13197             (defmonitor :name \"beta\")",
13198        )
13199        .unwrap();
13200        let mut e = Expander::new();
13201        let names: Vec<String> = e
13202            .expand_and_collect_calls_to(forms, "defmonitor", |args| {
13203                // Each defmonitor form's tail is `(:name "X")`; project
13204                // the second element's string to capture the name.
13205                Ok(args[1].as_string().unwrap().to_string())
13206            })
13207            .expect("macroexpanded + directly-authored forms must both flow");
13208        assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13209    }
13210
13211    #[test]
13212    fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13213        // The `keyword` argument flows straight into `iter_calls_to`'s
13214        // head-comparison gate — a regression that drifts the keyword
13215        // (e.g., uses `T::KEYWORD` from a different `T` at the helper,
13216        // or lowercases / trims the keyword en route) fails here. Pin
13217        // by passing TWO different keywords against the SAME forms
13218        // input and asserting each picks up only its matching forms.
13219        let forms = vec![
13220            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13221            Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
13222            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13223            Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
13224        ];
13225
13226        let mut e = Expander::new();
13227        let monitors: Vec<i64> = e
13228            .expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
13229                Ok(args[0].as_int().unwrap())
13230            })
13231            .unwrap();
13232        assert_eq!(monitors, vec![1, 3]);
13233
13234        let mut e2 = Expander::new();
13235        let alerts: Vec<i64> = e2
13236            .expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
13237                Ok(args[0].as_int().unwrap())
13238            })
13239            .unwrap();
13240        assert_eq!(alerts, vec![2]);
13241
13242        // A keyword nothing matches collects to the empty Vec — same
13243        // shape as the empty-forms case at the slice level.
13244        let mut e3 = Expander::new();
13245        let none: Vec<i64> = e3
13246            .expand_and_collect_calls_to(forms, "missing-keyword", |args| {
13247                Ok(args[0].as_int().unwrap())
13248            })
13249            .unwrap();
13250        assert!(none.is_empty());
13251    }
13252
13253    #[test]
13254    fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
13255        // Structural identity: for any (forms, keyword, project) triple
13256        // whose pieces succeed, the method's result equals the inlined
13257        // pre-lift pipeline `expand_program + iter_calls_to + map +
13258        // collect`. Pin shape AND ordering on a mixed forms input
13259        // (matching + non-matching + macroexpand-emitted matches) so a
13260        // regression that drifts the method's pipeline from the inlined
13261        // closed-form fails here. This is the lift's load-bearing
13262        // path-uniformity gate — the SAME assertion that holds for
13263        // both production consumers (compile.rs) holds at the
13264        // primitive's contract level.
13265        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13266                   (foo :idx 1)
13267                   (emit-foo 2)
13268                   (bar :idx 99)
13269                   (foo :idx 3)";
13270        let forms = read(src).unwrap();
13271
13272        // Inlined pre-lift pipeline.
13273        let mut exp_inline = Expander::new();
13274        let expanded = exp_inline.expand_program(forms.clone()).unwrap();
13275        let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
13276            .map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
13277            .collect::<Result<Vec<_>>>()
13278            .unwrap();
13279
13280        // Method pipeline.
13281        let mut exp_method = Expander::new();
13282        let via_method: Vec<i64> = exp_method
13283            .expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13284            .unwrap();
13285
13286        assert_eq!(via_inline, via_method);
13287        assert_eq!(via_inline, vec![1, 2, 3]);
13288    }
13289
13290    // ── Expander::expand_and_collect_calls_to_any: typed-decoded sibling ──
13291    //
13292    // The typed-decoded classifier sibling of `expand_and_collect_calls_to`:
13293    // composes `expand_program` with `iter_calls_to_any` and a per-form
13294    // mapper that receives BOTH the typed witness AND the args tail.
13295    // Closes the (keyword, classifier) 2×2 of expander-surface
13296    // compose-on-iter projections at the typed-decoded corner the prior
13297    // runs' slice-side `iter_calls_to_any` lift left open. The keyword
13298    // sibling now ROUTES through this primitive via a constant-classifier
13299    // composition — the tests below pin both the typed-decoded primitive's
13300    // contract directly AND the routing identity binding the keyword
13301    // sibling to it.
13302    //
13303    // The existing `expand_and_collect_calls_to_*` tests above remain
13304    // load-bearing — they pin the keyword sibling's contract through
13305    // its ROUTED implementation, so a regression that drifts the routing
13306    // composition surfaces at the existing keyword tests too.
13307
13308    #[test]
13309    fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
13310    {
13311        // The typed-decoded primitive's happy path: a closed-set
13312        // classifier decodes head symbols to a typed enum, and the
13313        // per-form projection receives `(decoded, args)` for every
13314        // matched form in source order. Sweep across THREE distinct
13315        // classifier outcomes — `Foo` / `Bar` / `Baz` — interleaved with
13316        // a non-matching form to pin both the typed-witness threading
13317        // AND the source-order yield AND the rejection of non-classifier
13318        // forms in ONE assertion. The decoder runs once per call form
13319        // (`FnMut`); the projection runs once per matched form.
13320        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
13321        enum Op {
13322            Foo,
13323            Bar,
13324            Baz,
13325        }
13326        let src = "(foo 1)
13327                   (bar 2)
13328                   (other 99)
13329                   (baz 3)
13330                   (foo 4)";
13331        let forms = read(src).unwrap();
13332        let mut e = Expander::new();
13333        let pairs: Vec<(Op, i64)> = e
13334            .expand_and_collect_calls_to_any(
13335                forms,
13336                |h| match h {
13337                    "foo" => Some(Op::Foo),
13338                    "bar" => Some(Op::Bar),
13339                    "baz" => Some(Op::Baz),
13340                    _ => None,
13341                },
13342                |op, args| Ok((op, args[0].as_int().unwrap())),
13343            )
13344            .unwrap();
13345        assert_eq!(
13346            pairs,
13347            vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
13348        );
13349    }
13350
13351    #[test]
13352    fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13353        // The soft-projection contract — every shape `iter_calls_to_any`
13354        // rejects (non-list, empty list, list whose head is not a
13355        // symbol, list whose head decodes through the classifier to
13356        // `None`) skips the projection silently. Pin a deliberately-
13357        // panicking projection so the assertion fires loudly if a
13358        // non-matching form reaches it. The decoder accepts only
13359        // `"defmonitor"`; every other shape (atom, list whose head is a
13360        // keyword, list whose head is a symbol the decoder rejects)
13361        // short-circuits before the projection runs.
13362        let src = r#":bare-keyword
13363                     "bare-string"
13364                     42
13365                     ()
13366                     (foo bar)
13367                     (defmonitor :name "matches")"#;
13368        let forms = read(src).unwrap();
13369        let mut e = Expander::new();
13370        let lengths: Vec<usize> = e
13371            .expand_and_collect_calls_to_any(
13372                forms,
13373                |h| (h == "defmonitor").then_some(()),
13374                |(), args| {
13375                    // The projection must run EXACTLY once — for the
13376                    // single matched form. Any non-matching form that
13377                    // reaches the projection panics here.
13378                    assert_eq!(args.len(), 2, "projection ran on non-matching form");
13379                    Ok(args.len())
13380                },
13381            )
13382            .unwrap();
13383        assert_eq!(lengths, vec![2]);
13384    }
13385
13386    #[test]
13387    fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13388        // The Result projection's short-circuit contract — when the
13389        // projection returns `Err` on a matched form, the walk
13390        // short-circuits and subsequent matched forms are NOT projected.
13391        // Pin the source-order short-circuit via a counter the
13392        // projection increments before deciding to fail: the counter
13393        // sits at exactly the index of the failing form (the second
13394        // match), proving the third match never reached the projection.
13395        let src = "(foo 1) (foo 2) (foo 3)";
13396        let forms = read(src).unwrap();
13397        let mut count = 0usize;
13398        let mut e = Expander::new();
13399        let err = e
13400            .expand_and_collect_calls_to_any::<i64, _, _, ()>(
13401                forms,
13402                |h| (h == "foo").then_some(()),
13403                |(), args| {
13404                    count += 1;
13405                    let v = args[0].as_int().unwrap();
13406                    if v == 2 {
13407                        return Err(crate::error::LispError::Missing("test-failure"));
13408                    }
13409                    Ok(v)
13410                },
13411            )
13412            .expect_err("projection must short-circuit on first Err");
13413        assert_eq!(
13414            count, 2,
13415            "projection must have run on first AND failing form, then stopped",
13416        );
13417        // Sanity: the error is the project-stage rejection — the same
13418        // typed `LispError` the projection returned, propagated verbatim.
13419        assert!(
13420            matches!(err, crate::error::LispError::Missing("test-failure")),
13421            "expected the projection's typed Err verbatim, got {err:?}",
13422        );
13423    }
13424
13425    #[test]
13426    fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
13427        // Ordering contract — `expand_program` runs BEFORE
13428        // `iter_calls_to_any` walks the slice. A `(defmacro …)` form
13429        // that emits a classifier-decoded body must have its expanded
13430        // body decoded by the classifier (not the unexpanded macro-call
13431        // head). Pin the ordering with a macro `(emit-foo n)` that
13432        // expands to `(foo :idx n)` — the classifier sees the expanded
13433        // `foo` head, not `emit-foo`. Mirrors the parallel keyword test
13434        // `expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword`
13435        // — the SAME composition contract holds on the typed-decoded
13436        // sibling.
13437        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13438                   (foo :idx 1)
13439                   (emit-foo 2)
13440                   (bar :idx 99)
13441                   (foo :idx 3)";
13442        let forms = read(src).unwrap();
13443        let mut e = Expander::new();
13444        // Classifier returns `()` for "foo" only; the keyword sibling's
13445        // routing identity is the same composition we exercise here
13446        // directly.
13447        let idxs: Vec<i64> = e
13448            .expand_and_collect_calls_to_any(
13449                forms,
13450                |h| (h == "foo").then_some(()),
13451                |(), args| Ok(args[1].as_int().unwrap()),
13452            )
13453            .unwrap();
13454        // 1 from the literal call, 2 from the macro-emitted call, 3 from
13455        // the final literal call. The macro-emitted `(foo :idx 2)` is
13456        // present BECAUSE `expand_program` ran first.
13457        assert_eq!(idxs, vec![1, 2, 3]);
13458    }
13459
13460    #[test]
13461    fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
13462        // The `FnMut` constraint — the slice walk calls the classifier
13463        // once per call form, and a decoder that captures mutable state
13464        // (a counter, a registry cache, a visited-set) maintains that
13465        // state across the walk. Pin the `FnMut` contract with a counter
13466        // closure that increments per shape-gate-passing form (calls
13467        // whose head is a symbol), asserting the counter equals the
13468        // total call count regardless of whether the classifier accepts
13469        // each form. Mirrors the parallel slice-side test
13470        // `iter_calls_to_any_admits_fnmut_classifier_maintaining_state_across_batch_walk`
13471        // — the SAME `FnMut` contract holds on the expander surface.
13472        let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
13473        let forms = read(src).unwrap();
13474        let mut e = Expander::new();
13475        let mut classifier_calls = 0usize;
13476        let projected: Vec<i64> = e
13477            .expand_and_collect_calls_to_any(
13478                forms,
13479                |h| {
13480                    classifier_calls += 1;
13481                    (h == "foo").then_some(())
13482                },
13483                |(), args| Ok(args[0].as_int().unwrap()),
13484            )
13485            .unwrap();
13486        assert_eq!(
13487            classifier_calls, 5,
13488            "classifier must run once per call form (5 forms, all calls with symbol heads)",
13489        );
13490        assert_eq!(
13491            projected,
13492            vec![1, 3, 5],
13493            "projection must run only for classifier-accepted forms in source order",
13494        );
13495    }
13496
13497    #[test]
13498    fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
13499    ) {
13500        // Structural identity: the keyword sibling
13501        // `expand_and_collect_calls_to` ROUTES through the typed-decoded
13502        // primitive via a constant-classifier composition — the
13503        // post-lift composition law binding the two siblings:
13504        //
13505        //   expand_and_collect_calls_to(forms, k, p) ==
13506        //       expand_and_collect_calls_to_any(forms,
13507        //           |h| (h == k).then_some(()),
13508        //           |(), args| p(args))
13509        //
13510        // Pin shape AND ordering across THREE representative keyword
13511        // values: one that matches multiple forms, one that matches
13512        // none, one that matches exactly one form. Same mixed input
13513        // (literal + macroexpand-emitted + non-matching) the keyword
13514        // path-uniformity test exercises, so the routing identity holds
13515        // on the SAME input shape the keyword sibling's contract pins.
13516        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13517                   (foo :idx 1)
13518                   (emit-foo 2)
13519                   (bar :idx 99)
13520                   (foo :idx 3)";
13521        let forms = read(src).unwrap();
13522
13523        for keyword in ["foo", "bar", "absent"] {
13524            let mut exp_keyword = Expander::new();
13525            let via_keyword: Vec<i64> = exp_keyword
13526                .expand_and_collect_calls_to(forms.clone(), keyword, |args| {
13527                    Ok(args[1].as_int().unwrap())
13528                })
13529                .unwrap();
13530            let mut exp_classifier = Expander::new();
13531            let via_classifier: Vec<i64> = exp_classifier
13532                .expand_and_collect_calls_to_any(
13533                    forms.clone(),
13534                    |h| (h == keyword).then_some(()),
13535                    |(), args| Ok(args[1].as_int().unwrap()),
13536                )
13537                .unwrap();
13538            assert_eq!(
13539                via_keyword, via_classifier,
13540                "routing identity drifted for keyword {keyword:?}",
13541            );
13542        }
13543    }
13544
13545    #[test]
13546    fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
13547    {
13548        // The expand_program-stage short-circuit contract — when
13549        // `expand_program` rejects (e.g. a `(defmacro …)` form whose
13550        // param list is malformed), the walk short-circuits BEFORE the
13551        // classifier or projection runs. Pin the ordering with a
13552        // deliberately-panicking classifier AND projection: any
13553        // post-expand-program execution fires the panic. Mirrors the
13554        // parallel keyword test
13555        // `expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs`
13556        // — the SAME composition contract holds on the typed-decoded
13557        // sibling.
13558        // `(defmacro 5 (x) `,x)` — macro NAME slot is an int, not a
13559        // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13560        // fires during `expand_program`. Same rejection shape the
13561        // parallel keyword test uses, so the ordering contract observed
13562        // here is structurally identical to the keyword sibling's.
13563        let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
13564        let mut e = Expander::new();
13565        let err = e
13566            .expand_and_collect_calls_to_any::<(), _, _, ()>(
13567                forms,
13568                |_h| -> Option<()> {
13569                    panic!("classifier must not run when expand_program errors");
13570                },
13571                |(), _args| {
13572                    panic!("project must not run when expand_program errors");
13573                },
13574            )
13575            .expect_err("expand_program error must short-circuit before classifier or project");
13576        // Sanity: the error IS an expand_program-stage rejection — not a
13577        // classifier or projection error — so the ordering is observable.
13578        let rendered = format!("{err}");
13579        assert!(
13580            rendered.contains("NAME") || rendered.contains("symbol"),
13581            "expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
13582        );
13583    }
13584
13585    // ── Expander::expand_source_and_collect_calls_to: from-source walk ──
13586    //
13587    // The from-source sibling of `expand_and_collect_calls_to`: composes
13588    // `crate::reader::read(src)?` with the from-forms primitive in ONE
13589    // method on the `Expander` surface. All four typed-program dispatchers
13590    // (free-function `compile_typed` / `compile_named`, preloaded-expander
13591    // `RealizedCompiler::compile_typed` / `compile_named`) route through
13592    // it; the tests below pin its contract directly. The existing
13593    // compile.rs + compiler_spec.rs dispatch tests are the path-uniformity
13594    // guards proving the four production sites route through it without
13595    // behavior drift.
13596
13597    #[test]
13598    fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
13599        // Happy path: a source string with three top-level forms (two
13600        // matching `defmonitor`, one matching `defalert`) flows through
13601        // `read` then through the from-forms primitive. Pin the SAME
13602        // emission shape the from-forms test pins for the matching
13603        // sub-set, sourced from a `&str` rather than a `Vec<Sexp>`.
13604        let src = r#"(defmonitor :name "first")
13605                     (defalert :name "not-a-match")
13606                     (defmonitor :solo)"#;
13607        let mut e = Expander::new();
13608        let lengths: Vec<usize> = e
13609            .expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
13610            .expect("matching forms must compose");
13611        assert_eq!(lengths, vec![2, 1]);
13612    }
13613
13614    #[test]
13615    fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
13616        // The reader runs BEFORE `expand_program` — so a reader error (an
13617        // unterminated string, an unbalanced paren, an unknown escape)
13618        // must short-circuit the entire composition and `expand_program`
13619        // / the keyword filter / `project` must NEVER run. Pin the
13620        // ordering: an unbalanced open-paren is rejected by the reader
13621        // at parse time, never reaches the from-forms primitive.
13622        let mut count = 0usize;
13623        let mut e = Expander::new();
13624        let err = e
13625            .expand_source_and_collect_calls_to::<(), _>(
13626                "(defmonitor :name \"unbalanced",
13627                "defmonitor",
13628                |_args| {
13629                    count += 1;
13630                    Ok(())
13631                },
13632            )
13633            .expect_err("reader error must short-circuit before expand_program");
13634        assert_eq!(
13635            count, 0,
13636            "project must never run when reader errors at parse time"
13637        );
13638        // Sanity: the error IS a reader-stage rejection — the rendered
13639        // diagnostic mentions the lexer's gate, not the expander's or
13640        // projector's (path-uniformity for the read-then-walk ordering).
13641        let rendered = format!("{err}");
13642        assert!(
13643            rendered.to_lowercase().contains("string")
13644                || rendered.to_lowercase().contains("paren")
13645                || rendered.to_lowercase().contains("eof")
13646                || rendered.to_lowercase().contains("unexpected")
13647                || rendered.to_lowercase().contains("unterminated")
13648                || rendered.to_lowercase().contains("unclosed"),
13649            "error must be the reader-stage rejection, got: {rendered}"
13650        );
13651    }
13652
13653    #[test]
13654    fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
13655    ) {
13656        // Reader succeeds, but `expand_program` rejects a malformed
13657        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13658        // ordering: `expand_program` rejects BEFORE the keyword filter
13659        // walks anything, `project` must NEVER run. Sibling of the
13660        // from-forms test of the same name — one level up the
13661        // composition (sourced from `&str` rather than `Vec<Sexp>`).
13662        let mut count = 0usize;
13663        let mut e = Expander::new();
13664        let err = e
13665            .expand_source_and_collect_calls_to::<(), _>(
13666                "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13667                "defmonitor",
13668                |_args| {
13669                    count += 1;
13670                    Ok(())
13671                },
13672            )
13673            .expect_err("expand_program error must short-circuit before project");
13674        assert_eq!(
13675            count, 0,
13676            "project must never run when expand_program errors"
13677        );
13678        let rendered = format!("{err}");
13679        assert!(
13680            rendered.contains("NAME") || rendered.contains("symbol"),
13681            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13682        );
13683    }
13684
13685    #[test]
13686    fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13687        // Reader + `expand_program` both succeed; the per-form `project`
13688        // errors on the SECOND matched form. Pin: the collect's short-
13689        // circuit fires at the second match's error, the third match's
13690        // `project` is NEVER invoked, and the returned error is exactly
13691        // the one the closure raised. Mirrors `compile_named`'s
13692        // short-circuit on the first `NamedFormMissingName` /
13693        // `NamedFormNonSymbolName` / typed-entry rejection — sourced
13694        // from `&str`.
13695        let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
13696        let mut seen = Vec::new();
13697        let mut e = Expander::new();
13698        let err = e
13699            .expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
13700                let n = args[1].as_int().expect("test args are ints");
13701                seen.push(n);
13702                if n == 2 {
13703                    Err(LispError::Compile {
13704                        form: "test".to_string(),
13705                        message: "stop at two".to_string(),
13706                    })
13707                } else {
13708                    Ok(())
13709                }
13710            })
13711            .expect_err("project's error must short-circuit collect");
13712        assert_eq!(seen, vec![1, 2]);
13713        assert!(
13714            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13715            "short-circuit must propagate the project's error verbatim, got {err:?}"
13716        );
13717    }
13718
13719    #[test]
13720    fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
13721        // Boundary: empty source string yields zero items regardless of
13722        // keyword. Pin the degenerate boundary — empty in, empty out —
13723        // and the closure is never invoked. Sibling of the from-forms
13724        // test of the same name, one level up the composition: the
13725        // from-source posture is fused-empty when `read` yields an
13726        // empty `Vec<Sexp>` (which, fed into `expand_program`, yields
13727        // an empty `Vec<Sexp>`, which iter_calls_to walks to zero
13728        // matches).
13729        let mut count = 0usize;
13730        let mut e = Expander::new();
13731        let out: Vec<()> = e
13732            .expand_source_and_collect_calls_to("", "anything", |_args| {
13733                count += 1;
13734                Ok(())
13735            })
13736            .expect("empty source is not an error");
13737        assert!(out.is_empty());
13738        assert_eq!(count, 0);
13739    }
13740
13741    #[test]
13742    fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13743        // Critical ordering preserved across the from-source posture:
13744        // `defmacro` source registers in `expand_program`, its call
13745        // sites expand to the matching keyword, AND the keyword walk
13746        // sees both the macro-emitted form and any directly-authored
13747        // matches in source order. Sibling of the from-forms test of
13748        // the same name, sourced from `&str`.
13749        let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13750                   (emit-monitor \"alpha\")
13751                   (defmonitor :name \"beta\")";
13752        let mut e = Expander::new();
13753        let names: Vec<String> = e
13754            .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13755                Ok(args[1].as_string().unwrap().to_string())
13756            })
13757            .expect("macroexpanded + directly-authored forms must both flow");
13758        assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13759    }
13760
13761    #[test]
13762    fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
13763        // Structural identity: for any (src, keyword, project) triple
13764        // whose pieces succeed, the from-source method's result equals
13765        // the inlined pre-lift pipeline `read + expand_and_collect_
13766        // calls_to`. Pin shape AND ordering on a mixed source
13767        // (matching + non-matching + macroexpand-emitted matches) so a
13768        // regression that drifts the method's pipeline from the
13769        // inlined closed-form fails here. This is the lift's
13770        // load-bearing path-uniformity gate — the SAME assertion that
13771        // holds for all four production consumers (free-function
13772        // `compile_typed`/`compile_named` + `RealizedCompiler::compile_typed`/`compile_named`)
13773        // holds at the primitive's contract level.
13774        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13775                   (foo :idx 1)
13776                   (emit-foo 2)
13777                   (bar :idx 99)
13778                   (foo :idx 3)";
13779
13780        // Inlined pre-lift pipeline.
13781        let mut exp_inline = Expander::new();
13782        let inline_forms = read(src).unwrap();
13783        let via_inline: Vec<i64> = exp_inline
13784            .expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13785            .unwrap();
13786
13787        // From-source method pipeline.
13788        let mut exp_method = Expander::new();
13789        let via_method: Vec<i64> = exp_method
13790            .expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
13791            .unwrap();
13792
13793        assert_eq!(via_inline, via_method);
13794        assert_eq!(via_inline, vec![1, 2, 3]);
13795    }
13796
13797    #[test]
13798    fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13799        // The `keyword` argument flows straight into the from-forms
13800        // primitive's head-comparison gate, which inherits from
13801        // `iter_calls_to` — a regression that drifts the keyword (e.g.,
13802        // lowercases / trims it en route through the read step) fails
13803        // here. Pin by passing TWO different keywords against the SAME
13804        // source and asserting each picks up only its matching forms.
13805        let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
13806
13807        let mut e1 = Expander::new();
13808        let monitors: Vec<i64> = e1
13809            .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13810                Ok(args[1].as_int().unwrap())
13811            })
13812            .unwrap();
13813        assert_eq!(monitors, vec![1, 3]);
13814
13815        let mut e2 = Expander::new();
13816        let alerts: Vec<i64> = e2
13817            .expand_source_and_collect_calls_to(src, "defalert", |args| {
13818                Ok(args[1].as_int().unwrap())
13819            })
13820            .unwrap();
13821        assert_eq!(alerts, vec![2]);
13822
13823        let mut e3 = Expander::new();
13824        let none: Vec<i64> = e3
13825            .expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
13826                Ok(args[1].as_int().unwrap())
13827            })
13828            .unwrap();
13829        assert!(none.is_empty());
13830    }
13831
13832    #[test]
13833    fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13834        // Path-uniformity for the soft-projection posture inherited
13835        // from `iter_calls_to`: non-matching forms in the source are
13836        // skipped silently, and `project` is never invoked on them.
13837        // Sibling of the from-forms test of the same name, sourced
13838        // from `&str` with a mix of atoms-and-lists that the reader
13839        // emits at the top level.
13840        let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
13841        let mut count = 0usize;
13842        let mut e = Expander::new();
13843        let out: Vec<()> = e
13844            .expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
13845                count += 1;
13846                Ok(())
13847            })
13848            .expect("non-matching-only source must collect to empty Vec");
13849        assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13850        assert_eq!(count, 0, "project must never run for non-matching forms");
13851    }
13852
13853    // ── Expander::expand_source_and_collect_calls_to_any — from-source × classifier ─
13854    //
13855    // The from-source posture of the typed-decoded classifier sibling on
13856    // the `Expander` surface — closes the (from-forms, from-source) ×
13857    // (keyword, classifier) 2×2 of compose-on-iter projections at the
13858    // from-source classifier corner the prior runs left open. The
13859    // pre-existing `expand_source_and_collect_calls_to` (from-source ×
13860    // keyword) is the constant-classifier projection of this primitive:
13861    // its body composes this function with a `|h| (h == keyword).
13862    // then_some(())` decoder, so the from-source pipeline lives at ONE
13863    // site. These tests pin both the typed-decoded primitive's contract
13864    // directly AND the post-lift routing of
13865    // `expand_source_and_collect_calls_to` through it.
13866
13867    #[test]
13868    fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
13869    ) {
13870        // Happy path: a source string with four top-level forms (two
13871        // matching "foo", one matching "bar", one rejected-head
13872        // "defmonitor") flows through `read` → `expand_program` → the
13873        // typed-decoded classifier walk. The closed-set classifier
13874        // `Op::from_keyword` admits "foo" and "bar" but rejects
13875        // "defmonitor". Pin the typed-decoded yield shape (decoded
13876        // witness alongside args tail) AND source-order preservation
13877        // sourced from `&str` rather than a pre-parsed `Vec<Sexp>`.
13878        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13879        enum Op {
13880            Foo,
13881            Bar,
13882        }
13883        fn op_from_keyword(head: &str) -> Option<Op> {
13884            match head {
13885                "foo" => Some(Op::Foo),
13886                "bar" => Some(Op::Bar),
13887                _ => None,
13888            }
13889        }
13890        let src = r#"(foo :idx 1)
13891                     (defmonitor :idx 99)
13892                     (bar :idx 2)
13893                     (foo :idx 3)"#;
13894        let mut e = Expander::new();
13895        let yielded: Vec<(Op, i64)> = e
13896            .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
13897                Ok((op, args[1].as_int().expect("test args are ints")))
13898            })
13899            .expect("matching forms must compose through from-source classifier walk");
13900        assert_eq!(
13901            yielded,
13902            vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
13903            "yields must be in source order, decoded witness paired with per-form mapper output"
13904        );
13905    }
13906
13907    #[test]
13908    fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
13909    ) {
13910        // The reader runs BEFORE `expand_program` AND BEFORE the
13911        // classifier — so a reader error (an unterminated string)
13912        // short-circuits the entire composition and both the classifier
13913        // AND `project` must NEVER run. Pin the ordering with BOTH
13914        // decoder and projection explicitly panicking — any post-reader
13915        // execution fires the panic. Sibling of
13916        // `expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program`
13917        // one corner over in the 2×2 (the classifier version of the
13918        // same ordering pin).
13919        let mut e = Expander::new();
13920        let err = e
13921            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13922                "(defmonitor :name \"unbalanced",
13923                |_h: &str| -> Option<()> {
13924                    panic!("classifier must not run when reader errors at parse time")
13925                },
13926                |(), _args| -> Result<()> { panic!("project must not run when reader errors") },
13927            )
13928            .expect_err("reader error must short-circuit before classifier");
13929        // Sanity: the error is a reader-stage rejection.
13930        let rendered = format!("{err}");
13931        assert!(
13932            rendered.to_lowercase().contains("string")
13933                || rendered.to_lowercase().contains("paren")
13934                || rendered.to_lowercase().contains("eof")
13935                || rendered.to_lowercase().contains("unexpected")
13936                || rendered.to_lowercase().contains("unterminated")
13937                || rendered.to_lowercase().contains("unclosed"),
13938            "error must be the reader-stage rejection, got: {rendered}"
13939        );
13940    }
13941
13942    #[test]
13943    fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
13944    ) {
13945        // Reader succeeds, but `expand_program` rejects a malformed
13946        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13947        // ordering: `expand_program` rejects BEFORE the classifier walks
13948        // anything, both decoder AND `project` must NEVER run. Sibling
13949        // of the keyword-from-source variant — one corner over in the
13950        // 2×2 with both classifier and projection explicitly panicking.
13951        let mut e = Expander::new();
13952        let err = e
13953            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13954                "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13955                |_h: &str| -> Option<()> {
13956                    panic!("classifier must not run when expand_program errors")
13957                },
13958                |(), _args| -> Result<()> {
13959                    panic!("project must not run when expand_program errors")
13960                },
13961            )
13962            .expect_err("expand_program error must short-circuit before classifier");
13963        let rendered = format!("{err}");
13964        assert!(
13965            rendered.contains("NAME") || rendered.contains("symbol"),
13966            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13967        );
13968    }
13969
13970    #[test]
13971    fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13972        // Path-uniformity for the soft-projection posture inherited
13973        // from `iter_calls_to_any`: forms whose head the classifier
13974        // rejects are skipped silently, and `project` is never invoked
13975        // on them. Pin via an explicitly-panicking projection across a
13976        // mix of forms (atom, list-with-non-symbol-head, list with
13977        // unrecognized-by-classifier symbol head). A regression that
13978        // wrongly invokes `project` on a rejected form fires the panic.
13979        let src = r#"bare-atom
13980                     (5 :not-a-symbol-head)
13981                     (defmonitor :name "decoder-rejects-me")"#;
13982        let mut e = Expander::new();
13983        // The closed-set classifier admits NOTHING in this source.
13984        let out: Vec<()> = e
13985            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13986                src,
13987                |_h: &str| -> Option<()> { None },
13988                |(), _args| -> Result<()> {
13989                    panic!("project must not run for classifier-rejected forms")
13990                },
13991            )
13992            .expect("classifier-rejects-all source must collect to empty Vec");
13993        assert!(
13994            out.is_empty(),
13995            "no classifier-accepted forms — empty Vec, got {out:?}"
13996        );
13997    }
13998
13999    #[test]
14000    fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
14001        // Reader + `expand_program` + classifier all succeed; the
14002        // per-form `project` errors on the SECOND matched form. Pin: the
14003        // collect's short-circuit fires at the second match's error, the
14004        // third match's `project` is NEVER invoked, and the returned
14005        // error is exactly the one the closure raised. Sibling of the
14006        // keyword-from-source variant — one corner over in the 2×2.
14007        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14008        enum Op {
14009            Foo,
14010        }
14011        fn op_from_keyword(head: &str) -> Option<Op> {
14012            (head == "foo").then_some(Op::Foo)
14013        }
14014        let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
14015        let mut seen = Vec::new();
14016        let mut e = Expander::new();
14017        let err = e
14018            .expand_source_and_collect_calls_to_any::<(), _, _, _>(
14019                src,
14020                op_from_keyword,
14021                |_op: Op, args: &[Sexp]| -> Result<()> {
14022                    let n = args[1].as_int().expect("test args are ints");
14023                    seen.push(n);
14024                    if n == 2 {
14025                        Err(LispError::Compile {
14026                            form: "test".to_string(),
14027                            message: "stop at two".to_string(),
14028                        })
14029                    } else {
14030                        Ok(())
14031                    }
14032                },
14033            )
14034            .expect_err("project's error must short-circuit collect");
14035        assert_eq!(seen, vec![1, 2], "third match's project must never run");
14036        assert!(
14037            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
14038            "short-circuit must propagate the project's error verbatim, got {err:?}"
14039        );
14040    }
14041
14042    #[test]
14043    fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
14044    ) {
14045        // The post-lift composition law binding the keyword-from-source
14046        // sibling to the typed-decoded primitive:
14047        //   `expand_source_and_collect_calls_to(src, k, project) ==
14048        //    expand_source_and_collect_calls_to_any(src,
14049        //        |h| (h == k).then_some(()),
14050        //        |(), args| project(args))`
14051        // Pin shape AND ordering across THREE representative keywords
14052        // (matching some, matching exactly one, matching none) on the
14053        // SAME mixed source the keyword path-uniformity test exercises.
14054        // A regression that re-implements the keyword-from-source filter
14055        // as a parallel `read + expand_and_collect_calls_to` pipeline
14056        // (rather than routing through the typed-decoded primitive)
14057        // fails the value pin.
14058        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
14059                   (foo :idx 1)
14060                   (emit-foo 2)
14061                   (bar :idx 99)
14062                   (foo :idx 3)";
14063        for k in ["foo", "bar", "missing"] {
14064            let mut e_keyword = Expander::new();
14065            let via_keyword: Vec<i64> = e_keyword
14066                .expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
14067                .unwrap();
14068
14069            let mut e_classifier = Expander::new();
14070            let via_classifier: Vec<i64> = e_classifier
14071                .expand_source_and_collect_calls_to_any(
14072                    src,
14073                    |h: &str| (h == k).then_some(()),
14074                    |(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
14075                )
14076                .unwrap();
14077
14078            assert_eq!(
14079                via_keyword, via_classifier,
14080                "keyword from-source path must equal classifier from-source path for {k:?}"
14081            );
14082        }
14083    }
14084
14085    #[test]
14086    fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
14087        // Critical ordering preserved across the typed-decoded
14088        // from-source posture: `defmacro` source registers in
14089        // `expand_program`, its call sites expand to a classifier-
14090        // accepted form, AND the classifier walk sees both the
14091        // macro-emitted form and any directly-authored matches in
14092        // source order. Sibling of the keyword-from-source ordering
14093        // test, one corner over in the 2×2.
14094        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14095        enum Op {
14096            Foo,
14097        }
14098        fn op_from_keyword(head: &str) -> Option<Op> {
14099            (head == "foo").then_some(Op::Foo)
14100        }
14101        let src = "(defmacro emit-foo (n) `(foo :name ,n))
14102                   (emit-foo \"alpha\")
14103                   (foo :name \"beta\")";
14104        let mut e = Expander::new();
14105        let names: Vec<(Op, String)> = e
14106            .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
14107                Ok((op, args[1].as_string().unwrap().to_string()))
14108            })
14109            .expect("macroexpanded + directly-authored forms must both flow through classifier");
14110        assert_eq!(
14111            names,
14112            vec![
14113                (Op::Foo, "alpha".to_string()),
14114                (Op::Foo, "beta".to_string()),
14115            ]
14116        );
14117    }
14118
14119    // ── expand_source_program — from-source × yield-all primitive ───
14120    //
14121    // Closes the 2×2 program-level walk family on the `Expander` surface:
14122    // from-forms × yield-all (`expand_program`), from-forms × keyword-
14123    // projected (`expand_and_collect_calls_to`), from-source × keyword-
14124    // projected (`expand_source_and_collect_calls_to`), AND now
14125    // from-source × yield-all (`expand_source_program`). Pins (a) the
14126    // happy path (mixed forms flow through reader then expand_program),
14127    // (b) reader-stage short-circuit (no expand_program side-effects),
14128    // (c) expand_program-stage short-circuit (defmacro-NAME-not-symbol
14129    // is rejected at its offending form), (d) defmacro absorption (the
14130    // primitive's side-effect on `self.macros` matches the from-forms
14131    // posture's side-effect verbatim), (e) the empty-source boundary,
14132    // and (f) structural identity vs. the inlined pre-lift `read +
14133    // expand_program` pipeline (path-uniformity gate).
14134
14135    #[test]
14136    fn expand_source_program_routes_through_reader_then_expand_program() {
14137        // Happy path: a source string with mixed forms (a `defmacro`
14138        // definition, a `defmonitor` form, a plain symbol literal) flows
14139        // through `read` then through the from-forms primitive. Pin the
14140        // SAME emission shape the from-forms test pins, sourced from a
14141        // `&str` rather than a `Vec<Sexp>`.
14142        let src = r#"(defmacro id (x) `,x)
14143                     (defmonitor :name "alpha")
14144                     bare-symbol"#;
14145        let mut e = Expander::new();
14146        let out = e
14147            .expand_source_program(src)
14148            .expect("mixed forms must compose");
14149        // `(defmacro id …)` is consumed as a side-effect (registers `id`
14150        // in `e.macros`), so the returned `Vec<Sexp>` contains the
14151        // remaining two top-level forms in source order.
14152        assert_eq!(out.len(), 2);
14153        assert_eq!(
14154            out[0].as_call_to("defmonitor").map(<[_]>::len),
14155            Some(2),
14156            "first surviving form is the defmonitor with two args, got: {:?}",
14157            out[0]
14158        );
14159        assert_eq!(
14160            out[1].as_symbol(),
14161            Some("bare-symbol"),
14162            "second surviving form is the bare symbol literal, got: {:?}",
14163            out[1]
14164        );
14165        // The defmacro side-effect landed in the expander's macro table.
14166        assert!(
14167            e.has("id"),
14168            "defmacro must register `id` in the expander's macro table"
14169        );
14170    }
14171
14172    #[test]
14173    fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
14174        // The reader runs BEFORE `expand_program` — so a reader error (an
14175        // unterminated string, an unbalanced paren) must short-circuit
14176        // the entire composition and `expand_program` must NEVER run.
14177        // Pin the ordering: a `defmacro` BEFORE the unterminated string
14178        // is NOT registered, because the reader fails at parse time
14179        // before `expand_program` sees any form.
14180        let mut e = Expander::new();
14181        let err = e
14182            .expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
14183            .expect_err("reader error must short-circuit before expand_program");
14184        // Sanity: the error IS a reader-stage rejection.
14185        let rendered = format!("{err}").to_lowercase();
14186        assert!(
14187            rendered.contains("string")
14188                || rendered.contains("paren")
14189                || rendered.contains("eof")
14190                || rendered.contains("unexpected")
14191                || rendered.contains("unterminated")
14192                || rendered.contains("unclosed"),
14193            "error must be the reader-stage rejection, got: {err}"
14194        );
14195        // Path-uniformity gate: the `defmacro` lexically BEFORE the
14196        // unterminated string must NOT have been absorbed — `read`'s
14197        // failure short-circuits the entire pipeline before
14198        // `expand_program` walks any forms.
14199        assert!(
14200            !e.has("should-not-register"),
14201            "reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
14202        );
14203    }
14204
14205    #[test]
14206    fn expand_source_program_short_circuits_on_expand_program_error() {
14207        // Reader succeeds, but `expand_program` rejects a malformed
14208        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14209        // ordering: the expand_program-stage rejection bubbles up
14210        // verbatim, sourced from `&str` rather than `Vec<Sexp>`.
14211        let mut e = Expander::new();
14212        let err = e
14213            .expand_source_program("(defmacro 5 (x) `,x)")
14214            .expect_err("expand_program error must propagate");
14215        let rendered = format!("{err}");
14216        assert!(
14217            rendered.contains("NAME") || rendered.contains("symbol"),
14218            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14219        );
14220    }
14221
14222    #[test]
14223    fn expand_source_program_yields_empty_vec_for_empty_source() {
14224        // Boundary: empty source string yields zero items. Pin the
14225        // degenerate boundary — empty in, empty out — sibling of the
14226        // from-source × keyword-projected test of the same name, one
14227        // level down the composition (no keyword filter).
14228        let mut e = Expander::new();
14229        let out = e
14230            .expand_source_program("")
14231            .expect("empty source is not an error");
14232        assert!(
14233            out.is_empty(),
14234            "empty source must yield empty Vec, got: {out:?}"
14235        );
14236    }
14237
14238    #[test]
14239    fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
14240        // Critical pipeline order: a `defmacro` in the source registers
14241        // AND a subsequent call to it expands AND the expanded form
14242        // surfaces in the returned Vec. Sourced from `&str`, the
14243        // from-source posture preserves the side-effect-then-expansion
14244        // semantics of the from-forms primitive verbatim.
14245        let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
14246                     (emit-monitor "alpha")
14247                     (emit-monitor "beta")"#;
14248        let mut e = Expander::new();
14249        let out = e
14250            .expand_source_program(src)
14251            .expect("defmacro absorption then expansion must compose");
14252        // Two surviving forms (the `defmacro` itself is consumed as a
14253        // side-effect), each expanded to a `(defmonitor :name "<n>")`.
14254        assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
14255        for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
14256            let args = out[i]
14257                .as_call_to("defmonitor")
14258                .unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
14259            assert_eq!(args[0].as_keyword(), Some("name"));
14260            assert_eq!(args[1].as_string(), Some(expected_name));
14261        }
14262        // The macro is now in the table — a subsequent call would
14263        // expand against this SAME expander.
14264        assert!(e.has("emit-monitor"));
14265    }
14266
14267    #[test]
14268    fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
14269        // Structural identity: for any `src` whose pieces succeed, the
14270        // from-source method's result equals the inlined pre-lift
14271        // pipeline `read + expand_program`. Pin shape AND ordering on a
14272        // mixed source (defmacro definition + macro call + plain form)
14273        // so a regression that drifts the method's pipeline from the
14274        // inlined closed-form fails here. This is the lift's
14275        // load-bearing path-uniformity gate — the SAME assertion holds
14276        // for both production consumers (`RealizedCompiler::compile` +
14277        // `realize_in_memory`'s `:macros` loop) at the primitive's
14278        // contract level.
14279        let src = r#"(defmacro id (x) `,x)
14280                     (id (foo 1 2))
14281                     (bar)"#;
14282
14283        // Inlined pre-lift pipeline.
14284        let mut e_inline = Expander::new();
14285        let inline_forms = read(src).unwrap();
14286        let via_inline = e_inline
14287            .expand_program(inline_forms)
14288            .expect("inlined pipeline must succeed");
14289
14290        // From-source method pipeline.
14291        let mut e_method = Expander::new();
14292        let via_method = e_method
14293            .expand_source_program(src)
14294            .expect("from-source method pipeline must succeed");
14295
14296        assert_eq!(
14297            via_inline, via_method,
14298            "from-source method must emit byte-identical result to inlined read+expand_program"
14299        );
14300        // Both pipelines registered the same defmacro side-effect.
14301        assert_eq!(e_inline.has("id"), e_method.has("id"));
14302        assert!(e_method.has("id"));
14303    }
14304
14305    #[test]
14306    fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
14307        // Per-`&mut self` absorption posture pin: a `defmacro` registered
14308        // in call 1 SURVIVES into call 2 on the SAME `Expander`. This is
14309        // the load-bearing semantic `realize_in_memory`'s per-spec-macro
14310        // build-up depends on — every iteration's `expand_source_program(
14311        // macro_src)?` lands its `defmacro` heads into the SAME mutable
14312        // `preloaded` expander, so a follow-up `:macros` source can
14313        // invoke macros defined in earlier ones.
14314        let mut e = Expander::new();
14315        let _ = e
14316            .expand_source_program("(defmacro outer (n) `(inner ,n))")
14317            .unwrap();
14318        assert!(e.has("outer"));
14319        // Call 2: defines `inner` AND invokes `outer`, which expands to
14320        // `(inner <n>)`. The expansion proves call 1's `outer` survived
14321        // into call 2's expansion.
14322        let out = e
14323            .expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
14324            .unwrap();
14325        assert_eq!(out.len(), 1, "call 2 yields one expanded form");
14326        // (outer 42) → (inner 42) → (wrapped 42)
14327        let args = out[0]
14328            .as_call_to("wrapped")
14329            .expect("nested expansion must reach `wrapped`");
14330        assert_eq!(args[0].as_int(), Some(42));
14331    }
14332
14333    // ── Expander::register_macro_def: the macro-registration primitive ──
14334    //
14335    // `register_macro_def(&mut self, def: MacroDef) -> Result<()>` lifts
14336    // the byte-identical two-step block —
14337    //
14338    //   if self.compile_templates {
14339    //       self.templates.insert(def.name.clone(), compile_template(&def)?);
14340    //   }
14341    //   self.macros.insert(def.name.clone(), def);
14342    //
14343    // — that lived inline at `with_macros` (the bulk-from-iterator
14344    // constructor) AND `expand_program`'s `(defmacro …)`-head arm (the
14345    // program-walk-time registration site) into ONE named method on the
14346    // `Expander` surface. The tests below pin:
14347    //
14348    //   (a) the bytecode-default expander (`Expander::new()`) populates
14349    //       BOTH `macros` AND `templates` keyed by `def.name`,
14350    //   (b) the substitute-only expander (`Expander::new_substitute_only()`)
14351    //       populates `macros` but SKIPS `templates` — the
14352    //       `compile_templates: false` gate fires structurally,
14353    //   (c) a template that fails to compile (`,foo` against `params:
14354    //       []`) short-circuits BEFORE `self.macros.insert` runs, so the
14355    //       failed registration leaves both tables untouched — no
14356    //       partial-write window in which `self.macros.has(name)` is
14357    //       true but `self.templates.has(name)` is missing,
14358    //   (d) `with_macros([def])` and `register_macro_def(def)` on a fresh
14359    //       expander produce structurally identical state (both tables'
14360    //       sets of keys + the same `MacroDef` body under each key),
14361    //   (e) `expand_program` of one `(defmacro …)` form and
14362    //       `register_macro_def` of the parsed `MacroDef` produce
14363    //       structurally identical state — closing path-uniformity
14364    //       across the lift's two consumers AT the registration site.
14365    //
14366    // The tests bind on `Expander::has(name)` / `Expander::len()` for
14367    // the macros table and on bytecode-vs-substitute output equivalence
14368    // (a registered macro must expand to the SAME result regardless of
14369    // whether the bytecode path's `self.templates` entry exists) for the
14370    // templates table.
14371
14372    fn macro_def_id() -> MacroDef {
14373        // `(defmacro id (x) `,x)` — the simplest well-formed MacroDef:
14374        // one required param, a single-symbol unquote body. Compiles to
14375        // a valid `CompiledTemplate` (no unbound vars), so
14376        // `register_macro_def` succeeds on every Expander posture.
14377        MacroDef {
14378            name: "id".into(),
14379            params: MacroParams {
14380                required: vec!["x".into()],
14381                optional: vec![],
14382                rest: None,
14383            },
14384            // ` ` quasi-quoted `,x` — `Quasiquote(Unquote(Symbol("x")))`.
14385            body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14386        }
14387    }
14388
14389    fn macro_def_bad_template() -> MacroDef {
14390        // `(defmacro bad () `,unbound)` — a quasi-quote body with `,unbound`
14391        // against an EMPTY required-params list. `compile_template`
14392        // rejects it with `UnboundTemplateVar` because `unbound` is not
14393        // in `params.names()`. Used to exercise the
14394        // `compile_template`-failed-but-self.macros-still-pristine
14395        // path-uniformity pin.
14396        MacroDef {
14397            name: "bad".into(),
14398            params: MacroParams::default(),
14399            body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
14400        }
14401    }
14402
14403    #[test]
14404    fn register_macro_def_bytecode_default_populates_macros_and_templates() {
14405        // The bytecode-default `Expander::new()` carries
14406        // `compile_templates: true`; `register_macro_def` MUST populate
14407        // BOTH `self.macros` (the substitute path's registry) AND
14408        // `self.templates` (the bytecode path's pre-compiled bytecode
14409        // index) keyed by `def.name`. Fail-before-pass-after: this
14410        // assert requires the new method to exist AND to compose the
14411        // two side-effects in canonical order on a single-call
14412        // registration — pre-lift `register_macro_def` did not exist.
14413        let mut e = Expander::new();
14414        e.register_macro_def(macro_def_id())
14415            .expect("well-formed MacroDef must register");
14416        assert!(
14417            e.has("id"),
14418            "self.macros must carry the registered name after register_macro_def"
14419        );
14420        assert!(
14421            e.templates.contains_key("id"),
14422            "self.templates must carry the compiled bytecode under the bytecode-default posture"
14423        );
14424        // The registered macro must expand correctly through the
14425        // bytecode strategy — proves the inserted template is the right
14426        // bytecode for the body.
14427        let out = e
14428            .expand_program(read("(id 42)").unwrap())
14429            .expect("registered macro must expand");
14430        assert_eq!(out.len(), 1);
14431        assert_eq!(out[0], Sexp::int(42));
14432    }
14433
14434    #[test]
14435    fn register_macro_def_substitute_only_skips_templates() {
14436        // `Expander::new_substitute_only()` carries `compile_templates:
14437        // false`; `register_macro_def` MUST populate `self.macros` for
14438        // the substitute path but SKIP `self.templates` (no bytecode
14439        // pre-compile). Pin the `compile_templates: false` gate fires
14440        // structurally — a regression that drifts the conditional from
14441        // the registration primitive (e.g. a future emitter that
14442        // unconditionally pre-compiles) would silently double the
14443        // benchmark baseline's allocation footprint.
14444        let mut e = Expander::new_substitute_only();
14445        e.register_macro_def(macro_def_id())
14446            .expect("well-formed MacroDef must register under substitute-only");
14447        assert!(
14448            e.has("id"),
14449            "self.macros must carry the registered name even under substitute-only"
14450        );
14451        assert!(
14452            !e.templates.contains_key("id"),
14453            "self.templates MUST be empty under compile_templates: false — the gate fires"
14454        );
14455        // The registered macro must still expand correctly through the
14456        // substitute strategy — proves the inserted MacroDef is the
14457        // right body for substitute-walking.
14458        let out = e
14459            .expand_program(read("(id 42)").unwrap())
14460            .expect("registered macro must expand via substitute path");
14461        assert_eq!(out.len(), 1);
14462        assert_eq!(out[0], Sexp::int(42));
14463    }
14464
14465    #[test]
14466    fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
14467        // The structural ordering pin: when `compile_template(&def)?`
14468        // rejects (e.g. `,unbound` against empty params), the `?` MUST
14469        // short-circuit BEFORE `self.macros.insert(def.name.clone(),
14470        // def)` runs, so a failed registration leaves BOTH tables
14471        // exactly as they were. Without this ordering a future
14472        // bytecode-strategy lookup would resolve `self.macros.get(name)`
14473        // (the body was inserted) AND find `self.templates.get(name)`
14474        // returning `None` (the pre-compile failed), silently coercing
14475        // the macro onto the substitute fallback path despite
14476        // `compile_templates: true`. The lift preserves the pre-lift
14477        // ordering (`compile_template` precedes `self.macros.insert`)
14478        // structurally — the test pins it across the lift.
14479        let mut e = Expander::new();
14480        let err = e
14481            .register_macro_def(macro_def_bad_template())
14482            .expect_err("unbound-template body must reject");
14483        // The rejection is the structural variant the bytecode
14484        // strategy's template-gate emits.
14485        assert!(
14486            matches!(err, LispError::UnboundTemplateVar { .. }),
14487            "expected UnboundTemplateVar, got: {err:?}"
14488        );
14489        // Both tables MUST be pristine — no partial write.
14490        assert!(
14491            !e.has("bad"),
14492            "self.macros must be untouched after compile_template failure"
14493        );
14494        assert!(
14495            !e.templates.contains_key("bad"),
14496            "self.templates must be untouched after compile_template failure"
14497        );
14498    }
14499
14500    #[test]
14501    fn with_macros_routes_through_register_macro_def_path_uniformity() {
14502        // Path-uniformity pin across the bulk-constructor consumer:
14503        // `with_macros([def])` MUST produce the same final state as
14504        // `Expander::new()` + `register_macro_def(def)`. A regression
14505        // that drifts the constructor's per-MacroDef inline block from
14506        // the registration primitive (e.g. a future emitter that
14507        // re-inlines the two-step block at `with_macros` rather than
14508        // delegating) would fail loudly here.
14509        let mut via_register = Expander::new();
14510        via_register
14511            .register_macro_def(macro_def_id())
14512            .expect("register must succeed");
14513        let mut via_with_macros =
14514            Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
14515        assert_eq!(via_register.len(), via_with_macros.len());
14516        assert!(via_register.has("id"));
14517        assert!(via_with_macros.has("id"));
14518        // Both tables key on the same set across both postures.
14519        assert_eq!(
14520            via_register.templates.contains_key("id"),
14521            via_with_macros.templates.contains_key("id"),
14522            "self.templates key-presence must agree across with_macros and register_macro_def"
14523        );
14524        // Both registered expanders expand the registered macro
14525        // identically — the strongest behavioral parity.
14526        let out_a = via_register
14527            .expand_program(read("(id 99)").unwrap())
14528            .unwrap();
14529        let out_b = via_with_macros
14530            .expand_program(read("(id 99)").unwrap())
14531            .unwrap();
14532        assert_eq!(out_a, out_b);
14533        assert_eq!(out_a, vec![Sexp::int(99)]);
14534    }
14535
14536    #[test]
14537    fn expand_program_routes_through_register_macro_def_path_uniformity() {
14538        // Path-uniformity pin across the program-walk consumer:
14539        // `expand_program` of `(defmacro id (x) `,x)` MUST produce the
14540        // same final state as a direct
14541        // `register_macro_def(macro_def_id())`. A regression that
14542        // drifts `expand_program`'s `(defmacro …)`-head arm from the
14543        // registration primitive (e.g. a future emitter that re-inlines
14544        // the two-step block at `expand_program` rather than delegating)
14545        // would fail loudly here.
14546        let mut via_register = Expander::new();
14547        via_register
14548            .register_macro_def(macro_def_id())
14549            .expect("register must succeed");
14550        let mut via_expand_program = Expander::new();
14551        let yielded = via_expand_program
14552            .expand_program(read("(defmacro id (x) `,x)").unwrap())
14553            .expect("expand_program of one defmacro must succeed");
14554        // expand_program drops the (defmacro …) form from its returned
14555        // Vec<Sexp> (defmacro is a definition, not a program form), so
14556        // the yielded list is empty — pin the side-effect-only posture.
14557        assert!(
14558            yielded.is_empty(),
14559            "(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
14560        );
14561        assert!(via_register.has("id"));
14562        assert!(via_expand_program.has("id"));
14563        assert_eq!(via_register.len(), via_expand_program.len());
14564        // Both tables key on the same set across both postures.
14565        assert_eq!(
14566            via_register.templates.contains_key("id"),
14567            via_expand_program.templates.contains_key("id"),
14568            "self.templates key-presence must agree across expand_program and register_macro_def"
14569        );
14570        // Both registered expanders expand the registered macro
14571        // identically — the strongest behavioral parity, closing
14572        // path-uniformity across BOTH consumers (with_macros above,
14573        // expand_program here) at ONE primitive.
14574        let out_a = via_register
14575            .expand_program(read("(id 7)").unwrap())
14576            .unwrap();
14577        let out_b = via_expand_program
14578            .expand_program(read("(id 7)").unwrap())
14579            .unwrap();
14580        assert_eq!(out_a, out_b);
14581        assert_eq!(out_a, vec![Sexp::int(7)]);
14582    }
14583
14584    // ── as_unquote: path-uniformity across the three substitute / compile_node sites ──
14585    //
14586    // The new typed-marker projection `Sexp::as_unquote` lifts the
14587    // (Sexp::Unquote/UnquoteSplice variant, UnquoteForm::Unquote/Splice
14588    // literal) pair into ONE structural query. These tests pin behavioral
14589    // parity end-to-end across BOTH expansion strategies:
14590    //   * `compile_node` — the bytecode-template strategy's typed marker
14591    //     dispatch routes through as_unquote at the compile step.
14592    //   * `substitute` (top-level + list-inner) — the substitute-walker
14593    //     fallback strategy's typed marker dispatch routes through
14594    //     as_unquote at both per-form sites.
14595    // The structural invariant the prior runs' `expansion_layers_agree_on_
14596    // output_and_cache_wins` benchmark observes — bytecode AND substitute
14597    // produce byte-identical output — is anchored here at the macro-template
14598    // level for every distinct unquote-family shape the substitute and
14599    // bytecode strategies discriminate.
14600
14601    #[test]
14602    fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
14603        // A template body whose only marker is a top-level `,x`. Both
14604        // expansion strategies route through `as_unquote` (compile_node for
14605        // bytecode, substitute for the fallback walker), each pairing
14606        // Sexp::Unquote ↔ UnquoteForm::Unquote at ONE typed projection.
14607        // Pin byte-identical output across both strategies — the
14608        // structural-invariant the new projection's lift was designed to
14609        // make load-bearing structural rather than per-site discipline.
14610        let src = "(defmacro id (x) ,x) (id 42)";
14611        let mut bc = Expander::new();
14612        let mut sub = Expander::new_substitute_only();
14613        let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14614        let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14615        assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
14616        assert_eq!(out_bc, vec![Sexp::int(42)]);
14617    }
14618
14619    #[test]
14620    fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
14621        // A template body whose marker is a list-inner `,@xs`. The substitute
14622        // strategy's list-inner Splice arm routes through `as_unquote`
14623        // matching Some((UnquoteForm::Splice, inner)); the bytecode strategy's
14624        // compile_node Splice arm routes through `as_unquote` matching the
14625        // same. Pin byte-identical output across both strategies for the
14626        // splice path — the projection's typed-marker pairing
14627        // Sexp::UnquoteSplice ↔ UnquoteForm::Splice is structurally
14628        // identical at every consumer post-lift.
14629        let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
14630        let mut bc = Expander::new();
14631        let mut sub = Expander::new_substitute_only();
14632        let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14633        let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14634        assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
14635        let expected = Sexp::List(vec![
14636            Sexp::symbol("list"),
14637            Sexp::int(0),
14638            Sexp::int(1),
14639            Sexp::int(2),
14640            Sexp::int(3),
14641            Sexp::int(99),
14642        ]);
14643        assert_eq!(out_bc, vec![expected]);
14644    }
14645
14646    #[test]
14647    fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
14648        // The substitute strategy's top-level `,@x` arm rejects with
14649        // LispError::SpliceOutsideList (a splice marker with no containing
14650        // list to flatten into). Pre-lift the arm was
14651        // `Sexp::UnquoteSplice(inner) => Err(splice_outside_list(inner))`;
14652        // post-lift the arm routes through `as_unquote` matching
14653        // Some((UnquoteForm::Splice, inner)) and dispatches on the typed
14654        // marker. Pin path-uniformity: the rejection MUST fire identically
14655        // through the new projection. The substitute-only strategy bypasses
14656        // the bytecode path, exposing this gate directly.
14657        //
14658        // `(defmacro bad (xs) ,@xs) (bad (1 2 3))` — the body is a bare
14659        // `,@xs` at top level (NOT wrapped in a containing list), so the
14660        // substitute fallback's top-level Splice arm fires.
14661        let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
14662        let mut sub = Expander::new_substitute_only();
14663        let err = sub.expand_program(read(src).unwrap()).unwrap_err();
14664        assert!(
14665            matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14666            "expected SpliceOutsideList through as_unquote, got: {err:?}"
14667        );
14668    }
14669
14670    #[test]
14671    fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
14672        // A `,unbound` template body — the inner symbol isn't a param —
14673        // fires gate-2 (must-be-bound-in-scope). The typed marker
14674        // `UnquoteForm::Unquote` MUST thread through `as_unquote` →
14675        // `resolve_unquote_in_params(inner, params, form)` → gate-2's
14676        // rejection-builder. Pre-lift the marker was bound at the per-arm
14677        // literal `UnquoteForm::Unquote`; post-lift it's bound at the
14678        // typed projection. Pin: the rejection's `prefix` slot is
14679        // UnquoteForm::Unquote, structurally derived from the typed
14680        // projection's typed marker, NOT a literal at the arm body.
14681        let src = "(defmacro bad (x) ,unbound)";
14682        let mut bc = Expander::new();
14683        let err = bc.expand_program(read(src).unwrap()).unwrap_err();
14684        match err {
14685            crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14686                assert_eq!(
14687                    prefix,
14688                    UnquoteForm::Unquote,
14689                    "typed marker drifted from UnquoteForm::Unquote at gate-2"
14690                );
14691            }
14692            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14693        }
14694        // Sibling negative control: `,@unbound` inside a containing list
14695        // threads UnquoteForm::Splice through the same projection. The
14696        // splice MUST be inside a list — `compile_template` rejects
14697        // top-level `,@X` bodies with SpliceOutsideList before compile_node
14698        // runs, so the typed-marker dispatch on Splice is observable only
14699        // for well-positioned splice bodies. Closes path-uniformity across
14700        // BOTH typed marker variants at the compile path.
14701        let src_splice = "(defmacro bad (x) (foo ,@unbound))";
14702        let mut bc2 = Expander::new();
14703        let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
14704        match err_splice {
14705            crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14706                assert_eq!(
14707                    prefix,
14708                    UnquoteForm::Splice,
14709                    "typed marker drifted from UnquoteForm::Splice at gate-2"
14710                );
14711            }
14712            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14713        }
14714    }
14715
14716    // ── compile_template + contains_unquote path-uniformity through as_unquote ──
14717    //
14718    // The prior `as_unquote` projection lift (eb00684) routed `compile_node`'s
14719    // two arms and `substitute`'s top-level + list-inner arms through the
14720    // typed-marker projection but LEFT `compile_template`'s top-level
14721    // splice-outside-list gate and `contains_unquote`'s family check inline
14722    // matching `Sexp::UnquoteSplice(inner)` / `Sexp::Unquote(_) |
14723    // Sexp::UnquoteSplice(_)`. After this lift both production sites route
14724    // through `as_unquote`: `compile_template` matches `Some((UnquoteForm::
14725    // Splice, inner))` (same shape as `substitute`'s list-inner Splice arm),
14726    // `contains_unquote` uses `as_unquote().is_some()` for the family check.
14727    // Every production-site recognizer of an unquote-family wrapper now
14728    // shares ONE typed-marker projection — the (Sexp variant, UnquoteForm
14729    // variant) pairing for `,@-outside-list` is bound at ONE structural
14730    // query across all FOUR reachable splice-recognition sites, and a future
14731    // regression that drifts the marker pairing at any production site
14732    // becomes a type error at the helper boundary rather than a silent
14733    // per-site divergence.
14734
14735    #[test]
14736    fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
14737        // The bytecode path's `compile_template` gate pre-rejects top-level
14738        // `,@X` bodies via `as_unquote()` matching `Some((UnquoteForm::Splice,
14739        // inner))`. Pre-lift the arm was `if let Sexp::UnquoteSplice(inner) =
14740        // body` — the LAST production-site inline `Sexp::UnquoteSplice(_)`
14741        // match outside the projection itself. Post-lift the (Sexp variant,
14742        // UnquoteForm variant) pairing for the splice-outside-list gate is
14743        // bound at ONE projection function across all three reachable
14744        // emission sites (compile_template top-level, substitute top-level,
14745        // substitute list-inner). Path-uniformity guard at the new boundary:
14746        // the bytecode-path compile-time gate now routes through the same
14747        // shape `substitute`'s list-inner Splice arm uses.
14748        //
14749        // `(defmacro bad (xs) \`,@xs)` — the body's outer quasi-quote is
14750        // peeled by `template_body`, leaving `,@xs` at top level for
14751        // `compile_template` to gate before `compile_node` walks anything.
14752        // The bytecode strategy is default-on (`Expander::new()` sets
14753        // `compile_templates: true`), so the gate fires at
14754        // `register_macro_def` time.
14755        let mut e = Expander::new();
14756        let err = e
14757            .expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
14758            .expect_err("compile_template must reject top-level ,@X via as_unquote");
14759        assert!(
14760            matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14761            "expected SpliceOutsideList through as_unquote, got: {err:?}"
14762        );
14763    }
14764
14765    #[test]
14766    fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
14767        // Negative control on the typed-marker dispatch at `compile_template`'s
14768        // top-level gate. A top-level `,X` body (Unquote, NOT Splice) is a
14769        // valid template — `compile_node` lowers it to a single `Subst(idx)`
14770        // op. The new `Some((UnquoteForm::Splice, inner))` pattern at the
14771        // gate MUST NOT fire on the Unquote variant — only Splice — so the
14772        // typed marker is observably load-bearing: a regression that drifts
14773        // the pattern from `UnquoteForm::Splice` to the wider
14774        // `Some((_, inner))` would mis-reject `(defmacro id (x) ,x)` here.
14775        //
14776        // `(defmacro id (x) ,x) (id 42)` — `id`'s body is bare `,x` at top
14777        // level; `compile_template` admits it via the typed-marker gate,
14778        // `compile_node` emits `Subst(0)`, and the call expands to `42`.
14779        let src = "(defmacro id (x) ,x) (id 42)";
14780        let mut e = Expander::new();
14781        let expanded = e
14782            .expand_program(read(src).unwrap())
14783            .expect("top-level ,X body must compile through as_unquote-typed gate");
14784        assert_eq!(expanded, vec![Sexp::int(42)]);
14785    }
14786
14787    #[test]
14788    fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
14789        // The fast-path optimizer in `compile_node` short-circuits on
14790        // `contains_unquote(node)` — true iff the subtree carries an
14791        // unquote-family wrapper. Pre-lift the family check inlined
14792        // `Sexp::Unquote(_) | Sexp::UnquoteSplice(_) => true`; post-lift it
14793        // routes through `as_unquote().is_some()`. Pin path-uniformity at
14794        // the family-recognition boundary: every production-site unquote
14795        // recognizer (compile_template's gate, compile_node's per-arm
14796        // dispatch, substitute's top-level + list-inner arms, AND this
14797        // fast-path predicate) now shares ONE typed-marker projection.
14798        //
14799        // Both variants must trigger the fast-path bail — the optimizer's
14800        // gate keys on the family, not the inner. The recursion into
14801        // Quote/Quasiquote/List subtrees ALSO observes the same family
14802        // gate at every level, so a `,@xs` buried under a `\`...` outer
14803        // wrapper still fires it.
14804        let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14805        let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
14806        let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
14807            Sexp::symbol("foo"),
14808            Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
14809        ])));
14810        // The projection's `is_some()` face MUST agree with the pre-lift
14811        // `matches!()` discriminant on every variant — closed-set guarantee
14812        // shared with `as_unquote`'s contract pin in `ast.rs`.
14813        assert!(super::contains_unquote(&bare_unquote));
14814        assert!(super::contains_unquote(&bare_splice));
14815        assert!(super::contains_unquote(&nested));
14816        // Negative control: shapes the projection rejects (Nil, Atom,
14817        // bare List, Quote-family without inner unquote) must NOT trigger
14818        // the fast-path bail — the projection's None face stays observably
14819        // distinct from its Some face.
14820        assert!(!super::contains_unquote(&Sexp::Nil));
14821        assert!(!super::contains_unquote(&Sexp::symbol("plain")));
14822        assert!(!super::contains_unquote(&Sexp::int(5)));
14823        assert!(!super::contains_unquote(&Sexp::List(vec![
14824            Sexp::symbol("plain"),
14825            Sexp::int(1),
14826        ])));
14827        assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
14828            Sexp::symbol("inert")
14829        ))));
14830    }
14831
14832    #[test]
14833    fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
14834        // Pin the lift: `contains_unquote`'s quote-family recognition
14835        // now routes through `Sexp::as_quote_form` (the 4-of-4 wrapper
14836        // projection) AND `QuoteForm::as_unquote_form` (the 2-of-4
14837        // substitution-subset gate) at ONE site — not through the pair
14838        // of `as_unquote().is_some()` + inline
14839        // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm. The
14840        // path-uniformity assertion: for every quote-family wrapper, the
14841        // function's behavior agrees with the manual composition through
14842        // the two typed-marker projections. A regression that re-inlines
14843        // either arm (e.g. drops the `as_unquote_form().is_some()` gate
14844        // and just returns true for any `as_quote_form().is_some()`, or
14845        // restores the per-variant `Quote | Quasiquote` arm) drifts from
14846        // this composition and surfaces here.
14847        use crate::ast::QuoteForm;
14848
14849        // Sweep every QuoteForm variant under both axes:
14850        //   * unquote-only-inner (inner = symbol):
14851        //       Unquote(s)        → true via `as_unquote_form() == Some`
14852        //       UnquoteSplice(s)  → true via `as_unquote_form() == Some`
14853        //       Quote(s)          → false (inner has no unquote, recurses to false)
14854        //       Quasiquote(s)     → false (same)
14855        //   * unquote-inner-inside-quote-wrapper:
14856        //       Quote(Unquote(s)) → true (Quote arm recurses via `as_quote_form`,
14857        //                                 inner `Unquote` returns true through the
14858        //                                 SAME projection — both arms share the
14859        //                                 ONE typed-marker site post-lift)
14860        let inner_plain = Sexp::symbol("x");
14861        let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14862
14863        for qf in QuoteForm::ALL {
14864            let wrapped_plain = match qf {
14865                QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
14866                QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
14867                QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
14868                QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
14869            };
14870            // Path-uniformity: behavior derives from the manual two-marker
14871            // composition through `as_quote_form` + `as_unquote_form`.
14872            // The substitution-subset gate (`as_unquote_form().is_some()`)
14873            // returns true for Unquote/UnquoteSplice and false for
14874            // Quote/Quasiquote — directly encoding the 2-of-4 partition.
14875            let via_manual =
14876                qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
14877            assert_eq!(
14878                super::contains_unquote(&wrapped_plain),
14879                via_manual,
14880                "contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
14881            );
14882
14883            let wrapped_unquote = match qf {
14884                QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
14885                QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
14886                QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
14887                QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
14888            };
14889            // EVERY quote-family wrapper around an inner `Unquote` must
14890            // contain unquote — either the outer projects as `Some` on
14891            // the substitution subset (Unquote/UnquoteSplice arms), OR
14892            // the outer is Quote/Quasiquote and the recursion into the
14893            // inner re-fires the projection. The pre-lift Quote arm
14894            // recursed via the inline `Sexp::Quote(inner)` match;
14895            // post-lift the SAME recursion fires via `as_quote_form`'s
14896            // typed projection. Both yield `true`.
14897            assert!(
14898                super::contains_unquote(&wrapped_unquote),
14899                "contains_unquote missed an inner Unquote under {qf:?} — \
14900                 the quote-family recursion through as_quote_form drifted"
14901            );
14902        }
14903
14904        // Negative control: a non-quote-family wrapper (List, Atom,
14905        // Nil) must NOT route through `as_quote_form` at all. The
14906        // `if let Some(...) = node.as_quote_form()` gate stays `None`
14907        // and falls through to the List/_  arms. This is the structural
14908        // partition: the projection's `None` face MUST agree with
14909        // `!matches!(node, Sexp::Quote(_) | Sexp::Quasiquote(_) |
14910        // Sexp::Unquote(_) | Sexp::UnquoteSplice(_))`.
14911        let nested_in_list = Sexp::List(vec![
14912            Sexp::symbol("outer"),
14913            Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14914        ]);
14915        assert!(
14916            super::contains_unquote(&nested_in_list),
14917            "contains_unquote failed to descend into a List subtree containing a \
14918             Quasiquote(Unquote(_)) — list recursion arm drifted"
14919        );
14920    }
14921
14922    // ── expand_and_collect_named_calls_to_any (from-forms × named ────
14923    // × typed-decoded classifier) ─────────────────────────────────────
14924    //
14925    // The (named NAME-then-kwargs × typed-decoded classifier) cell of
14926    // the dispatcher matrix on the `Expander` surface — pre-lift the
14927    // matrix had the bare-kwargs row closed (expand_and_collect_calls_to
14928    // + expand_and_collect_calls_to_any) AND the constant-keyword
14929    // column closed on the named axis (expand_to_named +
14930    // expand_source_to_named) but the (named × classifier) cell was
14931    // open — every consumer that wanted to walk a program by typed-
14932    // decoded classifier AND extract the NAME slot per matched form
14933    // had to compose `expand_and_collect_calls_to_any` with
14934    // `split_name_slot` inline. Post-lift the cell is ONE named primitive
14935    // on the `Expander` surface, and the constant-`T::KEYWORD` named
14936    // sibling routes through it as the (constant-classifier × named)
14937    // specialization.
14938    //
14939    // The tests below pin: (a) the happy path yields the decoded triple
14940    // `(T, &str, &[Sexp])` for every matched form in source order; (b)
14941    // non-matching forms are skipped (soft-projection contract inherited
14942    // from the typed-decoded primitive); (c) the `NamedFormMissingName`
14943    // variant fires for matched forms with no NAME slot, threading the
14944    // classifier-supplied `&'static str` keyword; (d) the
14945    // `NamedFormNonSymbolName` variant fires for matched forms with a
14946    // non-symbol-or-string NAME slot, again threading the classifier-
14947    // supplied keyword; (e) the projection's `Err` short-circuits at
14948    // the first failing matched form; (f) `expand_program` runs BEFORE
14949    // the classifier filter walks — a `(defmacro …)` emitting a named
14950    // form is decoded by the classifier on the EXPANDED head, not the
14951    // macro-call head; (g) the from-source sibling agrees with `read +
14952    // from-forms` on the same source.
14953
14954    #[test]
14955    fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
14956    ) {
14957        // The typed-decoded named primitive's happy path: a closed-set
14958        // classifier decodes head symbols to a typed enum PAIRED with
14959        // its canonical static keyword, and the per-form projection
14960        // receives `(decoded, name, spec_args)` for every matched form
14961        // in source order. Sweep across TWO distinct classifier outcomes
14962        // — `Monitor` / `Notify` — interleaved with a non-matching form
14963        // to pin the typed-witness threading AND the NAME-slot
14964        // projection AND the source-order yield AND the rejection of
14965        // non-classifier forms in ONE assertion.
14966        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14967        enum Kind {
14968            Monitor,
14969            Notify,
14970        }
14971        let src = r#"(defmonitor cpu :threshold 80)
14972                     (other-form 99)
14973                     (defnotify email :to "ops@example.com")
14974                     (defmonitor mem :threshold 90)"#;
14975        let forms = read(src).unwrap();
14976        let mut e = Expander::new();
14977        let rows: Vec<(Kind, String, usize)> = e
14978            .expand_and_collect_named_calls_to_any(
14979                forms,
14980                |h| match h {
14981                    "defmonitor" => Some((Kind::Monitor, "defmonitor")),
14982                    "defnotify" => Some((Kind::Notify, "defnotify")),
14983                    _ => None,
14984                },
14985                |kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
14986            )
14987            .unwrap();
14988        assert_eq!(
14989            rows,
14990            vec![
14991                (Kind::Monitor, "cpu".to_string(), 2),
14992                (Kind::Notify, "email".to_string(), 2),
14993                (Kind::Monitor, "mem".to_string(), 2),
14994            ],
14995        );
14996    }
14997
14998    #[test]
14999    fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
15000        // Soft-projection contract — every shape the typed-decoded
15001        // primitive rejects (non-list, empty list, list whose head is
15002        // not a symbol, list whose head decodes to `None`) skips the
15003        // projection silently. The named gate inside the wrapper
15004        // projection runs ONLY for classifier-matched forms; a non-
15005        // matching form that reaches the projection panics here.
15006        let src = r#":bare-keyword
15007                     "bare-string"
15008                     42
15009                     ()
15010                     (foo bar)
15011                     (defmonitor cpu :threshold 80)"#;
15012        let forms = read(src).unwrap();
15013        let mut e = Expander::new();
15014        let names: Vec<String> = e
15015            .expand_and_collect_named_calls_to_any(
15016                forms,
15017                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15018                |(), name, _args| Ok(name.to_string()),
15019            )
15020            .unwrap();
15021        assert_eq!(names, vec!["cpu".to_string()]);
15022    }
15023
15024    #[test]
15025    fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
15026    ) {
15027        // `(defmonitor)` matches the classifier but has no NAME slot —
15028        // `split_name_slot` fires `NamedFormMissingName { keyword:
15029        // "defmonitor" }`. The classifier's `&'static str` keyword is
15030        // threaded VERBATIM through the named gate; a regression that
15031        // hardcodes a different keyword (e.g. the matched head text or
15032        // a default) would fail the structural variant identity
15033        // assertion. Fail-before-pass-after: pre-lift the primitive did
15034        // not exist; the inline `expand_and_collect_calls_to_any +
15035        // split_name_slot` composition at the consumer site was the
15036        // only path, with the keyword bound at the consumer's call
15037        // boundary rather than at the named primitive's body.
15038        let forms = read("(defmonitor)").unwrap();
15039        let mut e = Expander::new();
15040        let err = e
15041            .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15042                forms,
15043                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15044                |(), _name, _args| Ok(()),
15045            )
15046            .unwrap_err();
15047        assert!(
15048            matches!(
15049                err,
15050                crate::error::LispError::NamedFormMissingName {
15051                    keyword: "defmonitor"
15052                }
15053            ),
15054            "expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
15055        );
15056    }
15057
15058    #[test]
15059    fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
15060    ) {
15061        // `(defmonitor 42 :threshold 80)` matches the classifier but
15062        // the NAME slot is an int — `split_name_slot` fires
15063        // `NamedFormNonSymbolName { keyword: "defmonitor", got:
15064        // SexpShape::Int }`. The classifier's `&'static str` keyword is
15065        // threaded through the structural rejection variant verbatim,
15066        // and the typed `SexpShape` projection on the offending slot
15067        // is preserved across the named primitive.
15068        let forms = read("(defmonitor 42 :threshold 80)").unwrap();
15069        let mut e = Expander::new();
15070        let err = e
15071            .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15072                forms,
15073                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15074                |(), _name, _args| Ok(()),
15075            )
15076            .unwrap_err();
15077        assert!(
15078            matches!(
15079                err,
15080                crate::error::LispError::NamedFormNonSymbolName {
15081                    keyword: "defmonitor",
15082                    got: crate::error::SexpShape::Int,
15083                }
15084            ),
15085            "expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
15086        );
15087    }
15088
15089    #[test]
15090    fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
15091        // Result projection's short-circuit contract — when the
15092        // projection returns `Err` on a matched named form, the walk
15093        // stops and subsequent matched forms are NOT projected. Pin
15094        // source-order short-circuit via a counter the projection
15095        // increments before deciding to fail: the counter sits at
15096        // exactly the index of the failing form (second match),
15097        // proving the third match never reached the projection.
15098        let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
15099        let mut count = 0usize;
15100        let mut e = Expander::new();
15101        let err = e
15102            .expand_and_collect_named_calls_to_any::<String, _, _, ()>(
15103                forms,
15104                |h| (h == "defmon").then_some(((), "defmon")),
15105                |(), name, _args| {
15106                    count += 1;
15107                    if name == "b" {
15108                        return Err(crate::error::LispError::Missing("test-failure"));
15109                    }
15110                    Ok(name.to_string())
15111                },
15112            )
15113            .expect_err("projection must short-circuit on first Err");
15114        assert_eq!(
15115            count, 2,
15116            "projection must have run on first matched form AND the failing form, then stopped"
15117        );
15118        assert!(
15119            matches!(err, crate::error::LispError::Missing("test-failure")),
15120            "expected the projection's typed Err verbatim, got: {err:?}"
15121        );
15122    }
15123
15124    #[test]
15125    fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
15126        // Ordering contract — `expand_program` runs BEFORE the
15127        // typed-decoded named walk. A `(defmacro …)` form that emits a
15128        // classifier-decoded named form must have its EXPANDED head
15129        // decoded by the classifier, not the macro-call head. Pin the
15130        // ordering with a macro `(emit-mon n thr)` expanding to
15131        // `(defmonitor ,n :threshold ,thr)` — the classifier sees the
15132        // expanded `defmonitor` head and the NAME slot's symbol value
15133        // from the macro arg.
15134        let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15135                     (defmonitor cpu :threshold 80)
15136                     (emit-mon mem 90)
15137                     (other-form 99)
15138                     (emit-mon disk 70)"#;
15139        let forms = read(src).unwrap();
15140        let mut e = Expander::new();
15141        let names: Vec<String> = e
15142            .expand_and_collect_named_calls_to_any(
15143                forms,
15144                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15145                |(), name, _args| Ok(name.to_string()),
15146            )
15147            .unwrap();
15148        // `cpu` from the literal call, `mem` from the first macro
15149        // emit, `disk` from the second macro emit. The macro-emitted
15150        // forms are present BECAUSE `expand_program` ran first.
15151        assert_eq!(names, vec!["cpu", "mem", "disk"]);
15152    }
15153
15154    #[test]
15155    fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
15156        // From-source posture parity: feeding source through
15157        // `expand_source_and_collect_named_calls_to_any` is byte-
15158        // identical to feeding `read(src)?` through the from-forms
15159        // sibling on a fresh expander, because the from-source posture
15160        // is `read(src)? + from-forms` by construction. A regression
15161        // that drifts the from-source posture's pipeline from the
15162        // from-forms posture's pipeline would fail here.
15163        let src = r#"(defmonitor cpu :threshold 80)
15164                     (defmonitor mem :threshold 90)"#;
15165        let via_src: Vec<String> = Expander::new()
15166            .expand_source_and_collect_named_calls_to_any(
15167                src,
15168                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15169                |(), name, _args| Ok(name.to_string()),
15170            )
15171            .unwrap();
15172        let forms = read(src).unwrap();
15173        let via_forms: Vec<String> = Expander::new()
15174            .expand_and_collect_named_calls_to_any(
15175                forms,
15176                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15177                |(), name, _args| Ok(name.to_string()),
15178            )
15179            .unwrap();
15180        assert_eq!(via_src, via_forms);
15181        assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15182    }
15183
15184    #[test]
15185    fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
15186    ) {
15187        // `?`-routing through `read` short-circuits BEFORE the
15188        // classifier or the named gate runs. Unbalanced paren — reader
15189        // error fires; classifier panic-decoder MUST NOT execute,
15190        // proving the read step gates the entire pipeline.
15191        let mut e = Expander::new();
15192        let err = e
15193            .expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
15194                "(defmonitor cpu :threshold 80",
15195                |_h| panic!("classifier must not run when reader fails"),
15196                |(), _name, _args| Ok(()),
15197            )
15198            .unwrap_err();
15199        // The named gate's variants MUST NOT fire — the reader error
15200        // is structurally distinct.
15201        assert!(
15202            !matches!(
15203                err,
15204                crate::error::LispError::NamedFormMissingName { .. }
15205                    | crate::error::LispError::NamedFormNonSymbolName { .. }
15206            ),
15207            "expected reader error, not named-gate variant; got: {err:?}"
15208        );
15209    }
15210
15211    // ── expand_and_collect_named_calls_to (from-forms × named × ──────
15212    // constant-keyword × untyped R) ─────────────────────────────────
15213    //
15214    // The (named NAME-then-kwargs × constant-keyword × untyped `R`)
15215    // cell of the `Expander` typed-walk family — pre-lift the cell was
15216    // reachable ONLY through `expand_to_named<T>` with the
15217    // `T: TataraDomain` parameter baking BOTH the `T::KEYWORD` filter
15218    // AND the `T::compile_from_args`-based projection through
15219    // `expand_and_collect_calls_to(forms, T::KEYWORD,
15220    // named_form_projection::<T>)`. Post-lift the cell surfaces as ONE
15221    // method that takes the keyword and the `(name, args) -> R`
15222    // projection as caller-supplied parameters, AND
15223    // `expand_to_named<T>` routes through it as the typed
15224    // `T::KEYWORD`-constant specialization — so the named-form
15225    // `split_name_slot` composition lives at ONE site
15226    // (`expand_and_collect_named_calls_to_any` body) post-lift rather
15227    // than at TWO sites pre-lift (the bare-kwargs path through
15228    // `named_form_projection<T>` AND the classifier path).
15229    //
15230    // The tests below pin: (a) the happy path yields the `(name, args)`
15231    // pair for every matched form in source order; (b) non-matching
15232    // forms are skipped (soft-projection contract inherited from the
15233    // named typed-decoded primitive); (c) the `NamedFormMissingName`
15234    // variant fires for matched forms with no NAME slot, threading the
15235    // primitive's `&'static str` keyword; (d) the
15236    // `NamedFormNonSymbolName` variant fires for matched forms with a
15237    // non-symbol-or-string NAME slot, again threading the primitive's
15238    // keyword; (e) the projection's `Err` short-circuits at the first
15239    // failing matched form; (f) `expand_program` runs BEFORE the
15240    // keyword filter walks — a `(defmacro …)` emitting a named form
15241    // has its EXPANDED head matched by the constant-keyword filter,
15242    // not the macro-call head; (g) the from-source sibling agrees with
15243    // `read + from-forms` on the same source; (h) the constant-
15244    // classifier composition law binds the runtime-keyword cell
15245    // (`expand_and_collect_named_calls_to`) to the typed-decoded
15246    // classifier cell (`expand_and_collect_named_calls_to_any`) via a
15247    // constant-classifier decoder.
15248
15249    #[test]
15250    fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
15251    ) {
15252        // The constant-keyword named primitive's happy path: a runtime
15253        // `&'static str` keyword filters matched forms, and the per-
15254        // form projection receives `(name, spec_args)` for every
15255        // matched form in source order. Three matched forms
15256        // interleaved with a non-matcher pin the source-order yield,
15257        // the NAME-slot projection, AND the rejection of non-keyword
15258        // forms in ONE assertion.
15259        let src = r#"(defmonitor cpu :threshold 80)
15260                     (other-form 99)
15261                     (defmonitor mem :threshold 90 :unit "MiB")
15262                     (defmonitor disk :threshold 70)"#;
15263        let forms = read(src).unwrap();
15264        let mut e = Expander::new();
15265        let rows: Vec<(String, usize)> = e
15266            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
15267                Ok((name.to_string(), spec_args.len()))
15268            })
15269            .unwrap();
15270        assert_eq!(
15271            rows,
15272            vec![
15273                ("cpu".to_string(), 2),
15274                ("mem".to_string(), 4),
15275                ("disk".to_string(), 2),
15276            ],
15277        );
15278    }
15279
15280    #[test]
15281    fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
15282        // Soft-projection contract — every shape the named typed-
15283        // decoded primitive rejects (non-list, empty list, list whose
15284        // head is not a symbol, list whose head doesn't match the
15285        // constant keyword) skips the projection silently. The named
15286        // gate inside the wrapper projection runs ONLY for matched
15287        // forms; a non-matching form that reaches the projection
15288        // panics here.
15289        let src = r#":bare-keyword
15290                     "bare-string"
15291                     42
15292                     ()
15293                     (foo bar)
15294                     (defmonitor cpu :threshold 80)"#;
15295        let forms = read(src).unwrap();
15296        let mut e = Expander::new();
15297        let names: Vec<String> = e
15298            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15299                Ok(name.to_string())
15300            })
15301            .unwrap();
15302        assert_eq!(names, vec!["cpu".to_string()]);
15303    }
15304
15305    #[test]
15306    fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
15307        // The named-form gate's missing-NAME variant must thread the
15308        // primitive's `&'static str` keyword (NOT a hardcoded literal,
15309        // NOT the projection's own copy) — the `NamedFormMissingName
15310        // { keyword }` slot inside `split_name_slot` is bound at the
15311        // call site BY THE PRIMITIVE itself, via the constant-
15312        // classifier decoder threading `((), keyword)` into
15313        // `expand_and_collect_named_calls_to_any`.
15314        let src = r#"(defmonitor)"#;
15315        let forms = read(src).unwrap();
15316        let mut e = Expander::new();
15317        let err = e
15318            .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15319            .unwrap_err();
15320        assert!(
15321            matches!(
15322                err,
15323                crate::error::LispError::NamedFormMissingName {
15324                    keyword: "defmonitor"
15325                }
15326            ),
15327            "expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
15328        );
15329    }
15330
15331    #[test]
15332    fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
15333    ) {
15334        // The named-form gate's non-symbol-NAME variant must thread
15335        // the primitive's `&'static str` keyword AND the typed-shape
15336        // witness (`SexpShape::Int` for an integer NAME slot). The
15337        // shape projection through `split_name_slot`'s
15338        // `as_symbol_or_string()` gate fires on the first matched
15339        // form whose NAME slot is not a symbol or string.
15340        let src = r#"(defmonitor 42 :threshold 80)"#;
15341        let forms = read(src).unwrap();
15342        let mut e = Expander::new();
15343        let err = e
15344            .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15345            .unwrap_err();
15346        match err {
15347            crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
15348                assert_eq!(keyword, "defmonitor");
15349                assert_eq!(got, crate::error::SexpShape::Int);
15350            }
15351            other => panic!(
15352                "expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
15353            ),
15354        }
15355    }
15356
15357    #[test]
15358    fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
15359        // `Iterator::collect::<Result<Vec<R>, _>>()` short-circuits at
15360        // the first failing projection. The second matched form's
15361        // projection error MUST fire AND the third matched form's
15362        // projection MUST NOT run. Pin both halves with a counter +
15363        // a fail-on-call sentinel.
15364        let src = r#"(defmonitor cpu :threshold 80)
15365                     (defmonitor mem :threshold 90)
15366                     (defmonitor disk :threshold 70)"#;
15367        let forms = read(src).unwrap();
15368        let mut count: usize = 0;
15369        let mut e = Expander::new();
15370        let err = e
15371            .expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
15372                count += 1;
15373                if name == "mem" {
15374                    Err(crate::error::LispError::Compile {
15375                        form: "test".into(),
15376                        message: format!("rejecting at NAME={name}"),
15377                    })
15378                } else {
15379                    Ok(name.to_string())
15380                }
15381            })
15382            .unwrap_err();
15383        assert_eq!(count, 2, "third matched form must not be projected");
15384        match err {
15385            crate::error::LispError::Compile { message, .. } => {
15386                assert!(message.contains("NAME=mem"));
15387            }
15388            other => panic!("expected projection-driven Compile error; got: {other:?}"),
15389        }
15390    }
15391
15392    #[test]
15393    fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
15394        // Ordering contract — `expand_program` runs BEFORE the
15395        // constant-keyword named walk. A `(defmacro …)` form that
15396        // emits a matched named form must have its EXPANDED head
15397        // matched by the constant-keyword filter, not the macro-call
15398        // head. Pin the ordering with a macro `(emit-mon n thr)`
15399        // expanding to `(defmonitor ,n :threshold ,thr)` — the
15400        // constant-keyword filter matches the expanded `defmonitor`
15401        // head and the NAME slot projects the symbol-author value.
15402        let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15403                     (defmonitor cpu :threshold 80)
15404                     (emit-mon mem 90)
15405                     (other-form 99)
15406                     (emit-mon disk 70)"#;
15407        let forms = read(src).unwrap();
15408        let mut e = Expander::new();
15409        let names: Vec<String> = e
15410            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15411                Ok(name.to_string())
15412            })
15413            .unwrap();
15414        // `cpu` from the literal call, `mem` from the first macro
15415        // emit, `disk` from the second macro emit. The macro-emitted
15416        // forms are present BECAUSE `expand_program` ran first.
15417        assert_eq!(names, vec!["cpu", "mem", "disk"]);
15418    }
15419
15420    #[test]
15421    fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
15422        // From-source posture parity: feeding source through
15423        // `expand_source_and_collect_named_calls_to` is byte-identical
15424        // to feeding `read(src)?` through the from-forms sibling on a
15425        // fresh expander, because the from-source posture is
15426        // `read(src)? + from-forms` by construction. A regression that
15427        // drifts the from-source posture's pipeline from the from-
15428        // forms posture's pipeline would fail here.
15429        let src = r#"(defmonitor cpu :threshold 80)
15430                     (defmonitor mem :threshold 90)"#;
15431        let via_src: Vec<String> = Expander::new()
15432            .expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
15433                Ok(name.to_string())
15434            })
15435            .unwrap();
15436        let forms = read(src).unwrap();
15437        let via_forms: Vec<String> = Expander::new()
15438            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15439                Ok(name.to_string())
15440            })
15441            .unwrap();
15442        assert_eq!(via_src, via_forms);
15443        assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15444    }
15445
15446    #[test]
15447    fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
15448    ) {
15449        // `?`-routing through `read` short-circuits BEFORE the
15450        // constant-keyword filter, the named gate, or the projection
15451        // runs. Unbalanced paren — reader error fires; the projection
15452        // panic-payload MUST NOT execute, proving the read step gates
15453        // the entire pipeline.
15454        let mut e = Expander::new();
15455        let err = e
15456            .expand_source_and_collect_named_calls_to::<(), _>(
15457                "(defmonitor cpu :threshold 80",
15458                "defmonitor",
15459                |_name, _args| panic!("projection must not run when reader fails"),
15460            )
15461            .unwrap_err();
15462        // The named gate's variants MUST NOT fire — the reader error
15463        // is structurally distinct.
15464        assert!(
15465            !matches!(
15466                err,
15467                crate::error::LispError::NamedFormMissingName { .. }
15468                    | crate::error::LispError::NamedFormNonSymbolName { .. }
15469            ),
15470            "expected reader error, not named-gate variant; got: {err:?}",
15471        );
15472    }
15473
15474    #[test]
15475    fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
15476    ) {
15477        // Composition-identity test pinning the runtime-keyword named
15478        // cell (`expand_and_collect_named_calls_to`) to the typed-
15479        // decoded named-classifier cell
15480        // (`expand_and_collect_named_calls_to_any`) via the constant-
15481        // classifier decoder shape. Post-lift the identity:
15482        //
15483        //   expand_and_collect_named_calls_to(forms, kw, project) ==
15484        //       expand_and_collect_named_calls_to_any(forms,
15485        //           |h| (h == kw).then_some(((), kw)),
15486        //           |(), name, args| project(name, args))
15487        //
15488        // Pinning the identity here makes the typed-decoded named-
15489        // classifier primitive the CANONICAL composition point the
15490        // runtime-keyword sibling routes through — parallel to how
15491        // `expand_and_collect_calls_to` routes through
15492        // `expand_and_collect_calls_to_any` via a `|h| (h == k).
15493        // then_some(())` decoder on the bare-kwargs axis. A future
15494        // regression that drifts ONE cell's NAME-slot rejection chain
15495        // from the other becomes loudly visible at this assertion.
15496        let src = r#"(defmonitor cpu :threshold 80)
15497                     (other-form 99)
15498                     (defmonitor mem :threshold 90)"#;
15499        let forms = read(src).unwrap();
15500        let via_constant_keyword: Vec<(String, usize)> = Expander::new()
15501            .expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
15502                Ok((name.to_string(), args.len()))
15503            })
15504            .unwrap();
15505        let via_classifier: Vec<(String, usize)> = Expander::new()
15506            .expand_and_collect_named_calls_to_any(
15507                forms,
15508                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15509                |(), name, args| Ok((name.to_string(), args.len())),
15510            )
15511            .unwrap();
15512        assert_eq!(via_constant_keyword, via_classifier);
15513        assert_eq!(
15514            via_constant_keyword,
15515            vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
15516        );
15517    }
15518
15519    #[test]
15520    fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
15521    ) {
15522        // End-to-end path-uniformity: `Expander::expand_to_named<T>`
15523        // (the typed constant-keyword named cell) must yield the same
15524        // payload as `expand_and_collect_named_calls_to(forms,
15525        // T::KEYWORD, |name, args| { T::compile_from_args(args)? +
15526        // NamedDefinition })` — the lift's structural identity.
15527        //
15528        // Pre-lift `expand_to_named<T>` routed through
15529        // `expand_and_collect_calls_to(forms, T::KEYWORD,
15530        // named_form_projection::<T>)` (the bare-kwargs constant
15531        // primitive with `named_form_projection<T>` doing the NAME
15532        // extraction internally). Post-lift `expand_to_named<T>`
15533        // routes through the named constant-keyword primitive (which
15534        // itself routes through the classifier primitive via a
15535        // constant-classifier decoder) — so the `split_name_slot`
15536        // composition lives at ONE site (the `_any` primitive body)
15537        // post-lift rather than at TWO sites.
15538        use crate::compile::NamedDefinition;
15539        use crate::compiler_spec::CompilerSpec;
15540        let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
15541                     (defcompiler beta-compiler  :name "y" :dialect "standard")"#;
15542        let forms = read(src).unwrap();
15543        let via_expand_to_named = Expander::new()
15544            .expand_to_named::<CompilerSpec>(forms.clone())
15545            .unwrap();
15546        let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
15547            .expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
15548                let spec =
15549                    <CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
15550                Ok(NamedDefinition {
15551                    name: name.to_string(),
15552                    spec,
15553                })
15554            })
15555            .unwrap();
15556        assert_eq!(via_expand_to_named.len(), 2);
15557        assert_eq!(via_expand_to_named.len(), via_named_constant.len());
15558        for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
15559            assert_eq!(a.name, b.name, "NAME slot must agree across cells");
15560            assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
15561        }
15562        assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
15563        assert_eq!(via_expand_to_named[0].spec.name, "x");
15564        assert_eq!(via_expand_to_named[1].name, "beta-compiler");
15565        assert_eq!(via_expand_to_named[1].spec.name, "y");
15566    }
15567
15568    // ── ExpansionDepthExceeded: runaway macro rejected instead of aborting ──
15569
15570    #[test]
15571    fn expander_default_max_expansion_depth_is_the_module_constant() {
15572        // Both constructors seed `max_expansion_depth` from the module
15573        // constant — a regression that drifts one constructor's default
15574        // from the constant (e.g. a hardcoded `256` at ONE site with the
15575        // other left uninitialized after a `Default` derive addition)
15576        // fails this pin.
15577        assert_eq!(
15578            Expander::new().max_expansion_depth(),
15579            DEFAULT_MAX_EXPANSION_DEPTH
15580        );
15581        assert_eq!(
15582            Expander::new_substitute_only().max_expansion_depth(),
15583            DEFAULT_MAX_EXPANSION_DEPTH
15584        );
15585        assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
15586    }
15587
15588    #[test]
15589    fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
15590        let mut e = Expander::new();
15591        e.set_max_expansion_depth(7);
15592        assert_eq!(e.max_expansion_depth(), 7);
15593    }
15594
15595    #[test]
15596    fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
15597        // `(defmacro loop (x) `(loop ,x))` applied to `(loop hello)`
15598        // pre-lift stack-overflowed the process — a below-floor abort.
15599        // Post-lift the expander returns a typed
15600        // `LispError::ExpansionDepthExceeded` at the ceiling with
15601        // `macro_name` populated from the offending call's head.
15602        let mut e = Expander::new();
15603        e.set_max_expansion_depth(4);
15604        let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15605        let err = e.expand_program(forms).unwrap_err();
15606        match err {
15607            LispError::ExpansionDepthExceeded { macro_name, limit } => {
15608                assert_eq!(macro_name, "loop");
15609                assert_eq!(limit, 4);
15610            }
15611            other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15612        }
15613    }
15614
15615    #[test]
15616    fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
15617        // The substitute strategy shares the same `expand` outer walker
15618        // with the bytecode strategy, so the ceiling fires at the SAME
15619        // gate regardless of which apply-layer is active. The pin makes
15620        // that shared-outer-walker property structural: a regression
15621        // that ever branches the depth guard on strategy (e.g. moves it
15622        // into `apply_compiled` alone) fails this test on the
15623        // substitute expander.
15624        let mut e = Expander::new_substitute_only();
15625        e.set_max_expansion_depth(4);
15626        let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15627        let err = e.expand_program(forms).unwrap_err();
15628        match err {
15629            LispError::ExpansionDepthExceeded { macro_name, limit } => {
15630                assert_eq!(macro_name, "loop");
15631                assert_eq!(limit, 4);
15632            }
15633            other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15634        }
15635    }
15636
15637    #[test]
15638    fn expand_lawful_nested_macros_within_ceiling_succeed() {
15639        // Lawful nested macros (`(twice (twice hey))`) accrete depth in
15640        // the low single digits because the tree-child arm doesn't
15641        // count — only the post-apply re-expansion path bumps the
15642        // counter. A ceiling of 3 is enough for a two-deep nested
15643        // expansion. This pin is the fail-before/pass-after negative
15644        // control for the recursive-macro rejection tests above: the
15645        // depth counter must NOT reject lawful macro-nesting within
15646        // the ceiling, and only reject the runaway shape.
15647        let mut e = Expander::new();
15648        e.set_max_expansion_depth(3);
15649        let forms = read(
15650            "(defmacro twice (x) `(list ,x ,x))
15651             (twice (twice hey))",
15652        )
15653        .unwrap();
15654        let out = e.expand_program(forms).unwrap();
15655        assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
15656    }
15657
15658    #[test]
15659    fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
15660        // A macro-free lawful deep tree — five levels of plain nesting —
15661        // is expanded at a ceiling well below the tree's height. The
15662        // tree-child arm does not accrete depth, so tree height is
15663        // orthogonal to the ceiling; only macro re-expansion counts.
15664        // A regression that ever bumps depth on tree-child descent
15665        // fails this pin.
15666        let mut e = Expander::new();
15667        e.set_max_expansion_depth(2);
15668        let forms = read("(a (b (c (d (e f)))))").unwrap();
15669        let out = e.expand_program(forms).unwrap();
15670        assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
15671    }
15672
15673    #[test]
15674    fn expansion_depth_exceeded_position_is_none() {
15675        // The variant joins the non-positional cohort — the offending
15676        // macro's byte offset is not what a runaway diagnostic anchors
15677        // to; the OFFENDING MACRO NAME (`macro_name`) is. This pin
15678        // keeps the variant in the `position() -> None` cohort so a
15679        // future span-carrying edit lands the field in ONE place
15680        // across every non-positional variant simultaneously.
15681        let err = LispError::ExpansionDepthExceeded {
15682            macro_name: "loop".to_string(),
15683            limit: 256,
15684        };
15685        assert_eq!(err.position(), None);
15686    }
15687
15688    #[test]
15689    fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
15690        // Display projection carries both `macro_name` and `limit`, so
15691        // authoring surfaces that substring-grep the rendered
15692        // diagnostic (`tatara-check`, REPL, LSP) see BOTH halves at
15693        // the variant boundary and never need to substring-parse
15694        // free-form text to recover the identity of the offending
15695        // macro or the ceiling it breached.
15696        let err = LispError::ExpansionDepthExceeded {
15697            macro_name: "loop".to_string(),
15698            limit: 256,
15699        };
15700        let rendered = err.to_string();
15701        assert!(rendered.contains("loop"), "rendered: {rendered}");
15702        assert!(rendered.contains("256"), "rendered: {rendered}");
15703        assert!(
15704            rendered.contains("macro expansion depth exceeded"),
15705            "rendered: {rendered}"
15706        );
15707    }
15708
15709    // ── max_cache_entries: bounded-memoization ceiling ──────────────────
15710
15711    #[test]
15712    fn expander_default_max_cache_entries_is_the_module_constant() {
15713        // Both cache-enabling constructors seed `max_cache_entries` from
15714        // the module constant — a regression that drifts one
15715        // constructor's default from the constant (e.g. a hardcoded
15716        // `8192` at ONE site with the other left uninitialized after a
15717        // `Default` derive addition) fails this pin. The peer
15718        // constructor `new_substitute_only` seeds the same ceiling so
15719        // the field stays initialized under `#[derive(Clone)]` (a
15720        // zero-initialized clone would be a silent bifurcation of the
15721        // cache-ceiling surface).
15722        assert_eq!(
15723            Expander::new().max_cache_entries(),
15724            DEFAULT_MAX_CACHE_ENTRIES
15725        );
15726        assert_eq!(
15727            Expander::new_substitute_only().max_cache_entries(),
15728            DEFAULT_MAX_CACHE_ENTRIES
15729        );
15730        assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
15731    }
15732
15733    #[test]
15734    fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
15735        // Setter mirrors accessor — a regression that ever forgets to
15736        // write through to the field, or writes to a shadow field the
15737        // accessor does not read, fails this pin.
15738        let mut e = Expander::new();
15739        e.set_max_cache_entries(7);
15740        assert_eq!(e.max_cache_entries(), 7);
15741    }
15742
15743    #[test]
15744    fn expand_cache_size_is_bounded_by_max_cache_entries() {
15745        // Five distinct `(name, args)` pairs expanded against a cache
15746        // ceiling of `2` populate the cache with EXACTLY the first two
15747        // insertions and skip the remaining three. Every expansion
15748        // still returns the correct result — the cache is a pure
15749        // PERFORMANCE optimization; the ceiling gates memoization,
15750        // never correctness. LOAD-BEARING true-arm catch on the
15751        // bounded-cache contract: a regression that ignored the
15752        // ceiling (unbounded insert) would land `cache_size() == 5`
15753        // here; a regression that skipped insertion entirely (broken
15754        // cache) would still see correct expansion output but would
15755        // fail the interior `cache_size() == 2` pin.
15756        let mut e = Expander::new();
15757        e.set_max_cache_entries(2);
15758        let src = "
15759            (defmacro id (x) `,x)
15760            (id one)
15761            (id two)
15762            (id three)
15763            (id four)
15764            (id five)
15765        ";
15766        let out = e.expand_program(read(src).unwrap()).unwrap();
15767        assert_eq!(out.len(), 5);
15768        assert_eq!(out[0], parse("one"));
15769        assert_eq!(out[1], parse("two"));
15770        assert_eq!(out[2], parse("three"));
15771        assert_eq!(out[3], parse("four"));
15772        assert_eq!(out[4], parse("five"));
15773        assert_eq!(
15774            e.cache_size(),
15775            2,
15776            "cache grew past the max_cache_entries ceiling",
15777        );
15778    }
15779
15780    #[test]
15781    fn expand_zero_cache_ceiling_disables_caching_effectively() {
15782        // Zero ceiling admits every fresh `(name, args)` pair through
15783        // the compute path and never grows the cache — the same
15784        // observable behavior as `set_cache_enabled(false)` but
15785        // reached through the ceiling knob. Correctness is preserved:
15786        // the miss path always recomputes the same value the cache
15787        // would have returned, so an operator that dials the ceiling
15788        // to `0` (e.g. to hunt a cache-related regression) never
15789        // bifurcates the output surface. Peer to
15790        // `set_max_expansion_depth(0)` one RESOURCE axis over — both
15791        // ceilings admit the zero endpoint as a well-defined
15792        // extremum.
15793        let mut e = Expander::new();
15794        e.set_max_cache_entries(0);
15795        let src = "
15796            (defmacro id (x) `,x)
15797            (id one)
15798            (id two)
15799        ";
15800        let out = e.expand_program(read(src).unwrap()).unwrap();
15801        assert_eq!(out.len(), 2);
15802        assert_eq!(out[0], parse("one"));
15803        assert_eq!(out[1], parse("two"));
15804        assert_eq!(
15805            e.cache_size(),
15806            0,
15807            "zero cache ceiling grew the cache — the ceiling must be respected on every insert",
15808        );
15809    }
15810
15811    #[test]
15812    fn expand_cached_hits_survive_past_the_ceiling() {
15813        // Once a `(name, args)` pair sits IN the cache under the
15814        // ceiling, subsequent calls hit it — the ceiling only gates
15815        // NEW-KEY inserts, never lookups. This test seeds the cache
15816        // with two entries under a ceiling of `2`, then re-issues the
15817        // SAME calls to prove the cached hits still fire (cache_size
15818        // stays at `2` because no new keys are inserted, and the
15819        // expansion output is stable). LOAD-BEARING true-arm catch
15820        // that the ceiling gates the INSERT path alone — a regression
15821        // that ever refused cache LOOKUPS at the ceiling would
15822        // silently bifurcate correctness on the second call.
15823        let mut e = Expander::new();
15824        e.set_max_cache_entries(2);
15825        let src = "
15826            (defmacro id (x) `,x)
15827            (id one)
15828            (id two)
15829            (id one)
15830            (id two)
15831        ";
15832        let out = e.expand_program(read(src).unwrap()).unwrap();
15833        assert_eq!(out.len(), 4);
15834        assert_eq!(out[0], parse("one"));
15835        assert_eq!(out[1], parse("two"));
15836        assert_eq!(out[2], parse("one"));
15837        assert_eq!(out[3], parse("two"));
15838        assert_eq!(
15839            e.cache_size(),
15840            2,
15841            "cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
15842        );
15843    }
15844
15845    #[test]
15846    fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
15847        // After the cache reaches its ceiling and subsequent inserts
15848        // are skipped, `clear_cache` empties the memo and RE-OPENS the
15849        // insert path — the next expansion populates the freshly-
15850        // empty cache without operator intervention on the ceiling
15851        // itself. The pin binds the operator-facing recovery path
15852        // documented on [`DEFAULT_MAX_CACHE_ENTRIES`]: "skip cache
15853        // insertion until the operator clears the cache via
15854        // [`Expander::clear_cache`]".
15855        let mut e = Expander::new();
15856        e.set_max_cache_entries(1);
15857        let src_fill = "
15858            (defmacro id (x) `,x)
15859            (id one)
15860            (id two)
15861        ";
15862        let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
15863        assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
15864        e.clear_cache();
15865        assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
15866        let out = e
15867            .expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
15868            .unwrap();
15869        assert_eq!(out.len(), 1);
15870        assert_eq!(out[0], parse("three"));
15871        assert_eq!(
15872            e.cache_size(),
15873            1,
15874            "clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
15875        );
15876    }
15877
15878    // ── max_expansion_size: bounded-output ceiling ─────────────────────
15879
15880    #[test]
15881    fn expander_default_max_expansion_size_is_the_module_constant() {
15882        // Both constructors seed `max_expansion_size` from the module
15883        // constant — a regression that drifts one constructor's
15884        // default from the constant (e.g. a hardcoded `65_536` at
15885        // ONE site with the other left uninitialized after a
15886        // `Default` derive addition) fails this pin. Peer to the
15887        // `max_expansion_depth` + `max_cache_entries` default-
15888        // coherence pins one RESOURCE-DIMENSION axis over.
15889        assert_eq!(
15890            Expander::new().max_expansion_size(),
15891            DEFAULT_MAX_EXPANSION_SIZE
15892        );
15893        assert_eq!(
15894            Expander::new_substitute_only().max_expansion_size(),
15895            DEFAULT_MAX_EXPANSION_SIZE
15896        );
15897        assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
15898    }
15899
15900    #[test]
15901    fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
15902        // Setter mirrors accessor — a regression that ever forgets to
15903        // write through to the field, or writes to a shadow field the
15904        // accessor does not read, fails this pin. Peer to the
15905        // setter/accessor coherence pins on the depth + cache axes.
15906        let mut e = Expander::new();
15907        e.set_max_expansion_size(64);
15908        assert_eq!(e.max_expansion_size(), 64);
15909    }
15910
15911    #[test]
15912    fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
15913        // `(defmacro bomb (x) `(list ,x ,x ,x ,x))` applied to
15914        // `(bomb hey)` produces `(list hey hey hey hey)` — 6 nodes
15915        // (outer List + 5 children). With a ceiling of `4` this
15916        // crosses the OUTPUT-SIZE threshold at the first apply and
15917        // the expander returns a typed
15918        // `LispError::ExpansionSizeExceeded` with `macro_name`,
15919        // `size`, and `limit` all populated. Pre-lift the runaway
15920        // just accumulated in a growing tree until the process
15921        // heap ran out; post-lift the rejection sits at the same
15922        // call boundary as every other macro-expansion failure.
15923        let mut e = Expander::new();
15924        e.set_max_expansion_size(4);
15925        let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15926        let err = e.expand_program(forms).unwrap_err();
15927        match err {
15928            LispError::ExpansionSizeExceeded {
15929                macro_name,
15930                size,
15931                limit,
15932            } => {
15933                assert_eq!(macro_name, "bomb");
15934                assert_eq!(size, 6);
15935                assert_eq!(limit, 4);
15936            }
15937            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15938        }
15939    }
15940
15941    #[test]
15942    fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
15943        // The substitute strategy shares the same `expand_with_depth`
15944        // outer walker with the bytecode strategy, so the ceiling
15945        // fires at the SAME gate regardless of which apply-layer is
15946        // active. The pin makes that shared-outer-walker property
15947        // structural: a regression that ever branches the size guard
15948        // on strategy (e.g. moves it into `apply_compiled` alone)
15949        // fails this test on the substitute expander. Peer to
15950        // `expand_recursive_macro_rejects_at_depth_limit_substitute_path`
15951        // one RESOURCE axis over.
15952        let mut e = Expander::new_substitute_only();
15953        e.set_max_expansion_size(4);
15954        let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15955        let err = e.expand_program(forms).unwrap_err();
15956        match err {
15957            LispError::ExpansionSizeExceeded {
15958                macro_name,
15959                size,
15960                limit,
15961            } => {
15962                assert_eq!(macro_name, "bomb");
15963                assert_eq!(size, 6);
15964                assert_eq!(limit, 4);
15965            }
15966            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15967        }
15968    }
15969
15970    #[test]
15971    fn expand_lawful_output_within_size_ceiling_succeeds() {
15972        // `(defmacro twice (x) `(list ,x ,x))` applied to `(twice hey)`
15973        // produces `(list hey hey)` — 4 nodes. With a ceiling of `4`
15974        // the output sits AT the ceiling (`<=` admits equality; only
15975        // `>` rejects), so the expansion succeeds. Fail-before/pass-
15976        // after negative control for the bomb rejection tests: a
15977        // regression that ever rejected at-ceiling output would fail
15978        // this pin. Peer to `expand_lawful_nested_macros_within_ceiling_succeed`
15979        // one RESOURCE axis over.
15980        let mut e = Expander::new();
15981        e.set_max_expansion_size(4);
15982        let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
15983        let out = e.expand_program(forms).unwrap();
15984        assert_eq!(out[0], parse("(list hey hey)"));
15985    }
15986
15987    #[test]
15988    fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
15989        // A macro-free lawful large tree is NOT gated by the size
15990        // ceiling — the ceiling gates macro-`apply` outputs alone,
15991        // not tree-child descents through non-macro forms. A `usize::MAX`
15992        // ceiling would trivially admit anything; this pin uses a
15993        // deliberately small ceiling of `4` on a tree with 6 nodes
15994        // to catch a regression that ever bumps the ceiling on tree
15995        // descent. A tree-only walk should NEVER fire the ceiling.
15996        // Peer to `expand_depth_ceiling_ignores_lawful_tree_nesting_depth`
15997        // one RESOURCE axis over.
15998        let mut e = Expander::new();
15999        e.set_max_expansion_size(4);
16000        // `(a (b (c d)))` — 6 nodes, no macros. Passes cleanly.
16001        let forms = read("(a (b (c d)))").unwrap();
16002        let out = e.expand_program(forms).unwrap();
16003        assert_eq!(out[0], parse("(a (b (c d)))"));
16004    }
16005
16006    #[test]
16007    fn expansion_size_exceeded_position_is_none() {
16008        // The variant joins the non-positional cohort — the offending
16009        // macro's byte offset is not what an expansion-bomb diagnostic
16010        // anchors to; the (offending macro name, observed size,
16011        // configured ceiling) triple is. This pin keeps the variant
16012        // in the `position() -> None` cohort so a future span-carrying
16013        // edit lands the field in ONE place across every non-positional
16014        // variant simultaneously. Peer to
16015        // `expansion_depth_exceeded_position_is_none` one RESOURCE
16016        // axis over.
16017        let err = LispError::ExpansionSizeExceeded {
16018            macro_name: "bomb".to_string(),
16019            size: 512,
16020            limit: 256,
16021        };
16022        assert_eq!(err.position(), None);
16023    }
16024
16025    #[test]
16026    fn expansion_size_exceeded_display_matches_typed_variant_shape() {
16027        // Display projection carries `macro_name`, `size`, AND
16028        // `limit`, so authoring surfaces that substring-grep the
16029        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16030        // THREE halves at the variant boundary and never need to
16031        // substring-parse free-form text to recover the identity of
16032        // the offending macro, the observed size, or the ceiling it
16033        // breached. Peer to `expansion_depth_exceeded_display_matches_typed_variant_shape`
16034        // one RESOURCE axis over.
16035        let err = LispError::ExpansionSizeExceeded {
16036            macro_name: "bomb".to_string(),
16037            size: 512,
16038            limit: 256,
16039        };
16040        let rendered = err.to_string();
16041        assert!(rendered.contains("bomb"), "rendered: {rendered}");
16042        assert!(rendered.contains("512"), "rendered: {rendered}");
16043        assert!(rendered.contains("256"), "rendered: {rendered}");
16044        assert!(
16045            rendered.contains("macro expansion output size exceeded"),
16046            "rendered: {rendered}"
16047        );
16048    }
16049
16050    #[test]
16051    fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
16052        // A lawful outer macro that expands to a call of a bomb inner
16053        // macro rejects at the INNER call — `macro_name` at the
16054        // variant boundary is the name of the macro whose `apply`
16055        // output crossed the ceiling, not the outermost caller.
16056        // Load-bearing name-provenance pin: an operator whose bomb
16057        // sits N levels deep in a compose chain needs to see the
16058        // INNER macro's name in the diagnostic, not the wrapping
16059        // shape. A regression that ever populates `macro_name` from
16060        // the outermost caller's head fails this pin.
16061        let mut e = Expander::new();
16062        e.set_max_expansion_size(4);
16063        // `wrapper` re-expands into `bomb`; `bomb` produces a size-6
16064        // output that crosses the ceiling. Expected: the diagnostic
16065        // names `bomb`, not `wrapper`.
16066        let src = "
16067            (defmacro bomb (x) `(list ,x ,x ,x ,x))
16068            (defmacro wrapper (x) `(bomb ,x))
16069            (wrapper hey)
16070        ";
16071        let err = e.expand_program(read(src).unwrap()).unwrap_err();
16072        match err {
16073            LispError::ExpansionSizeExceeded {
16074                macro_name,
16075                size,
16076                limit,
16077            } => {
16078                assert_eq!(macro_name, "bomb");
16079                assert_eq!(size, 6);
16080                assert_eq!(limit, 4);
16081            }
16082            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16083        }
16084    }
16085
16086    // ── max_macro_body_size: REGISTRATION-time body-size ceiling ──────
16087    // Peer to depth (recursion length) + cache (memoization width) +
16088    // expansion size (single-`apply` OUTPUT size) one PIPELINE-STAGE
16089    // axis over — the three prior guards fire at EXPAND time; this
16090    // one fires at REGISTER time. Closes the expander's RESOURCE
16091    // surface at a fourth typed dimension across two pipeline stages.
16092
16093    #[test]
16094    fn expander_default_max_macro_body_size_is_the_module_constant() {
16095        // Both constructors seed `max_macro_body_size` from the
16096        // module constant — a regression that drifts one
16097        // constructor's default from the constant (e.g. a hardcoded
16098        // `16_384` at ONE site with the other left uninitialized
16099        // after a `Default` derive addition) fails this pin. Peer
16100        // to the `expander_default_max_expansion_{depth,size}_is_the_module_constant`
16101        // pins one PIPELINE-STAGE axis over.
16102        assert_eq!(
16103            Expander::new().max_macro_body_size(),
16104            DEFAULT_MAX_MACRO_BODY_SIZE
16105        );
16106        assert_eq!(
16107            Expander::new_substitute_only().max_macro_body_size(),
16108            DEFAULT_MAX_MACRO_BODY_SIZE
16109        );
16110        assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
16111    }
16112
16113    #[test]
16114    fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
16115        // Setter mirrors accessor — a regression that ever forgets to
16116        // write through to the field, or writes to a shadow field the
16117        // accessor does not read, fails this pin. Peer to the
16118        // setter/accessor coherence pins on the depth + cache +
16119        // expansion-size axes.
16120        let mut e = Expander::new();
16121        e.set_max_macro_body_size(64);
16122        assert_eq!(e.max_macro_body_size(), 64);
16123    }
16124
16125    #[test]
16126    fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
16127        // `(defmacro huge (x) `(list a b c d e))` has body
16128        // `` `(list a b c d e) `` — a `Sexp::Quasiquote` wrapping a
16129        // list of 6 nodes (outer List + 5 children); the outer
16130        // quasi-quote wrapper adds one → 8 nodes total for
16131        // `def.body.node_count()`. With a ceiling of `4` this
16132        // crosses the REGISTRATION-time BODY-SIZE threshold and the
16133        // expander returns a typed `LispError::MacroBodySizeExceeded`
16134        // with `macro_name`, `size`, and `limit` all populated. Peer
16135        // to `expand_expansion_bomb_rejects_at_size_limit_bytecode_path`
16136        // one PIPELINE-STAGE axis over.
16137        let mut e = Expander::new();
16138        e.set_max_macro_body_size(4);
16139        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16140        let err = e.expand_program(forms).unwrap_err();
16141        match err {
16142            LispError::MacroBodySizeExceeded {
16143                macro_name,
16144                size,
16145                limit,
16146            } => {
16147                assert_eq!(macro_name, "huge");
16148                assert_eq!(size, 8);
16149                assert_eq!(limit, 4);
16150            }
16151            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16152        }
16153    }
16154
16155    #[test]
16156    fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
16157        // The substitute strategy shares the same
16158        // `register_macro_def` outer registration entry with the
16159        // bytecode strategy, so the REGISTRATION-time BODY-SIZE
16160        // ceiling fires at the SAME gate regardless of which
16161        // apply-layer is active. The pin makes that shared-registration
16162        // property structural: a regression that ever branches the
16163        // body-size guard on strategy (e.g. moves it into the
16164        // `compile_templates` arm alone) fails this test on the
16165        // substitute expander. Peer to
16166        // `expand_expansion_bomb_rejects_at_size_limit_substitute_path`
16167        // one PIPELINE-STAGE axis over.
16168        let mut e = Expander::new_substitute_only();
16169        e.set_max_macro_body_size(4);
16170        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16171        let err = e.expand_program(forms).unwrap_err();
16172        match err {
16173            LispError::MacroBodySizeExceeded {
16174                macro_name,
16175                size,
16176                limit,
16177            } => {
16178                assert_eq!(macro_name, "huge");
16179                assert_eq!(size, 8);
16180                assert_eq!(limit, 4);
16181            }
16182            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16183        }
16184    }
16185
16186    #[test]
16187    fn register_macro_body_at_ceiling_admits() {
16188        // `(defmacro twice (x) `(list ,x))` has body
16189        // `` `(list ,x) `` — `Sexp::Quasiquote` wrapping a 3-node
16190        // list (List + `list` atom + `,x` unquote which is
16191        // 1 + inner atom = 2 nodes) = 1 + (1 + 1 + 2) = 5 nodes.
16192        // With a ceiling of `5` the body sits AT the ceiling
16193        // (`<=` admits equality; only `>` rejects), so the
16194        // registration succeeds and the subsequent expansion
16195        // proceeds normally. Fail-before/pass-after negative
16196        // control for the body-bomb rejection tests: a regression
16197        // that ever rejected at-ceiling bodies would fail this pin.
16198        let mut e = Expander::new();
16199        e.set_max_macro_body_size(5);
16200        let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
16201        let out = e.expand_program(forms).unwrap();
16202        assert_eq!(out[0], parse("(list hey)"));
16203    }
16204
16205    #[test]
16206    fn register_failure_leaves_both_tables_pristine() {
16207        // A rejected registration MUST leave BOTH `self.macros`
16208        // AND `self.templates` exactly as they were — no
16209        // partial-write window in which one table carries the
16210        // over-ceiling entry while the other does not, or in
16211        // which the template pre-compile already ran. This pin
16212        // catches a regression that ever moves the body-size
16213        // gate below `self.templates.insert` or below
16214        // `self.macros.insert`.
16215        let mut e = Expander::new();
16216        e.set_max_macro_body_size(4);
16217        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16218        assert!(e.expand_program(forms).is_err());
16219        assert!(
16220            !e.has("huge"),
16221            "macros table must stay pristine after rejection"
16222        );
16223        assert_eq!(e.len(), 0);
16224    }
16225
16226    #[test]
16227    fn macro_body_size_exceeded_position_is_none() {
16228        // The variant joins the non-positional cohort — the
16229        // offending macro's byte offset is not what a body-bomb
16230        // diagnostic anchors to; the (offending macro name,
16231        // observed body size, configured ceiling) triple is.
16232        // This pin keeps the variant in the `position() -> None`
16233        // cohort so a future span-carrying edit lands the field
16234        // in ONE place across every non-positional variant
16235        // simultaneously. Peer to
16236        // `expansion_{depth,size}_exceeded_position_is_none` one
16237        // PIPELINE-STAGE axis over.
16238        let err = LispError::MacroBodySizeExceeded {
16239            macro_name: "huge".to_string(),
16240            size: 512,
16241            limit: 256,
16242        };
16243        assert_eq!(err.position(), None);
16244    }
16245
16246    #[test]
16247    fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
16248        // Display projection carries `macro_name`, `size`, AND
16249        // `limit`, so authoring surfaces that substring-grep the
16250        // rendered diagnostic (`tatara-check`, REPL, LSP) see
16251        // ALL THREE halves at the variant boundary and never need
16252        // to substring-parse free-form text to recover the
16253        // identity of the offending macro, the observed body
16254        // size, or the ceiling it breached. Peer to
16255        // `expansion_size_exceeded_display_matches_typed_variant_shape`
16256        // one PIPELINE-STAGE axis over.
16257        let err = LispError::MacroBodySizeExceeded {
16258            macro_name: "huge".to_string(),
16259            size: 512,
16260            limit: 256,
16261        };
16262        let rendered = err.to_string();
16263        assert!(rendered.contains("huge"), "rendered: {rendered}");
16264        assert!(rendered.contains("512"), "rendered: {rendered}");
16265        assert!(rendered.contains("256"), "rendered: {rendered}");
16266        assert!(
16267            rendered.contains("macro body size exceeded"),
16268            "rendered: {rendered}"
16269        );
16270    }
16271
16272    #[test]
16273    fn lawful_macro_body_within_ceiling_registers_and_expands() {
16274        // A lawful hand-authored macro body sits well below any
16275        // realistic ceiling — the default `DEFAULT_MAX_MACRO_BODY_SIZE`
16276        // is deliberately generous so operators never brush it on
16277        // real authoring workloads. This pin locks the default
16278        // ceiling's non-interference posture: a regression that
16279        // ever lowered the default enough to reject the reference
16280        // `(defmacro when (cond x) `(if ,cond ,x))` shape would
16281        // fail here.
16282        let mut e = Expander::new();
16283        let forms = read(
16284            "(defmacro when (cond x) `(if ,cond ,x))
16285             (when #t hey)",
16286        )
16287        .unwrap();
16288        let out = e.expand_program(forms).unwrap();
16289        assert_eq!(out[0], parse("(if #t hey)"));
16290    }
16291
16292    #[test]
16293    fn register_macro_def_direct_call_respects_body_size_ceiling() {
16294        // `register_macro_def` is the substrate primitive both
16295        // `expand_program`'s `defmacro` recognition AND
16296        // `with_macros`'s bulk load route through. The body-size
16297        // ceiling fires on the primitive itself, not on any one
16298        // consumer — a regression that ever moved the gate into
16299        // `expand_program`'s arm alone would fail this pin.
16300        let mut e = Expander::new();
16301        e.set_max_macro_body_size(4);
16302        let def = MacroDef {
16303            name: "huge".to_string(),
16304            params: MacroParams {
16305                required: vec!["x".to_string()],
16306                optional: Vec::new(),
16307                rest: None,
16308            },
16309            body: Sexp::List(vec![
16310                Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
16311                Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
16312                Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
16313                Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
16314                Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
16315            ]),
16316        };
16317        // Body node_count: List(5 atoms) = 1 + 5 = 6 nodes.
16318        let err = e.register_macro_def(def).unwrap_err();
16319        match err {
16320            LispError::MacroBodySizeExceeded {
16321                macro_name,
16322                size,
16323                limit,
16324            } => {
16325                assert_eq!(macro_name, "huge");
16326                assert_eq!(size, 6);
16327                assert_eq!(limit, 4);
16328            }
16329            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16330        }
16331        assert!(!e.has("huge"));
16332    }
16333
16334    // ── max_registered_macros: REGISTRATION-time table-count ceiling ──
16335    // Peer to body-size (REGISTER-time SIZE) one RESOURCE-DIMENSION
16336    // axis over — where body-size bounds a per-registration SIZE,
16337    // this ceiling bounds cumulative registration COUNT. Peer to
16338    // cache-entries (EXPAND-time COUNT) one PIPELINE-STAGE axis over
16339    // — the two entry-count guards close the two pipeline stages
16340    // symmetrically. Closes the expander's RESOURCE surface at a
16341    // FIFTH typed dimension across two pipeline stages: the
16342    // REGISTER-time face is now a two-dimensional (size × count)
16343    // closure symmetric with the EXPAND-time (depth + count + size)
16344    // triple.
16345
16346    #[test]
16347    fn expander_default_max_registered_macros_is_the_module_constant() {
16348        // Both constructors seed `max_registered_macros` from the
16349        // module constant — a regression that drifts one
16350        // constructor's default from the constant (e.g. a hardcoded
16351        // `4096` at ONE site with the other left uninitialized
16352        // after a `Default` derive addition) fails this pin. Peer
16353        // to the four prior default-coherence pins on depth,
16354        // cache-entries, expansion-size, and body-size.
16355        assert_eq!(
16356            Expander::new().max_registered_macros(),
16357            DEFAULT_MAX_REGISTERED_MACROS
16358        );
16359        assert_eq!(
16360            Expander::new_substitute_only().max_registered_macros(),
16361            DEFAULT_MAX_REGISTERED_MACROS
16362        );
16363        assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
16364    }
16365
16366    #[test]
16367    fn set_max_registered_macros_takes_effect_on_subsequent_register() {
16368        // Setter mirrors accessor — a regression that ever forgets
16369        // to write through to the field, or writes to a shadow
16370        // field the accessor does not read, fails this pin. Peer
16371        // to the four prior setter/accessor coherence pins.
16372        let mut e = Expander::new();
16373        e.set_max_registered_macros(64);
16374        assert_eq!(e.max_registered_macros(), 64);
16375    }
16376
16377    #[test]
16378    fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
16379        // With a ceiling of `2`, the first two fresh registrations
16380        // land in the table and the third fresh key rejects with
16381        // `LispError::RegisteredMacrosExceeded { macro_name: "c",
16382        // count: 2, limit: 2 }`. `count` is the pre-insert table
16383        // length (which equals `limit` at rejection in the common
16384        // case); `macro_name` is the OFFENDING call's name (the
16385        // fresh key we could not admit), not the outermost caller.
16386        let mut e = Expander::new();
16387        e.set_max_registered_macros(2);
16388        let forms = read(
16389            "(defmacro a (x) `(list ,x))
16390             (defmacro b (x) `(list ,x))
16391             (defmacro c (x) `(list ,x))",
16392        )
16393        .unwrap();
16394        let err = e.expand_program(forms).unwrap_err();
16395        match err {
16396            LispError::RegisteredMacrosExceeded {
16397                macro_name,
16398                count,
16399                limit,
16400            } => {
16401                assert_eq!(macro_name, "c");
16402                assert_eq!(count, 2);
16403                assert_eq!(limit, 2);
16404            }
16405            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16406        }
16407        // First two admitted; third rejected leaves the table at
16408        // its pre-third-call size.
16409        assert!(e.has("a"));
16410        assert!(e.has("b"));
16411        assert!(!e.has("c"));
16412        assert_eq!(e.len(), 2);
16413    }
16414
16415    #[test]
16416    fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
16417        // The substitute strategy shares the same
16418        // `register_macro_def` outer registration entry with the
16419        // bytecode strategy, so the REGISTRATION-time TABLE-COUNT
16420        // ceiling fires at the SAME gate regardless of which
16421        // apply-layer is active. The pin makes that shared-
16422        // registration property structural: a regression that ever
16423        // branches the table-count guard on strategy (e.g. moves
16424        // it into the `compile_templates` arm alone) fails this
16425        // test on the substitute expander. Peer to the four prior
16426        // shared-registration pins.
16427        let mut e = Expander::new_substitute_only();
16428        e.set_max_registered_macros(2);
16429        let forms = read(
16430            "(defmacro a (x) `(list ,x))
16431             (defmacro b (x) `(list ,x))
16432             (defmacro c (x) `(list ,x))",
16433        )
16434        .unwrap();
16435        let err = e.expand_program(forms).unwrap_err();
16436        match err {
16437            LispError::RegisteredMacrosExceeded {
16438                macro_name,
16439                count,
16440                limit,
16441            } => {
16442                assert_eq!(macro_name, "c");
16443                assert_eq!(count, 2);
16444                assert_eq!(limit, 2);
16445            }
16446            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16447        }
16448        assert!(e.has("a"));
16449        assert!(e.has("b"));
16450        assert!(!e.has("c"));
16451        assert_eq!(e.len(), 2);
16452    }
16453
16454    #[test]
16455    fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
16456        // OVERWRITES (a re-registration of an already-registered
16457        // key) do not grow the table and MUST be admitted at any
16458        // ceiling — operators redefining a macro at capacity is a
16459        // legitimate authoring pattern. This pin catches a
16460        // regression that ever treats overwrites the same as fresh
16461        // keys (a `contains_key` check that inverts, or a
16462        // `set_max_registered_macros(0)` that would reject even
16463        // pre-registered overwrites).
16464        let mut e = Expander::new();
16465        // Land two macros with a permissive ceiling.
16466        e.expand_program(
16467            read(
16468                "(defmacro a (x) `(list ,x))
16469                 (defmacro b (x) `(list ,x))",
16470            )
16471            .unwrap(),
16472        )
16473        .unwrap();
16474        assert_eq!(e.len(), 2);
16475        // Drop the ceiling to exactly the current size — a fresh
16476        // third key would now reject, but an overwrite of `a` MUST
16477        // admit.
16478        e.set_max_registered_macros(2);
16479        let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
16480        e.expand_program(forms).unwrap();
16481        assert_eq!(e.len(), 2, "overwrite must not grow the table");
16482        // Re-expand a call to `a` — the NEW definition (two
16483        // required params) is active, and the OLD definition (one
16484        // required param) is gone. Under the new definition,
16485        // `(a 1 2)` expands to `(pair 1 2)`.
16486        let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
16487        assert_eq!(expanded[0], parse("(pair foo bar)"));
16488    }
16489
16490    #[test]
16491    fn register_at_table_ceiling_admits_up_to_but_not_past() {
16492        // Fail-before/pass-after negative control on the exact
16493        // boundary: with a ceiling of `3`, exactly three fresh keys
16494        // admit and the fourth rejects. A regression that ever
16495        // shifted the boundary by one (e.g. keyed on `>` instead of
16496        // `>=`, or checked `len() > cap` before insert instead of
16497        // `len() >= cap`) would fail one of the two arms — either
16498        // the third admission or the fourth rejection.
16499        let mut e = Expander::new();
16500        e.set_max_registered_macros(3);
16501        // Three fresh keys admit.
16502        e.expand_program(
16503            read(
16504                "(defmacro a (x) `(list ,x))
16505                 (defmacro b (x) `(list ,x))
16506                 (defmacro c (x) `(list ,x))",
16507            )
16508            .unwrap(),
16509        )
16510        .unwrap();
16511        assert_eq!(e.len(), 3);
16512        // Fourth fresh key rejects.
16513        let err = e
16514            .expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
16515            .unwrap_err();
16516        match err {
16517            LispError::RegisteredMacrosExceeded {
16518                macro_name,
16519                count,
16520                limit,
16521            } => {
16522                assert_eq!(macro_name, "d");
16523                assert_eq!(count, 3);
16524                assert_eq!(limit, 3);
16525            }
16526            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16527        }
16528        // Table stays at pre-rejection size — no partial write.
16529        assert_eq!(e.len(), 3);
16530        assert!(!e.has("d"));
16531    }
16532
16533    #[test]
16534    fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
16535        // A rejected registration MUST leave BOTH `self.macros` AND
16536        // `self.templates` exactly as they were — no partial-write
16537        // window in which one table carries the over-ceiling entry
16538        // while the other does not, or in which the template
16539        // pre-compile already ran. Table-count gate fires BEFORE
16540        // body-size gate walks the body AND BEFORE any insert lands,
16541        // so a regression that ever moved the table-count gate
16542        // below `self.templates.insert` or below
16543        // `self.macros.insert` would fail this pin. Peer to
16544        // `register_failure_leaves_both_tables_pristine` on the
16545        // body-size axis.
16546        let mut e = Expander::new();
16547        e.set_max_registered_macros(1);
16548        // Land one macro at ceiling.
16549        e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16550            .unwrap();
16551        assert!(e.has("a"));
16552        // Reject a fresh second key.
16553        let err = e
16554            .expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
16555            .unwrap_err();
16556        assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
16557        // Table stays at pre-rejection state — `b` is NOT present
16558        // in either self.macros OR self.templates.
16559        assert_eq!(e.len(), 1);
16560        assert!(e.has("a"));
16561        assert!(!e.has("b"));
16562        // Expanding a call to `a` still works (templates table for
16563        // `a` untouched by the failed `b` registration).
16564        let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
16565        assert_eq!(expanded[0], parse("(list hey)"));
16566    }
16567
16568    #[test]
16569    fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
16570        // Zero ceiling is the "contract-test that this expander
16571        // accepts no new macros" posture — every fresh key rejects
16572        // at the first `defmacro` because `0 >= 0`. Peer to the
16573        // depth-ceiling zero-admits-and-rejects-every-macro-call
16574        // pin one RESOURCE axis over.
16575        let mut e = Expander::new();
16576        e.set_max_registered_macros(0);
16577        let err = e
16578            .expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16579            .unwrap_err();
16580        match err {
16581            LispError::RegisteredMacrosExceeded {
16582                macro_name,
16583                count,
16584                limit,
16585            } => {
16586                assert_eq!(macro_name, "a");
16587                assert_eq!(count, 0);
16588                assert_eq!(limit, 0);
16589            }
16590            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16591        }
16592        assert!(e.is_empty());
16593    }
16594
16595    #[test]
16596    fn registered_macros_exceeded_position_is_none() {
16597        // The variant joins the non-positional cohort — the
16598        // offending macro's byte offset is not what a table-count
16599        // diagnostic anchors to; the (offending fresh macro name,
16600        // observed table count, configured ceiling) triple is.
16601        // This pin keeps the variant in the `position() -> None`
16602        // cohort so a future span-carrying edit lands the field
16603        // in ONE place across every non-positional variant
16604        // simultaneously. Peer to the four prior
16605        // `*_exceeded_position_is_none` pins on the resource-
16606        // ceiling cohort.
16607        let err = LispError::RegisteredMacrosExceeded {
16608            macro_name: "fresh-1000".to_string(),
16609            count: 4096,
16610            limit: 4096,
16611        };
16612        assert_eq!(err.position(), None);
16613    }
16614
16615    #[test]
16616    fn registered_macros_exceeded_display_matches_typed_variant_shape() {
16617        // Display projection carries `macro_name`, `count`, AND
16618        // `limit`, so authoring surfaces that substring-grep the
16619        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16620        // THREE halves at the variant boundary and never need to
16621        // substring-parse free-form text to recover the identity
16622        // of the offending fresh key, the observed table count, or
16623        // the ceiling it breached. Peer to the four prior
16624        // `*_exceeded_display_matches_typed_variant_shape` pins.
16625        let err = LispError::RegisteredMacrosExceeded {
16626            macro_name: "fresh-1000".to_string(),
16627            count: 4096,
16628            limit: 4096,
16629        };
16630        let rendered = err.to_string();
16631        assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
16632        assert!(rendered.contains("4096"), "rendered: {rendered}");
16633        assert!(
16634            rendered.contains("registered macros count exceeded"),
16635            "rendered: {rendered}"
16636        );
16637    }
16638
16639    #[test]
16640    fn lawful_typescape_within_ceiling_registers_and_expands() {
16641        // A lawful hand-authored typescape (a handful of macros)
16642        // sits well below any realistic ceiling — the default
16643        // `DEFAULT_MAX_REGISTERED_MACROS` is deliberately generous
16644        // so operators never brush it on real authoring workloads.
16645        // This pin locks the default ceiling's non-interference
16646        // posture: a regression that ever lowered the default
16647        // enough to reject a small typescape would fail here. Peer
16648        // to `lawful_macro_body_within_ceiling_registers_and_expands`.
16649        let mut e = Expander::new();
16650        let forms = read(
16651            "(defmacro when (cond x) `(if ,cond ,x))
16652             (defmacro twice (x) `(list ,x ,x))
16653             (defmacro pair (x y) `(list ,x ,y))
16654             (when #t (twice hey))",
16655        )
16656        .unwrap();
16657        let out = e.expand_program(forms).unwrap();
16658        assert_eq!(out[0], parse("(if #t (list hey hey))"));
16659        assert_eq!(e.len(), 3);
16660    }
16661
16662    #[test]
16663    fn register_macro_def_direct_call_respects_table_ceiling() {
16664        // `register_macro_def` is the substrate primitive both
16665        // `expand_program`'s `defmacro` recognition AND
16666        // `with_macros`'s bulk load route through. The table-count
16667        // ceiling fires on the primitive itself, not on any one
16668        // consumer — a regression that ever moved the gate into
16669        // `expand_program`'s arm alone would fail this pin. Peer
16670        // to `register_macro_def_direct_call_respects_body_size_ceiling`
16671        // one RESOURCE-DIMENSION axis over.
16672        let mut e = Expander::new();
16673        e.set_max_registered_macros(1);
16674        let def_a = MacroDef {
16675            name: "a".to_string(),
16676            params: MacroParams {
16677                required: vec!["x".to_string()],
16678                optional: Vec::new(),
16679                rest: None,
16680            },
16681            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16682        };
16683        let def_b = MacroDef {
16684            name: "b".to_string(),
16685            params: MacroParams {
16686                required: vec!["x".to_string()],
16687                optional: Vec::new(),
16688                rest: None,
16689            },
16690            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16691        };
16692        // First admits — table sits at ceiling.
16693        e.register_macro_def(def_a).unwrap();
16694        assert!(e.has("a"));
16695        assert_eq!(e.len(), 1);
16696        // Second rejects — fresh key past ceiling.
16697        let err = e.register_macro_def(def_b).unwrap_err();
16698        match err {
16699            LispError::RegisteredMacrosExceeded {
16700                macro_name,
16701                count,
16702                limit,
16703            } => {
16704                assert_eq!(macro_name, "b");
16705                assert_eq!(count, 1);
16706                assert_eq!(limit, 1);
16707            }
16708            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16709        }
16710        assert!(!e.has("b"));
16711        assert_eq!(e.len(), 1);
16712    }
16713
16714    #[test]
16715    fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
16716        // Direct `register_macro_def` calls must ALSO admit
16717        // overwrites at ceiling — the overwrite discipline lives
16718        // on the primitive, not on `expand_program`'s arm alone.
16719        // Sibling of the source-level overwrite admission test.
16720        let mut e = Expander::new();
16721        e.set_max_registered_macros(1);
16722        let def_a_v1 = MacroDef {
16723            name: "a".to_string(),
16724            params: MacroParams {
16725                required: vec!["x".to_string()],
16726                optional: Vec::new(),
16727                rest: None,
16728            },
16729            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16730        };
16731        let def_a_v2 = MacroDef {
16732            name: "a".to_string(),
16733            params: MacroParams {
16734                required: vec!["x".to_string(), "y".to_string()],
16735                optional: Vec::new(),
16736                rest: None,
16737            },
16738            body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
16739        };
16740        e.register_macro_def(def_a_v1).unwrap();
16741        assert_eq!(e.len(), 1);
16742        // Overwrite of `a` at ceiling admits — no growth.
16743        e.register_macro_def(def_a_v2).unwrap();
16744        assert_eq!(e.len(), 1);
16745    }
16746
16747    // ── `MacroParams::total_arity` — the total-slots projection ──
16748    //
16749    // `total_arity()` names `required.len() + optional.len() +
16750    // rest.is_some() as usize` on the typed `MacroParams` algebra —
16751    // the `Vec`-free peer of `names().len()` AND the axis both
16752    // `MacroParams::bind`'s `Vec::with_capacity` hint AND the
16753    // REGISTER-time arity-ceiling gate consult. These pins cover:
16754    // structural identity vs. `names().len()`, the
16755    // `fixed_arity + rest.is_some() as usize` decomposition, and the
16756    // three canonical shapes (empty, all-required, mixed with rest).
16757    #[test]
16758    fn macro_params_total_arity_matches_names_len() {
16759        // The structural identity `total_arity() == names().len()`
16760        // — the `Vec`-free primitive's projection equals the flat-
16761        // index list's length. A regression that ever miscounts
16762        // one slot (drops rest, double-counts optional) fails this
16763        // pin. Sibling to `fixed_arity`'s peer-arithmetic pins.
16764        let params = MacroParams {
16765            required: vec!["a".into(), "b".into()],
16766            optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
16767            rest: Some("e".into()),
16768        };
16769        assert_eq!(params.total_arity(), params.names().len());
16770        assert_eq!(params.total_arity(), 5);
16771    }
16772
16773    #[test]
16774    fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
16775        // The `fixed_arity + rest.is_some() as usize` decomposition
16776        // — pins the composition on the [`MacroParams`] algebra so
16777        // a future consumer that wants "total slots" via
16778        // `fixed_arity + rest?` binds to the same arithmetic
16779        // `total_arity` names.
16780        let rest_none = MacroParams {
16781            required: vec!["a".into()],
16782            optional: vec![OptionalParam::bare("b")],
16783            rest: None,
16784        };
16785        assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
16786        assert_eq!(rest_none.total_arity(), 2);
16787        let rest_some = MacroParams {
16788            required: vec!["a".into()],
16789            optional: vec![OptionalParam::bare("b")],
16790            rest: Some("r".into()),
16791        };
16792        assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
16793        assert_eq!(rest_some.total_arity(), 3);
16794    }
16795
16796    #[test]
16797    fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
16798        // Nullary macro edge: `(defmacro nullary () BODY)` has
16799        // `total_arity() == 0`. Under a `set_max_macro_arity(0)`
16800        // ceiling the arity gate keys on `>` (equality admits), so
16801        // this nullary registration succeeds while any non-nullary
16802        // one rejects — the exact contract the arity-gate
16803        // zero-admits-nullary test below relies on.
16804        let params = MacroParams::default();
16805        assert_eq!(params.total_arity(), 0);
16806    }
16807
16808    // ── max_macro_arity: REGISTRATION-time param-count ceiling ──
16809    // Peer to body-size (REGISTER-time BODY SIZE) one RESOURCE-
16810    // DIMENSION axis over — where body-size bounds a per-registration
16811    // BODY node count, this ceiling bounds a per-registration PARAM
16812    // slot count. Peer to registered-macros (REGISTER-time TABLE
16813    // COUNT) one RESOURCE-DIMENSION axis over — where
16814    // registered-macros bounds cumulative registration COUNT, this
16815    // ceiling bounds a per-registration PARAM-LIST WIDTH. Together
16816    // the three REGISTER-time ceilings close the (per-body SIZE,
16817    // per-body ARITY, per-table COUNT) three-corner surface.
16818
16819    #[test]
16820    fn expander_default_max_macro_arity_is_the_module_constant() {
16821        // Both constructors seed `max_macro_arity` from the module
16822        // constant — a regression that drifts one constructor's
16823        // default from the constant (e.g. a hardcoded `128` at ONE
16824        // site with the other left uninitialized after a `Default`
16825        // derive addition) fails this pin. Peer to the five prior
16826        // default-coherence pins on depth, cache-entries,
16827        // expansion-size, body-size, and registered-macros.
16828        assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
16829        assert_eq!(
16830            Expander::new_substitute_only().max_macro_arity(),
16831            DEFAULT_MAX_MACRO_ARITY
16832        );
16833        assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
16834    }
16835
16836    #[test]
16837    fn set_max_macro_arity_takes_effect_on_subsequent_register() {
16838        // Setter mirrors accessor — a regression that ever forgets to
16839        // write through to the field, or writes to a shadow field the
16840        // accessor does not read, fails this pin. Peer to the five
16841        // prior setter/accessor coherence pins.
16842        let mut e = Expander::new();
16843        e.set_max_macro_arity(16);
16844        assert_eq!(e.max_macro_arity(), 16);
16845    }
16846
16847    #[test]
16848    fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
16849        // `(defmacro huge (a b c) `,a)` declares 3 required params.
16850        // Under `set_max_macro_arity(2)` the arity gate fires:
16851        // 3 > 2 → `LispError::MacroArityExceeded { macro_name: "huge",
16852        // arity: 3, limit: 2 }`. `arity` is the observed
16853        // `def.params.total_arity()`, NOT `required.len()` alone —
16854        // even an optional-only or rest-only overrun trips the gate
16855        // (pinned separately). Peer to the register-time body-size
16856        // rejection tests one RESOURCE-DIMENSION axis over.
16857        let mut e = Expander::new();
16858        e.set_max_macro_arity(2);
16859        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16860        let err = e.expand_program(forms).unwrap_err();
16861        match err {
16862            LispError::MacroArityExceeded {
16863                macro_name,
16864                arity,
16865                limit,
16866            } => {
16867                assert_eq!(macro_name, "huge");
16868                assert_eq!(arity, 3);
16869                assert_eq!(limit, 2);
16870            }
16871            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16872        }
16873        assert!(!e.has("huge"));
16874    }
16875
16876    #[test]
16877    fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
16878        // The substitute strategy shares the same
16879        // `register_macro_def` outer registration entry with the
16880        // bytecode strategy, so the REGISTRATION-time ARITY ceiling
16881        // fires at the SAME gate regardless of which apply-layer is
16882        // active. Peer to
16883        // `register_arity_bomb_rejects_at_arity_limit_bytecode_path`.
16884        let mut e = Expander::new_substitute_only();
16885        e.set_max_macro_arity(2);
16886        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16887        let err = e.expand_program(forms).unwrap_err();
16888        match err {
16889            LispError::MacroArityExceeded {
16890                macro_name,
16891                arity,
16892                limit,
16893            } => {
16894                assert_eq!(macro_name, "huge");
16895                assert_eq!(arity, 3);
16896                assert_eq!(limit, 2);
16897            }
16898            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16899        }
16900        assert!(!e.has("huge"));
16901    }
16902
16903    #[test]
16904    fn register_arity_gate_counts_optional_slots() {
16905        // `(defmacro foo (a &optional b c) `,a)` has 1 required + 2
16906        // optional + 0 rest = arity 3 via `total_arity`. Under a
16907        // ceiling of `2` the gate rejects, matching the required-only
16908        // arity-bomb — the arity axis is `total_arity()`, not
16909        // `required.len()`. A regression that drops the optional slots
16910        // from the arity computation would ADMIT this shape and fail
16911        // this pin.
16912        let mut e = Expander::new();
16913        e.set_max_macro_arity(2);
16914        let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
16915        let err = e.expand_program(forms).unwrap_err();
16916        match err {
16917            LispError::MacroArityExceeded {
16918                macro_name,
16919                arity,
16920                limit,
16921            } => {
16922                assert_eq!(macro_name, "foo");
16923                assert_eq!(arity, 3);
16924                assert_eq!(limit, 2);
16925            }
16926            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16927        }
16928    }
16929
16930    #[test]
16931    fn register_arity_gate_counts_rest_slot() {
16932        // `(defmacro spread (a b &rest r) `,a)` has 2 required + 0
16933        // optional + 1 rest = arity 3 via `total_arity`. Under a
16934        // ceiling of `2` the gate rejects, matching the required-only
16935        // arity-bomb — the arity axis includes the rest slot as ONE
16936        // named param, not zero. A regression that drops the rest slot
16937        // from the arity computation would ADMIT this shape and fail
16938        // this pin.
16939        let mut e = Expander::new();
16940        e.set_max_macro_arity(2);
16941        let forms = read("(defmacro spread (a b &rest r) `,a)").unwrap();
16942        let err = e.expand_program(forms).unwrap_err();
16943        match err {
16944            LispError::MacroArityExceeded {
16945                macro_name,
16946                arity,
16947                limit,
16948            } => {
16949                assert_eq!(macro_name, "spread");
16950                assert_eq!(arity, 3);
16951                assert_eq!(limit, 2);
16952            }
16953            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16954        }
16955    }
16956
16957    #[test]
16958    fn register_arity_at_ceiling_admits() {
16959        // `(defmacro pair (x y) `(list ,x ,y))` has 2 required = arity
16960        // 2 via `total_arity`. Under a ceiling of `2` the arity gate
16961        // keys on `>` (equality admits; only strict overrun rejects),
16962        // so the registration succeeds and the expansion proceeds
16963        // normally. Fail-before/pass-after negative control on the
16964        // exact boundary: a regression that ever shifted the boundary
16965        // by one (e.g. keyed on `>=` instead of `>`) would fail this
16966        // pin. Peer to `register_macro_body_at_ceiling_admits` on the
16967        // body-size axis.
16968        let mut e = Expander::new();
16969        e.set_max_macro_arity(2);
16970        let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
16971        let out = e.expand_program(forms).unwrap();
16972        assert_eq!(out[0], parse("(list a b)"));
16973        assert_eq!(e.len(), 1);
16974    }
16975
16976    #[test]
16977    fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
16978        // A rejected arity-gated registration MUST leave BOTH
16979        // `self.macros` AND `self.templates` exactly as they were —
16980        // no partial-write window in which one table carries the
16981        // over-ceiling entry while the other does not, or in which
16982        // the template pre-compile already ran. Arity gate fires
16983        // BEFORE the body-size gate walks the body AND BEFORE any
16984        // insert lands, so a regression that ever moved the arity
16985        // gate below `self.templates.insert` or below
16986        // `self.macros.insert` would fail this pin. Peer to
16987        // `register_failure_leaves_both_tables_pristine` (body-size)
16988        // and `register_table_ceiling_leaves_both_tables_pristine_on_rejection`
16989        // (table-count).
16990        let mut e = Expander::new();
16991        e.set_max_macro_arity(1);
16992        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16993        assert!(e.expand_program(forms).is_err());
16994        assert!(
16995            !e.has("huge"),
16996            "macros table must stay pristine after rejection"
16997        );
16998        assert_eq!(e.len(), 0);
16999        // Expanding a NEW lawful macro afterwards still works — the
17000        // failed arity-gate rejection has not corrupted expander
17001        // state.
17002        let out = e
17003            .expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
17004            .unwrap();
17005        assert_eq!(out[0], parse("hey"));
17006    }
17007
17008    #[test]
17009    fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
17010        // Zero ceiling is the "contract-test that this expander
17011        // accepts only nullary macros" posture — every fresh
17012        // non-nullary key rejects at the arity gate because
17013        // `total_arity() > 0`, while nullary `(defmacro nullary ()
17014        // BODY)` still admits (its arity is exactly 0, and only
17015        // strict overrun rejects). Peer to the body-size zero-rejects-
17016        // every-non-empty-body pin one RESOURCE-DIMENSION axis over
17017        // (though body-size zero rejects even nullary-body shapes,
17018        // since `Sexp::Nil::node_count() == 1 > 0`; arity zero
17019        // preserves the nullary registration surface).
17020        let mut e = Expander::new();
17021        e.set_max_macro_arity(0);
17022        // Nullary admits — arity 0 <= 0.
17023        e.expand_program(read("(defmacro nullary () `unit)").unwrap())
17024            .unwrap();
17025        assert!(e.has("nullary"));
17026        // Non-nullary rejects — arity 1 > 0.
17027        let err = e
17028            .expand_program(read("(defmacro id (x) `,x)").unwrap())
17029            .unwrap_err();
17030        match err {
17031            LispError::MacroArityExceeded {
17032                macro_name,
17033                arity,
17034                limit,
17035            } => {
17036                assert_eq!(macro_name, "id");
17037                assert_eq!(arity, 1);
17038                assert_eq!(limit, 0);
17039            }
17040            other => panic!("expected MacroArityExceeded, got: {other:?}"),
17041        }
17042        assert!(!e.has("id"));
17043    }
17044
17045    #[test]
17046    fn macro_arity_exceeded_position_is_none() {
17047        // The variant joins the non-positional cohort — the offending
17048        // macro's byte offset is not what an arity-bomb diagnostic
17049        // anchors to; the (macro name, observed arity, configured
17050        // ceiling) triple is. This pin keeps the variant in the
17051        // `position() -> None` cohort so a future span-carrying edit
17052        // lands the field in ONE place across every non-positional
17053        // variant simultaneously. Peer to the five prior
17054        // `*_exceeded_position_is_none` pins on the resource-ceiling
17055        // cohort.
17056        let err = LispError::MacroArityExceeded {
17057            macro_name: "huge".to_string(),
17058            arity: 512,
17059            limit: 128,
17060        };
17061        assert_eq!(err.position(), None);
17062    }
17063
17064    #[test]
17065    fn macro_arity_exceeded_display_matches_typed_variant_shape() {
17066        // Display projection carries `macro_name`, `arity`, AND
17067        // `limit`, so authoring surfaces that substring-grep the
17068        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
17069        // THREE halves at the variant boundary and never need to
17070        // substring-parse free-form text to recover the identity of
17071        // the offending macro, the observed arity, or the ceiling it
17072        // breached. Peer to the five prior
17073        // `*_exceeded_display_matches_typed_variant_shape` pins.
17074        let err = LispError::MacroArityExceeded {
17075            macro_name: "huge".to_string(),
17076            arity: 512,
17077            limit: 128,
17078        };
17079        let rendered = err.to_string();
17080        assert!(rendered.contains("huge"), "rendered: {rendered}");
17081        assert!(rendered.contains("512"), "rendered: {rendered}");
17082        assert!(rendered.contains("128"), "rendered: {rendered}");
17083        assert!(
17084            rendered.contains("macro arity exceeded"),
17085            "rendered: {rendered}"
17086        );
17087    }
17088
17089    #[test]
17090    fn lawful_macro_arity_within_ceiling_registers_and_expands() {
17091        // A lawful hand-authored macro arity sits well below any
17092        // realistic ceiling — the default `DEFAULT_MAX_MACRO_ARITY`
17093        // is deliberately generous so operators never brush it on
17094        // real authoring workloads. This pin locks the default
17095        // ceiling's non-interference posture: a regression that
17096        // ever lowered the default enough to reject the reference
17097        // multi-slot `(defmacro when (cond x) …)` shape would fail
17098        // here. Peer to `lawful_macro_body_within_ceiling_registers_and_expands`
17099        // and `lawful_typescape_within_ceiling_registers_and_expands`.
17100        let mut e = Expander::new();
17101        let forms = read(
17102            "(defmacro when (cond x) `(if ,cond ,x))
17103             (defmacro triple (x y z) `(list ,x ,y ,z))
17104             (when #t (triple a b c))",
17105        )
17106        .unwrap();
17107        let out = e.expand_program(forms).unwrap();
17108        assert_eq!(out[0], parse("(if #t (list a b c))"));
17109    }
17110
17111    #[test]
17112    fn register_macro_def_direct_call_respects_arity_ceiling() {
17113        // `register_macro_def` is the substrate primitive both
17114        // `expand_program`'s `defmacro` recognition AND `with_macros`'s
17115        // bulk load route through. The arity ceiling fires on the
17116        // primitive itself, not on any one consumer — a regression
17117        // that ever moved the gate into `expand_program`'s arm alone
17118        // would fail this pin. Peer to
17119        // `register_macro_def_direct_call_respects_body_size_ceiling`
17120        // AND `register_macro_def_direct_call_respects_table_ceiling`
17121        // one RESOURCE-DIMENSION axis over.
17122        let mut e = Expander::new();
17123        e.set_max_macro_arity(2);
17124        let def = MacroDef {
17125            name: "huge".to_string(),
17126            params: MacroParams {
17127                required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
17128                optional: Vec::new(),
17129                rest: None,
17130            },
17131            body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
17132        };
17133        // Arity 3 > ceiling 2 — rejected before body-size gate.
17134        let err = e.register_macro_def(def).unwrap_err();
17135        match err {
17136            LispError::MacroArityExceeded {
17137                macro_name,
17138                arity,
17139                limit,
17140            } => {
17141                assert_eq!(macro_name, "huge");
17142                assert_eq!(arity, 3);
17143                assert_eq!(limit, 2);
17144            }
17145            other => panic!("expected MacroArityExceeded, got: {other:?}"),
17146        }
17147        assert!(!e.has("huge"));
17148    }
17149
17150    #[test]
17151    fn arity_gate_fires_before_body_size_gate() {
17152        // ORDERING PIN — the arity gate fires BEFORE the body-size
17153        // gate. With both `set_max_macro_arity(1)` AND
17154        // `set_max_macro_body_size(2)` set below a shape that violates
17155        // BOTH ceilings — `(defmacro dual (a b) `(list ,a ,b))` has
17156        // arity 2 AND body node count > 2 — the arity gate's
17157        // rejection wins, so the returned variant is
17158        // `MacroArityExceeded`, not `MacroBodySizeExceeded`. A
17159        // regression that ever swapped the two gates' order would fail
17160        // this pin. The ordering isn't cosmetic: arity is O(1)
17161        // (three-arm addition on `def.params` fields), body-size is
17162        // O(body_nodes) (`def.body.node_count()` walks the AST), so
17163        // the O(1)-first order minimises the cost of rejecting the
17164        // worst case (a large-body large-arity macro).
17165        let mut e = Expander::new();
17166        e.set_max_macro_arity(1);
17167        e.set_max_macro_body_size(2);
17168        let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
17169        let err = e.expand_program(forms).unwrap_err();
17170        assert!(
17171            matches!(err, LispError::MacroArityExceeded { .. }),
17172            "arity gate must fire before body-size gate; got: {err:?}"
17173        );
17174    }
17175
17176    #[test]
17177    fn arity_gate_fires_after_table_count_gate() {
17178        // ORDERING PIN — the table-count gate fires BEFORE the arity
17179        // gate. Under `set_max_registered_macros(1)` at capacity AND
17180        // `set_max_macro_arity(1)`, a fresh over-arity registration
17181        // trips the table-count gate FIRST (its `HashMap::len`
17182        // comparison is O(1) cache-friendly and gates cumulative
17183        // registration cost); the arity gate is O(1) but on `def`,
17184        // not on shared state, so it sits AFTER the table-count gate
17185        // on the O(1)-first ordering of the REGISTER-time chain. A
17186        // regression that ever swapped the two would fail this pin.
17187        let mut e = Expander::new();
17188        e.set_max_registered_macros(1);
17189        e.set_max_macro_arity(1);
17190        // Land one macro at table ceiling.
17191        e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
17192            .unwrap();
17193        assert_eq!(e.len(), 1);
17194        // Fresh registration with BOTH violations — table-count wins.
17195        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17196        let err = e.expand_program(forms).unwrap_err();
17197        assert!(
17198            matches!(err, LispError::RegisteredMacrosExceeded { .. }),
17199            "table-count gate must fire before arity gate; got: {err:?}"
17200        );
17201    }
17202
17203    // ── ResourceLimits: bundled resource-ceiling snapshot ────────────
17204
17205    #[test]
17206    fn default_resource_limits_binds_each_field_to_matching_module_constant() {
17207        // Pin the (constant × aggregation) corner: the const-form
17208        // aggregate MUST bind every field to its individually-defined
17209        // module constant. A re-tuning of ONE module constant
17210        // (e.g. `DEFAULT_MAX_EXPANSION_DEPTH` raised from 256 to 512)
17211        // has to propagate through the aggregate at ONE site; this
17212        // test fires the divergence loudly if a future edit sets a
17213        // module constant to a fresh value but forgets to update the
17214        // matching aggregate slot. Field-by-field equality carries
17215        // the message better than one deep-equality assertion.
17216        assert_eq!(
17217            DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
17218            DEFAULT_MAX_EXPANSION_DEPTH
17219        );
17220        assert_eq!(
17221            DEFAULT_RESOURCE_LIMITS.max_cache_entries,
17222            DEFAULT_MAX_CACHE_ENTRIES
17223        );
17224        assert_eq!(
17225            DEFAULT_RESOURCE_LIMITS.max_expansion_size,
17226            DEFAULT_MAX_EXPANSION_SIZE
17227        );
17228        assert_eq!(
17229            DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
17230            DEFAULT_MAX_MACRO_BODY_SIZE
17231        );
17232        assert_eq!(
17233            DEFAULT_RESOURCE_LIMITS.max_registered_macros,
17234            DEFAULT_MAX_REGISTERED_MACROS
17235        );
17236        assert_eq!(
17237            DEFAULT_RESOURCE_LIMITS.max_macro_arity,
17238            DEFAULT_MAX_MACRO_ARITY
17239        );
17240    }
17241
17242    #[test]
17243    fn resource_limits_default_impl_matches_const_form() {
17244        // The two projections of the shipped posture — the `const`
17245        // form [`DEFAULT_RESOURCE_LIMITS`] and the runtime
17246        // `Default::default()` form — must reach the SAME value. Pins
17247        // that a future edit which flips one to a new default cannot
17248        // silently drift the other.
17249        assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
17250    }
17251
17252    #[test]
17253    fn expander_new_resource_limits_matches_shipped_defaults() {
17254        // Fresh-expander posture on the bundled-getter surface — the
17255        // six ceilings [`Expander::new`] seeds MUST snapshot to
17256        // [`DEFAULT_RESOURCE_LIMITS`] verbatim. Peer of the six
17257        // individual `expander_default_max_*_is_the_module_constant`
17258        // pins one AGGREGATION axis over — where each individual
17259        // pin catches ONE field drift, this pin catches ANY of the
17260        // six drifting AND the snapshot projection itself losing a
17261        // field.
17262        assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
17263    }
17264
17265    #[test]
17266    fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
17267        // Substitute-only expander seeds the SAME six ceilings from
17268        // the SAME module constants as [`Expander::new`] — the two
17269        // constructors share the resource-ceiling posture even
17270        // though they diverge on `compile_templates` +
17271        // `cache_enabled`. Pins that a future constructor which
17272        // forgets to seed one of the six ceilings from its module
17273        // constant fails loudly through the bundled getter.
17274        assert_eq!(
17275            Expander::new_substitute_only().resource_limits(),
17276            DEFAULT_RESOURCE_LIMITS
17277        );
17278    }
17279
17280    #[test]
17281    fn resource_limits_snapshot_reflects_each_individual_setter() {
17282        // Coherence pin between the six individual setters and the
17283        // bundled getter — flipping ONE ceiling via its individual
17284        // setter must project through [`Expander::resource_limits`]
17285        // to the SAME value the individual getter returns. A
17286        // regression that leaves ONE field out of the snapshot
17287        // projection (a copy-paste omission in the struct literal
17288        // constructor) fires here. Distinct low-non-default values
17289        // so a stray field carrying the default fails loudly.
17290        let mut e = Expander::new();
17291        e.set_max_expansion_depth(3);
17292        e.set_max_cache_entries(5);
17293        e.set_max_expansion_size(7);
17294        e.set_max_macro_body_size(11);
17295        e.set_max_registered_macros(13);
17296        e.set_max_macro_arity(17);
17297        let snap = e.resource_limits();
17298        assert_eq!(snap.max_expansion_depth, 3);
17299        assert_eq!(snap.max_cache_entries, 5);
17300        assert_eq!(snap.max_expansion_size, 7);
17301        assert_eq!(snap.max_macro_body_size, 11);
17302        assert_eq!(snap.max_registered_macros, 13);
17303        assert_eq!(snap.max_macro_arity, 17);
17304        // And the individual getters see the same values — the
17305        // snapshot is a projection, not a divergent copy.
17306        assert_eq!(e.max_expansion_depth(), 3);
17307        assert_eq!(e.max_cache_entries(), 5);
17308        assert_eq!(e.max_expansion_size(), 7);
17309        assert_eq!(e.max_macro_body_size(), 11);
17310        assert_eq!(e.max_registered_macros(), 13);
17311        assert_eq!(e.max_macro_arity(), 17);
17312    }
17313
17314    #[test]
17315    fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
17316        // Sibling pin to `resource_limits_snapshot_reflects_...` on the
17317        // WRITE axis — bulk-setting via [`Expander::set_resource_limits`]
17318        // must reach every individual `max_*` getter. A regression
17319        // that drops one field from the destructuring assignment in
17320        // the bulk setter fires here. Distinct low-non-default values
17321        // so a stray field carrying the previous value fails loudly.
17322        let mut e = Expander::new();
17323        e.set_resource_limits(ResourceLimits {
17324            max_expansion_depth: 2,
17325            max_cache_entries: 4,
17326            max_expansion_size: 8,
17327            max_macro_body_size: 16,
17328            max_registered_macros: 32,
17329            max_macro_arity: 64,
17330        });
17331        assert_eq!(e.max_expansion_depth(), 2);
17332        assert_eq!(e.max_cache_entries(), 4);
17333        assert_eq!(e.max_expansion_size(), 8);
17334        assert_eq!(e.max_macro_body_size(), 16);
17335        assert_eq!(e.max_registered_macros(), 32);
17336        assert_eq!(e.max_macro_arity(), 64);
17337    }
17338
17339    #[test]
17340    fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
17341        // Round-trip theorem — for any starting expander, the
17342        // composition `set_resource_limits(resource_limits())` is
17343        // the identity on the resource-ceiling surface. This is the
17344        // ONE typed statement that pins the (getter, setter) pair as
17345        // strict projections of the same six-field surface, no
17346        // hidden lossy field, no ordering asymmetry between the
17347        // struct literal and the destructuring pattern.
17348        let mut e = Expander::new();
17349        e.set_max_expansion_depth(9);
17350        e.set_max_cache_entries(19);
17351        e.set_max_expansion_size(29);
17352        e.set_max_macro_body_size(39);
17353        e.set_max_registered_macros(49);
17354        e.set_max_macro_arity(59);
17355        let snap = e.resource_limits();
17356        // Reset to defaults so any leftover state would show up in
17357        // the post-round-trip snapshot as the default rather than
17358        // the intended value.
17359        e.set_resource_limits(ResourceLimits::default());
17360        assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
17361        // Apply the captured snapshot — the expander must land at
17362        // the same six values `snap` recorded.
17363        e.set_resource_limits(snap);
17364        assert_eq!(e.resource_limits(), snap);
17365    }
17366
17367    #[test]
17368    fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
17369        // The `Copy` posture on [`ResourceLimits`] gives callers the
17370        // cheap ergonomic of a struct-update literal — take the
17371        // shipped posture and override ONE ceiling — that a chain of
17372        // six independent setters cannot express in ONE typed expression.
17373        // Pins the intended use-shape so a future refactor which
17374        // removes `Copy` from the struct fails to compile at THIS
17375        // site rather than at every downstream consumer.
17376        let mut e = Expander::new();
17377        e.set_resource_limits(ResourceLimits {
17378            max_macro_arity: 4,
17379            ..DEFAULT_RESOURCE_LIMITS
17380        });
17381        // Overridden field carries the fresh value.
17382        assert_eq!(e.max_macro_arity(), 4);
17383        // Every other field carries the shipped default — the
17384        // struct-update literal is a full projection, not a
17385        // selective mutation.
17386        assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17387        assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17388        assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17389        assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17390        assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17391    }
17392
17393    #[test]
17394    fn expander_default_derives_resource_limits_from_bundled_field_default() {
17395        // Post the [`Expander::limits`]-field consolidation the derived
17396        // `Default` on [`Expander`] projects through [`ResourceLimits`]'s
17397        // own `Default` — which returns [`DEFAULT_RESOURCE_LIMITS`] — so
17398        // `Expander::default().resource_limits()` lands at the shipped
17399        // ceiling posture verbatim. Pre-consolidation the six independent
17400        // `max_*: usize` fields defaulted to the bare `usize` zero, so
17401        // `Expander::default()` yielded a below-floor posture with every
17402        // ceiling clamped to `0` — a shape no consumer wanted and every
17403        // test path avoided by routing through [`Expander::new`] instead.
17404        // The consolidation is what turned that latent below-floor
17405        // derived-`Default` shape into a lawful shipped posture on the
17406        // structural axis. Peer of
17407        // `expander_new_resource_limits_matches_shipped_defaults` one
17408        // CONSTRUCTOR axis over — where that test pins the explicit
17409        // [`Expander::new`] constructor, this test pins the derived-
17410        // [`Default`] constructor route to the SAME six values through
17411        // the SAME bundled projection. Sibling of
17412        // `resource_limits_default_impl_matches_const_form` one
17413        // TYPE-LEVEL axis over — that test pins [`ResourceLimits::default`]
17414        // to [`DEFAULT_RESOURCE_LIMITS`]; this test pins [`Expander`]'s
17415        // derived `Default` to project through it.
17416        assert_eq!(
17417            Expander::default().resource_limits(),
17418            DEFAULT_RESOURCE_LIMITS
17419        );
17420    }
17421
17422    #[test]
17423    fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
17424        // Behavioral pin — the bundled setter is not a decorative
17425        // getter/setter pair; the six ceilings it propagates must
17426        // reach the SAME check sites the individual setters do. Set
17427        // `max_macro_arity` to 1 via the bulk setter and confirm the
17428        // arity gate fires on a two-arg macro registration. A
17429        // regression that wired the bulk setter to a shadow field
17430        // rather than the actual `Expander` state fails here.
17431        let mut e = Expander::new();
17432        e.set_resource_limits(ResourceLimits {
17433            max_macro_arity: 1,
17434            ..DEFAULT_RESOURCE_LIMITS
17435        });
17436        let forms = read("(defmacro two (a b) `,a)").unwrap();
17437        let err = e.expand_program(forms).unwrap_err();
17438        assert!(
17439            matches!(
17440                err,
17441                LispError::MacroArityExceeded {
17442                    arity: 2,
17443                    limit: 1,
17444                    ..
17445                }
17446            ),
17447            "bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
17448        );
17449    }
17450
17451    // ── Expander::with_limits — at-construction resource-posture ───────
17452
17453    #[test]
17454    fn expander_with_limits_seeds_the_provided_resource_posture() {
17455        // Round-trip pin at CONSTRUCTION time — the bundle the caller
17456        // passes into [`Expander::with_limits`] MUST project back
17457        // through [`Expander::resource_limits`] verbatim. Peer of the
17458        // `set_resource_limits(l); resource_limits() == l` round-trip
17459        // one CONSTRUCTOR-STAGE axis over — that pin covers the
17460        // POST-construction assignment; this pin covers the
17461        // AT-construction assignment. A regression that dropped the
17462        // `limits` argument on the floor (a copy-paste that left
17463        // `limits: DEFAULT_RESOURCE_LIMITS` in the struct literal
17464        // instead of `limits`) fires here on every non-default field.
17465        // Distinct low-non-default values so a stray field defaulting
17466        // fails loudly.
17467        let want = ResourceLimits {
17468            max_expansion_depth: 3,
17469            max_cache_entries: 5,
17470            max_expansion_size: 7,
17471            max_macro_body_size: 11,
17472            max_registered_macros: 13,
17473            max_macro_arity: 17,
17474        };
17475        assert_eq!(Expander::with_limits(want).resource_limits(), want);
17476    }
17477
17478    #[test]
17479    fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
17480        // Delegation identity — [`Expander::new`] delegates to
17481        // [`Expander::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`] so
17482        // the two constructors MUST agree on the resource-ceiling
17483        // projection. Pins the "no drift between the two paths to a
17484        // fresh default-posture expander" theorem: a future refactor
17485        // that breaks the delegation (an inline field literal reappearing
17486        // in [`Expander::new`], an accidental hardcoded const at ONE
17487        // site with the other unchanged) fires here. Peer of
17488        // `resource_limits_default_impl_matches_const_form` one
17489        // TYPE-LEVEL axis over — that test pins the two projections of
17490        // the shipped posture on [`ResourceLimits`] itself; this test
17491        // pins the two constructor paths on [`Expander`] to reach the
17492        // SAME six values through the SAME bundle.
17493        assert_eq!(
17494            Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
17495            Expander::new().resource_limits()
17496        );
17497    }
17498
17499    #[test]
17500    fn expander_with_limits_composes_with_struct_update_override() {
17501        // The `Copy` posture on [`ResourceLimits`] lets a caller compose
17502        // "the shipped posture with ONE ceiling overridden" as ONE
17503        // struct-update literal AND thread that composition through
17504        // [`Expander::with_limits`] as ONE typed call — the ergonomic
17505        // shape that six independent setter invocations cannot express
17506        // in ONE expression. Overridden ceiling carries the fresh value,
17507        // every other ceiling carries its shipped default. A future
17508        // refactor that removes `Copy` from [`ResourceLimits`] fails to
17509        // compile at THIS site rather than at every downstream consumer.
17510        let e = Expander::with_limits(ResourceLimits {
17511            max_macro_arity: 4,
17512            ..DEFAULT_RESOURCE_LIMITS
17513        });
17514        assert_eq!(e.max_macro_arity(), 4);
17515        assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17516        assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17517        assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17518        assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17519        assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17520    }
17521
17522    #[test]
17523    fn expander_with_limits_carries_the_new_execution_strategy() {
17524        // The (compile_templates, cache_enabled) execution-strategy pair
17525        // MUST carry the [`Expander::new`] default posture — bytecode +
17526        // cache both on — regardless of which resource posture the
17527        // caller passes in. Cache-count observation is the least-
17528        // invasive check available on the public surface: a fresh
17529        // `with_limits` expander that expands a repeat-call site must
17530        // populate at least one cache entry (bytecode + cache both on),
17531        // and a substitute-only sibling would leave `cache_size()` at
17532        // zero even after every call site expanded. A regression that
17533        // wired the wrong execution strategy into `with_limits`
17534        // (compile_templates: false, cache_enabled: false) fires here.
17535        let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
17536        let forms = read(
17537            "(defmacro id (x) `,x)
17538             (id one)
17539             (id one)",
17540        )
17541        .unwrap();
17542        e.expand_program(forms).unwrap();
17543        assert!(
17544            e.cache_size() >= 1,
17545            "with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
17546            e.cache_size()
17547        );
17548    }
17549
17550    #[test]
17551    fn expander_with_limits_fires_the_arity_gate_from_construction() {
17552        // Behavioral pin — the at-construction bundle assignment MUST
17553        // reach the SAME check sites the individual + bulk POST-
17554        // construction setters do. Construct an expander with
17555        // `max_macro_arity: 1` directly and confirm the arity gate
17556        // fires on a two-arg macro registration. Sibling of
17557        // `resource_limits_bulk_setter_keeps_the_arity_gate_in_effect`
17558        // one CONSTRUCTOR-STAGE axis over — that pin covers the POST-
17559        // construction setter reaching the gate; this pin covers the
17560        // AT-construction assignment reaching the SAME gate through
17561        // the SAME `self.limits.max_macro_arity` load. A regression
17562        // that wired `with_limits` to a shadow field rather than
17563        // `self.limits` fails here.
17564        let mut e = Expander::with_limits(ResourceLimits {
17565            max_macro_arity: 1,
17566            ..DEFAULT_RESOURCE_LIMITS
17567        });
17568        let forms = read("(defmacro two (a b) `,a)").unwrap();
17569        let err = e.expand_program(forms).unwrap_err();
17570        assert!(
17571            matches!(
17572                err,
17573                LispError::MacroArityExceeded {
17574                    arity: 2,
17575                    limit: 1,
17576                    ..
17577                }
17578            ),
17579            "at-construction arity ceiling must reach the register-time gate; got: {err:?}"
17580        );
17581    }
17582
17583    // ── UNBOUNDED_RESOURCE_LIMITS — ceiling-lifted preset posture ──────
17584
17585    #[test]
17586    fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
17587        // Field-level pin — the ceiling-lifted preset carries
17588        // [`usize::MAX`] on every axis of the six-field surface. Pinned
17589        // as six independent field asserts (rather than one struct
17590        // equality) so a drift on ONE field carries a distinct-name
17591        // failure that names the drifting axis. A future extension
17592        // that adds a SEVENTH ceiling requires an additional field
17593        // assert here in lockstep with the const literal — rustc's
17594        // field-exhaustiveness on the const forces the new field to
17595        // appear, and this pin exercises it.
17596        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
17597        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
17598        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
17599        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
17600        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
17601        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
17602    }
17603
17604    #[test]
17605    fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
17606        // Structural-distinctness pin — the two constants
17607        // [`DEFAULT_RESOURCE_LIMITS`] and [`UNBOUNDED_RESOURCE_LIMITS`]
17608        // sit at DISTINCT points on the posture axis of the (posture ×
17609        // aggregation) grid, and the disagreement is EXHAUSTIVE on the
17610        // six-field surface — every shipped `DEFAULT_MAX_*` module
17611        // constant is a concrete positive value strictly less than
17612        // [`usize::MAX`]. Pins that a future re-tuning which cranked one
17613        // shipped default up to [`usize::MAX`] (collapsing the two
17614        // presets on that field) fires here loudly rather than
17615        // silently letting the two presets become indistinguishable
17616        // on that axis. A regression which flipped ONE unbounded-const
17617        // field back to its default value also fires here — either
17618        // half of the disagreement breaks the theorem.
17619        assert_ne!(
17620            UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
17621            DEFAULT_RESOURCE_LIMITS.max_expansion_depth
17622        );
17623        assert_ne!(
17624            UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
17625            DEFAULT_RESOURCE_LIMITS.max_cache_entries
17626        );
17627        assert_ne!(
17628            UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
17629            DEFAULT_RESOURCE_LIMITS.max_expansion_size
17630        );
17631        assert_ne!(
17632            UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
17633            DEFAULT_RESOURCE_LIMITS.max_macro_body_size
17634        );
17635        assert_ne!(
17636            UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
17637            DEFAULT_RESOURCE_LIMITS.max_registered_macros
17638        );
17639        assert_ne!(
17640            UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
17641            DEFAULT_RESOURCE_LIMITS.max_macro_arity
17642        );
17643    }
17644
17645    #[test]
17646    fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
17647        // Round-trip pin at CONSTRUCTION time — an expander built at
17648        // the ceiling-lifted preset MUST project back through
17649        // [`Expander::resource_limits`] to the SAME constant verbatim.
17650        // Peer of `expander_with_limits_seeds_the_provided_resource_posture`
17651        // one PRESET-POSTURE axis over — that pin exercises the
17652        // constructor's carry via a distinct-low-value bundle; this
17653        // pin exercises it via the ceiling-lifted preset constant.
17654        // Cross-pin the six-field snapshot equals the ceiling-lifted
17655        // constant so a regression that broke either the constant OR
17656        // the constructor's carry fires here.
17657        assert_eq!(
17658            Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
17659            UNBOUNDED_RESOURCE_LIMITS
17660        );
17661    }
17662
17663    #[test]
17664    fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
17665        // Behavioral pin — the ceiling-lifted preset MUST reach the
17666        // register-time body-size gate as an actually-lifted ceiling.
17667        // Register a macro whose body node count strictly exceeds
17668        // [`DEFAULT_MAX_MACRO_BODY_SIZE`] (a body which under the
17669        // shipped default would fail [`register_macro_def`] with a
17670        // [`LispError::MacroBodySizeExceeded`]) and confirm the
17671        // registration succeeds under the ceiling-lifted preset. Only
17672        // the body-size ceiling being lifted lets a body of that
17673        // magnitude land; the arity ceiling is left at 1 param via
17674        // struct-update override to prove the preset's other five
17675        // ceilings STILL project cleanly at [`usize::MAX`] alongside
17676        // the ceiling under test. A regression that failed to lift
17677        // ONE ceiling across the preset (a stale `DEFAULT_MAX_*` field
17678        // in the const literal) fires here on the axis it dropped.
17679        let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17680        // Build a huge quasi-quoted list body: `(k k k k k … k) whose
17681        // node_count() eclipses DEFAULT_MAX_MACRO_BODY_SIZE = 16384.
17682        let mut body = String::from("(defmacro huge (k) `(");
17683        for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
17684            body.push_str(",k ");
17685        }
17686        body.push_str("))");
17687        let forms = read(&body).unwrap();
17688        e.expand_program(forms)
17689            .expect("unbounded body-size ceiling must admit a body over the default limit");
17690        assert!(e.has("huge"));
17691    }
17692
17693    #[test]
17694    fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
17695        // Behavioral pin sibling of the body-size pin above one
17696        // RESOURCE-DIMENSION axis over on the REGISTER-time surface —
17697        // the ceiling-lifted preset MUST also reach the arity gate as
17698        // an actually-lifted ceiling. Register a macro whose param
17699        // list strictly exceeds [`DEFAULT_MAX_MACRO_ARITY`] (a
17700        // 129-slot lambda list which under the shipped default fails
17701        // with [`LispError::MacroArityExceeded`]) and confirm the
17702        // registration succeeds under the ceiling-lifted preset.
17703        use std::fmt::Write as _;
17704        let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17705        let mut src = String::from("(defmacro many-arity (");
17706        for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
17707            write!(src, "a-{i} ").unwrap();
17708        }
17709        src.push_str(") `,a-0)");
17710        let forms = read(&src).unwrap();
17711        e.expand_program(forms)
17712            .expect("unbounded arity ceiling must admit a param list over the default limit");
17713        assert!(e.has("many-arity"));
17714    }
17715
17716    #[test]
17717    fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
17718        // Ergonomic pin — the `Copy` posture on [`ResourceLimits`] lets
17719        // a caller build "every ceiling lifted EXCEPT this ONE" as ONE
17720        // struct-update literal on the ceiling-lifted preset AND thread
17721        // that composition through [`Expander::with_limits`] as ONE
17722        // typed call. This is the "only THIS ceiling gates" fixture
17723        // shape a chain of six independent setters cannot express in
17724        // ONE expression, and the shape the last commit named as a
17725        // future benefit of the (getter, setter, at-construction)
17726        // three-corner face closing at ONE bundle.
17727        //
17728        // Set only `max_macro_arity: 1`; confirm the arity gate fires
17729        // on a two-arg registration while the five other lifted
17730        // ceilings project through the getter as [`usize::MAX`]. A
17731        // regression that broke the ceiling-lifted preset would EITHER
17732        // fail to project [`usize::MAX`] on one of the five other
17733        // fields OR fail to gate the isolated arity ceiling at 1.
17734        let mut e = Expander::with_limits(ResourceLimits {
17735            max_macro_arity: 1,
17736            ..UNBOUNDED_RESOURCE_LIMITS
17737        });
17738        assert_eq!(e.max_macro_arity(), 1);
17739        assert_eq!(e.max_expansion_depth(), usize::MAX);
17740        assert_eq!(e.max_cache_entries(), usize::MAX);
17741        assert_eq!(e.max_expansion_size(), usize::MAX);
17742        assert_eq!(e.max_macro_body_size(), usize::MAX);
17743        assert_eq!(e.max_registered_macros(), usize::MAX);
17744        let forms = read("(defmacro two (a b) `,a)").unwrap();
17745        let err = e.expand_program(forms).unwrap_err();
17746        assert!(
17747            matches!(
17748                err,
17749                LispError::MacroArityExceeded {
17750                    arity: 2,
17751                    limit: 1,
17752                    ..
17753                }
17754            ),
17755            "struct-update override on the ceiling-lifted preset must \
17756             leave the isolated arity ceiling gating; got: {err:?}"
17757        );
17758    }
17759
17760    #[test]
17761    fn unbounded_resource_limits_carries_through_the_bulk_setter() {
17762        // Coherence pin between the AT-construction constructor and
17763        // the POST-construction bulk setter — both paths that reach the
17764        // ceiling-lifted preset must land at the SAME six values on
17765        // the expander state. Pre-lift a caller wanting the unbounded
17766        // posture composed six individual setter invocations at the
17767        // call site; post-lift both `Expander::with_limits(UNBOUNDED_
17768        // RESOURCE_LIMITS)` and `e.set_resource_limits(UNBOUNDED_
17769        // RESOURCE_LIMITS)` reach the SAME snapshot through the SAME
17770        // constant. A regression that wired the bulk setter to a
17771        // shadow field, or the ceiling-lifted preset to a stale field
17772        // literal, fires here on the constructor-vs-setter identity.
17773        let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17774        let mut b = Expander::new();
17775        b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
17776        assert_eq!(a.resource_limits(), b.resource_limits());
17777        assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
17778    }
17779
17780    // ── ResourceLimits::strictest / ::most_permissive — meet/join ──────
17781
17782    /// A hand-authored asymmetric posture used across the lattice-law
17783    /// tests. Every field is a distinct positive value strictly less
17784    /// than [`usize::MAX`] and distinct from every corresponding
17785    /// [`DEFAULT_MAX_*`] module constant, so composition witnesses
17786    /// (meet, join, absorption, distributivity) discriminate across
17787    /// all three postures unambiguously.
17788    const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
17789        max_expansion_depth: 7,
17790        max_cache_entries: 11,
17791        max_expansion_size: 13,
17792        max_macro_body_size: 17,
17793        max_registered_macros: 19,
17794        max_macro_arity: 23,
17795    };
17796
17797    /// A second asymmetric posture whose per-axis values are on both
17798    /// sides of `HAND_AUTHORED_MID_POSTURE`'s values — three axes
17799    /// smaller (so the meet picks THIS posture on those axes and the
17800    /// join picks `MID` on those axes) and three axes larger (mirror).
17801    /// Together the two postures exercise every ordering combination
17802    /// on the six-axis pointwise `min`/`max` cascade.
17803    const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
17804        max_expansion_depth: 3,   // smaller than MID's 7
17805        max_cache_entries: 29,    // larger  than MID's 11
17806        max_expansion_size: 5,    // smaller than MID's 13
17807        max_macro_body_size: 31,  // larger  than MID's 17
17808        max_registered_macros: 2, // smaller than MID's 19
17809        max_macro_arity: 41,      // larger  than MID's 23
17810    };
17811
17812    #[test]
17813    fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
17814        // Concrete-preset pin — the meet of the (shipped default,
17815        // ceiling-lifted unbounded) preset pair is the DEFAULT preset
17816        // structurally: every `DEFAULT_MAX_*` module constant is a
17817        // concrete positive value strictly less than [`usize::MAX`],
17818        // so on every axis the pointwise-`min` picks the DEFAULT side.
17819        // Peer of `most_permissive_of_default_and_unbounded_projects_the_unbounded`
17820        // one COMBINATOR axis over on the same shipped-preset-pair
17821        // surface.
17822        assert_eq!(
17823            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17824            DEFAULT_RESOURCE_LIMITS,
17825        );
17826        assert_eq!(
17827            UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17828            DEFAULT_RESOURCE_LIMITS,
17829        );
17830    }
17831
17832    #[test]
17833    fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
17834        // Concrete-preset pin — the join of the (shipped default,
17835        // ceiling-lifted unbounded) preset pair is the UNBOUNDED
17836        // preset structurally: every axis's pointwise-`max` picks the
17837        // [`usize::MAX`] side over any concrete default. Peer of
17838        // `strictest_of_default_and_unbounded_projects_the_default`.
17839        assert_eq!(
17840            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17841            UNBOUNDED_RESOURCE_LIMITS,
17842        );
17843        assert_eq!(
17844            UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17845            UNBOUNDED_RESOURCE_LIMITS,
17846        );
17847    }
17848
17849    #[test]
17850    fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
17851        // Field-level pin — the meet operation projects the pointwise
17852        // `min` across the six-field surface. Pinned as six
17853        // independent field asserts (rather than one struct equality)
17854        // so a drift on ONE axis carries a distinct-name failure that
17855        // names the drifting axis; a future SEVENTH ceiling extension
17856        // requires an additional field assert here in lockstep.
17857        let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
17858        assert_eq!(m.max_expansion_depth, 3);
17859        assert_eq!(m.max_cache_entries, 11);
17860        assert_eq!(m.max_expansion_size, 5);
17861        assert_eq!(m.max_macro_body_size, 17);
17862        assert_eq!(m.max_registered_macros, 2);
17863        assert_eq!(m.max_macro_arity, 23);
17864    }
17865
17866    #[test]
17867    fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
17868        // Field-level pin — the join operation projects the pointwise
17869        // `max` across the six-field surface. Pinned as six
17870        // independent field asserts so a drift on ONE axis carries a
17871        // distinct-name failure that names the drifting axis.
17872        let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
17873        assert_eq!(j.max_expansion_depth, 7);
17874        assert_eq!(j.max_cache_entries, 29);
17875        assert_eq!(j.max_expansion_size, 13);
17876        assert_eq!(j.max_macro_body_size, 31);
17877        assert_eq!(j.max_registered_macros, 19);
17878        assert_eq!(j.max_macro_arity, 41);
17879    }
17880
17881    #[test]
17882    fn resource_limits_strictest_is_idempotent() {
17883        // Lattice law — `a ∧ a = a`. Every posture is a fixed point of
17884        // its own meet with itself; a regression that changed the
17885        // per-axis primitive to something other than pointwise `min`
17886        // (a `min - 1` off-by-one, or an arbitrary tie-breaker on
17887        // equal values) fires here.
17888        assert_eq!(
17889            DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17890            DEFAULT_RESOURCE_LIMITS,
17891        );
17892        assert_eq!(
17893            UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17894            UNBOUNDED_RESOURCE_LIMITS,
17895        );
17896        assert_eq!(
17897            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17898            HAND_AUTHORED_MID_POSTURE,
17899        );
17900    }
17901
17902    #[test]
17903    fn resource_limits_most_permissive_is_idempotent() {
17904        // Lattice law — `a ∨ a = a`. Sibling of the strictest
17905        // idempotence pin one COMBINATOR axis over.
17906        assert_eq!(
17907            DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17908            DEFAULT_RESOURCE_LIMITS,
17909        );
17910        assert_eq!(
17911            UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17912            UNBOUNDED_RESOURCE_LIMITS,
17913        );
17914        assert_eq!(
17915            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17916            HAND_AUTHORED_MID_POSTURE,
17917        );
17918    }
17919
17920    #[test]
17921    fn resource_limits_strictest_is_commutative() {
17922        // Lattice law — `a ∧ b = b ∧ a`. Pointwise `min` is symmetric
17923        // on every axis, so the composition inherits commutativity
17924        // structurally. A regression that broke ordering (e.g. a
17925        // per-axis "prefer left on tie") fires here.
17926        assert_eq!(
17927            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
17928            HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17929        );
17930        assert_eq!(
17931            DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
17932            HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
17933        );
17934    }
17935
17936    #[test]
17937    fn resource_limits_most_permissive_is_commutative() {
17938        // Lattice law — `a ∨ b = b ∨ a`. Sibling of the strictest
17939        // commutativity pin one COMBINATOR axis over.
17940        assert_eq!(
17941            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
17942            HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17943        );
17944        assert_eq!(
17945            DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
17946            HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
17947        );
17948    }
17949
17950    #[test]
17951    fn resource_limits_strictest_is_associative() {
17952        // Lattice law — `(a ∧ b) ∧ c = a ∧ (b ∧ c)`. Pointwise `min`
17953        // is associative on every axis, so the composition inherits
17954        // associativity structurally. A regression that broke
17955        // grouping (e.g. an accidental accumulator that skewed the
17956        // order of `min` invocations) fires here.
17957        let a = DEFAULT_RESOURCE_LIMITS;
17958        let b = HAND_AUTHORED_MID_POSTURE;
17959        let c = HAND_AUTHORED_OTHER_POSTURE;
17960        assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
17961    }
17962
17963    #[test]
17964    fn resource_limits_most_permissive_is_associative() {
17965        // Lattice law — `(a ∨ b) ∨ c = a ∨ (b ∨ c)`. Sibling of the
17966        // strictest associativity pin one COMBINATOR axis over.
17967        let a = DEFAULT_RESOURCE_LIMITS;
17968        let b = HAND_AUTHORED_MID_POSTURE;
17969        let c = HAND_AUTHORED_OTHER_POSTURE;
17970        assert_eq!(
17971            a.most_permissive(b).most_permissive(c),
17972            a.most_permissive(b.most_permissive(c)),
17973        );
17974    }
17975
17976    #[test]
17977    fn resource_limits_meet_and_join_satisfy_absorption() {
17978        // Lattice absorption identities — `a ∧ (a ∨ b) = a` AND
17979        // `a ∨ (a ∧ b) = a`. Together with commutativity, idempotence,
17980        // and associativity they define the lattice axioms; the two
17981        // absorption laws are the operator PAIR's interlock (each
17982        // undoes the other's contribution on the shared operand).
17983        // Any per-axis primitive pair for which `min(a, max(a, b)) = a`
17984        // AND `max(a, min(a, b)) = a` inherits this at the pointwise
17985        // composition; pointwise-`min`/`max` on [`usize`] satisfy both.
17986        let a = HAND_AUTHORED_MID_POSTURE;
17987        let b = HAND_AUTHORED_OTHER_POSTURE;
17988        assert_eq!(a.strictest(a.most_permissive(b)), a);
17989        assert_eq!(a.most_permissive(a.strictest(b)), a);
17990    }
17991
17992    #[test]
17993    fn resource_limits_meet_distributes_over_join() {
17994        // Distributive-lattice pin — `a ∧ (b ∨ c) = (a ∧ b) ∨ (a ∧ c)`.
17995        // Pointwise-`min` distributes over pointwise-`max` on [`usize`]
17996        // on every axis, so the composition inherits distributivity
17997        // structurally. A regression that broke the pointwise
17998        // structure (e.g. a per-axis operation that used SUBTRACTION
17999        // rather than MIN/MAX) fires here — subtraction does NOT
18000        // distribute over addition and would break the law on the
18001        // asymmetric-posture witness pair.
18002        let a = DEFAULT_RESOURCE_LIMITS;
18003        let b = HAND_AUTHORED_MID_POSTURE;
18004        let c = HAND_AUTHORED_OTHER_POSTURE;
18005        assert_eq!(
18006            a.strictest(b.most_permissive(c)),
18007            a.strictest(b).most_permissive(a.strictest(c)),
18008        );
18009    }
18010
18011    #[test]
18012    fn resource_limits_join_distributes_over_meet() {
18013        // Distributive-lattice pin, sibling of the meet-over-join
18014        // distributivity pin one COMBINATOR axis over — `a ∨ (b ∧ c) =
18015        // (a ∨ b) ∧ (a ∨ c)`. In a distributive lattice BOTH
18016        // distributivity identities hold; pin both so a regression
18017        // that broke ONE without the other cannot slip through.
18018        let a = DEFAULT_RESOURCE_LIMITS;
18019        let b = HAND_AUTHORED_MID_POSTURE;
18020        let c = HAND_AUTHORED_OTHER_POSTURE;
18021        assert_eq!(
18022            a.most_permissive(b.strictest(c)),
18023            a.most_permissive(b).strictest(a.most_permissive(c)),
18024        );
18025    }
18026
18027    #[test]
18028    fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
18029        // Meet-lower-bound pin — the meet lies at or below BOTH
18030        // operands on every axis. This is the structural GLB property
18031        // of the meet in the pointwise partial order (tighter-first):
18032        // any admissible input under the meet is admissible under
18033        // both operands, so the meet's ceiling must not exceed either
18034        // operand's ceiling on any axis.
18035        let a = HAND_AUTHORED_MID_POSTURE;
18036        let b = HAND_AUTHORED_OTHER_POSTURE;
18037        let m = a.strictest(b);
18038        assert!(m.max_expansion_depth <= a.max_expansion_depth);
18039        assert!(m.max_expansion_depth <= b.max_expansion_depth);
18040        assert!(m.max_cache_entries <= a.max_cache_entries);
18041        assert!(m.max_cache_entries <= b.max_cache_entries);
18042        assert!(m.max_expansion_size <= a.max_expansion_size);
18043        assert!(m.max_expansion_size <= b.max_expansion_size);
18044        assert!(m.max_macro_body_size <= a.max_macro_body_size);
18045        assert!(m.max_macro_body_size <= b.max_macro_body_size);
18046        assert!(m.max_registered_macros <= a.max_registered_macros);
18047        assert!(m.max_registered_macros <= b.max_registered_macros);
18048        assert!(m.max_macro_arity <= a.max_macro_arity);
18049        assert!(m.max_macro_arity <= b.max_macro_arity);
18050    }
18051
18052    #[test]
18053    fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
18054        // Join-upper-bound pin — the join lies at or above BOTH
18055        // operands on every axis. Sibling of the strictest-lower-
18056        // bound pin one COMBINATOR axis over; the structural LUB
18057        // property of the join in the pointwise partial order.
18058        let a = HAND_AUTHORED_MID_POSTURE;
18059        let b = HAND_AUTHORED_OTHER_POSTURE;
18060        let j = a.most_permissive(b);
18061        assert!(j.max_expansion_depth >= a.max_expansion_depth);
18062        assert!(j.max_expansion_depth >= b.max_expansion_depth);
18063        assert!(j.max_cache_entries >= a.max_cache_entries);
18064        assert!(j.max_cache_entries >= b.max_cache_entries);
18065        assert!(j.max_expansion_size >= a.max_expansion_size);
18066        assert!(j.max_expansion_size >= b.max_expansion_size);
18067        assert!(j.max_macro_body_size >= a.max_macro_body_size);
18068        assert!(j.max_macro_body_size >= b.max_macro_body_size);
18069        assert!(j.max_registered_macros >= a.max_registered_macros);
18070        assert!(j.max_registered_macros >= b.max_registered_macros);
18071        assert!(j.max_macro_arity >= a.max_macro_arity);
18072        assert!(j.max_macro_arity >= b.max_macro_arity);
18073    }
18074
18075    #[test]
18076    fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
18077        // Const-fn pin — the meet is evaluable in const context, so a
18078        // caller can stitch a new `pub const` preset from two shipped
18079        // presets without deferring the composition to runtime. Pinned
18080        // as a `const` binding at the item level so the compiler
18081        // exercises the const-fn path structurally; the runtime
18082        // assertion is redundant for a `const` value that already
18083        // evaluated at compile time, but pins the identity as a typed
18084        // theorem alongside every OTHER `strictest` pin above.
18085        const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
18086            DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
18087        assert_eq!(
18088            DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
18089            min_usize(
18090                DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18091                HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
18092            ),
18093        );
18094        // Extra check: the composition MUST project the MID posture's
18095        // small value on every axis where MID is smaller than DEFAULT
18096        // — MID's per-axis values (7, 11, 13, 17, 19, 23) are all
18097        // strictly less than every `DEFAULT_MAX_*` constant, so the
18098        // meet projects MID verbatim.
18099        assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
18100    }
18101
18102    #[test]
18103    fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
18104        // Const-fn pin sibling of the strictest const-composition pin
18105        // one COMBINATOR axis over — the join is also `const fn`, so
18106        // a caller can stitch a new `pub const` preset via the LUB
18107        // combinator at compile time.
18108        const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
18109            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
18110        assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
18111    }
18112
18113    // ── ResourceLimits::leq — pointwise partial order ─────────────────
18114
18115    #[test]
18116    fn resource_limits_leq_is_pointwise_field_conjunction() {
18117        // Field-level pin — the leq relation projects the pointwise
18118        // `<=` conjunction across the six-field surface. `LOOSER` is
18119        // `MID + 1` on every axis, so `MID.leq(LOOSER)` holds; a
18120        // per-axis tighten on ANY field alone flips the relation to
18121        // false. Pinned as six independent tighten-per-axis asserts so
18122        // a drift on ONE axis carries a distinct-name failure that
18123        // names the drifting axis; a future SEVENTH ceiling extension
18124        // requires an additional per-axis tighten assert here in
18125        // lockstep.
18126        const LOOSER: ResourceLimits = ResourceLimits {
18127            max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18128            max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18129            max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18130            max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18131            max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18132            max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18133        };
18134        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18135        assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
18136
18137        // Per-axis exceed pins: raise MID's field ONE axis above LOOSER
18138        // on that axis; the leq must flip to false. Every other axis
18139        // stays at MID, so LOOSER still dominates on those axes —
18140        // isolating the single-axis violation.
18141        let exceed_depth = ResourceLimits {
18142            max_expansion_depth: LOOSER.max_expansion_depth + 1,
18143            ..HAND_AUTHORED_MID_POSTURE
18144        };
18145        assert!(!exceed_depth.leq(LOOSER));
18146        let exceed_cache = ResourceLimits {
18147            max_cache_entries: LOOSER.max_cache_entries + 1,
18148            ..HAND_AUTHORED_MID_POSTURE
18149        };
18150        assert!(!exceed_cache.leq(LOOSER));
18151        let exceed_expansion = ResourceLimits {
18152            max_expansion_size: LOOSER.max_expansion_size + 1,
18153            ..HAND_AUTHORED_MID_POSTURE
18154        };
18155        assert!(!exceed_expansion.leq(LOOSER));
18156        let exceed_body = ResourceLimits {
18157            max_macro_body_size: LOOSER.max_macro_body_size + 1,
18158            ..HAND_AUTHORED_MID_POSTURE
18159        };
18160        assert!(!exceed_body.leq(LOOSER));
18161        let exceed_registered = ResourceLimits {
18162            max_registered_macros: LOOSER.max_registered_macros + 1,
18163            ..HAND_AUTHORED_MID_POSTURE
18164        };
18165        assert!(!exceed_registered.leq(LOOSER));
18166        let exceed_arity = ResourceLimits {
18167            max_macro_arity: LOOSER.max_macro_arity + 1,
18168            ..HAND_AUTHORED_MID_POSTURE
18169        };
18170        assert!(!exceed_arity.leq(LOOSER));
18171    }
18172
18173    #[test]
18174    fn resource_limits_leq_is_reflexive() {
18175        // Partial-order law — `a ≤ a` for every posture. Reflexivity
18176        // is the identity axiom on any lattice's underlying partial
18177        // order; a regression that changed the per-axis primitive to
18178        // strict `<` (or that added an accidental "prefer other on
18179        // tie" branch) fires here on every shipped posture.
18180        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18181        assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18182        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18183        assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18184    }
18185
18186    #[test]
18187    fn resource_limits_leq_is_antisymmetric() {
18188        // Partial-order law — `a ≤ b ∧ b ≤ a ⇒ a = b`. The mutual-
18189        // dominance witness on a partial order pins the two operands
18190        // to structural equality. On the pointwise `<=` composition
18191        // this holds because `a.max_X <= b.max_X && b.max_X <= a.max_X
18192        // ⇒ a.max_X == b.max_X` on every axis, and struct equality
18193        // is field-conjunction. Sibling to reflexivity one AXIOM axis
18194        // over on the partial-order axiom surface.
18195        //
18196        // Two distinct constructions of the SAME six-field posture
18197        // (one via struct literal, one via `..DEFAULT_RESOURCE_LIMITS`
18198        // spread of the shipped preset) — mutual leq holds; struct
18199        // equality holds; antisymmetry pin closes.
18200        let a = DEFAULT_RESOURCE_LIMITS;
18201        let b = ResourceLimits {
18202            max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18203            max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
18204            max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
18205            max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
18206            max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
18207            max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
18208        };
18209        assert!(a.leq(b));
18210        assert!(b.leq(a));
18211        assert_eq!(a, b);
18212    }
18213
18214    #[test]
18215    fn resource_limits_leq_is_transitive() {
18216        // Partial-order law — `a ≤ b ∧ b ≤ c ⇒ a ≤ c`. Transitivity
18217        // is the composition axiom on any lattice's partial order.
18218        // Pointwise `<=` on [`usize`] is transitive per axis, so the
18219        // six-field conjunction inherits it structurally.
18220        //
18221        // Chain witness: MID's every-axis `+1` shift is LOOSER, and
18222        // LOOSER's every-axis `+1` shift is LOOSEST; the chain MID
18223        // ≤ LOOSER ≤ LOOSEST implies MID ≤ LOOSEST.
18224        const LOOSER: ResourceLimits = ResourceLimits {
18225            max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18226            max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18227            max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18228            max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18229            max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18230            max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18231        };
18232        const LOOSEST: ResourceLimits = ResourceLimits {
18233            max_expansion_depth: LOOSER.max_expansion_depth + 1,
18234            max_cache_entries: LOOSER.max_cache_entries + 1,
18235            max_expansion_size: LOOSER.max_expansion_size + 1,
18236            max_macro_body_size: LOOSER.max_macro_body_size + 1,
18237            max_registered_macros: LOOSER.max_registered_macros + 1,
18238            max_macro_arity: LOOSER.max_macro_arity + 1,
18239        };
18240        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18241        assert!(LOOSER.leq(LOOSEST));
18242        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
18243    }
18244
18245    #[test]
18246    fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
18247        // Concrete-preset pin — the leq relation on the shipped
18248        // preset pair is STRICT: DEFAULT ≤ UNBOUNDED holds (every
18249        // `DEFAULT_MAX_*` module constant is a concrete positive
18250        // value at most [`usize::MAX`]) AND UNBOUNDED ≤ DEFAULT
18251        // fails (every `DEFAULT_MAX_*` is strictly less than
18252        // [`usize::MAX`], so on every axis [`usize::MAX`] exceeds
18253        // the matching default). Peer of
18254        // `strictest_of_default_and_unbounded_projects_the_default`
18255        // one PRIMITIVE-KIND axis over on the same shipped-preset-
18256        // pair surface: where the meet composition projects DEFAULT,
18257        // this relation asserts DEFAULT is the tighter of the two.
18258        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18259        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18260    }
18261
18262    #[test]
18263    fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
18264        // Partial-order NON-total pin — the two hand-authored asymmetric
18265        // postures have MID smaller on three axes (depth, expansion,
18266        // registered) and OTHER smaller on the other three (cache,
18267        // body, arity); neither dominates the other, so BOTH
18268        // directions of the leq relation fail. The pin discriminates
18269        // a regression that promoted `leq` from partial to total
18270        // (e.g. an accidental `||` swap in the conjunction, which
18271        // would make every pair comparable).
18272        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18273        assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18274    }
18275
18276    #[test]
18277    fn resource_limits_leq_agrees_with_meet() {
18278        // Lattice cross-check axiom — `a ≤ b ⇔ a ⊓ b = a`. Every
18279        // classification lattice one crate over
18280        // (`tatara-lattice/src/lib.rs`'s preamble) declares this
18281        // agreement axiom binding the partial order to the meet
18282        // combinator, and the default-impl body on
18283        // `tatara_lattice::Lattice::leq` derives `leq` FROM `meet`
18284        // through it. This pin discharges the same axiom on the
18285        // ResourceLimits algebra as a two-direction structural
18286        // equality across the strict-order preset pair AND the
18287        // reflexivity witness.
18288        //
18289        // Forward (a ≤ b ⇒ a ⊓ b = a):
18290        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18291        assert_eq!(
18292            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18293            DEFAULT_RESOURCE_LIMITS,
18294        );
18295        // Reverse (a ⊓ b = a ⇒ a ≤ b), reflexivity witness:
18296        assert_eq!(
18297            DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18298            DEFAULT_RESOURCE_LIMITS,
18299        );
18300        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18301
18302        // Non-agreement witness — the incomparable asymmetric postures
18303        // have meet distinct from EITHER operand, and leq false in
18304        // both directions; agreement holds on the negative side too.
18305        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18306        assert_ne!(
18307            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18308            HAND_AUTHORED_MID_POSTURE,
18309        );
18310    }
18311
18312    #[test]
18313    fn resource_limits_leq_agrees_with_join() {
18314        // Lattice cross-check axiom, sibling of the meet-agreement
18315        // pin one COMBINATOR axis over — `a ≤ b ⇔ a ⊔ b = b`.
18316        //
18317        // Forward (a ≤ b ⇒ a ⊔ b = b):
18318        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18319        assert_eq!(
18320            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18321            UNBOUNDED_RESOURCE_LIMITS,
18322        );
18323        // Reverse (a ⊔ b = b ⇒ a ≤ b), reflexivity witness:
18324        assert_eq!(
18325            DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18326            DEFAULT_RESOURCE_LIMITS,
18327        );
18328        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18329
18330        // Non-agreement witness — the incomparable asymmetric postures
18331        // have join distinct from EITHER operand.
18332        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18333        assert_ne!(
18334            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18335            HAND_AUTHORED_OTHER_POSTURE,
18336        );
18337    }
18338
18339    #[test]
18340    fn resource_limits_strictest_is_leq_both_operands() {
18341        // Meet-lower-bound pin, typed lattice-relation companion to
18342        // `resource_limits_strictest_is_dominated_by_both_operands_pointwise`
18343        // one PRIMITIVE-KIND axis over — where the per-axis pin
18344        // discharged the six independent `<=` asserts inline, this pin
18345        // routes through the typed `leq` primitive, so the
18346        // greatest-lower-bound property is stated in the LATTICE
18347        // vocabulary (meet-is-leq-both-operands) rather than the
18348        // six-fold field-conjunction vocabulary.
18349        let a = HAND_AUTHORED_MID_POSTURE;
18350        let b = HAND_AUTHORED_OTHER_POSTURE;
18351        let m = a.strictest(b);
18352        assert!(m.leq(a));
18353        assert!(m.leq(b));
18354    }
18355
18356    #[test]
18357    fn resource_limits_most_permissive_is_geq_both_operands() {
18358        // Join-upper-bound pin, typed lattice-relation companion to
18359        // `resource_limits_most_permissive_dominates_both_operands_pointwise`
18360        // one PRIMITIVE-KIND axis over. Stated in the LATTICE
18361        // vocabulary as `a ≤ a ⊔ b ∧ b ≤ a ⊔ b` — sibling of the
18362        // meet-lower-bound pin one COMBINATOR axis over.
18363        let a = HAND_AUTHORED_MID_POSTURE;
18364        let b = HAND_AUTHORED_OTHER_POSTURE;
18365        let j = a.most_permissive(b);
18366        assert!(a.leq(j));
18367        assert!(b.leq(j));
18368    }
18369
18370    #[test]
18371    fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
18372        // Const-fn pin — the leq relation is evaluable in const
18373        // context, so a caller can pin a preset-relation identity at
18374        // compile time. Sibling of the const-fn composition pins on
18375        // `strictest` / `most_permissive` one PRIMITIVE-KIND axis
18376        // over: those two operations compose two presets into a
18377        // const-time third, and this operation decides a const-time
18378        // yes/no on two presets. `const _: ()` at item scope is a
18379        // compile-time proof — the compiler rejects the module if
18380        // either identity does not hold; a runtime `assert!` over the
18381        // same expression would fire at test time on the same value
18382        // clippy's `assertions_on_constants` correctly names as a
18383        // redundant late check on a compile-time constant.
18384        //
18385        // Strict-order preset-pair pin — DEFAULT ≤ UNBOUNDED (every
18386        // `DEFAULT_MAX_*` module constant is at most [`usize::MAX`]):
18387        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18388        // Reverse direction fails — [`usize::MAX`] exceeds every
18389        // concrete `DEFAULT_MAX_*` constant, so the partial order is
18390        // STRICT on the shipped preset pair:
18391        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18392    }
18393
18394    // ── EMPTY_RESOURCE_LIMITS — bounded-lattice bottom preset ─────────
18395
18396    #[test]
18397    fn empty_resource_limits_binds_every_ceiling_to_zero() {
18398        // Field-level pin — the zero preset carries `0` on every axis
18399        // of the six-field surface. Pinned as six independent field
18400        // asserts (rather than one struct equality) so a drift on ONE
18401        // field carries a distinct-name failure that names the drifting
18402        // axis. A future extension that adds a SEVENTH ceiling requires
18403        // an additional field assert here in lockstep with the const
18404        // literal — rustc's field-exhaustiveness on the const forces
18405        // the new field to appear, and this pin exercises the seed
18406        // value. Structural peer of
18407        // `unbounded_resource_limits_binds_every_ceiling_to_usize_max`
18408        // one LATTICE-POLE axis over on the (bottom, top)
18409        // bounded-lattice preset-pair surface.
18410        assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
18411        assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
18412        assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
18413        assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
18414        assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
18415        assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
18416    }
18417
18418    #[test]
18419    fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
18420        // Structural-distinctness pin — the two constants
18421        // [`DEFAULT_RESOURCE_LIMITS`] and [`EMPTY_RESOURCE_LIMITS`] sit
18422        // at DISTINCT points on the preset-posture axis, and the
18423        // disagreement is EXHAUSTIVE on the six-field surface — every
18424        // shipped `DEFAULT_MAX_*` module constant is a concrete
18425        // strictly-positive value, so every axis distinguishes DEFAULT
18426        // from the all-zero bottom. Peer of
18427        // `unbounded_resource_limits_disagrees_with_default_on_every_ceiling`
18428        // one LATTICE-POLE axis over.
18429        assert_ne!(
18430            EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18431            DEFAULT_RESOURCE_LIMITS.max_expansion_depth
18432        );
18433        assert_ne!(
18434            EMPTY_RESOURCE_LIMITS.max_cache_entries,
18435            DEFAULT_RESOURCE_LIMITS.max_cache_entries
18436        );
18437        assert_ne!(
18438            EMPTY_RESOURCE_LIMITS.max_expansion_size,
18439            DEFAULT_RESOURCE_LIMITS.max_expansion_size
18440        );
18441        assert_ne!(
18442            EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18443            DEFAULT_RESOURCE_LIMITS.max_macro_body_size
18444        );
18445        assert_ne!(
18446            EMPTY_RESOURCE_LIMITS.max_registered_macros,
18447            DEFAULT_RESOURCE_LIMITS.max_registered_macros
18448        );
18449        assert_ne!(
18450            EMPTY_RESOURCE_LIMITS.max_macro_arity,
18451            DEFAULT_RESOURCE_LIMITS.max_macro_arity
18452        );
18453    }
18454
18455    #[test]
18456    fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
18457        // Diagonal-distinctness pin — the (BOTTOM, TOP) preset pair
18458        // disagrees on every axis (`0 != usize::MAX` on every field).
18459        // Structural companion to the DEFAULT-vs-EMPTY and
18460        // UNBOUNDED-vs-DEFAULT disagreement pins one PRESET-PAIR axis
18461        // over — together the three pin the six-field surface at the
18462        // full diagonal of the (EMPTY, DEFAULT, UNBOUNDED) triple.
18463        assert_ne!(
18464            EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18465            UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
18466        );
18467        assert_ne!(
18468            EMPTY_RESOURCE_LIMITS.max_cache_entries,
18469            UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
18470        );
18471        assert_ne!(
18472            EMPTY_RESOURCE_LIMITS.max_expansion_size,
18473            UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
18474        );
18475        assert_ne!(
18476            EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18477            UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
18478        );
18479        assert_ne!(
18480            EMPTY_RESOURCE_LIMITS.max_registered_macros,
18481            UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
18482        );
18483        assert_ne!(
18484            EMPTY_RESOURCE_LIMITS.max_macro_arity,
18485            UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
18486        );
18487    }
18488
18489    #[test]
18490    fn empty_resource_limits_is_the_join_identity() {
18491        // Lattice-identity law — `a ⊔ ⊥ = a`. Every posture's
18492        // pointwise `max` against 0 returns the posture verbatim, so
18493        // [`EMPTY_RESOURCE_LIMITS`] acts as the identity element of
18494        // the join monoid. Peer of
18495        // `empty_resource_limits_is_the_meet_annihilator` one
18496        // COMBINATOR axis over on the (meet, join) surface.
18497        //
18498        // Cross-preset exhaustion — verify the identity against every
18499        // named preset (DEFAULT, UNBOUNDED, both hand-authored
18500        // asymmetric postures) so a regression that broke ONE axis of
18501        // the algebra fires here on the operand whose field crosses
18502        // that axis's boundary.
18503        assert_eq!(
18504            DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18505            DEFAULT_RESOURCE_LIMITS,
18506        );
18507        assert_eq!(
18508            EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18509            DEFAULT_RESOURCE_LIMITS,
18510        );
18511        assert_eq!(
18512            UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18513            UNBOUNDED_RESOURCE_LIMITS,
18514        );
18515        assert_eq!(
18516            EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18517            UNBOUNDED_RESOURCE_LIMITS,
18518        );
18519        assert_eq!(
18520            HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18521            HAND_AUTHORED_MID_POSTURE,
18522        );
18523        assert_eq!(
18524            HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18525            HAND_AUTHORED_OTHER_POSTURE,
18526        );
18527        // Idempotence at the identity itself — sibling witness of the
18528        // `most_permissive_is_idempotent` general pin one PRESET axis
18529        // over.
18530        assert_eq!(
18531            EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18532            EMPTY_RESOURCE_LIMITS,
18533        );
18534    }
18535
18536    #[test]
18537    fn empty_resource_limits_is_the_meet_annihilator() {
18538        // Lattice-annihilator law — `a ⊓ ⊥ = ⊥`. Every posture's
18539        // pointwise `min` against 0 returns 0 on every axis, so
18540        // [`EMPTY_RESOURCE_LIMITS`] acts as the annihilator (absorbing
18541        // element) of the meet operation. Peer of
18542        // `empty_resource_limits_is_the_join_identity` one COMBINATOR
18543        // axis over — the identity on one side of the bounded lattice
18544        // is the annihilator on the other side.
18545        assert_eq!(
18546            DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18547            EMPTY_RESOURCE_LIMITS,
18548        );
18549        assert_eq!(
18550            EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18551            EMPTY_RESOURCE_LIMITS,
18552        );
18553        assert_eq!(
18554            UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18555            EMPTY_RESOURCE_LIMITS,
18556        );
18557        assert_eq!(
18558            EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18559            EMPTY_RESOURCE_LIMITS,
18560        );
18561        assert_eq!(
18562            HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18563            EMPTY_RESOURCE_LIMITS,
18564        );
18565        assert_eq!(
18566            HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18567            EMPTY_RESOURCE_LIMITS,
18568        );
18569    }
18570
18571    #[test]
18572    fn empty_resource_limits_is_the_lattice_minimum() {
18573        // Partial-order minimum law — `⊥ ≤ a` for every `a`. Every
18574        // posture's field is a `usize` and `0 <= x` holds for every
18575        // `usize x`, so [`EMPTY_RESOURCE_LIMITS`] sits at or below
18576        // every posture on every axis of the pointwise `<=` conjunction.
18577        // Peer of the UNBOUNDED-is-maximum property on the
18578        // partial-order face one LATTICE-POLE axis over — those two
18579        // pin the (min, max) bounds of the bounded lattice.
18580        assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18581        assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18582        assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18583        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
18584        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
18585        // Reverse direction FAILS on every non-EMPTY preset (each has
18586        // at least one field strictly greater than 0). The partial
18587        // order is STRICT between EMPTY and every other named preset,
18588        // structurally exhausting the diagonal of the
18589        // (EMPTY, DEFAULT, UNBOUNDED, MID, OTHER) 5-preset ordering
18590        // face at the BOTTOM row.
18591        assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18592        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18593        assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18594        assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18595    }
18596
18597    #[test]
18598    fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
18599        // Const-fn pin — the bounded-lattice identity + annihilator +
18600        // minimum laws hold at COMPILE time. Sibling of
18601        // `resource_limits_leq_evaluates_at_compile_time_via_const_fn`
18602        // one LATTICE-STRUCTURE axis over — that pin fixed the strict
18603        // order of the (DEFAULT, UNBOUNDED) preset pair at compile
18604        // time; this pin fixes the bounded-lattice pole role of the
18605        // (EMPTY, UNBOUNDED) preset pair. `const _: ()` at item scope
18606        // is a compile-time proof — a regression that broke either
18607        // identity element or the bottom-is-minimum invariant fires at
18608        // rustc time, not test-run time.
18609        //
18610        // Bottom-is-minimum: EMPTY ≤ DEFAULT ≤ UNBOUNDED (the
18611        // three-preset ordering chain on the shipped preset diagonal).
18612        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18613        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18614        // Reverse strictness — neither DEFAULT nor UNBOUNDED sits
18615        // below EMPTY (each has a strictly-positive field).
18616        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18617        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18618        // Join-identity + meet-annihilator laws cross-pinned through
18619        // the `leq` primitive at const time — the round-trip identity
18620        // (`a ⊔ ⊥ = a ⇔ ⊥ ≤ a`, `a ⊓ ⊥ = ⊥ ⇔ ⊥ ≤ a`) means the two
18621        // leq asserts above discharge both algebraic laws by the
18622        // `leq_agrees_with_meet` + `leq_agrees_with_join` lattice
18623        // cross-check axiom already pinned in this cohort.
18624    }
18625
18626    #[test]
18627    fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
18628        // Fold-identity behavioral pin — a slice of postures folded
18629        // through [`ResourceLimits::most_permissive`] with EMPTY as
18630        // the seed produces the pointwise least upper bound across
18631        // the slice on every axis. The join-identity property
18632        // (`a.most_permissive(EMPTY) == a`) guarantees the seed
18633        // contributes nothing to the fold's result — every field's
18634        // pointwise `max` against the seed's `0` returns the field's
18635        // value verbatim, so the fold reduces to the joined-across-
18636        // -operands upper bound. This is the destination usage the
18637        // constant's docstring names as its fold-identity role.
18638        //
18639        // A regression that seeded from ANY non-empty posture (e.g.
18640        // `DEFAULT_RESOURCE_LIMITS`) would inflate the fold's result
18641        // on every axis where the seed's field exceeds the pointwise
18642        // max of the slice — fires as a distinct-axis inequality here.
18643        let postures: [ResourceLimits; 3] = [
18644            HAND_AUTHORED_MID_POSTURE,
18645            HAND_AUTHORED_OTHER_POSTURE,
18646            DEFAULT_RESOURCE_LIMITS,
18647        ];
18648        let joined = postures
18649            .iter()
18650            .copied()
18651            .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
18652        // Every operand is `leq` the joined result — the least
18653        // upper bound dominates each contributor on every axis.
18654        assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
18655        assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
18656        assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
18657        // Field-level pin — each axis is the pointwise `max` of the
18658        // three sources' values. Six independent field asserts so a
18659        // drift on ONE axis carries a distinct-name failure.
18660        assert_eq!(
18661            joined.max_expansion_depth,
18662            HAND_AUTHORED_MID_POSTURE
18663                .max_expansion_depth
18664                .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18665                .max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18666        );
18667        assert_eq!(
18668            joined.max_cache_entries,
18669            HAND_AUTHORED_MID_POSTURE
18670                .max_cache_entries
18671                .max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18672                .max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18673        );
18674        assert_eq!(
18675            joined.max_expansion_size,
18676            HAND_AUTHORED_MID_POSTURE
18677                .max_expansion_size
18678                .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18679                .max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18680        );
18681        assert_eq!(
18682            joined.max_macro_body_size,
18683            HAND_AUTHORED_MID_POSTURE
18684                .max_macro_body_size
18685                .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18686                .max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18687        );
18688        assert_eq!(
18689            joined.max_registered_macros,
18690            HAND_AUTHORED_MID_POSTURE
18691                .max_registered_macros
18692                .max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18693                .max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18694        );
18695        assert_eq!(
18696            joined.max_macro_arity,
18697            HAND_AUTHORED_MID_POSTURE
18698                .max_macro_arity
18699                .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18700                .max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18701        );
18702        // Empty-slice edge case — folding over an EMPTY slice returns
18703        // the seed verbatim (the identity element as the null aggregate).
18704        // A wrapper-type approach (`Option<ResourceLimits>` for the
18705        // aggregate) would need a distinct absent branch here; the
18706        // typed identity-element approach resolves it structurally.
18707        let empty_slice: [ResourceLimits; 0] = [];
18708        assert_eq!(
18709            empty_slice
18710                .iter()
18711                .copied()
18712                .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18713            EMPTY_RESOURCE_LIMITS,
18714        );
18715    }
18716
18717    #[test]
18718    fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
18719        // Dual fold-identity pin — a slice of postures folded through
18720        // [`ResourceLimits::strictest`] with UNBOUNDED as the seed
18721        // produces the pointwise greatest lower bound across the slice
18722        // on every axis. Peer of the EMPTY-seeds-most_permissive-fold
18723        // pin one COMBINATOR axis over — the identity element for
18724        // meet is the join's ANNIHILATOR (`UNBOUNDED`), and the
18725        // identity element for join is the meet's annihilator
18726        // (`EMPTY`); the two together bind the bounded-lattice's
18727        // aggregation surface at BOTH poles.
18728        let postures: [ResourceLimits; 3] = [
18729            HAND_AUTHORED_MID_POSTURE,
18730            HAND_AUTHORED_OTHER_POSTURE,
18731            DEFAULT_RESOURCE_LIMITS,
18732        ];
18733        let met = postures
18734            .iter()
18735            .copied()
18736            .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
18737        // The greatest lower bound sits `leq` every operand.
18738        assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
18739        assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
18740        assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
18741        // Field-level pin — each axis is the pointwise `min` of the
18742        // three sources' values (the UNBOUNDED seed's `usize::MAX`
18743        // never wins the min against a concrete positive value).
18744        assert_eq!(
18745            met.max_expansion_depth,
18746            HAND_AUTHORED_MID_POSTURE
18747                .max_expansion_depth
18748                .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18749                .min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18750        );
18751        assert_eq!(
18752            met.max_cache_entries,
18753            HAND_AUTHORED_MID_POSTURE
18754                .max_cache_entries
18755                .min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18756                .min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18757        );
18758        assert_eq!(
18759            met.max_expansion_size,
18760            HAND_AUTHORED_MID_POSTURE
18761                .max_expansion_size
18762                .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18763                .min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18764        );
18765        assert_eq!(
18766            met.max_macro_body_size,
18767            HAND_AUTHORED_MID_POSTURE
18768                .max_macro_body_size
18769                .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18770                .min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18771        );
18772        assert_eq!(
18773            met.max_registered_macros,
18774            HAND_AUTHORED_MID_POSTURE
18775                .max_registered_macros
18776                .min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18777                .min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18778        );
18779        assert_eq!(
18780            met.max_macro_arity,
18781            HAND_AUTHORED_MID_POSTURE
18782                .max_macro_arity
18783                .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18784                .min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18785        );
18786        // Empty-slice edge — folding over an empty slice returns the
18787        // UNBOUNDED seed verbatim (the meet-identity as the null
18788        // aggregate). Symmetric to the EMPTY-seed empty-slice pin.
18789        let empty_slice: [ResourceLimits; 0] = [];
18790        assert_eq!(
18791            empty_slice
18792                .iter()
18793                .copied()
18794                .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18795            UNBOUNDED_RESOURCE_LIMITS,
18796        );
18797    }
18798
18799    #[test]
18800    fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
18801        // Null-aggregate pin — the N-ary meet over an empty slice is
18802        // the meet-identity element [`UNBOUNDED_RESOURCE_LIMITS`]. The
18803        // typed identity-element approach resolves the empty-input case
18804        // at the algebra layer; a wrapper-type approach
18805        // (`Option<ResourceLimits>` for the aggregate) would need a
18806        // distinct absent branch here, and a panic-on-empty seed-from-
18807        // -first-element approach would abort the caller.
18808        assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
18809    }
18810
18811    #[test]
18812    fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
18813        // Null-aggregate dual pin — the N-ary join over an empty slice
18814        // is the join-identity element [`EMPTY_RESOURCE_LIMITS`]. Peer
18815        // to `strictest_of_empty_slice` one COMBINATOR axis over on the
18816        // (N-ary meet, N-ary join) surface — the identity element for
18817        // join is the meet's ANNIHILATOR and vice versa; the two
18818        // together bind both bounded-lattice poles as null aggregates.
18819        assert_eq!(
18820            ResourceLimits::most_permissive_of(&[]),
18821            EMPTY_RESOURCE_LIMITS,
18822        );
18823    }
18824
18825    #[test]
18826    fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
18827        // Meet-identity absorption pin at N=1 — the N-ary meet of ONE
18828        // input is the input verbatim, since the meet-identity seed
18829        // (`UNBOUNDED_RESOURCE_LIMITS`) satisfies
18830        // `UNBOUNDED.strictest(a) == a` by the pointwise `min` behavior
18831        // against [`usize::MAX`]. This is the identity-law-at-N=1
18832        // pin: the fold's seed does not distort the 1-input case.
18833        assert_eq!(
18834            ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
18835            HAND_AUTHORED_MID_POSTURE,
18836        );
18837        assert_eq!(
18838            ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18839            HAND_AUTHORED_OTHER_POSTURE,
18840        );
18841        assert_eq!(
18842            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
18843            DEFAULT_RESOURCE_LIMITS,
18844        );
18845    }
18846
18847    #[test]
18848    fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
18849        // Join-identity absorption pin at N=1 — dual of
18850        // `strictest_of_single_element`, using the join-identity seed
18851        // (`EMPTY_RESOURCE_LIMITS`) whose per-axis `0` satisfies
18852        // `EMPTY.most_permissive(a) == a` against any non-zero value.
18853        assert_eq!(
18854            ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
18855            HAND_AUTHORED_MID_POSTURE,
18856        );
18857        assert_eq!(
18858            ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18859            HAND_AUTHORED_OTHER_POSTURE,
18860        );
18861        assert_eq!(
18862            ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
18863            DEFAULT_RESOURCE_LIMITS,
18864        );
18865    }
18866
18867    #[test]
18868    fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
18869        // N=2 reduction pin — the N-ary meet on a 2-element slice
18870        // reduces to the pairwise combinator, since
18871        // `UNBOUNDED.strictest(a).strictest(b) == a.strictest(b)`. The
18872        // strictest_of(&[a, b]) call MUST agree with a.strictest(b) on
18873        // every input pair the algebra distinguishes.
18874        assert_eq!(
18875            ResourceLimits::strictest_of(
18876                &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
18877            ),
18878            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18879        );
18880        assert_eq!(
18881            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
18882            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18883        );
18884    }
18885
18886    #[test]
18887    fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
18888        // N=2 reduction dual pin — the N-ary join on a 2-element slice
18889        // reduces to the pairwise combinator.
18890        assert_eq!(
18891            ResourceLimits::most_permissive_of(&[
18892                HAND_AUTHORED_MID_POSTURE,
18893                HAND_AUTHORED_OTHER_POSTURE,
18894            ]),
18895            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18896        );
18897        assert_eq!(
18898            ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
18899            DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18900        );
18901    }
18902
18903    #[test]
18904    fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
18905        // Method-agrees-with-fold pin — the sole behavioral claim the
18906        // N-ary aggregator makes is that it IS the inline
18907        // `.iter().copied().fold(UNBOUNDED, strictest)` cascade the
18908        // pre-lift caller composed at every consumption site. A drift
18909        // between the method's implementation and the inline fold
18910        // would break this equality on every non-empty slice.
18911        let postures: [ResourceLimits; 3] = [
18912            HAND_AUTHORED_MID_POSTURE,
18913            HAND_AUTHORED_OTHER_POSTURE,
18914            DEFAULT_RESOURCE_LIMITS,
18915        ];
18916        assert_eq!(
18917            ResourceLimits::strictest_of(&postures),
18918            postures
18919                .iter()
18920                .copied()
18921                .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18922        );
18923    }
18924
18925    #[test]
18926    fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
18927        // Method-agrees-with-fold dual pin.
18928        let postures: [ResourceLimits; 3] = [
18929            HAND_AUTHORED_MID_POSTURE,
18930            HAND_AUTHORED_OTHER_POSTURE,
18931            DEFAULT_RESOURCE_LIMITS,
18932        ];
18933        assert_eq!(
18934            ResourceLimits::most_permissive_of(&postures),
18935            postures
18936                .iter()
18937                .copied()
18938                .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18939        );
18940    }
18941
18942    #[test]
18943    fn resource_limits_strictest_of_is_order_independent() {
18944        // Order-independence pin — the fold inherits commutativity AND
18945        // associativity of the pairwise `strictest`, so any permutation
18946        // of the input slice yields the same aggregate. Reversal is
18947        // the maximal permutation for a 3-element input (indices
18948        // 0,1,2 → 2,1,0); an aggregator that leaked fold order would
18949        // discriminate here.
18950        let forward = ResourceLimits::strictest_of(&[
18951            HAND_AUTHORED_MID_POSTURE,
18952            HAND_AUTHORED_OTHER_POSTURE,
18953            DEFAULT_RESOURCE_LIMITS,
18954        ]);
18955        let reversed = ResourceLimits::strictest_of(&[
18956            DEFAULT_RESOURCE_LIMITS,
18957            HAND_AUTHORED_OTHER_POSTURE,
18958            HAND_AUTHORED_MID_POSTURE,
18959        ]);
18960        assert_eq!(forward, reversed);
18961    }
18962
18963    #[test]
18964    fn resource_limits_most_permissive_of_is_order_independent() {
18965        let forward = ResourceLimits::most_permissive_of(&[
18966            HAND_AUTHORED_MID_POSTURE,
18967            HAND_AUTHORED_OTHER_POSTURE,
18968            DEFAULT_RESOURCE_LIMITS,
18969        ]);
18970        let reversed = ResourceLimits::most_permissive_of(&[
18971            DEFAULT_RESOURCE_LIMITS,
18972            HAND_AUTHORED_OTHER_POSTURE,
18973            HAND_AUTHORED_MID_POSTURE,
18974        ]);
18975        assert_eq!(forward, reversed);
18976    }
18977
18978    #[test]
18979    fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
18980        // Compile-time-composition pin — `strictest_of` is `const fn`,
18981        // so a caller binds an N-ary aggregate to a `pub const` and the
18982        // fold evaluates at compile time. Every `DEFAULT_MAX_*` is
18983        // strictly less than `usize::MAX`, so the meet of `[DEFAULT,
18984        // UNBOUNDED]` picks DEFAULT on every axis — a preset-pair
18985        // identity already pinned pairwise, verified here through the
18986        // N-ary method's const-context evaluation. The N-ary const-fn
18987        // peer of the pairwise const-fn pins on
18988        // [`ResourceLimits::strictest`]; a regression that turned
18989        // `strictest_of` into a runtime `fn` would break the `const`
18990        // binding below at compile time.
18991        const AGGREGATED: ResourceLimits =
18992            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
18993        assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
18994    }
18995
18996    #[test]
18997    fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
18998        // Compile-time-composition dual pin — every `DEFAULT_MAX_*` is
18999        // strictly greater than `0`, so the join of `[EMPTY, DEFAULT]`
19000        // picks DEFAULT on every axis.
19001        const AGGREGATED: ResourceLimits =
19002            ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
19003        assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19004    }
19005
19006    #[test]
19007    fn resource_limits_strictest_of_result_is_leq_every_operand() {
19008        // Lattice-relation pin — the N-ary meet sits `leq` every
19009        // operand. This is the N-ary extension of the pairwise
19010        // `strictest_is_leq_both_operands` pin; the meet aggregate
19011        // dominates from below (the greatest lower bound of the slice
19012        // sits at-or-below each input on every axis).
19013        let postures: [ResourceLimits; 3] = [
19014            HAND_AUTHORED_MID_POSTURE,
19015            HAND_AUTHORED_OTHER_POSTURE,
19016            DEFAULT_RESOURCE_LIMITS,
19017        ];
19018        let met = ResourceLimits::strictest_of(&postures);
19019        for p in postures {
19020            assert!(met.leq(p));
19021        }
19022    }
19023
19024    #[test]
19025    fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
19026        // Lattice-relation dual pin — every operand sits `leq` the
19027        // N-ary join. The join aggregate is the least upper bound of
19028        // the slice.
19029        let postures: [ResourceLimits; 3] = [
19030            HAND_AUTHORED_MID_POSTURE,
19031            HAND_AUTHORED_OTHER_POSTURE,
19032            DEFAULT_RESOURCE_LIMITS,
19033        ];
19034        let joined = ResourceLimits::most_permissive_of(&postures);
19035        for p in postures {
19036            assert!(p.leq(joined));
19037        }
19038    }
19039
19040    #[test]
19041    fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
19042        // Behavioral pin — an expander constructed from the N-ary meet
19043        // of a slice of postures MUST gate at the tightest ceiling
19044        // across the slice on every axis. Peer of
19045        // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19046        // one AGGREGATION-ARITY axis over: where the pairwise pin
19047        // composes exactly two postures, this pin composes THREE
19048        // through the N-ary aggregator, confirming the arity extension
19049        // preserves the "meet gates at tightest" behavioral contract.
19050        //
19051        // The slice tightens macro_arity to 1 (the tightest across the
19052        // three postures) via the middle operand; every other axis
19053        // stays at usize::MAX (the outer two operands supply
19054        // usize::MAX; the middle operand's struct-update literal
19055        // preserves usize::MAX everywhere except macro_arity).
19056        let tightened = ResourceLimits::strictest_of(&[
19057            UNBOUNDED_RESOURCE_LIMITS,
19058            ResourceLimits {
19059                max_macro_arity: 1,
19060                ..UNBOUNDED_RESOURCE_LIMITS
19061            },
19062            UNBOUNDED_RESOURCE_LIMITS,
19063        ]);
19064        assert_eq!(tightened.max_macro_arity, 1);
19065        assert_eq!(tightened.max_expansion_depth, usize::MAX);
19066        assert_eq!(tightened.max_cache_entries, usize::MAX);
19067        assert_eq!(tightened.max_expansion_size, usize::MAX);
19068        assert_eq!(tightened.max_macro_body_size, usize::MAX);
19069        assert_eq!(tightened.max_registered_macros, usize::MAX);
19070        let mut e = Expander::with_limits(tightened);
19071        let forms = read("(defmacro two (a b) `,a)").unwrap();
19072        let err = e.expand_program(forms).unwrap_err();
19073        assert!(
19074            matches!(
19075                err,
19076                LispError::MacroArityExceeded {
19077                    arity: 2,
19078                    limit: 1,
19079                    ..
19080                }
19081            ),
19082            "expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
19083        );
19084    }
19085
19086    #[test]
19087    fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
19088        // Behavioral pin — an expander constructed from the meet of
19089        // two postures MUST gate at the tighter of the two ceilings
19090        // on every axis. Build the meet of the ceiling-lifted preset
19091        // and a hand-authored posture whose `max_macro_arity` sits at
19092        // 1; the meet inherits the 1-slot arity ceiling (tighter than
19093        // [`usize::MAX`]) AND the [`usize::MAX`] ceiling on every
19094        // other axis (tighter than the hand-authored posture's small
19095        // values — no, wait: the OTHER posture's small values ARE
19096        // tighter. So the meet inherits 1 on arity and the OTHER
19097        // small values elsewhere.) Confirm the constructed expander
19098        // rejects a 2-arg macro registration with the arity gate.
19099        let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
19100            max_macro_arity: 1,
19101            ..UNBOUNDED_RESOURCE_LIMITS
19102        });
19103        assert_eq!(tightened.max_macro_arity, 1);
19104        // Other five axes remain at `usize::MAX` (both operands carry
19105        // `usize::MAX` on those, so the pointwise `min` is `usize::MAX`).
19106        assert_eq!(tightened.max_expansion_depth, usize::MAX);
19107        assert_eq!(tightened.max_cache_entries, usize::MAX);
19108        assert_eq!(tightened.max_expansion_size, usize::MAX);
19109        assert_eq!(tightened.max_macro_body_size, usize::MAX);
19110        assert_eq!(tightened.max_registered_macros, usize::MAX);
19111        let mut e = Expander::with_limits(tightened);
19112        let forms = read("(defmacro two (a b) `,a)").unwrap();
19113        let err = e.expand_program(forms).unwrap_err();
19114        assert!(
19115            matches!(
19116                err,
19117                LispError::MacroArityExceeded {
19118                    arity: 2,
19119                    limit: 1,
19120                    ..
19121                }
19122            ),
19123            "the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
19124        );
19125    }
19126
19127    // ── ResourceLimits::clamp — bounded-lattice bracket combinator ────
19128
19129    /// A tighter-than-MID lower-bracket witness — every axis strictly
19130    /// smaller than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the
19131    /// pointwise `max(mid, floor) == mid` on every axis (`floor.leq(mid)`
19132    /// holds structurally). Peer of `HAND_AUTHORED_CLAMP_CEILING` one
19133    /// BRACKET-BOUND axis over on the (floor, ceiling) bracket-witness
19134    /// pair.
19135    const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
19136        max_expansion_depth: 2,
19137        max_cache_entries: 3,
19138        max_expansion_size: 5,
19139        max_macro_body_size: 7,
19140        max_registered_macros: 11,
19141        max_macro_arity: 13,
19142    };
19143
19144    /// A looser-than-MID upper-bracket witness — every axis strictly larger
19145    /// than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the pointwise
19146    /// `min(mid, ceiling) == mid` on every axis (`mid.leq(ceiling)` holds
19147    /// structurally). Peer of `HAND_AUTHORED_CLAMP_FLOOR` one BRACKET-
19148    /// BOUND axis over. Together with `HAND_AUTHORED_CLAMP_FLOOR` the two
19149    /// bracket `HAND_AUTHORED_MID_POSTURE` strictly on every axis:
19150    /// `FLOOR.leq(MID) && MID.leq(CEILING) && FLOOR != MID && MID !=
19151    /// CEILING`.
19152    const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
19153        max_expansion_depth: 47,
19154        max_cache_entries: 53,
19155        max_expansion_size: 59,
19156        max_macro_body_size: 61,
19157        max_registered_macros: 67,
19158        max_macro_arity: 71,
19159    };
19160
19161    #[test]
19162    fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
19163        // In-range identity — when the input already sits within the
19164        // `[lower, upper]` bracket (`lower.leq(a) && a.leq(upper)` per-
19165        // axis), the clamp is the identity function: the pointwise
19166        // `max(a, lower) == a` (since `lower ≤ a` per-axis picks `a`),
19167        // then the pointwise `min(a, upper) == a` (since `a ≤ upper`
19168        // per-axis picks `a`). The primary invariant on the "input
19169        // already satisfies the bracket" arm of the three-arm bracket-
19170        // membership surface.
19171        //
19172        // Prerequisite pins — the two hand-authored bounds bracket
19173        // `MID` strictly on every axis (structural check that discharges
19174        // the "already in range" premise). A regression that inflated
19175        // `FLOOR` or deflated `CEILING` past `MID` would fail these
19176        // asserts before the clamp identity was tested, making the
19177        // premise's failure name itself rather than showing up as a
19178        // downstream misleading clamp mismatch.
19179        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19180        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19181        let clamped =
19182            HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19183        assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
19184    }
19185
19186    #[test]
19187    fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
19188        // Below-lower floor — when the input is tighter than the
19189        // bracket floor on every axis (`a.leq(lower)` per-axis), the
19190        // clamp lifts the input up to `lower`: the pointwise `max(a,
19191        // lower) == lower` (since `a ≤ lower` per-axis picks `lower`),
19192        // then the pointwise `min(lower, upper) == lower` (since
19193        // `lower ≤ upper` per-axis picks `lower`). Peer of
19194        // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19195        // one BRACKET-POSITION axis over on the (in-range, below-lower,
19196        // above-upper) three-arm bracket-membership surface.
19197        //
19198        // `EMPTY_RESOURCE_LIMITS` sits `leq` every posture (bounded-
19199        // lattice bottom), so `EMPTY.leq(FLOOR)` holds structurally
19200        // for any FLOOR; a clamp of `EMPTY` into `[FLOOR, CEILING]`
19201        // MUST return `FLOOR`.
19202        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
19203        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19204        let clamped =
19205            EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19206        assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
19207    }
19208
19209    #[test]
19210    fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
19211        // Above-upper ceiling — when the input is looser than the
19212        // bracket ceiling on every axis (`upper.leq(a)` per-axis), the
19213        // clamp lowers the input down to `upper`: the pointwise
19214        // `max(a, lower) == a` (since `lower ≤ upper ≤ a` per-axis picks
19215        // `a`), then the pointwise `min(a, upper) == upper` (since
19216        // `upper ≤ a` per-axis picks `upper`). Peer of the
19217        // below-lower arm one BRACKET-POSITION axis over on the three-
19218        // arm bracket-membership surface.
19219        //
19220        // `UNBOUNDED_RESOURCE_LIMITS` sits `geq` every posture (bounded-
19221        // lattice top), so `CEILING.leq(UNBOUNDED)` holds structurally
19222        // for any CEILING; a clamp of `UNBOUNDED` into `[FLOOR, CEILING]`
19223        // MUST return `CEILING`.
19224        assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
19225        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19226        let clamped =
19227            UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19228        assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
19229    }
19230
19231    #[test]
19232    fn resource_limits_clamp_result_sits_within_the_bracket() {
19233        // Bracket-membership contract — the DEFINING invariant on the
19234        // clamp primitive: for every well-formed bracket
19235        // (`lower.leq(upper)`) and every input `a`, the result sits
19236        // within the bracket `lower.leq(a.clamp(lower, upper)) &&
19237        // a.clamp(lower, upper).leq(upper)`. Exercised on ALL FOUR arm
19238        // positions of the bracket-membership surface via the four
19239        // known-preset inputs (below-lower → in-range → above-upper
19240        // → asymmetric-crossing) so a regression that inflated the
19241        // ceiling arm or deflated the floor arm exhausts all four
19242        // input positions and names the failing arm.
19243        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19244        let inputs = [
19245            EMPTY_RESOURCE_LIMITS,       // below-lower
19246            HAND_AUTHORED_MID_POSTURE,   // in-range
19247            UNBOUNDED_RESOURCE_LIMITS,   // above-upper
19248            HAND_AUTHORED_OTHER_POSTURE, // asymmetric (crosses the bracket)
19249        ];
19250        for a in inputs {
19251            let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19252            assert!(
19253                HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
19254                "clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
19255            );
19256            assert!(
19257                clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
19258                "clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
19259            );
19260        }
19261    }
19262
19263    #[test]
19264    fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
19265        // Extrema-bracket identity — clamping against the bounded-
19266        // lattice extrema (`EMPTY_RESOURCE_LIMITS` as the leq-minimum,
19267        // `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum) is the
19268        // identity function on every posture: every posture already
19269        // sits within the widest possible bracket. Cross-checks the
19270        // clamp primitive against the bounded-lattice identity elements
19271        // previously lifted; a regression that swapped the two extrema
19272        // in the impl would collapse every input to
19273        // `UNBOUNDED.strictest(EMPTY) == EMPTY` and fail this pin on
19274        // every non-EMPTY input.
19275        for a in [
19276            DEFAULT_RESOURCE_LIMITS,
19277            HAND_AUTHORED_MID_POSTURE,
19278            HAND_AUTHORED_OTHER_POSTURE,
19279            EMPTY_RESOURCE_LIMITS,
19280            UNBOUNDED_RESOURCE_LIMITS,
19281        ] {
19282            assert_eq!(
19283                a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19284                a,
19285                "clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
19286            );
19287        }
19288    }
19289
19290    #[test]
19291    fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
19292        // Degenerate-bracket collapse — a zero-width bracket
19293        // (`lower == upper`) collapses every input to the single point.
19294        // The pointwise `max(a, x)` lifts every axis to at least `x`,
19295        // then the pointwise `min(., x)` clamps every axis to exactly
19296        // `x` regardless of `a`'s per-axis position (since `x ≤ max(a,
19297        // x) ≤ max(x, x) == x` when `a.leq(x)` and `min(max(a, x), x)
19298        // == x` when `x.leq(a)`).
19299        for x in [
19300            HAND_AUTHORED_MID_POSTURE,
19301            HAND_AUTHORED_OTHER_POSTURE,
19302            DEFAULT_RESOURCE_LIMITS,
19303        ] {
19304            for a in [
19305                EMPTY_RESOURCE_LIMITS,
19306                HAND_AUTHORED_MID_POSTURE,
19307                HAND_AUTHORED_OTHER_POSTURE,
19308                UNBOUNDED_RESOURCE_LIMITS,
19309            ] {
19310                assert_eq!(
19311                    a.clamp(x, x),
19312                    x,
19313                    "zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
19314                );
19315            }
19316        }
19317    }
19318
19319    #[test]
19320    fn resource_limits_clamp_is_idempotent() {
19321        // Idempotence — re-applying the same bracket to an already-
19322        // clamped posture returns the same result. The clamp primitive
19323        // is idempotent because its output already sits within the
19324        // `[lower, upper]` bracket (pinned by
19325        // `resource_limits_clamp_result_sits_within_the_bracket`), so
19326        // the second application hits the in-range identity arm. Peer
19327        // of the idempotence pins on `strictest` and `most_permissive`
19328        // one ARITY axis over (2-input primitive → 3-input bracket) on
19329        // the lattice-algebra idempotence-law surface.
19330        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19331        for a in [
19332            EMPTY_RESOURCE_LIMITS,
19333            HAND_AUTHORED_MID_POSTURE,
19334            HAND_AUTHORED_OTHER_POSTURE,
19335            UNBOUNDED_RESOURCE_LIMITS,
19336        ] {
19337            let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19338            let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19339            assert_eq!(
19340                once, twice,
19341                "clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
19342            );
19343        }
19344    }
19345
19346    #[test]
19347    fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
19348        // Const-fn pin — the bracket combinator is evaluable in const
19349        // context, so a caller can pre-clamp a shipped preset against
19350        // an operator-supplied policy bracket at compile time. Sibling
19351        // of the const-fn composition pins on `strictest` /
19352        // `most_permissive` / `strictest_of` / `most_permissive_of`
19353        // one PRIMITIVE-KIND axis over: those four combinators compose
19354        // presets at const time, and this bracket combinator projects
19355        // a preset into a const-time bracket.
19356        //
19357        // A regression to a runtime `fn` here would fail the `pub
19358        // const` binding below at compile time — a stronger guarantee
19359        // than a runtime `assert!` because the const binding is
19360        // evaluated once at compile time, not per test invocation.
19361        const CLAMPED_DEFAULT: ResourceLimits =
19362            DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
19363        assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
19364
19365        // Compile-time bracket-membership pin — const `assert!` on the
19366        // lattice-relation contract discharges the "clamp result sits
19367        // within the bracket" invariant at compile time on the
19368        // (DEFAULT, EMPTY, UNBOUNDED) preset triple.
19369        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
19370        const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
19371    }
19372
19373    #[test]
19374    fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
19375        // Structural-equivalence pin — the sole behavioral claim the
19376        // clamp primitive makes is that it IS the
19377        // `self.most_permissive(lower).strictest(upper)` two-step
19378        // cascade the pre-lift caller composed. A drift between the
19379        // method and the inline cascade would break this equality on
19380        // every input; the pin exhausts the four bracket-position arms
19381        // of the input surface.
19382        for a in [
19383            EMPTY_RESOURCE_LIMITS,
19384            HAND_AUTHORED_MID_POSTURE,
19385            HAND_AUTHORED_OTHER_POSTURE,
19386            UNBOUNDED_RESOURCE_LIMITS,
19387        ] {
19388            let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19389            let via_cascade = a
19390                .most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
19391                .strictest(HAND_AUTHORED_CLAMP_CEILING);
19392            assert_eq!(via_method, via_cascade);
19393        }
19394    }
19395
19396    #[test]
19397    fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
19398        // Behavioral pin — an expander constructed from a preset
19399        // clamped into `[EMPTY, TIGHT_BRACKET]` MUST gate at the
19400        // bracket ceiling's arity axis. Confirms the clamp primitive
19401        // reaches through to the runtime gates that consume
19402        // `ResourceLimits`, so a future consumer that pre-clamps a
19403        // preset against a policy bracket inherits the bracket's
19404        // ceilings at every guard. Peer of
19405        // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19406        // one COMBINATOR-ARITY axis over on the (pairwise-meet,
19407        // N-ary-meet, bracket) combinator-arity face.
19408        //
19409        // The bracket ceiling tightens `max_macro_arity` to 1; every
19410        // other axis stays at `usize::MAX` (the CEILING preset carries
19411        // `usize::MAX` on those, and the input UNBOUNDED preset also
19412        // carries `usize::MAX` on every axis — the clamp's `strictest`
19413        // arm picks `usize::MAX` from either side).
19414        let bracket_ceiling = ResourceLimits {
19415            max_macro_arity: 1,
19416            ..UNBOUNDED_RESOURCE_LIMITS
19417        };
19418        let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19419        assert_eq!(clamped.max_macro_arity, 1);
19420        assert_eq!(clamped.max_expansion_depth, usize::MAX);
19421        assert_eq!(clamped.max_cache_entries, usize::MAX);
19422        assert_eq!(clamped.max_expansion_size, usize::MAX);
19423        assert_eq!(clamped.max_macro_body_size, usize::MAX);
19424        assert_eq!(clamped.max_registered_macros, usize::MAX);
19425        let mut e = Expander::with_limits(clamped);
19426        let forms = read("(defmacro two (a b) `,a)").unwrap();
19427        let err = e.expand_program(forms).unwrap_err();
19428        assert!(
19429            matches!(
19430                err,
19431                LispError::MacroArityExceeded {
19432                    arity: 2,
19433                    limit: 1,
19434                    ..
19435                }
19436            ),
19437            "the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
19438        );
19439    }
19440
19441    // ── ResourceLimits::within — bracket-membership predicate ─────────
19442
19443    #[test]
19444    fn resource_limits_within_of_posture_in_range_is_true() {
19445        // In-range identity — when the input already sits within the
19446        // `[lower, upper]` bracket on every axis (`lower.leq(a) &&
19447        // a.leq(upper)`), the predicate returns `true`. Peer of
19448        // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19449        // one PRIMITIVE-KIND axis over on the (combinator, predicate)
19450        // face of the bracket-primitive surface.
19451        //
19452        // Prerequisite pins — the two hand-authored bounds bracket
19453        // `MID` strictly on every axis, structurally discharging the
19454        // "already in range" premise.
19455        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19456        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19457        assert!(HAND_AUTHORED_MID_POSTURE
19458            .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19459    }
19460
19461    #[test]
19462    fn resource_limits_within_of_posture_below_lower_is_false() {
19463        // Below-lower rejection — when the input is strictly tighter
19464        // than the bracket floor on some axis (`!lower.leq(a)`), the
19465        // predicate returns `false`. `EMPTY_RESOURCE_LIMITS` has every
19466        // axis at `0`, strictly below `HAND_AUTHORED_CLAMP_FLOOR`'s
19467        // per-axis ceilings; so `FLOOR.leq(EMPTY)` fails on every axis,
19468        // making the conjunction reject regardless of `EMPTY`'s relation
19469        // to `CEILING`. Peer of
19470        // `resource_limits_clamp_of_posture_below_lower_returns_lower`
19471        // one PRIMITIVE-KIND axis over.
19472        assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
19473        assert!(
19474            !EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
19475        );
19476    }
19477
19478    #[test]
19479    fn resource_limits_within_of_posture_above_upper_is_false() {
19480        // Above-upper rejection — when the input is strictly looser
19481        // than the bracket ceiling on some axis (`!a.leq(upper)`), the
19482        // predicate returns `false`. `UNBOUNDED_RESOURCE_LIMITS` has
19483        // every axis at `usize::MAX`, strictly above
19484        // `HAND_AUTHORED_CLAMP_CEILING`'s per-axis ceilings; so
19485        // `UNBOUNDED.leq(CEILING)` fails on every axis. Peer of
19486        // `resource_limits_clamp_of_posture_above_upper_returns_upper`
19487        // one PRIMITIVE-KIND axis over.
19488        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
19489        assert!(!UNBOUNDED_RESOURCE_LIMITS
19490            .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19491    }
19492
19493    #[test]
19494    fn resource_limits_within_with_lattice_extrema_is_true() {
19495        // Extrema-bracket identity — every posture sits within the
19496        // widest possible bracket `[EMPTY_RESOURCE_LIMITS,
19497        // UNBOUNDED_RESOURCE_LIMITS]`. Cross-checks the predicate
19498        // against the bounded-lattice identity elements (`EMPTY` is
19499        // the leq-minimum, `UNBOUNDED` is the leq-maximum). Peer of
19500        // `resource_limits_clamp_with_lattice_extrema_returns_the_input`
19501        // one PRIMITIVE-KIND axis over — the predicate view of the
19502        // "clamp is the identity" property.
19503        for a in [
19504            DEFAULT_RESOURCE_LIMITS,
19505            HAND_AUTHORED_MID_POSTURE,
19506            HAND_AUTHORED_OTHER_POSTURE,
19507            EMPTY_RESOURCE_LIMITS,
19508            UNBOUNDED_RESOURCE_LIMITS,
19509        ] {
19510            assert!(
19511                a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19512                "every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
19513            );
19514        }
19515    }
19516
19517    #[test]
19518    fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
19519        // Degenerate-bracket collapse — a zero-width bracket admits
19520        // only the single point at the bounds. `a.within(x, x)` holds
19521        // iff `x.leq(a) && a.leq(x)` iff `a == x` by antisymmetry of
19522        // `leq`. Peer of `resource_limits_clamp_with_equal_bounds_returns_the_bound`
19523        // one PRIMITIVE-KIND axis over: the clamp collapses to `x`
19524        // unconditionally, and the predicate distinguishes the "was
19525        // already equal" case from the "would have been clamped" case.
19526        let bounds = [
19527            HAND_AUTHORED_MID_POSTURE,
19528            HAND_AUTHORED_OTHER_POSTURE,
19529            DEFAULT_RESOURCE_LIMITS,
19530            EMPTY_RESOURCE_LIMITS,
19531            UNBOUNDED_RESOURCE_LIMITS,
19532        ];
19533        for x in bounds {
19534            for a in bounds {
19535                assert_eq!(
19536                    a.within(x, x),
19537                    a == x,
19538                    "a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
19539                );
19540            }
19541        }
19542    }
19543
19544    #[test]
19545    fn resource_limits_within_of_self_is_reflexive() {
19546        // Reflexive-bracket identity — every posture sits within the
19547        // zero-width bracket at itself, since `a.leq(a)` holds by
19548        // reflexivity of `leq`. The reflexive-bracket peer of the
19549        // reflexive-leq pin (`resource_limits_leq_is_reflexive`) one
19550        // ARITY axis over (pairwise → three-input).
19551        for a in [
19552            EMPTY_RESOURCE_LIMITS,
19553            DEFAULT_RESOURCE_LIMITS,
19554            HAND_AUTHORED_MID_POSTURE,
19555            HAND_AUTHORED_OTHER_POSTURE,
19556            UNBOUNDED_RESOURCE_LIMITS,
19557        ] {
19558            assert!(
19559                a.within(a, a),
19560                "a.within(a, a) must hold by reflexivity of leq; a={a:?}",
19561            );
19562        }
19563    }
19564
19565    #[test]
19566    fn resource_limits_within_agrees_with_clamp_fixed_point() {
19567        // Clamp fixed-point theorem — `a.within(lower, upper) ⇔
19568        // a.clamp(lower, upper) == a`. The CANONICAL cross-check axiom
19569        // binding the `within` predicate to the `clamp` combinator,
19570        // analogous to the meet-agreement (`a.leq(b) ⇔ a.strictest(b)
19571        // == a`, pinned by `resource_limits_leq_agrees_with_meet`) and
19572        // join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`,
19573        // pinned by `resource_limits_leq_agrees_with_join`) axioms
19574        // binding the `leq` relation to its combinator peers.
19575        //
19576        // Exercised on all four bracket-position arms of the input
19577        // surface (below-lower / in-range / above-upper / asymmetric-
19578        // crossing) so a regression that broke either direction of the
19579        // fixed-point equivalence exhausts every position and names the
19580        // failing arm.
19581        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19582        for a in [
19583            EMPTY_RESOURCE_LIMITS,       // below-lower — clamp != a, within false
19584            HAND_AUTHORED_MID_POSTURE,   // in-range   — clamp == a, within true
19585            UNBOUNDED_RESOURCE_LIMITS,   // above-upper — clamp != a, within false
19586            HAND_AUTHORED_OTHER_POSTURE, // asymmetric — clamp != a, within false
19587        ] {
19588            let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19589            let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19590            let clamp_fixed = clamped == a;
19591            assert_eq!(
19592                within_holds, clamp_fixed,
19593                "within(lower, upper) must agree with clamp fixed-point; \
19594                 a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
19595            );
19596        }
19597    }
19598
19599    #[test]
19600    fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
19601        // Structural-equivalence pin — the sole behavioral claim the
19602        // predicate makes is that it IS the `lower.leq(a) && a.leq(upper)`
19603        // two-primitive conjunction the pre-lift caller composed. A drift
19604        // between the method and the inline conjunction would break this
19605        // equality on some input; the pin exhausts the four bracket-
19606        // position arms of the input surface. Peer of
19607        // `resource_limits_clamp_agrees_with_direct_two_step_cascade`
19608        // one PRIMITIVE-KIND axis over (combinator ↔ predicate).
19609        for a in [
19610            EMPTY_RESOURCE_LIMITS,
19611            HAND_AUTHORED_MID_POSTURE,
19612            HAND_AUTHORED_OTHER_POSTURE,
19613            UNBOUNDED_RESOURCE_LIMITS,
19614        ] {
19615            let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19616            let via_conjunction =
19617                HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
19618            assert_eq!(via_method, via_conjunction, "a={a:?}");
19619        }
19620    }
19621
19622    #[test]
19623    fn resource_limits_within_composes_at_compile_time_via_const_fn() {
19624        // Const-fn pin — the bracket predicate is evaluable in const
19625        // context, so a caller can pin a preset-bracket-membership
19626        // identity at compile time. Sibling of the const-fn evaluability
19627        // pins on `leq` and `clamp` one PRIMITIVE-KIND axis over: the
19628        // relation and combinator both evaluate at const time, and the
19629        // predicate closing the (combinator, predicate) row does too.
19630        //
19631        // A regression to a runtime `fn` here would fail the `const _:
19632        // () = assert!(...)` bindings below at compile time.
19633        const _: () = assert!(
19634            DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
19635        );
19636        const _: () = assert!(
19637            DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
19638        );
19639        const _: () = assert!(
19640            !UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
19641        );
19642    }
19643
19644    #[test]
19645    fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
19646        // Behavioral pin — a caller that CHECKS bracket-membership
19647        // before constructing an expander can safely reach through to
19648        // the runtime gates: if `within` reports true, `clamp` is a
19649        // no-op (by the fixed-point theorem), so the expander's
19650        // ceilings are the input posture's own. Peer of
19651        // `expander_built_at_clamped_preset_gates_at_the_bracket_ceiling`
19652        // one PRIMITIVE-KIND axis over — the predicate view of the
19653        // "clamp reaches through to the runtime gates" property.
19654        //
19655        // The candidate sits at the bracket ceiling (arity=1, other
19656        // axes at usize::MAX), so it MUST sit within the [EMPTY,
19657        // bracket_ceiling] bracket — the maximal within-membership arm
19658        // of the fixed-point theorem. Every other axis at usize::MAX
19659        // clears the register-time gates, so the arity gate at 1 is
19660        // the one that trips on a 2-parameter macro registration.
19661        let bracket_ceiling = ResourceLimits {
19662            max_macro_arity: 1,
19663            ..UNBOUNDED_RESOURCE_LIMITS
19664        };
19665        let candidate = ResourceLimits {
19666            max_macro_arity: 1,
19667            ..UNBOUNDED_RESOURCE_LIMITS
19668        };
19669        assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
19670        // Since candidate sits within the bracket, clamp is the
19671        // identity; the expander inherits candidate's ceilings verbatim.
19672        let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19673        assert_eq!(clamped, candidate);
19674        let mut e = Expander::with_limits(clamped);
19675        let forms = read("(defmacro two (a b) `,a)").unwrap();
19676        let err = e.expand_program(forms).unwrap_err();
19677        assert!(
19678            matches!(
19679                err,
19680                LispError::MacroArityExceeded {
19681                    arity: 2,
19682                    limit: 1,
19683                    ..
19684                }
19685            ),
19686            "the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
19687        );
19688    }
19689
19690    // ── ResourceLimits::is_lower_bound_of / is_upper_bound_of ─────────
19691    //   N-ary boolean-conjunction peers of the pairwise `leq` relation,
19692    //   closing the (combinator, predicate) row on the N-ary-aggregation
19693    //   face of the lattice-algebra surface. See the docstring on
19694    //   `ResourceLimits::is_lower_bound_of` for the (pairwise, N-ary,
19695    //   bracket) × (combinator, predicate) placement.
19696
19697    #[test]
19698    fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
19699        // Empty-conjunction identity — every posture is trivially a
19700        // lower bound of the empty set. Peer of `strictest_of(&[]) ==
19701        // UNBOUNDED_RESOURCE_LIMITS`'s empty-slice identity one
19702        // PRIMITIVE-KIND axis over: the empty slice aggregates to the
19703        // identity element of the underlying operation (`true` for
19704        // boolean conjunction, `UNBOUNDED` for pointwise `min`).
19705        assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
19706        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19707        assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19708        assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19709    }
19710
19711    #[test]
19712    fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
19713        // Empty-conjunction dual — every posture is trivially an upper
19714        // bound of the empty set. Peer of
19715        // `is_lower_bound_of_empty_slice_is_vacuously_true` one
19716        // COMBINATOR-DIRECTION axis over, AND peer of
19717        // `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
19718        // slice identity one PRIMITIVE-KIND axis over.
19719        assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
19720        assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19721        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19722        assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19723    }
19724
19725    #[test]
19726    fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
19727        // 1-input identity — the singleton predicate reduces to the
19728        // pairwise partial-order relation. The N-ary predicate on a
19729        // 1-element slice is the pairwise `leq` verbatim, mirroring
19730        // `strictest_of(&[a]) == a`'s reduction on the combinator side.
19731        let cases: [(ResourceLimits, ResourceLimits); 6] = [
19732            (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19733            (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19734            (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19735            (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19736            (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19737            (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19738        ];
19739        for (a, b) in cases {
19740            assert_eq!(
19741                a.is_lower_bound_of(&[b]),
19742                a.leq(b),
19743                "1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
19744            );
19745        }
19746    }
19747
19748    #[test]
19749    fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
19750        // 1-input dual identity — the singleton upper-bound predicate
19751        // is the pairwise `leq` with the direction flipped
19752        // (`b.leq(a)`), since `self` sits ABOVE the operand rather than
19753        // BELOW it.
19754        let cases: [(ResourceLimits, ResourceLimits); 6] = [
19755            (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19756            (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19757            (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19758            (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19759            (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19760            (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19761        ];
19762        for (a, b) in cases {
19763            assert_eq!(
19764                a.is_upper_bound_of(&[b]),
19765                b.leq(a),
19766                "1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
19767            );
19768        }
19769    }
19770
19771    #[test]
19772    fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
19773        // Definitional bridge — the N-ary meet is always a lower bound
19774        // of the slice it aggregates. Pins the link between the N-ary
19775        // COMBINATOR (`strictest_of`) and the N-ary PREDICATE
19776        // (`is_lower_bound_of`): the meet is an ELEMENT of the lower-
19777        // bound set the predicate CHARACTERIZES.
19778        let postures: [ResourceLimits; 3] = [
19779            HAND_AUTHORED_MID_POSTURE,
19780            HAND_AUTHORED_OTHER_POSTURE,
19781            DEFAULT_RESOURCE_LIMITS,
19782        ];
19783        let met = ResourceLimits::strictest_of(&postures);
19784        assert!(met.is_lower_bound_of(&postures));
19785    }
19786
19787    #[test]
19788    fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
19789        // Dual definitional bridge — the N-ary join is always an upper
19790        // bound of the slice it aggregates.
19791        let postures: [ResourceLimits; 3] = [
19792            HAND_AUTHORED_MID_POSTURE,
19793            HAND_AUTHORED_OTHER_POSTURE,
19794            DEFAULT_RESOURCE_LIMITS,
19795        ];
19796        let joined = ResourceLimits::most_permissive_of(&postures);
19797        assert!(joined.is_upper_bound_of(&postures));
19798    }
19799
19800    #[test]
19801    fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
19802        // Universal-bottom witness — the lattice bottom is a common
19803        // lower bound of every slice, since `EMPTY.leq(a) == true` for
19804        // every posture by the lattice-bottom axiom (`0 ≤ x` per-axis).
19805        // The bounded-lattice specialization of the general "the meet-
19806        // identity is a lower bound of every set" theorem.
19807        let postures: [ResourceLimits; 4] = [
19808            HAND_AUTHORED_MID_POSTURE,
19809            HAND_AUTHORED_OTHER_POSTURE,
19810            DEFAULT_RESOURCE_LIMITS,
19811            UNBOUNDED_RESOURCE_LIMITS,
19812        ];
19813        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
19814        // The three preset-carried postures each individually — the
19815        // universal-bottom witness holds on the singleton slice too.
19816        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19817        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19818        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19819    }
19820
19821    #[test]
19822    fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
19823        // Universal-top witness — the lattice top is a common upper
19824        // bound of every slice, since `a.leq(UNBOUNDED) == true` for
19825        // every posture by the lattice-top axiom (`x ≤ usize::MAX`
19826        // per-axis).
19827        let postures: [ResourceLimits; 4] = [
19828            HAND_AUTHORED_MID_POSTURE,
19829            HAND_AUTHORED_OTHER_POSTURE,
19830            DEFAULT_RESOURCE_LIMITS,
19831            EMPTY_RESOURCE_LIMITS,
19832        ];
19833        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
19834        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19835        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19836        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19837    }
19838
19839    #[test]
19840    fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
19841        // Any-operand short-circuit — if `self` fails `leq` against ANY
19842        // operand in the slice, the N-ary conjunction rejects. The dual
19843        // of the "meet is a lower bound" witness on the false side:
19844        // moving `self` STRICTLY ABOVE an operand on any axis costs the
19845        // lower-bound property.
19846        //
19847        // MID sits above the join (element-wise) on some axes and below
19848        // on others, so MID is NEITHER a lower bound of {MID, OTHER}
19849        // (fails against OTHER's smaller-axis values) NOR is OTHER a
19850        // lower bound of {MID, OTHER} (fails against MID's smaller-axis
19851        // values).
19852        assert!(!HAND_AUTHORED_MID_POSTURE
19853            .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19854        assert!(!HAND_AUTHORED_OTHER_POSTURE
19855            .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19856        // A UNIVERSAL rejection — the lattice top is a lower bound
19857        // ONLY of the empty set OR of slices whose every element equals
19858        // UNBOUNDED. Given a strictly-smaller preset in the slice, the
19859        // top's per-axis usize::MAX loses `leq` on every axis whose
19860        // preset value is strictly smaller.
19861        assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19862        assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19863    }
19864
19865    #[test]
19866    fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
19867        // Dual any-operand short-circuit — if ANY operand fails `leq`
19868        // against `self`, the N-ary conjunction rejects.
19869        assert!(!HAND_AUTHORED_MID_POSTURE
19870            .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19871        assert!(!HAND_AUTHORED_OTHER_POSTURE
19872            .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19873        // The bottom cannot bound anything from above except the empty
19874        // set OR a slice of EMPTY-only postures.
19875        assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19876        assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19877    }
19878
19879    #[test]
19880    fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
19881        // Method-vs-scaffold cross-check — the lifted N-ary predicate
19882        // agrees with the pre-lift two-primitive
19883        // `postures.iter().all(|p| self.leq(*p))` scaffolding on every
19884        // preset-carried posture across the shipped bounded-lattice
19885        // extrema AND the two hand-authored asymmetric witnesses. Pins
19886        // the lift is a semantic no-op — post-lift the consumer routes
19887        // through ONE name instead of composing the conjunction inline,
19888        // but the two paths compute the same boolean.
19889        let candidates: [ResourceLimits; 5] = [
19890            EMPTY_RESOURCE_LIMITS,
19891            DEFAULT_RESOURCE_LIMITS,
19892            UNBOUNDED_RESOURCE_LIMITS,
19893            HAND_AUTHORED_MID_POSTURE,
19894            HAND_AUTHORED_OTHER_POSTURE,
19895        ];
19896        let slice: [ResourceLimits; 3] = [
19897            HAND_AUTHORED_MID_POSTURE,
19898            HAND_AUTHORED_OTHER_POSTURE,
19899            DEFAULT_RESOURCE_LIMITS,
19900        ];
19901        for c in candidates {
19902            let direct = slice.iter().all(|p| c.leq(*p));
19903            assert_eq!(
19904                c.is_lower_bound_of(&slice),
19905                direct,
19906                "is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
19907            );
19908        }
19909    }
19910
19911    #[test]
19912    fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
19913        // Dual method-vs-scaffold cross-check.
19914        let candidates: [ResourceLimits; 5] = [
19915            EMPTY_RESOURCE_LIMITS,
19916            DEFAULT_RESOURCE_LIMITS,
19917            UNBOUNDED_RESOURCE_LIMITS,
19918            HAND_AUTHORED_MID_POSTURE,
19919            HAND_AUTHORED_OTHER_POSTURE,
19920        ];
19921        let slice: [ResourceLimits; 3] = [
19922            HAND_AUTHORED_MID_POSTURE,
19923            HAND_AUTHORED_OTHER_POSTURE,
19924            DEFAULT_RESOURCE_LIMITS,
19925        ];
19926        for c in candidates {
19927            let direct = slice.iter().all(|p| p.leq(c));
19928            assert_eq!(
19929                c.is_upper_bound_of(&slice),
19930                direct,
19931                "is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
19932            );
19933        }
19934    }
19935
19936    #[test]
19937    fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
19938        // Const-fn pin — the N-ary lower-bound predicate is evaluable
19939        // in const context, so a caller can pin an N-ary-bound-
19940        // membership identity at compile time. Sibling of the const-fn
19941        // evaluability pins on `leq` and `within` one PRIMITIVE-KIND
19942        // axis over, AND sibling of `strictest_of`'s const-fn pin one
19943        // COMBINATOR-KIND axis over.
19944        //
19945        // A regression to a runtime `fn` here would fail the `const _:
19946        // () = assert!(...)` bindings below at compile time.
19947        const _: () = assert!(EMPTY_RESOURCE_LIMITS
19948            .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
19949        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19950        const _: () =
19951            assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19952    }
19953
19954    #[test]
19955    fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
19956        // Const-fn dual pin.
19957        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
19958            .is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
19959        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19960        const _: () =
19961            assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19962    }
19963
19964    /// The full canonical preset roster the strict-order pins sweep
19965    /// through — the (bottom, middle, top) bounded-lattice preset
19966    /// triple plus the two hand-authored incomparable asymmetric
19967    /// postures. Every strict-order law below quantifies over this
19968    /// slice, so a regression that breaks strict `<` on any preset
19969    /// pair discriminates at ONE named fixture rather than at a
19970    /// per-test ad-hoc literal.
19971    const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
19972        EMPTY_RESOURCE_LIMITS,
19973        DEFAULT_RESOURCE_LIMITS,
19974        UNBOUNDED_RESOURCE_LIMITS,
19975        HAND_AUTHORED_MID_POSTURE,
19976        HAND_AUTHORED_OTHER_POSTURE,
19977    ];
19978
19979    #[test]
19980    fn resource_limits_lt_is_irreflexive() {
19981        // Strict partial-order law — `a < a == false` for every
19982        // posture. Irreflexivity is the strict-relation axiom peer of
19983        // reflexivity on `leq` one STRICTNESS axis over on the
19984        // partial-order axiom surface; a regression that dropped the
19985        // antisymmetric leg (`!other.leq(self)`) — leaving the body at
19986        // `self.leq(other)` — would fire here on every preset since
19987        // `leq` is reflexive.
19988        for &a in STRICT_ORDER_ROSTER {
19989            assert!(!a.lt(a), "irreflexivity failed on {a:?}");
19990        }
19991    }
19992
19993    #[test]
19994    fn resource_limits_lt_is_asymmetric() {
19995        // Strict partial-order law — `a < b ⇒ ¬(b < a)` for every
19996        // pair. The two directions of the strict relation cannot both
19997        // hold, since `a.lt(b)` implies `!b.leq(a)` and `b.lt(a)`
19998        // requires `b.leq(a)`. Sweeps every ordered pair in the
19999        // canonical preset roster; discriminates a regression that
20000        // dropped the antisymmetric leg in either direction.
20001        for &a in STRICT_ORDER_ROSTER {
20002            for &b in STRICT_ORDER_ROSTER {
20003                if a.lt(b) {
20004                    assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
20005                }
20006            }
20007        }
20008    }
20009
20010    #[test]
20011    fn resource_limits_lt_is_transitive() {
20012        // Strict partial-order law — `a < b ∧ b < c ⇒ a < c`. Sweeps
20013        // every ordered triple in the canonical preset roster; each
20014        // consequence is checked whenever both antecedents hold. On
20015        // the (bottom, middle, top) diagonal the canonical witness
20016        // `EMPTY < DEFAULT < UNBOUNDED` composes to
20017        // `EMPTY < UNBOUNDED`, exercising the transitivity chain on
20018        // the bounded-lattice diagonal explicitly; the sweep pins the
20019        // same law on every other transitively-related triple in the
20020        // roster.
20021        for &a in STRICT_ORDER_ROSTER {
20022            for &b in STRICT_ORDER_ROSTER {
20023                for &c in STRICT_ORDER_ROSTER {
20024                    if a.lt(b) && b.lt(c) {
20025                        assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20026                    }
20027                }
20028            }
20029        }
20030    }
20031
20032    #[test]
20033    fn resource_limits_lt_refines_leq() {
20034        // Refinement axiom — `a < b ⇒ a ≤ b`. The strict relation is
20035        // a refinement of the non-strict one; a witness of strict `<`
20036        // is also a witness of `≤`. Sweeps every ordered pair in the
20037        // canonical preset roster.
20038        for &a in STRICT_ORDER_ROSTER {
20039            for &b in STRICT_ORDER_ROSTER {
20040                if a.lt(b) {
20041                    assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
20042                }
20043            }
20044        }
20045    }
20046
20047    #[test]
20048    fn resource_limits_lt_agrees_with_leq_minus_equality() {
20049        // Antisymmetry consequence — on ANY antisymmetric partial
20050        // order (which the pointwise `≤` on `ResourceLimits` is),
20051        // `a < b ⇔ a ≤ b ∧ a ≠ b`. Cross-checks the shipped
20052        // antisymmetric-leq encoding against the leq-and-not-equal
20053        // encoding across the full 5×5 preset matrix; a regression to
20054        // a non-antisymmetric preorder encoding would fire here.
20055        for &a in STRICT_ORDER_ROSTER {
20056            for &b in STRICT_ORDER_ROSTER {
20057                let shipped = a.lt(b);
20058                let alt = a.leq(b) && a != b;
20059                assert_eq!(
20060                    shipped, alt,
20061                    "encoding cross-check failed on ({a:?}, {b:?})",
20062                );
20063            }
20064        }
20065    }
20066
20067    #[test]
20068    fn resource_limits_lt_of_bottom_diagonal_pinned() {
20069        // Concrete-preset pin — the strict chain across the (bottom,
20070        // middle, top) preset triple on the bounded-lattice diagonal.
20071        // Every `DEFAULT_MAX_*` module constant is a concrete positive
20072        // value strictly less than [`usize::MAX`] and strictly greater
20073        // than `0`, so on every axis the three endpoints are strictly
20074        // ordered; the six-axis pointwise conjunction lifts the
20075        // per-axis strict chain to the posture-level strict chain.
20076        //
20077        // Peer of `resource_limits_leq_of_default_and_unbounded_is_a_
20078        // strict_order` one STRICTNESS axis over — where the non-
20079        // strict pin asserted "leq holds one way and not the other"
20080        // via two `leq` calls, this pin asserts the strict-chain
20081        // conclusion via one `lt` call per link and pinpoints the
20082        // three-preset chain on the bounded-lattice diagonal.
20083        assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20084        assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20085        assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20086    }
20087
20088    #[test]
20089    fn resource_limits_lt_rejects_incomparable_postures() {
20090        // Partial-order non-total pin — the two hand-authored
20091        // asymmetric postures have MID smaller on three axes and OTHER
20092        // smaller on the other three, so neither `leq` direction
20093        // holds; strict `<` therefore fails in BOTH directions. The
20094        // pin discriminates a regression that promoted strict `<`
20095        // from partial to total (e.g. treating incomparable pairs as
20096        // strictly `<` in some canonical direction), and pairs with
20097        // `resource_limits_leq_is_not_total_on_asymmetric_postures`
20098        // one STRICTNESS axis over.
20099        assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
20100        assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
20101    }
20102
20103    #[test]
20104    fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
20105        // Direct-encoding cross-check — `a.lt(b) == (a.leq(b) &&
20106        // !b.leq(a))` at every pair by definition. Sibling of the
20107        // `leq_minus_equality` pin one ENCODING axis over: that pin
20108        // routes through the equality-based encoding (an
20109        // antisymmetry consequence), this pin routes through the
20110        // definitional antisymmetric-leq encoding. A regression that
20111        // shipped a different (e.g. axis-wise strict) encoding of
20112        // `lt` diverges from this cross-check on the incomparable
20113        // asymmetric-preset pair.
20114        for &a in STRICT_ORDER_ROSTER {
20115            for &b in STRICT_ORDER_ROSTER {
20116                let shipped = a.lt(b);
20117                let direct = a.leq(b) && !b.leq(a);
20118                assert_eq!(
20119                    shipped, direct,
20120                    "direct-encoding cross-check failed on ({a:?}, {b:?})",
20121                );
20122            }
20123        }
20124    }
20125
20126    #[test]
20127    fn resource_limits_gt_is_dual_of_lt() {
20128        // Direction-flip pin — `a.gt(b) == b.lt(a)` at every pair. The
20129        // shipped `gt` body delegates to `lt` verbatim so this
20130        // agreement holds definitionally, but the pin discriminates a
20131        // regression that re-implemented `gt` independently and
20132        // dropped the direction flip. Sweeps the full 5×5 preset
20133        // matrix so incomparable + strictly-ordered + equal pairs
20134        // are all exercised.
20135        for &a in STRICT_ORDER_ROSTER {
20136            for &b in STRICT_ORDER_ROSTER {
20137                assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
20138            }
20139        }
20140    }
20141
20142    #[test]
20143    fn resource_limits_gt_of_top_diagonal_pinned() {
20144        // Concrete-preset pin — the strict chain across the (top,
20145        // middle, bottom) preset triple on the bounded-lattice
20146        // diagonal, walked in the "above" direction. Peer of
20147        // `resource_limits_lt_of_bottom_diagonal_pinned` one
20148        // DIRECTION axis over.
20149        assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20150        assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20151        assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20152    }
20153
20154    #[test]
20155    fn resource_limits_gt_is_irreflexive() {
20156        // Strict partial-order law dual — `a > a == false` for every
20157        // posture. Inherits from the irreflexivity of `lt` via the
20158        // shipped `gt` body's delegation, but pinned explicitly so a
20159        // future non-delegating re-implementation of `gt` still has
20160        // its irreflexivity gated at the type-level test cohort.
20161        for &a in STRICT_ORDER_ROSTER {
20162            assert!(!a.gt(a), "irreflexivity failed on {a:?}");
20163        }
20164    }
20165
20166    #[test]
20167    fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
20168        // Const-fn pin — the strict `<` relation is evaluable in
20169        // const context, so a caller can pin a preset-strict-order
20170        // identity at compile time. Sibling of the const-fn
20171        // evaluability pin on `leq` one STRICTNESS axis over.
20172        //
20173        // A regression to a runtime `fn` here would fail the `const
20174        // _: () = assert!(...)` bindings below at compile time.
20175        const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20176        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20177        const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20178        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20179        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
20180    }
20181
20182    #[test]
20183    fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
20184        // Const-fn dual pin — sibling of `lt`'s const-fn pin one
20185        // DIRECTION axis over.
20186        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20187        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20188        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20189        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20190        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
20191    }
20192
20193    #[test]
20194    fn resource_limits_geq_is_dual_of_leq() {
20195        // Direction-flip pin — `a.geq(b) == b.leq(a)` at every pair.
20196        // The shipped `geq` body delegates to `leq` verbatim so this
20197        // agreement holds definitionally, but the pin discriminates a
20198        // regression that re-implemented `geq` independently and
20199        // dropped the direction flip. Sweeps the full 5×5 preset
20200        // matrix so incomparable + strictly-ordered + equal pairs are
20201        // all exercised. Peer of `resource_limits_gt_is_dual_of_lt`
20202        // one STRICTNESS axis over on the (below, above) × (non-strict,
20203        // strict) 2×2 pairwise partial-order face.
20204        for &a in STRICT_ORDER_ROSTER {
20205            for &b in STRICT_ORDER_ROSTER {
20206                assert_eq!(
20207                    a.geq(b),
20208                    b.leq(a),
20209                    "geq/leq duality failed on ({a:?}, {b:?})",
20210                );
20211            }
20212        }
20213    }
20214
20215    #[test]
20216    fn resource_limits_geq_is_reflexive() {
20217        // Non-strict partial-order law — `a ≥ a == true` for every
20218        // posture. Reflexivity is the non-strict-relation axiom peer of
20219        // irreflexivity on `gt` one STRICTNESS axis over on the
20220        // partial-order axiom surface; a regression that hardened the
20221        // delegation into a strict `!other.leq(self)` would fire here
20222        // on every preset since the reflexive pair collapses.
20223        for &a in STRICT_ORDER_ROSTER {
20224            assert!(a.geq(a), "reflexivity failed on {a:?}");
20225        }
20226    }
20227
20228    #[test]
20229    fn resource_limits_geq_is_antisymmetric() {
20230        // Non-strict partial-order law — `a ≥ b ∧ b ≥ a ⇒ a == b`.
20231        // Antisymmetry is inherited from `leq` via the shipped `geq`
20232        // body's `other.leq(self)` delegation (which combined with
20233        // `b.geq(a)` — i.e. `a.leq(b)` — gives `leq` antisymmetry on
20234        // the pair). Sweeps the full 5×5 preset matrix; the pin fires
20235        // on any regression that let two DISTINCT postures satisfy
20236        // `geq` in both directions.
20237        for &a in STRICT_ORDER_ROSTER {
20238            for &b in STRICT_ORDER_ROSTER {
20239                if a.geq(b) && b.geq(a) {
20240                    assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
20241                }
20242            }
20243        }
20244    }
20245
20246    #[test]
20247    fn resource_limits_geq_is_transitive() {
20248        // Non-strict partial-order law — `a ≥ b ∧ b ≥ c ⇒ a ≥ c`.
20249        // Inherits from the transitivity of `leq` via the shipped
20250        // delegation; sweeps every ordered triple in the canonical
20251        // preset roster. Peer of `resource_limits_lt_is_transitive`
20252        // one STRICTNESS axis over and `resource_limits_leq_is_
20253        // transitive` one DIRECTION axis over.
20254        for &a in STRICT_ORDER_ROSTER {
20255            for &b in STRICT_ORDER_ROSTER {
20256                for &c in STRICT_ORDER_ROSTER {
20257                    if a.geq(b) && b.geq(c) {
20258                        assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20259                    }
20260                }
20261            }
20262        }
20263    }
20264
20265    #[test]
20266    fn resource_limits_geq_refines_gt() {
20267        // Refinement axiom peer — `a > b ⇒ a ≥ b`. The strict
20268        // relation is a refinement of the non-strict one in the
20269        // "above" direction, exactly as `lt` refines `leq` in the
20270        // "below" direction (pinned by
20271        // `resource_limits_lt_refines_leq` one DIRECTION axis over).
20272        // A witness of strict `>` is also a witness of `≥`.
20273        for &a in STRICT_ORDER_ROSTER {
20274            for &b in STRICT_ORDER_ROSTER {
20275                if a.gt(b) {
20276                    assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
20277                }
20278            }
20279        }
20280    }
20281
20282    #[test]
20283    fn resource_limits_geq_agrees_with_gt_plus_equality() {
20284        // Antisymmetry consequence peer — on ANY antisymmetric partial
20285        // order, `a ≥ b ⇔ a > b ∨ a == b`. Cross-checks the shipped
20286        // delegation against the strict-or-equal encoding across the
20287        // full 5×5 preset matrix; a regression that shipped a non-
20288        // antisymmetric preorder encoding for `geq` fires here. Peer
20289        // of `resource_limits_lt_agrees_with_leq_minus_equality` one
20290        // DIRECTION-AND-STRICTNESS axis over.
20291        for &a in STRICT_ORDER_ROSTER {
20292            for &b in STRICT_ORDER_ROSTER {
20293                let shipped = a.geq(b);
20294                let alt = a.gt(b) || a == b;
20295                assert_eq!(
20296                    shipped, alt,
20297                    "encoding cross-check failed on ({a:?}, {b:?})",
20298                );
20299            }
20300        }
20301    }
20302
20303    #[test]
20304    fn resource_limits_geq_of_top_diagonal_pinned() {
20305        // Concrete-preset pin — the non-strict chain across the (top,
20306        // middle, bottom) preset triple on the bounded-lattice
20307        // diagonal, walked in the "above" direction. Peer of
20308        // `resource_limits_gt_of_top_diagonal_pinned` one STRICTNESS
20309        // axis over: the non-strict pin also holds at the reflexive
20310        // diagonal, so every posture sits above ITSELF via `geq` in
20311        // addition to sitting above the strictly-tighter presets.
20312        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20313        assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20314        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20315        assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20316        assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20317        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20318    }
20319
20320    #[test]
20321    fn resource_limits_geq_rejects_incomparable_postures() {
20322        // Partial-order non-total pin — the two hand-authored
20323        // asymmetric postures have MID smaller on three axes and OTHER
20324        // smaller on the other three, so neither `leq` direction holds
20325        // and therefore neither `geq` direction holds. The pin
20326        // discriminates a regression that promoted `geq` from partial
20327        // to total, and pairs with
20328        // `resource_limits_lt_rejects_incomparable_postures` one
20329        // STRICTNESS-AND-DIRECTION axis over.
20330        assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
20331        assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
20332    }
20333
20334    #[test]
20335    fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
20336        // Arity-reduction pin — `a.geq(b) == a.is_upper_bound_of(&[b])`
20337        // at every pair. The pairwise "above" relation is exactly the
20338        // 1-input specialization of the N-ary upper-bound predicate;
20339        // the pin cross-checks that the two named entries agree on
20340        // the single-element case, which discriminates a regression
20341        // that drifted one from the other. Peer of the single-element
20342        // identity docstring on `is_upper_bound_of` one ARITY axis
20343        // over.
20344        for &a in STRICT_ORDER_ROSTER {
20345            for &b in STRICT_ORDER_ROSTER {
20346                assert_eq!(
20347                    a.geq(b),
20348                    a.is_upper_bound_of(&[b]),
20349                    "arity-reduction failed on ({a:?}, {b:?})",
20350                );
20351            }
20352        }
20353    }
20354
20355    #[test]
20356    fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
20357        // Const-fn pin — the non-strict `≥` relation is evaluable in
20358        // const context, so a caller can pin a preset-non-strict-order
20359        // identity at compile time. Sibling of the const-fn evaluability
20360        // pins on `leq` one DIRECTION axis over AND on `gt` one
20361        // STRICTNESS axis over.
20362        //
20363        // A regression to a runtime `fn` here would fail the `const _:
20364        // () = assert!(...)` bindings below at compile time.
20365        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20366        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20367        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20368        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20369        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20370    }
20371
20372    #[test]
20373    fn resource_limits_is_incomparable_is_irreflexive() {
20374        // Antichain-corner reflexivity pin — `a.is_incomparable(a) ==
20375        // false` on every posture. Every posture is COMPARABLE to
20376        // itself via `leq`'s reflexivity (`a.leq(a) == true`), so the
20377        // conjunction's `!self.leq(other)` leg forces the verdict to
20378        // `false` at every diagonal input. Discriminates a regression
20379        // that dropped the `!` on `self.leq(other)` (leaving the body
20380        // at `self.leq(other) && !self.geq(other)`) — that would fold
20381        // the diagonal to `false` on the reflexive `leq` arm but
20382        // silently flip the antichain corner on non-diagonal cells.
20383        for &a in STRICT_ORDER_ROSTER {
20384            assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
20385        }
20386    }
20387
20388    #[test]
20389    fn resource_limits_is_incomparable_is_symmetric() {
20390        // Antichain-corner symmetry pin — `a.is_incomparable(b) ==
20391        // b.is_incomparable(a)` on every pair. The composition
20392        // `!self.leq(other) && !self.geq(other)` folds through the
20393        // `self.leq(other) == other.geq(self)` dual identity on `geq`
20394        // and the conjunction's commutativity into an order-
20395        // independent projection on its two inputs. Sweeps every
20396        // ordered pair in the canonical preset roster;
20397        // discriminates a regression that broke either leg's dual
20398        // identity — one direction would fold to `false` on cells the
20399        // other still folded to `true`.
20400        for &a in STRICT_ORDER_ROSTER {
20401            for &b in STRICT_ORDER_ROSTER {
20402                assert_eq!(
20403                    a.is_incomparable(b),
20404                    b.is_incomparable(a),
20405                    "symmetry failed on ({a:?}, {b:?})",
20406                );
20407            }
20408        }
20409    }
20410
20411    #[test]
20412    fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
20413        // De Morgan dual pin — `a.is_incomparable(b) == !(a.leq(b) ||
20414        // a.geq(b))` on every pair. The antichain corner is the SET-
20415        // COMPLEMENT of the union of the two pointwise-domination
20416        // directions on the (comparable, incomparable) two-cell
20417        // partition of the ordered pair × verdict surface. Sweeps
20418        // every ordered pair in the canonical preset roster;
20419        // discriminates a regression that drifted the antichain
20420        // characterization off the substrate's `!leq && !geq`
20421        // composition (e.g., a shortcut through `!self.leq(other) ||
20422        // !self.geq(other)` — the OR-form falsely reports
20423        // incomparability on non-equal comparable pairs).
20424        for &a in STRICT_ORDER_ROSTER {
20425            for &b in STRICT_ORDER_ROSTER {
20426                let via_named = a.is_incomparable(b);
20427                let via_composition = !(a.leq(b) || a.geq(b));
20428                assert_eq!(
20429                    via_named, via_composition,
20430                    "De Morgan dual failed on ({a:?}, {b:?})",
20431                );
20432            }
20433        }
20434    }
20435
20436    #[test]
20437    fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
20438        // Two-cell partition pin — on every ordered pair EITHER
20439        // (a.leq(b) || a.geq(b)) OR a.is_incomparable(b), and NEVER
20440        // both. The (comparable, incomparable) partition of the
20441        // ordered pair × verdict surface is exhaustive-and-disjoint —
20442        // a substrate-level EXCLUSIVITY THEOREM the projection pins
20443        // once. Sweeps every ordered pair in the canonical preset
20444        // roster; discriminates a regression that let the two cells
20445        // overlap (e.g., an implementation that reported the antichain
20446        // corner as `true` on the comparable diagonal).
20447        for &a in STRICT_ORDER_ROSTER {
20448            for &b in STRICT_ORDER_ROSTER {
20449                let comparable = a.leq(b) || a.geq(b);
20450                let incomparable = a.is_incomparable(b);
20451                assert!(
20452                    comparable != incomparable,
20453                    "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20454                );
20455            }
20456        }
20457    }
20458
20459    #[test]
20460    fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
20461        // Bottom-pole absorption pin — `a.is_incomparable(
20462        // EMPTY_RESOURCE_LIMITS) == false` on every posture `a`.
20463        // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20464        // `EMPTY.leq(a)` holds on every axis (usize zero is the
20465        // minimum), which gives `a.geq(EMPTY)` through the `geq`
20466        // dual identity, which falsifies the `!self.geq(other)` leg.
20467        // Sweeps the canonical preset roster; pairs with the top-
20468        // pole absorption sibling one POLE axis over.
20469        for &a in STRICT_ORDER_ROSTER {
20470            assert!(
20471                !a.is_incomparable(EMPTY_RESOURCE_LIMITS),
20472                "bottom-pole absorption failed on {a:?}",
20473            );
20474            assert!(
20475                !EMPTY_RESOURCE_LIMITS.is_incomparable(a),
20476                "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20477            );
20478        }
20479    }
20480
20481    #[test]
20482    fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
20483        // Top-pole absorption pin — `a.is_incomparable(
20484        // UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a`.
20485        // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20486        // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20487        // usize::MAX` unconditionally), which falsifies the
20488        // `!self.leq(other)` leg. Sweeps the canonical preset
20489        // roster; pairs with the bottom-pole absorption sibling one
20490        // POLE axis over.
20491        for &a in STRICT_ORDER_ROSTER {
20492            assert!(
20493                !a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
20494                "top-pole absorption failed on {a:?}",
20495            );
20496            assert!(
20497                !UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
20498                "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20499            );
20500        }
20501    }
20502
20503    #[test]
20504    fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
20505        // Antichain load-bearing arm — the two hand-authored
20506        // asymmetric postures sit on distinct branches (each is
20507        // smaller on three axes and larger on the other three), so
20508        // neither pointwise-domination direction closes and the
20509        // antichain verdict HOLDS in both orderings. Pairs with
20510        // `resource_limits_geq_rejects_incomparable_postures` +
20511        // `resource_limits_lt_rejects_incomparable_postures` one COVER
20512        // axis over: those pin the two comparable-direction predicates
20513        // FALSIFY on this pair; THIS pin confirms the antichain
20514        // verdict HOLDS on the SAME pair — the two are the negation-
20515        // paired sides of the (comparable, incomparable) two-cell
20516        // partition.
20517        assert!(
20518            HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
20519            "antichain arm failed on (MID, OTHER)",
20520        );
20521        assert!(
20522            HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
20523            "antichain arm failed on flipped (OTHER, MID)",
20524        );
20525    }
20526
20527    #[test]
20528    fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
20529        // Shipped preset comparability pin — every ordered pair drawn
20530        // from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset triangle
20531        // is COMPARABLE (in both directions the pairwise partial-order
20532        // relation gives a verdict). The three shipped presets form a
20533        // TOTALLY ORDERED chain on the pointwise partial-order (EMPTY
20534        // <= DEFAULT <= UNBOUNDED on every field), so the antichain
20535        // corner MUST fold to `false` on every ordered pair drawn from
20536        // them. Discriminates a regression that promoted a shipped
20537        // preset off the total-order chain by drifting ONE field
20538        // silently.
20539        const TRIANGLE: &[ResourceLimits] = &[
20540            EMPTY_RESOURCE_LIMITS,
20541            DEFAULT_RESOURCE_LIMITS,
20542            UNBOUNDED_RESOURCE_LIMITS,
20543        ];
20544        for &a in TRIANGLE {
20545            for &b in TRIANGLE {
20546                assert!(
20547                    !a.is_incomparable(b),
20548                    "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20549                );
20550            }
20551        }
20552    }
20553
20554    #[test]
20555    fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
20556        // Const-fn pin — the antichain characterization is evaluable
20557        // in const context, so a caller can pin an antichain-
20558        // disagreement identity at compile time. Sibling of the
20559        // const-fn evaluability pins on `leq` / `geq` / `lt` / `gt`
20560        // one COVER axis over on the pairwise-relation surface.
20561        //
20562        // A regression to a runtime `fn` here would fail the `const _:
20563        // () = assert!(...)` bindings below at compile time.
20564        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20565        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20566        const _: () =
20567            assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20568        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20569        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20570        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20571        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20572    }
20573
20574    // ── ResourceLimits::is_comparable ─────────────────────────────────
20575    //
20576    // The COMPARABLE CORNER on the pairwise-relation surface — the
20577    // DIRECT POSITIVE DUAL of `is_incomparable` one COVER-COMPLEMENT
20578    // axis over on the (comparable, incomparable) two-cell partition
20579    // of the ordered pair × verdict surface. See `ResourceLimits::is_comparable`
20580    // for the algebra; the pins below fix each documented property
20581    // exactly once so a regression on any leg fires on a NAMED test
20582    // rather than an ad-hoc downstream break.
20583
20584    #[test]
20585    fn resource_limits_is_comparable_is_reflexive() {
20586        // Comparable-corner reflexivity pin — `a.is_comparable(a) ==
20587        // true` on every posture. Every posture is COMPARABLE to
20588        // itself via `leq`'s reflexivity (`a.leq(a) == true`), so
20589        // the disjunction's `self.leq(other)` leg holds at every
20590        // diagonal input. Sibling of `is_incomparable`'s
20591        // irreflexivity one CELL axis over on the (comparable,
20592        // incomparable) partition — the diagonal MUST fold to `true`
20593        // here iff it folds to `false` there.
20594        for &a in STRICT_ORDER_ROSTER {
20595            assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
20596        }
20597    }
20598
20599    #[test]
20600    fn resource_limits_is_comparable_is_symmetric() {
20601        // Comparable-corner symmetry pin — `a.is_comparable(b) ==
20602        // b.is_comparable(a)` on every pair. The composition
20603        // `self.leq(other) || self.geq(other)` folds through the
20604        // `self.leq(other) == other.geq(self)` dual identity on
20605        // `geq` and the disjunction's commutativity into an order-
20606        // independent projection on its two inputs. Both cells of an
20607        // EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface
20608        // must themselves be symmetric — the two verdicts flip in
20609        // lockstep on argument swap.
20610        for &a in STRICT_ORDER_ROSTER {
20611            for &b in STRICT_ORDER_ROSTER {
20612                assert_eq!(
20613                    a.is_comparable(b),
20614                    b.is_comparable(a),
20615                    "symmetry failed on ({a:?}, {b:?})",
20616                );
20617            }
20618        }
20619    }
20620
20621    #[test]
20622    fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
20623        // De Morgan dual pin — `a.is_comparable(b) ==
20624        // !a.is_incomparable(b)` on every pair. The comparable corner
20625        // is the SET-COMPLEMENT of the antichain corner on the
20626        // (comparable, incomparable) two-cell partition of the
20627        // ordered pair × verdict surface. The DIRECT dual of
20628        // `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
20629        // one CELL axis over: that test pins the antichain corner as
20630        // the negation of the `leq || geq` disjunction; THIS test
20631        // pins the comparability corner as the negation of the
20632        // antichain projection. Together the two pin BOTH SIDES of
20633        // the partition, closing it as a substrate-level
20634        // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM.
20635        for &a in STRICT_ORDER_ROSTER {
20636            for &b in STRICT_ORDER_ROSTER {
20637                let via_named = a.is_comparable(b);
20638                let via_negated_dual = !a.is_incomparable(b);
20639                assert_eq!(
20640                    via_named, via_negated_dual,
20641                    "De Morgan dual failed on ({a:?}, {b:?})",
20642                );
20643            }
20644        }
20645    }
20646
20647    #[test]
20648    fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
20649        // Direct composition pin — `a.is_comparable(b) == a.leq(b) ||
20650        // a.geq(b)` on every pair. Discriminates a regression that
20651        // drifted the comparability body off the substrate's `leq ||
20652        // geq` composition (e.g., a shortcut through `a.leq(b) &&
20653        // a.geq(b)` — the AND-form falsely reports comparability
20654        // only on the equality diagonal, dropping the strict-order
20655        // cells). Anchors the two-primitive delegation as a
20656        // substrate-level identity.
20657        for &a in STRICT_ORDER_ROSTER {
20658            for &b in STRICT_ORDER_ROSTER {
20659                let via_named = a.is_comparable(b);
20660                let via_composition = a.leq(b) || a.geq(b);
20661                assert_eq!(
20662                    via_named, via_composition,
20663                    "leq||geq composition failed on ({a:?}, {b:?})",
20664                );
20665            }
20666        }
20667    }
20668
20669    #[test]
20670    fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
20671        // Bottom-pole absorption pin — `a.is_comparable(
20672        // EMPTY_RESOURCE_LIMITS) == true` on every posture `a`.
20673        // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20674        // `EMPTY.leq(a)` holds on every axis (`usize` zero is the
20675        // minimum), which gives `a.geq(EMPTY)` through the `geq`
20676        // dual identity, which satisfies the `self.geq(other)` leg.
20677        // The DIRECT COMPLEMENT of
20678        // `resource_limits_is_incomparable_folds_false_at_the_bottom_pole`
20679        // one CELL axis over on the SAME preset roster — where the
20680        // antichain verdict FOLDS FALSE at the bottom pole, the
20681        // comparability verdict FOLDS TRUE.
20682        for &a in STRICT_ORDER_ROSTER {
20683            assert!(
20684                a.is_comparable(EMPTY_RESOURCE_LIMITS),
20685                "bottom-pole absorption failed on {a:?}",
20686            );
20687            assert!(
20688                EMPTY_RESOURCE_LIMITS.is_comparable(a),
20689                "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20690            );
20691        }
20692    }
20693
20694    #[test]
20695    fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
20696        // Top-pole absorption pin — `a.is_comparable(
20697        // UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a`.
20698        // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20699        // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20700        // usize::MAX` unconditionally), which satisfies the
20701        // `self.leq(other)` leg. The DIRECT COMPLEMENT of
20702        // `resource_limits_is_incomparable_folds_false_at_the_top_pole`
20703        // one CELL axis over on the SAME preset roster.
20704        for &a in STRICT_ORDER_ROSTER {
20705            assert!(
20706                a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
20707                "top-pole absorption failed on {a:?}",
20708            );
20709            assert!(
20710                UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
20711                "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20712            );
20713        }
20714    }
20715
20716    #[test]
20717    fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
20718        // Antichain load-bearing arm — the two hand-authored
20719        // asymmetric postures sit on distinct branches (each is
20720        // smaller on three axes and larger on the other three), so
20721        // NEITHER pointwise-domination direction closes and the
20722        // comparability verdict FALSIFIES in both orderings. The
20723        // DIRECT NEGATION of
20724        // `resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair`
20725        // one CELL axis over on the SAME hand-authored pair — the
20726        // two are the negation-paired sides of the (comparable,
20727        // incomparable) two-cell partition, and pinning BOTH cells'
20728        // verdicts on the SAME load-bearing antichain pair closes
20729        // the partition as a substrate-level EXCLUSIVITY THEOREM.
20730        assert!(
20731            !HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
20732            "antichain arm failed on (MID, OTHER)",
20733        );
20734        assert!(
20735            !HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
20736            "antichain arm failed on flipped (OTHER, MID)",
20737        );
20738    }
20739
20740    #[test]
20741    fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
20742        // Shipped preset comparability pin — every ordered pair
20743        // drawn from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset
20744        // triangle is COMPARABLE (in at least one direction the
20745        // pairwise partial-order relation gives a verdict). The
20746        // three shipped presets form a TOTALLY ORDERED chain on the
20747        // pointwise partial-order (EMPTY <= DEFAULT <= UNBOUNDED on
20748        // every field), so the comparability corner MUST fold to
20749        // `true` on every ordered pair drawn from them. The DIRECT
20750        // COMPLEMENT of
20751        // `resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle`
20752        // one CELL axis over on the SAME triangle. Discriminates a
20753        // regression that promoted a shipped preset off the total-
20754        // order chain by drifting ONE field silently.
20755        const TRIANGLE: &[ResourceLimits] = &[
20756            EMPTY_RESOURCE_LIMITS,
20757            DEFAULT_RESOURCE_LIMITS,
20758            UNBOUNDED_RESOURCE_LIMITS,
20759        ];
20760        for &a in TRIANGLE {
20761            for &b in TRIANGLE {
20762                assert!(
20763                    a.is_comparable(b),
20764                    "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20765                );
20766            }
20767        }
20768    }
20769
20770    #[test]
20771    fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
20772        // Exhaustive-and-disjoint partition pin — on every ordered
20773        // pair EXACTLY ONE of `a.is_comparable(b)` OR
20774        // `a.is_incomparable(b)` holds. The (comparable,
20775        // incomparable) partition of the ordered pair × verdict
20776        // surface is exhaustive-and-disjoint — a substrate-level
20777        // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM the two projections
20778        // together pin. Complements
20779        // `resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable`
20780        // one CELL axis over: that test pins the partition against
20781        // the raw `a.leq(b) || a.geq(b)` composition; THIS test pins
20782        // the partition against the NAMED comparability projection —
20783        // a regression that drifted `is_comparable` off the
20784        // composition without breaking the composition-based test
20785        // would still fire here.
20786        for &a in STRICT_ORDER_ROSTER {
20787            for &b in STRICT_ORDER_ROSTER {
20788                let comparable = a.is_comparable(b);
20789                let incomparable = a.is_incomparable(b);
20790                assert!(
20791                    comparable != incomparable,
20792                    "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20793                );
20794            }
20795        }
20796    }
20797
20798    #[test]
20799    fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
20800        // Const-fn pin — the comparability characterization is
20801        // evaluable in const context, so a caller can pin a
20802        // comparability-agreement identity at compile time. Sibling
20803        // of the const-fn evaluability pins on `leq` / `geq` / `lt`
20804        // / `gt` / `is_incomparable` one COVER axis over on the
20805        // pairwise-relation surface.
20806        //
20807        // A regression to a runtime `fn` here would fail the `const _:
20808        // () = assert!(...)` bindings below at compile time.
20809        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20810        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20811        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20812        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20813        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20814        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20815        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20816    }
20817
20818    // ── ResourceLimits::partial_cmp ───────────────────────────────────
20819    //
20820    // The `Option<Ordering>` FULL-VERDICT corner on the pairwise-
20821    // relation surface — the JOINT LIFT of the (comparable, incomparable)
20822    // two-cell partition together with the ordering-direction refinement
20823    // on the comparable arm. See `ResourceLimits::partial_cmp` for the
20824    // algebra; the pins below fix each documented property exactly once
20825    // so a regression on any leg fires on a NAMED test rather than an
20826    // ad-hoc downstream break.
20827
20828    #[test]
20829    fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
20830        // Diagonal-equality pin — `a.partial_cmp(a) ==
20831        // Some(Ordering::Equal)` on every posture. The `leq`
20832        // reflexivity closes both arms of the equality-guarding
20833        // conjunction, so the diagonal cell of the (Less, Equal,
20834        // Greater, None) 4-tile partition pins to the Equal tile.
20835        // Sibling of `is_comparable_is_reflexive` one PROJECTION
20836        // axis over — where the projection folds TRUE at the
20837        // diagonal, THIS method refines the verdict to `Equal`.
20838        for &a in STRICT_ORDER_ROSTER {
20839            assert_eq!(
20840                a.partial_cmp(a),
20841                Some(Ordering::Equal),
20842                "diagonal-equality failed on {a:?}",
20843            );
20844        }
20845    }
20846
20847    #[test]
20848    fn resource_limits_partial_cmp_reverses_under_argument_swap() {
20849        // Antisymmetric-swap pin — `a.partial_cmp(b) ==
20850        // b.partial_cmp(a).map(Ordering::reverse)` on every pair.
20851        // Swapping the argument pair FLIPS the ordering-direction
20852        // verdict (`Less <-> Greater`), fixes the diagonal-equality
20853        // arm, and fixes the antichain arm. Discriminates
20854        // `is_incomparable` / `is_comparable` (both SYMMETRIC on
20855        // argument swap) one POSTURE axis over — the FULL verdict
20856        // carries directional content that inverts under swap where
20857        // the projections do not.
20858        for &a in STRICT_ORDER_ROSTER {
20859            for &b in STRICT_ORDER_ROSTER {
20860                let forward = a.partial_cmp(b);
20861                let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
20862                assert_eq!(
20863                    forward, swapped_reversed,
20864                    "swap-reverse failed on ({a:?}, {b:?})",
20865                );
20866            }
20867        }
20868    }
20869
20870    #[test]
20871    fn resource_limits_partial_cmp_none_iff_incomparable() {
20872        // Antichain-arm identity pin — `a.partial_cmp(b).is_none()
20873        // == a.is_incomparable(b)` on every pair. The `None` arm of
20874        // the return coincides EXACTLY with the antichain-corner
20875        // projection. Anchors the joint-lift claim: THIS method
20876        // subsumes `is_incomparable` at its `is_none()` projection
20877        // without drifting the antichain characterization.
20878        for &a in STRICT_ORDER_ROSTER {
20879            for &b in STRICT_ORDER_ROSTER {
20880                let via_partial_cmp = a.partial_cmp(b).is_none();
20881                let via_is_incomparable = a.is_incomparable(b);
20882                assert_eq!(
20883                    via_partial_cmp, via_is_incomparable,
20884                    "none-iff-incomparable failed on ({a:?}, {b:?})",
20885                );
20886            }
20887        }
20888    }
20889
20890    #[test]
20891    fn resource_limits_partial_cmp_some_iff_comparable() {
20892        // Comparability-arm identity pin — `a.partial_cmp(b).is_some()
20893        // == a.is_comparable(b)` on every pair. The `Some(_)` arm of
20894        // the return coincides EXACTLY with the comparability-corner
20895        // projection. The DUAL of the antichain-arm pin one CELL axis
20896        // over on the (comparable, incomparable) partition — together
20897        // the two pins close the JOINT LIFT of both cells at THIS
20898        // method.
20899        for &a in STRICT_ORDER_ROSTER {
20900            for &b in STRICT_ORDER_ROSTER {
20901                let via_partial_cmp = a.partial_cmp(b).is_some();
20902                let via_is_comparable = a.is_comparable(b);
20903                assert_eq!(
20904                    via_partial_cmp, via_is_comparable,
20905                    "some-iff-comparable failed on ({a:?}, {b:?})",
20906                );
20907            }
20908        }
20909    }
20910
20911    #[test]
20912    fn resource_limits_partial_cmp_less_iff_lt() {
20913        // Strict-less agreement pin — `a.partial_cmp(b) ==
20914        // Some(Ordering::Less)` iff `a.lt(b)`. The strict-less arm of
20915        // the (Less, Equal, Greater, None) 4-tile partition binds to
20916        // the named `lt` companion primitive at exactly one dispatch
20917        // site. A regression that drifted the arm off `lt` (e.g., to
20918        // `leq` — including the equality diagonal) would fire here.
20919        for &a in STRICT_ORDER_ROSTER {
20920            for &b in STRICT_ORDER_ROSTER {
20921                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
20922                let via_lt = a.lt(b);
20923                assert_eq!(
20924                    via_partial_cmp, via_lt,
20925                    "less-iff-lt failed on ({a:?}, {b:?})",
20926                );
20927            }
20928        }
20929    }
20930
20931    #[test]
20932    fn resource_limits_partial_cmp_greater_iff_gt() {
20933        // Strict-greater agreement pin — `a.partial_cmp(b) ==
20934        // Some(Ordering::Greater)` iff `a.gt(b)`. Dual of the strict-
20935        // less agreement pin one DIRECTION axis over on the (Less,
20936        // Greater) strict-order pair — a regression that dropped the
20937        // asymmetry between the two arms (e.g., collapsed both to
20938        // `Less`) would fire here.
20939        for &a in STRICT_ORDER_ROSTER {
20940            for &b in STRICT_ORDER_ROSTER {
20941                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
20942                let via_gt = a.gt(b);
20943                assert_eq!(
20944                    via_partial_cmp, via_gt,
20945                    "greater-iff-gt failed on ({a:?}, {b:?})",
20946                );
20947            }
20948        }
20949    }
20950
20951    #[test]
20952    fn resource_limits_partial_cmp_equal_iff_eq() {
20953        // Equality agreement pin — `a.partial_cmp(b) ==
20954        // Some(Ordering::Equal)` iff `a == b`. The equality arm of the
20955        // 4-tile partition binds through the pointwise antisymmetric-
20956        // equality characterization (`a.leq(b) && b.leq(a) → a == b`,
20957        // pinned by `resource_limits_leq_is_antisymmetric`) to the
20958        // `PartialEq::eq` companion. Discriminates a regression that
20959        // drifted the Equal arm off the antisymmetric conjunction.
20960        for &a in STRICT_ORDER_ROSTER {
20961            for &b in STRICT_ORDER_ROSTER {
20962                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
20963                let via_eq = a == b;
20964                assert_eq!(
20965                    via_partial_cmp, via_eq,
20966                    "equal-iff-eq failed on ({a:?}, {b:?})",
20967                );
20968            }
20969        }
20970    }
20971
20972    #[test]
20973    fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
20974        // Bottom-to-top-pole ordering pin — every strictly-ascending
20975        // pair on the shipped preset chain (EMPTY <= DEFAULT <=
20976        // UNBOUNDED) folds to `Some(Ordering::Less)`. The three
20977        // shipped presets form a TOTALLY ORDERED chain on the
20978        // pointwise partial-order (every DEFAULT_MAX_* is a concrete
20979        // positive value strictly less than usize::MAX), so the
20980        // strict-less arm fires on every ordered pair drawn in
20981        // ascending direction.
20982        assert_eq!(
20983            EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20984            Some(Ordering::Less),
20985        );
20986        assert_eq!(
20987            DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20988            Some(Ordering::Less),
20989        );
20990        assert_eq!(
20991            EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20992            Some(Ordering::Less),
20993        );
20994    }
20995
20996    #[test]
20997    fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
20998        // Top-to-bottom-pole ordering pin — every strictly-descending
20999        // pair on the shipped preset chain folds to
21000        // `Some(Ordering::Greater)`. The DUAL of the ascending pin
21001        // one DIRECTION axis over — together the two pins close the
21002        // (ascending, descending) direction pair on the shipped
21003        // preset chain at the ordering-direction refinement.
21004        assert_eq!(
21005            DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21006            Some(Ordering::Greater),
21007        );
21008        assert_eq!(
21009            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21010            Some(Ordering::Greater),
21011        );
21012        assert_eq!(
21013            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21014            Some(Ordering::Greater),
21015        );
21016    }
21017
21018    #[test]
21019    fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
21020        // Antichain load-bearing arm — the two hand-authored
21021        // asymmetric postures sit on distinct branches (each is
21022        // smaller on three axes and larger on the other three), so
21023        // NEITHER pointwise-domination direction closes and the
21024        // fall-through `None` arm fires in both orderings. The DIRECT
21025        // LIFT of `is_incomparable_holds_on_the_hand_authored_antichain_pair`
21026        // one PROJECTION axis over on the SAME hand-authored pair —
21027        // where the projection folds TRUE, THIS method folds to
21028        // `None`. The two are the negation-paired sides of the
21029        // (Some(_), None) two-cell partition of `Option<Ordering>`.
21030        assert_eq!(
21031            HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
21032            None,
21033        );
21034        assert_eq!(
21035            HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
21036            None,
21037        );
21038    }
21039
21040    #[test]
21041    fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
21042        // Const-fn pin — the full-verdict projection is evaluable in
21043        // const context, so a caller can pin an ordering-direction
21044        // identity at compile time. Sibling of the const-fn
21045        // evaluability pins on `leq` / `geq` / `lt` / `gt` /
21046        // `is_incomparable` / `is_comparable` one COVER axis over on
21047        // the pairwise-relation surface.
21048        //
21049        // `matches!` in const context requires the pattern's variants
21050        // to be const-constructible — `Ordering::Less` / `Equal` /
21051        // `Greater` are plain enum constructors, so the assertions
21052        // below stay inside stable const-fn scope. A regression to a
21053        // runtime `fn` here would fail the `const _: () = assert!(
21054        // matches!(...))` bindings below at compile time.
21055        const _: () = assert!(matches!(
21056            EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21057            Some(Ordering::Equal),
21058        ));
21059        const _: () = assert!(matches!(
21060            EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21061            Some(Ordering::Less),
21062        ));
21063        const _: () = assert!(matches!(
21064            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21065            Some(Ordering::Greater),
21066        ));
21067        const _: () = assert!(matches!(
21068            DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21069            Some(Ordering::Less),
21070        ));
21071        const _: () = assert!(matches!(
21072            EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21073            Some(Ordering::Less),
21074        ));
21075    }
21076
21077    #[test]
21078    fn resource_limits_is_chain_empty_slice_is_vacuously_true() {
21079        // Empty-slice vacuous truth — the empty conjunction is vacuously
21080        // true; the empty slice contains no distinct pairs to reject.
21081        // Peer of `is_lower_bound_of(&[]) == true`'s empty-slice identity
21082        // one PRIMITIVE-KIND axis over on the (leq, comparable) ×
21083        // (pairwise, N-ary) primitive surface.
21084        assert!(ResourceLimits::is_chain(&[]));
21085    }
21086
21087    #[test]
21088    fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
21089        // Empty-slice vacuous truth — the empty conjunction is vacuously
21090        // true; the empty slice contains no distinct pairs to reject.
21091        // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21092        // COVER-COMPLEMENT axis over: both cells of the (chain, antichain)
21093        // set-level pair AGREE at the empty slice on the vacuous-truth
21094        // verdict — the two cells coincide at the empty face of the
21095        // set-level verdict surface.
21096        assert!(ResourceLimits::is_antichain(&[]));
21097    }
21098
21099    #[test]
21100    fn resource_limits_is_chain_singleton_is_vacuously_true() {
21101        // Singleton vacuous truth — a one-element slice contains no
21102        // distinct pairs, so the outer-and-inner-index conjunction never
21103        // enters the inner loop and returns `true` vacuously. Pinned on
21104        // every canonical preset AND on both hand-authored asymmetric
21105        // postures so the vacuous-truth verdict holds regardless of the
21106        // singleton element's lattice position.
21107        assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21108        assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
21109        assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
21110        assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
21111        assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
21112    }
21113
21114    #[test]
21115    fn resource_limits_is_antichain_singleton_is_vacuously_true() {
21116        // Singleton vacuous truth — a one-element slice contains no
21117        // distinct pairs, so the outer-and-inner-index conjunction never
21118        // enters the inner loop and returns `true` vacuously. Peer of
21119        // `is_chain`'s singleton-vacuous-truth identity: both cells AGREE
21120        // at singleton slices — the (chain, antichain) verdict surface
21121        // coincides at every slice with strictly fewer than two distinct
21122        // pairs.
21123        assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21124        assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
21125        assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
21126        assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
21127        assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
21128    }
21129
21130    #[test]
21131    fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
21132        // Diagonal-duplicate identity — `is_comparable` is REFLEXIVE
21133        // (`a.is_comparable(a) == true`), so the single distinct index
21134        // pair `(0, 1)` binds `a.is_comparable(a) == true` and the
21135        // conjunction holds at every diagonal-duplicate slice.
21136        // Distinguishes it from `is_antichain` one COVER-COMPLEMENT axis
21137        // over, whose diagonal-duplicate verdict is `false` via
21138        // `is_incomparable`'s IRREFLEXIVITY.
21139        assert!(ResourceLimits::is_chain(&[
21140            DEFAULT_RESOURCE_LIMITS,
21141            DEFAULT_RESOURCE_LIMITS,
21142        ]));
21143        assert!(ResourceLimits::is_chain(&[
21144            HAND_AUTHORED_MID_POSTURE,
21145            HAND_AUTHORED_MID_POSTURE,
21146        ]));
21147    }
21148
21149    #[test]
21150    fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
21151        // Diagonal-duplicate rejection — `is_incomparable` is
21152        // IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
21153        // distinct index pair `(0, 1)` binds `a.is_incomparable(a) ==
21154        // false` and the conjunction rejects at every diagonal-duplicate
21155        // slice. The reflexivity/irreflexivity divergence between the
21156        // two pair-level projections PROPAGATES to the set-level
21157        // projections at any slice with a duplicated element — the two
21158        // set-level cells DIVERGE at every diagonal-duplicate slice, the
21159        // mirror of their AGREEMENT at every empty-or-singleton slice
21160        // one CARDINALITY axis over.
21161        assert!(!ResourceLimits::is_antichain(&[
21162            DEFAULT_RESOURCE_LIMITS,
21163            DEFAULT_RESOURCE_LIMITS,
21164        ]));
21165        assert!(!ResourceLimits::is_antichain(&[
21166            HAND_AUTHORED_MID_POSTURE,
21167            HAND_AUTHORED_MID_POSTURE,
21168        ]));
21169    }
21170
21171    #[test]
21172    fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
21173        // Ordered-chain closure — the shipped-preset triple on the
21174        // bounded-lattice diagonal is an ascending chain (`EMPTY <=
21175        // DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among
21176        // the three is comparable. Pinned in every permutation of the
21177        // triple so the doubly-indexed all-pairs walk closes on every
21178        // enumeration of the same chain.
21179        assert!(ResourceLimits::is_chain(&[
21180            EMPTY_RESOURCE_LIMITS,
21181            DEFAULT_RESOURCE_LIMITS,
21182            UNBOUNDED_RESOURCE_LIMITS,
21183        ]));
21184        assert!(ResourceLimits::is_chain(&[
21185            UNBOUNDED_RESOURCE_LIMITS,
21186            DEFAULT_RESOURCE_LIMITS,
21187            EMPTY_RESOURCE_LIMITS,
21188        ]));
21189        assert!(ResourceLimits::is_chain(&[
21190            DEFAULT_RESOURCE_LIMITS,
21191            EMPTY_RESOURCE_LIMITS,
21192            UNBOUNDED_RESOURCE_LIMITS,
21193        ]));
21194    }
21195
21196    #[test]
21197    fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
21198        // Chain rejection — the shipped-preset pair on the bounded-
21199        // lattice diagonal is a strict chain (`EMPTY.lt(DEFAULT)`), so
21200        // the single distinct pair is COMPARABLE and the antichain
21201        // conjunction rejects. The DIRECT FALSIFICATION of
21202        // `is_chain_holds_on_the_shipped_preset_triple` one CELL axis
21203        // over on the SAME lattice diagonal — the two set-level cells
21204        // carry the negation-paired verdict at every strictly-chained
21205        // slice, the mirror of their AGREEMENT at every empty-or-
21206        // singleton slice one CARDINALITY axis over.
21207        assert!(!ResourceLimits::is_antichain(&[
21208            EMPTY_RESOURCE_LIMITS,
21209            DEFAULT_RESOURCE_LIMITS,
21210        ]));
21211        assert!(!ResourceLimits::is_antichain(&[
21212            DEFAULT_RESOURCE_LIMITS,
21213            UNBOUNDED_RESOURCE_LIMITS,
21214        ]));
21215        assert!(!ResourceLimits::is_antichain(&[
21216            EMPTY_RESOURCE_LIMITS,
21217            DEFAULT_RESOURCE_LIMITS,
21218            UNBOUNDED_RESOURCE_LIMITS,
21219        ]));
21220    }
21221
21222    #[test]
21223    fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
21224        // Antichain rejection — the two hand-authored asymmetric
21225        // postures sit on distinct branches (each is smaller on three
21226        // axes and larger on the other three), so neither pointwise-
21227        // domination direction closes and the pair is INCOMPARABLE. The
21228        // chain conjunction rejects at the first incomparable pair.
21229        // Pinned in both orderings so the doubly-indexed pair walk
21230        // rejects regardless of index order (the diagonal-adjacent
21231        // pair-level rejection is symmetric on argument swap).
21232        assert!(!ResourceLimits::is_chain(&[
21233            HAND_AUTHORED_MID_POSTURE,
21234            HAND_AUTHORED_OTHER_POSTURE,
21235        ]));
21236        assert!(!ResourceLimits::is_chain(&[
21237            HAND_AUTHORED_OTHER_POSTURE,
21238            HAND_AUTHORED_MID_POSTURE,
21239        ]));
21240    }
21241
21242    #[test]
21243    fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
21244        // Hand-authored antichain closure — the two hand-authored
21245        // asymmetric postures sit on distinct branches (each is smaller
21246        // on three axes and larger on the other three), so neither
21247        // pointwise-domination direction closes and the pair is
21248        // INCOMPARABLE. The antichain conjunction accepts every distinct
21249        // pair of the slice. DIRECT LIFT of `is_chain`'s antichain
21250        // rejection one CELL axis over on the SAME hand-authored pair —
21251        // pinning BOTH cells' verdicts on the SAME load-bearing
21252        // antichain pair closes the (chain, antichain) two-cell face on
21253        // arity-2 slices as a substrate-level EXCLUSIVITY THEOREM.
21254        assert!(ResourceLimits::is_antichain(&[
21255            HAND_AUTHORED_MID_POSTURE,
21256            HAND_AUTHORED_OTHER_POSTURE,
21257        ]));
21258        assert!(ResourceLimits::is_antichain(&[
21259            HAND_AUTHORED_OTHER_POSTURE,
21260            HAND_AUTHORED_MID_POSTURE,
21261        ]));
21262    }
21263
21264    #[test]
21265    fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
21266        // Mixed-set rejection — both `(DEFAULT, MID)` and `(DEFAULT,
21267        // OTHER)` are comparable pairs (DEFAULT's shipped `DEFAULT_MAX_*`
21268        // per-axis values are all much larger than either hand-authored
21269        // posture's, so DEFAULT dominates both pointwise), but the
21270        // `(MID, OTHER)` pair is the hand-authored INCOMPARABLE pair.
21271        // The set-level chain verdict is stricter than the union of its
21272        // pair-level verdicts — one antichain pair anywhere in the
21273        // slice falsifies the whole projection.
21274        assert!(!ResourceLimits::is_chain(&[
21275            DEFAULT_RESOURCE_LIMITS,
21276            HAND_AUTHORED_MID_POSTURE,
21277            HAND_AUTHORED_OTHER_POSTURE,
21278        ]));
21279    }
21280
21281    #[test]
21282    fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
21283        // Mixed-set rejection — even though the second-and-third pair
21284        // is incomparable (the hand-authored antichain pair), the first
21285        // pair (`DEFAULT`-and-`MID`) is comparable, so the whole-set
21286        // conjunction rejects. Pins the three-cell (chain, antichain,
21287        // mixed) set-level face at the mixed-set cell: the same slice
21288        // both `is_chain` AND `is_antichain` reject at, and the third
21289        // cell of the three-cell face the two set-level projections
21290        // carry the ends of.
21291        assert!(!ResourceLimits::is_antichain(&[
21292            DEFAULT_RESOURCE_LIMITS,
21293            HAND_AUTHORED_MID_POSTURE,
21294            HAND_AUTHORED_OTHER_POSTURE,
21295        ]));
21296    }
21297
21298    #[test]
21299    fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
21300        // De Morgan mirror at arity 2 — on any two-element slice `[a,
21301        // b]` with `a != b`, the set-level (chain, antichain) two-cell
21302        // face reduces to the pair-level (comparable, incomparable)
21303        // two-cell partition and the two cells become MUTUALLY
21304        // EXCLUSIVE (the mixed cell requires ≥3 elements to open). The
21305        // set-level De Morgan mirror of the pair-level identity
21306        // `a.is_comparable(b) == !a.is_incomparable(b)` `is_comparable`
21307        // pins one CARDINALITY axis over.
21308        let presets: [ResourceLimits; 5] = [
21309            EMPTY_RESOURCE_LIMITS,
21310            DEFAULT_RESOURCE_LIMITS,
21311            UNBOUNDED_RESOURCE_LIMITS,
21312            HAND_AUTHORED_MID_POSTURE,
21313            HAND_AUTHORED_OTHER_POSTURE,
21314        ];
21315        for a in presets {
21316            for b in presets {
21317                if a == b {
21318                    continue;
21319                }
21320                let pair = [a, b];
21321                let chain = ResourceLimits::is_chain(&pair);
21322                let antichain = ResourceLimits::is_antichain(&pair);
21323                assert_ne!(
21324                    chain, antichain,
21325                    "distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
21326                );
21327            }
21328        }
21329    }
21330
21331    #[test]
21332    fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
21333        // Empty-and-singleton agreement — the two set-level projections
21334        // carry the SAME vacuous-truth verdict at every slice with
21335        // strictly fewer than two distinct pairs (empty AND singleton).
21336        // Pinned as a substrate-level identity so a future rewrite of
21337        // either projection cannot silently drift from the shared
21338        // vacuous-truth verdict.
21339        assert_eq!(
21340            ResourceLimits::is_chain(&[]),
21341            ResourceLimits::is_antichain(&[]),
21342        );
21343        let presets: [ResourceLimits; 5] = [
21344            EMPTY_RESOURCE_LIMITS,
21345            DEFAULT_RESOURCE_LIMITS,
21346            UNBOUNDED_RESOURCE_LIMITS,
21347            HAND_AUTHORED_MID_POSTURE,
21348            HAND_AUTHORED_OTHER_POSTURE,
21349        ];
21350        for a in presets {
21351            let single = [a];
21352            assert_eq!(
21353                ResourceLimits::is_chain(&single),
21354                ResourceLimits::is_antichain(&single),
21355                "singleton agreement failed on {a:?}",
21356            );
21357        }
21358    }
21359
21360    #[test]
21361    fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
21362        // Const-fn pin — the set-level chain projection is evaluable in
21363        // const context, so a caller can pin a chain-membership
21364        // identity at compile time. Sibling of the const-fn
21365        // evaluability pins on `is_comparable` one CARDINALITY axis
21366        // over and on `is_lower_bound_of` / `is_upper_bound_of` one
21367        // PRIMITIVE-KIND axis over on the N-ary aggregation surface.
21368        const _: () = assert!(ResourceLimits::is_chain(&[]));
21369        const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21370        const _: () = assert!(ResourceLimits::is_chain(&[
21371            EMPTY_RESOURCE_LIMITS,
21372            DEFAULT_RESOURCE_LIMITS,
21373            UNBOUNDED_RESOURCE_LIMITS,
21374        ]));
21375    }
21376
21377    #[test]
21378    fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
21379        // Const-fn pin — the set-level antichain projection is
21380        // evaluable in const context, so a caller can pin an antichain-
21381        // membership identity at compile time. Sibling of the const-fn
21382        // evaluability pins on `is_incomparable` one CARDINALITY axis
21383        // over and on `is_chain` one COVER-COMPLEMENT axis over.
21384        const _: () = assert!(ResourceLimits::is_antichain(&[]));
21385        const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21386        const _: () = assert!(ResourceLimits::is_antichain(&[
21387            HAND_AUTHORED_MID_POSTURE,
21388            HAND_AUTHORED_OTHER_POSTURE,
21389        ]));
21390    }
21391
21392    #[test]
21393    fn resource_limits_is_mixed_empty_slice_is_false() {
21394        // Empty-slice contract — is_chain and is_antichain BOTH return
21395        // `true` vacuously, so `!true && !true == false`. The empty
21396        // slice is NOT mixed. Sibling posture to the vacuous-truth
21397        // verdicts of is_chain + is_antichain one CELL axis over on the
21398        // (chain, antichain, mixed) three-cell face.
21399        assert!(!ResourceLimits::is_mixed(&[]));
21400    }
21401
21402    #[test]
21403    fn resource_limits_is_mixed_singleton_is_false() {
21404        // Singleton contract — a one-element slice contains no distinct
21405        // pairs; is_chain + is_antichain BOTH return `true` vacuously.
21406        // Pinned on every canonical preset AND on both hand-authored
21407        // asymmetric postures so the false-at-singleton verdict holds
21408        // regardless of the singleton element's lattice position.
21409        assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21410        assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
21411        assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
21412        assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
21413        assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
21414    }
21415
21416    #[test]
21417    fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
21418        // Diagonal-duplicate contract — is_chain returns `true` via
21419        // is_comparable's REFLEXIVITY; is_antichain returns `false` via
21420        // is_incomparable's IRREFLEXIVITY; the conjunction
21421        // `!true && !false == false`. The diagonal-duplicate slice is a
21422        // CHAIN (per the reflexivity arm), not mixed.
21423        assert!(!ResourceLimits::is_mixed(&[
21424            DEFAULT_RESOURCE_LIMITS,
21425            DEFAULT_RESOURCE_LIMITS,
21426        ]));
21427        assert!(!ResourceLimits::is_mixed(&[
21428            HAND_AUTHORED_MID_POSTURE,
21429            HAND_AUTHORED_MID_POSTURE,
21430        ]));
21431    }
21432
21433    #[test]
21434    fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
21435        // Chain rejection — the shipped-preset triple on the bounded-
21436        // lattice diagonal is an ascending chain, is_chain returns
21437        // `true`, and `!true && …` short-circuits to `false`. A PURE
21438        // chain is not mixed. Pinned in every permutation of the triple
21439        // so the projection factors through the ordering-agnostic
21440        // sibling is_chain verdict on the same chain.
21441        assert!(!ResourceLimits::is_mixed(&[
21442            EMPTY_RESOURCE_LIMITS,
21443            DEFAULT_RESOURCE_LIMITS,
21444            UNBOUNDED_RESOURCE_LIMITS,
21445        ]));
21446        assert!(!ResourceLimits::is_mixed(&[
21447            UNBOUNDED_RESOURCE_LIMITS,
21448            DEFAULT_RESOURCE_LIMITS,
21449            EMPTY_RESOURCE_LIMITS,
21450        ]));
21451    }
21452
21453    #[test]
21454    fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
21455        // Antichain rejection — the hand-authored antichain pair binds
21456        // is_antichain to `true`, and `!… && !true == false`. A PURE
21457        // antichain is not mixed. Pinned in both orderings so the
21458        // projection factors through the argument-swap-symmetric
21459        // sibling is_antichain verdict on the same antichain.
21460        assert!(!ResourceLimits::is_mixed(&[
21461            HAND_AUTHORED_MID_POSTURE,
21462            HAND_AUTHORED_OTHER_POSTURE,
21463        ]));
21464        assert!(!ResourceLimits::is_mixed(&[
21465            HAND_AUTHORED_OTHER_POSTURE,
21466            HAND_AUTHORED_MID_POSTURE,
21467        ]));
21468    }
21469
21470    #[test]
21471    fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
21472    ) {
21473        // Mixed-set closure — LOAD-BEARING `true`-arm catch. The
21474        // (DEFAULT, MID) pair is comparable (falsifying is_antichain)
21475        // while the (MID, OTHER) pair is incomparable (falsifying
21476        // is_chain); the conjunction `!false && !false == true`. This
21477        // is the SAME fixture the sibling is_chain + is_antichain BOTH
21478        // reject at — the DISCRIMINATING positive-arm catch for the
21479        // (mixed) cell of the three-cell face the two set-level
21480        // projections carry the ends of.
21481        assert!(ResourceLimits::is_mixed(&[
21482            DEFAULT_RESOURCE_LIMITS,
21483            HAND_AUTHORED_MID_POSTURE,
21484            HAND_AUTHORED_OTHER_POSTURE,
21485        ]));
21486    }
21487
21488    #[test]
21489    fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
21490    ) {
21491        // Trichotomy exhaustiveness at arity ≥2 with pairwise-distinct
21492        // elements — EXACTLY ONE of is_chain, is_antichain, is_mixed
21493        // returns `true` on every pair OR triple drawn from the shipped
21494        // presets whose elements are pairwise distinct. The three cells
21495        // partition the set-level verdict surface at every non-
21496        // degenerate slice. Substrate-level identity: pinning it here
21497        // catches a future rewrite of any of the three projections that
21498        // silently drifts from the partition contract.
21499        let presets: [ResourceLimits; 5] = [
21500            EMPTY_RESOURCE_LIMITS,
21501            DEFAULT_RESOURCE_LIMITS,
21502            UNBOUNDED_RESOURCE_LIMITS,
21503            HAND_AUTHORED_MID_POSTURE,
21504            HAND_AUTHORED_OTHER_POSTURE,
21505        ];
21506        // Arity-2 sweep — the mixed cell CANNOT open at arity 2 (only
21507        // one distinct pair), so is_mixed is always false and the
21508        // partition reduces to the (chain, antichain) two-cell face
21509        // that is_chain + is_antichain already pinned MUTUALLY
21510        // EXCLUSIVE. Cross-check that the three-cell projection
21511        // AGREES with the two-cell projection at arity 2.
21512        for a in presets {
21513            for b in presets {
21514                if a == b {
21515                    continue;
21516                }
21517                let pair = [a, b];
21518                let chain = ResourceLimits::is_chain(&pair);
21519                let antichain = ResourceLimits::is_antichain(&pair);
21520                let mixed = ResourceLimits::is_mixed(&pair);
21521                let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21522                assert_eq!(
21523                    true_count, 1,
21524                    "trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21525                );
21526                assert!(
21527                    !mixed,
21528                    "is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
21529                );
21530            }
21531        }
21532        // Arity-3 sweep on distinct triples — the mixed cell opens
21533        // whenever the triple carries both a comparable and an
21534        // incomparable pair (e.g. DEFAULT + MID + OTHER above).
21535        for a in presets {
21536            for b in presets {
21537                for c in presets {
21538                    if a == b || b == c || a == c {
21539                        continue;
21540                    }
21541                    let triple = [a, b, c];
21542                    let chain = ResourceLimits::is_chain(&triple);
21543                    let antichain = ResourceLimits::is_antichain(&triple);
21544                    let mixed = ResourceLimits::is_mixed(&triple);
21545                    let true_count =
21546                        usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21547                    assert_eq!(
21548                        true_count, 1,
21549                        "trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21550                    );
21551                }
21552            }
21553        }
21554    }
21555
21556    #[test]
21557    fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
21558        // Const-fn pin — the set-level mixed projection is evaluable
21559        // in const context, so a caller can pin a mixed-set identity
21560        // at compile time. Sibling of the const-fn evaluability pins
21561        // on is_chain + is_antichain one CELL axis over.
21562        const _: () = assert!(!ResourceLimits::is_mixed(&[]));
21563        const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21564        const _: () = assert!(ResourceLimits::is_mixed(&[
21565            DEFAULT_RESOURCE_LIMITS,
21566            HAND_AUTHORED_MID_POSTURE,
21567            HAND_AUTHORED_OTHER_POSTURE,
21568        ]));
21569    }
21570
21571    // ── ResourceLimits::is_ascending / ::is_descending — sequence-level ──
21572
21573    #[test]
21574    fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
21575        // Empty-slice vacuous truth — the empty conjunction is vacuously
21576        // true; the empty slice contains no consecutive pairs to reject.
21577        // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21578        // CONSECUTIVE-VS-ALL-PAIRS axis over: both cells of the (set-
21579        // level, sequence-level) projection pair AGREE at the empty slice
21580        // on the vacuous-truth verdict.
21581        assert!(ResourceLimits::is_ascending(&[]));
21582    }
21583
21584    #[test]
21585    fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
21586        // Peer of is_ascending's empty-slice identity one PAIR-LEVEL-
21587        // PRIMITIVE axis over: both cells of the (ascending, descending)
21588        // sequence-level pair AGREE at the empty slice.
21589        assert!(ResourceLimits::is_descending(&[]));
21590    }
21591
21592    #[test]
21593    fn resource_limits_is_ascending_singleton_is_vacuously_true() {
21594        // Singleton vacuous truth — a one-element slice contains no
21595        // consecutive pairs, so the walk never enters the loop and
21596        // returns `true` vacuously. Pinned on every canonical preset AND
21597        // on both hand-authored asymmetric postures.
21598        assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21599        assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
21600        assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
21601        assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
21602        assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
21603    }
21604
21605    #[test]
21606    fn resource_limits_is_descending_singleton_is_vacuously_true() {
21607        assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21608        assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
21609        assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
21610        assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
21611        assert!(ResourceLimits::is_descending(&[
21612            HAND_AUTHORED_OTHER_POSTURE
21613        ]));
21614    }
21615
21616    #[test]
21617    fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
21618        // Diagonal-duplicate identity — `leq` is REFLEXIVE
21619        // (`a.leq(a) == true` via each axis's `<=` reflexivity), so the
21620        // single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
21621        // the conjunction holds at every diagonal-duplicate slice.
21622        // AGREES with is_chain's diagonal-duplicate verdict — the two
21623        // projections diverge only where the ORDER matters, and
21624        // duplicated elements carry no ordering information.
21625        assert!(ResourceLimits::is_ascending(&[
21626            DEFAULT_RESOURCE_LIMITS,
21627            DEFAULT_RESOURCE_LIMITS,
21628        ]));
21629        assert!(ResourceLimits::is_ascending(&[
21630            HAND_AUTHORED_MID_POSTURE,
21631            HAND_AUTHORED_MID_POSTURE,
21632        ]));
21633    }
21634
21635    #[test]
21636    fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
21637        // Diagonal-duplicate identity — `geq` is REFLEXIVE, so the
21638        // single consecutive pair `(0, 1)` binds `a.geq(a) == true`.
21639        // AGREES with is_ascending's diagonal-duplicate verdict — both
21640        // leq and geq carry reflexivity, so the sequence-level (T, T)
21641        // corner opens at every diagonal-duplicate slice, distinct from
21642        // the set-level (T, F) verdict is_chain / is_antichain carry on
21643        // the same slice.
21644        assert!(ResourceLimits::is_descending(&[
21645            DEFAULT_RESOURCE_LIMITS,
21646            DEFAULT_RESOURCE_LIMITS,
21647        ]));
21648        assert!(ResourceLimits::is_descending(&[
21649            HAND_AUTHORED_MID_POSTURE,
21650            HAND_AUTHORED_MID_POSTURE,
21651        ]));
21652    }
21653
21654    #[test]
21655    fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
21656        // Ordered ascending closure — the shipped-preset triple on the
21657        // bounded-lattice diagonal enumerated leq-ascending has
21658        // `EMPTY.leq(DEFAULT) == true` and `DEFAULT.leq(UNBOUNDED) ==
21659        // true`, so every consecutive pair passes the conjunction.
21660        // Pinned on the arity-2 prefix AND on the full arity-3 chain so
21661        // the walk closes at every non-degenerate ascending enumeration
21662        // length.
21663        assert!(ResourceLimits::is_ascending(&[
21664            EMPTY_RESOURCE_LIMITS,
21665            DEFAULT_RESOURCE_LIMITS,
21666        ]));
21667        assert!(ResourceLimits::is_ascending(&[
21668            DEFAULT_RESOURCE_LIMITS,
21669            UNBOUNDED_RESOURCE_LIMITS,
21670        ]));
21671        assert!(ResourceLimits::is_ascending(&[
21672            EMPTY_RESOURCE_LIMITS,
21673            DEFAULT_RESOURCE_LIMITS,
21674            UNBOUNDED_RESOURCE_LIMITS,
21675        ]));
21676    }
21677
21678    #[test]
21679    fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
21680        // Ordered descending closure — the reversed enumeration is a
21681        // geq-monotone descending sequence. The DIRECT MIRROR of
21682        // is_ascending's ascending closure one PERMUTATION axis over.
21683        assert!(ResourceLimits::is_descending(&[
21684            DEFAULT_RESOURCE_LIMITS,
21685            EMPTY_RESOURCE_LIMITS,
21686        ]));
21687        assert!(ResourceLimits::is_descending(&[
21688            UNBOUNDED_RESOURCE_LIMITS,
21689            DEFAULT_RESOURCE_LIMITS,
21690        ]));
21691        assert!(ResourceLimits::is_descending(&[
21692            UNBOUNDED_RESOURCE_LIMITS,
21693            DEFAULT_RESOURCE_LIMITS,
21694            EMPTY_RESOURCE_LIMITS,
21695        ]));
21696    }
21697
21698    #[test]
21699    fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
21700        // Order-sensitivity — the reversed enumeration of the same
21701        // shipped-preset chain has `UNBOUNDED.leq(DEFAULT) == false` at
21702        // the first consecutive pair. is_chain accepts BOTH orderings
21703        // (order-INSENSITIVE); this projection accepts only the leq-
21704        // sorted one — the DISCRIMINATING arm between the sequence-level
21705        // and set-level projections.
21706        assert!(!ResourceLimits::is_ascending(&[
21707            UNBOUNDED_RESOURCE_LIMITS,
21708            DEFAULT_RESOURCE_LIMITS,
21709            EMPTY_RESOURCE_LIMITS,
21710        ]));
21711    }
21712
21713    #[test]
21714    fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
21715        // The direct falsification of is_ascending's ascending closure
21716        // one CELL axis over on the SAME slice.
21717        assert!(!ResourceLimits::is_descending(&[
21718            EMPTY_RESOURCE_LIMITS,
21719            DEFAULT_RESOURCE_LIMITS,
21720            UNBOUNDED_RESOURCE_LIMITS,
21721        ]));
21722    }
21723
21724    #[test]
21725    fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
21726        // Non-monotone chain permutation — [DEFAULT, EMPTY, UNBOUNDED]
21727        // is a permutation of the shipped-preset chain, so it IS a chain
21728        // (is_chain accepts it), but the first consecutive pair binds
21729        // `DEFAULT.leq(EMPTY) == false`, so the sequence-level ascending
21730        // walk rejects. Also is_descending rejects — the third
21731        // consecutive pair binds `EMPTY.geq(UNBOUNDED) == false`. Pins
21732        // the DISCRIMINATING arm between the sequence-level and set-
21733        // level projections at a slice that is a chain but neither
21734        // ascending nor descending.
21735        let slice = [
21736            DEFAULT_RESOURCE_LIMITS,
21737            EMPTY_RESOURCE_LIMITS,
21738            UNBOUNDED_RESOURCE_LIMITS,
21739        ];
21740        assert!(ResourceLimits::is_chain(&slice));
21741        assert!(!ResourceLimits::is_ascending(&slice));
21742        assert!(!ResourceLimits::is_descending(&slice));
21743    }
21744
21745    #[test]
21746    fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
21747        // The two hand-authored asymmetric postures are incomparable
21748        // (neither MID.leq(OTHER) nor OTHER.leq(MID)), so the single
21749        // consecutive pair rejects. Pinned in both orderings — the pair-
21750        // level leq verdict rejects on the antichain pair regardless of
21751        // which side is `self`.
21752        assert!(!ResourceLimits::is_ascending(&[
21753            HAND_AUTHORED_MID_POSTURE,
21754            HAND_AUTHORED_OTHER_POSTURE,
21755        ]));
21756        assert!(!ResourceLimits::is_ascending(&[
21757            HAND_AUTHORED_OTHER_POSTURE,
21758            HAND_AUTHORED_MID_POSTURE,
21759        ]));
21760    }
21761
21762    #[test]
21763    fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
21764        assert!(!ResourceLimits::is_descending(&[
21765            HAND_AUTHORED_MID_POSTURE,
21766            HAND_AUTHORED_OTHER_POSTURE,
21767        ]));
21768        assert!(!ResourceLimits::is_descending(&[
21769            HAND_AUTHORED_OTHER_POSTURE,
21770            HAND_AUTHORED_MID_POSTURE,
21771        ]));
21772    }
21773
21774    #[test]
21775    fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
21776        // Transitivity theorem — for every slice `postures`,
21777        // `is_ascending(postures) == true` implies `is_chain(postures)
21778        // == true`. The pointwise partial order is TRANSITIVE, so a
21779        // consecutive-pair leq-chain closes under transitive composition
21780        // into the all-pairs leq-chain. Substrate-level THEOREM: the
21781        // sequence-level ascending verdict is strictly STRONGER than the
21782        // set-level chain verdict. Same identity for is_descending —
21783        // both sequence-level projections imply the set-level chain
21784        // projection.
21785        let presets: [ResourceLimits; 5] = [
21786            EMPTY_RESOURCE_LIMITS,
21787            DEFAULT_RESOURCE_LIMITS,
21788            UNBOUNDED_RESOURCE_LIMITS,
21789            HAND_AUTHORED_MID_POSTURE,
21790            HAND_AUTHORED_OTHER_POSTURE,
21791        ];
21792        // Every arity-2 slice from the 5×5 preset matrix witnesses the
21793        // implication in both directions of the (ascending, descending)
21794        // pair.
21795        for a in presets {
21796            for b in presets {
21797                let slice = [a, b];
21798                if ResourceLimits::is_ascending(&slice) {
21799                    assert!(
21800                        ResourceLimits::is_chain(&slice),
21801                        "is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
21802                    );
21803                }
21804                if ResourceLimits::is_descending(&slice) {
21805                    assert!(
21806                        ResourceLimits::is_chain(&slice),
21807                        "is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
21808                    );
21809                }
21810            }
21811        }
21812        // Load-bearing arity-3 witness — the shipped ascending chain
21813        // triple satisfies is_ascending AND is_chain; the descending
21814        // reversal satisfies is_descending AND is_chain.
21815        let asc = [
21816            EMPTY_RESOURCE_LIMITS,
21817            DEFAULT_RESOURCE_LIMITS,
21818            UNBOUNDED_RESOURCE_LIMITS,
21819        ];
21820        assert!(ResourceLimits::is_ascending(&asc));
21821        assert!(ResourceLimits::is_chain(&asc));
21822        let desc = [
21823            UNBOUNDED_RESOURCE_LIMITS,
21824            DEFAULT_RESOURCE_LIMITS,
21825            EMPTY_RESOURCE_LIMITS,
21826        ];
21827        assert!(ResourceLimits::is_descending(&desc));
21828        assert!(ResourceLimits::is_chain(&desc));
21829    }
21830
21831    #[test]
21832    fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
21833        // Empty-and-singleton agreement — the two sequence-level
21834        // projections carry the SAME vacuous-truth verdict at every
21835        // slice with fewer than two consecutive pairs. Pinned as a
21836        // substrate-level identity so a future rewrite of either
21837        // projection cannot silently drift from the shared vacuous-
21838        // truth verdict.
21839        assert_eq!(
21840            ResourceLimits::is_ascending(&[]),
21841            ResourceLimits::is_descending(&[]),
21842        );
21843        let presets: [ResourceLimits; 5] = [
21844            EMPTY_RESOURCE_LIMITS,
21845            DEFAULT_RESOURCE_LIMITS,
21846            UNBOUNDED_RESOURCE_LIMITS,
21847            HAND_AUTHORED_MID_POSTURE,
21848            HAND_AUTHORED_OTHER_POSTURE,
21849        ];
21850        for a in presets {
21851            let single = [a];
21852            assert_eq!(
21853                ResourceLimits::is_ascending(&single),
21854                ResourceLimits::is_descending(&single),
21855                "empty-and-singleton agreement failed on singleton {a:?}",
21856            );
21857            let dup = [a, a];
21858            assert_eq!(
21859                ResourceLimits::is_ascending(&dup),
21860                ResourceLimits::is_descending(&dup),
21861                "diagonal-duplicate agreement failed on {a:?}",
21862            );
21863        }
21864    }
21865
21866    #[test]
21867    fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
21868        // Const-fn pin — the sequence-level ascending projection is
21869        // evaluable in const context, so a caller can pin an ascending-
21870        // sequence identity at compile time.
21871        const _: () = assert!(ResourceLimits::is_ascending(&[]));
21872        const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21873        const _: () = assert!(ResourceLimits::is_ascending(&[
21874            EMPTY_RESOURCE_LIMITS,
21875            DEFAULT_RESOURCE_LIMITS,
21876            UNBOUNDED_RESOURCE_LIMITS,
21877        ]));
21878        const _: () = assert!(!ResourceLimits::is_ascending(&[
21879            UNBOUNDED_RESOURCE_LIMITS,
21880            EMPTY_RESOURCE_LIMITS,
21881        ]));
21882    }
21883
21884    #[test]
21885    fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
21886        // Const-fn pin — sibling of is_ascending one PAIR-LEVEL-
21887        // PRIMITIVE axis over.
21888        const _: () = assert!(ResourceLimits::is_descending(&[]));
21889        const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21890        const _: () = assert!(ResourceLimits::is_descending(&[
21891            UNBOUNDED_RESOURCE_LIMITS,
21892            DEFAULT_RESOURCE_LIMITS,
21893            EMPTY_RESOURCE_LIMITS,
21894        ]));
21895        const _: () = assert!(!ResourceLimits::is_descending(&[
21896            EMPTY_RESOURCE_LIMITS,
21897            UNBOUNDED_RESOURCE_LIMITS,
21898        ]));
21899    }
21900}