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, and "modest" is
7392        /// the problem: the two paths differ by a few ms on a 10k workload, so
7393        /// on shared CI the comparison measures the runner, not the cache.
7394        /// Measured 1.03× locally (debug) and 0.90× on a GitHub runner in the
7395        /// same commit — a 13-point spread with no code change between them.
7396        ///
7397        /// The bound is therefore stated as what this test can actually prove:
7398        /// the memo must not be *catastrophically* worse, which catches it
7399        /// being wired up wrong (a cache that never hits pays lookup + insert
7400        /// on every call and lands far below this). Holding a tighter ratio
7401        /// would need a benchmark harness with a quiet machine — nextest on a
7402        /// shared runner is not that, and a gate that fails on hardware noise
7403        /// teaches people to re-run rather than to read it.
7404        const MAX_OVERHEAD_VS_BYTECODE: f64 = 1.30;
7405        const SAMPLES: usize = 5;
7406
7407        let forms = read(&cache_friendly_workload(10_000)).unwrap();
7408
7409        let best = |mut make: Box<dyn FnMut() -> Expander>| -> Duration {
7410            (0..SAMPLES)
7411                .map(|_| {
7412                    let mut e = make();
7413                    let f = forms.clone();
7414                    let t = Instant::now();
7415                    let out = e.expand_program(f).unwrap();
7416                    let d = t.elapsed();
7417                    std::hint::black_box(out);
7418                    d
7419                })
7420                .min()
7421                .expect("SAMPLES > 0")
7422        };
7423
7424        let t_subst = best(Box::new(Expander::new_substitute_only));
7425        let t_byte = best(Box::new(Expander::new_bytecode_no_cache));
7426        let t_cached = best(Box::new(Expander::new));
7427
7428        let vs_subst = t_subst.as_secs_f64() / t_cached.as_secs_f64();
7429        let vs_byte = t_byte.as_secs_f64() / t_cached.as_secs_f64();
7430        eprintln!(
7431            "\n=== macroexpand: 10k calls × 10 unique (macro, args) pairs, \
7432             best of {SAMPLES} ===\n\
7433             substitute only   : {t_subst:?}\n\
7434             bytecode no cache : {t_byte:?}\n\
7435             bytecode + cache  : {t_cached:?}\n\
7436             speedup           : {vs_subst:.2}× vs substitute, {vs_byte:.2}× vs bytecode\n"
7437        );
7438
7439        assert!(
7440            vs_subst >= MARGIN_VS_SUBSTITUTE,
7441            "the bytecode+cache path should beat substitution by \
7442             {MARGIN_VS_SUBSTITUTE}×, got {vs_subst:.2}× \
7443             ({t_cached:?} vs {t_subst:?})"
7444        );
7445        assert!(
7446            vs_byte >= 1.0 / MAX_OVERHEAD_VS_BYTECODE,
7447            "the memo must not cost more than it saves: {vs_byte:.2}× vs \
7448             bytecode-no-cache ({t_cached:?} vs {t_byte:?}). A ratio below 1 \
7449             means maintaining the cache is now pure overhead on this workload."
7450        );
7451    }
7452
7453    #[test]
7454    fn cache_respects_arg_changes() {
7455        // Cache must not return stale results when args differ.
7456        let src = "
7457            (defmacro wrap (x) `(list ,x ,x))
7458            (wrap a)
7459            (wrap b)
7460            (wrap a)   ;; same as first — cached hit
7461        ";
7462        let mut e = Expander::new();
7463        let out = e.expand_program(read(src).unwrap()).unwrap();
7464        assert_eq!(out.len(), 3);
7465        assert_eq!(out[0], parse("(list a a)"));
7466        assert_eq!(out[1], parse("(list b b)"));
7467        assert_eq!(out[2], parse("(list a a)"));
7468        // Two distinct args → 2 cache entries.
7469        assert_eq!(e.cache_size(), 2);
7470    }
7471
7472    #[test]
7473    fn clear_cache_empties_memo() {
7474        let mut e = Expander::new();
7475        let out = e
7476            .expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
7477            .unwrap();
7478        assert_eq!(out.len(), 2);
7479        assert_eq!(e.cache_size(), 2);
7480        e.clear_cache();
7481        assert_eq!(e.cache_size(), 0);
7482    }
7483
7484    // ── Unbound template-var: structural variant + did-you-mean hint ──
7485
7486    /// Helper for the unbound-template-var tests — pins the variant shape
7487    /// and carries any error context up to the assert site for legibility.
7488    fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
7489        match err {
7490            LispError::UnboundTemplateVar { prefix, name, hint } => {
7491                (*prefix, name.as_str(), hint.as_deref())
7492            }
7493            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7494        }
7495    }
7496
7497    #[test]
7498    fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
7499        // `,xs` against macro params `[x]` — distance 1, bound 1 — hints `,x`.
7500        // Path: compile_node Unquote (the bytecode-template compile, default
7501        // expander).
7502        let mut e = Expander::new();
7503        let err = e
7504            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7505            .expect_err("unbound template var must error");
7506        let (prefix, name, hint) = unbound_var(&err);
7507        assert_eq!(prefix, UnquoteForm::Unquote);
7508        assert_eq!(name, "xs");
7509        assert_eq!(hint, Some("x"));
7510    }
7511
7512    #[test]
7513    fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
7514        // `,@argz` against macro params `[args]` — distance 1, bound 2 —
7515        // hints `,@args`. Path: compile_node UnquoteSplice.
7516        let mut e = Expander::new();
7517        let err = e
7518            .expand_program(
7519                read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7520            )
7521            .expect_err("unbound splice must error");
7522        let (prefix, name, hint) = unbound_var(&err);
7523        assert_eq!(prefix, UnquoteForm::Splice);
7524        assert_eq!(name, "argz");
7525        assert_eq!(hint, Some("args"));
7526    }
7527
7528    #[test]
7529    fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
7530        // Same shape but routed through the substitute-only expander — proves
7531        // the substitute path emits the same variant as the bytecode path.
7532        let mut e = Expander::new_substitute_only();
7533        let err = e
7534            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7535            .expect_err("substitute unbound must error");
7536        let (prefix, name, hint) = unbound_var(&err);
7537        assert_eq!(prefix, UnquoteForm::Unquote);
7538        assert_eq!(name, "xs");
7539        assert_eq!(hint, Some("x"));
7540    }
7541
7542    #[test]
7543    fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
7544        // The substitute path's UnquoteSplice branch fires for splices that
7545        // appear inside a list during the recursive walk. `,@argz` against
7546        // `[args]` hints `,@args`.
7547        let mut e = Expander::new_substitute_only();
7548        let err = e
7549            .expand_program(
7550                read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7551            )
7552            .expect_err("substitute splice unbound must error");
7553        let (prefix, name, hint) = unbound_var(&err);
7554        assert_eq!(prefix, UnquoteForm::Splice);
7555        assert_eq!(name, "argz");
7556        assert_eq!(hint, Some("args"));
7557    }
7558
7559    #[test]
7560    fn unbound_template_var_omits_hint_when_no_close_match() {
7561        // `,wholly-unrelated` against `[x]` — far past the bound, so no
7562        // hint. Negative control: a wrong hint is worse than no hint, so
7563        // the slot must stay empty when the substrate isn't confident.
7564        let mut e = Expander::new();
7565        let err = e
7566            .expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
7567            .expect_err("unrelated unbound must error");
7568        let (prefix, name, hint) = unbound_var(&err);
7569        assert_eq!(prefix, UnquoteForm::Unquote);
7570        assert_eq!(name, "wholly-unrelated");
7571        assert_eq!(hint, None);
7572    }
7573
7574    #[test]
7575    fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
7576        // End-to-end through the Display impl — pins the rendered diagnostic
7577        // a downstream tool sees today (REPL, tatara-check). Hint stays
7578        // additive: the legacy `"unbound"` substring still appears, so any
7579        // assertion that pattern-matches on it keeps passing.
7580        let mut e = Expander::new();
7581        let err = e
7582            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7583            .expect_err("unbound must error");
7584        let msg = format!("{err}");
7585        assert!(
7586            msg.contains("did you mean ,x?"),
7587            "expected hint suffix in message, got: {msg}"
7588        );
7589        assert!(
7590            msg.contains("unbound"),
7591            "expected legacy `unbound` substring in message, got: {msg}"
7592        );
7593        assert!(
7594            msg.contains(",xs"),
7595            "expected the offending form in message, got: {msg}"
7596        );
7597    }
7598
7599    #[test]
7600    fn unbound_template_var_position_is_none_today() {
7601        // Negative control for the future-spans move: until `Sexp` carries
7602        // source positions, `position()` returns `None` for this variant.
7603        let mut e = Expander::new();
7604        let err = e
7605            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7606            .expect_err("unbound must error");
7607        assert_eq!(err.position(), None);
7608    }
7609
7610    // ── Non-symbol unquote target: structural variant ─────────────────
7611
7612    /// Helper for the non-symbol-unquote-target tests — pins the variant
7613    /// shape and carries any error context up to the assert site for
7614    /// legibility. Sibling of `unbound_var` and `splice_outside_list_got`;
7615    /// returns the `display` projection of the typed `SexpWitness` so the
7616    /// existing call sites stay byte-for-byte comparable to the legacy
7617    /// `got: String` shape.
7618    fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
7619        match err {
7620            LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
7621            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7622        }
7623    }
7624
7625    #[test]
7626    fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
7627        // `,(list 1 2)` — the inner is a list, not a symbol. Path:
7628        // compile_node Unquote (the bytecode-template compile, default
7629        // expander). Pins variant identity AND prefix AND the offending
7630        // literal so a regression that re-inlines the legacy
7631        // `LispError::Compile` shape fails-loudly here.
7632        let mut e = Expander::new();
7633        let err = e
7634            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7635            .expect_err("non-symbol unquote target must error");
7636        let (prefix, got) = non_symbol_target(&err);
7637        assert_eq!(prefix, UnquoteForm::Unquote);
7638        assert_eq!(got, "(list 1 2)");
7639    }
7640
7641    #[test]
7642    fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
7643        // `,@5` — the inner is an int atom, not a symbol. Path:
7644        // compile_node UnquoteSplice. The integer literal round-trips
7645        // through the variant's `got` slot via `Sexp::Display`.
7646        let mut e = Expander::new();
7647        let err = e
7648            .expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
7649            .expect_err("non-symbol splice target must error");
7650        let (prefix, got) = non_symbol_target(&err);
7651        assert_eq!(prefix, UnquoteForm::Splice);
7652        assert_eq!(got, "5");
7653    }
7654
7655    #[test]
7656    fn non_symbol_unquote_in_substitute_emits_structural_variant() {
7657        // Same shape as the bytecode path but routed through the
7658        // substitute-only expander — proves the substitute path emits the
7659        // same variant as the compile_node path. Pins that the lift is
7660        // path-uniform.
7661        let mut e = Expander::new_substitute_only();
7662        let err = e
7663            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7664            .expect_err("substitute non-symbol target must error");
7665        let (prefix, got) = non_symbol_target(&err);
7666        assert_eq!(prefix, UnquoteForm::Unquote);
7667        assert_eq!(got, "(list 1 2)");
7668    }
7669
7670    #[test]
7671    fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
7672        // The substitute path's UnquoteSplice-inside-list branch fires for
7673        // splices that appear inside a list during the recursive walk.
7674        // `,@(list 1 2)` inside the body — the inner is a literal list, not
7675        // a symbol — emits the same variant as the compile_node path.
7676        let mut e = Expander::new_substitute_only();
7677        let err = e
7678            .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7679            .expect_err("substitute non-symbol splice must error");
7680        let (prefix, got) = non_symbol_target(&err);
7681        assert_eq!(prefix, UnquoteForm::Splice);
7682        assert_eq!(got, "(list 1 2)");
7683    }
7684
7685    #[test]
7686    fn non_symbol_unquote_target_position_is_none_today() {
7687        // Negative control for the future-spans move: until `Sexp` carries
7688        // source positions, `position()` returns `None` for this variant.
7689        // A future run that gives `Sexp` source spans adds `pos:
7690        // Option<usize>` to ONE place; this test gives that change a
7691        // deliberate fail-before/pass-after delta.
7692        let mut e = Expander::new();
7693        let err = e
7694            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7695            .expect_err("non-symbol target must error");
7696        assert_eq!(err.position(), None);
7697    }
7698
7699    // ── unquote_target_symbol: typed gate-1 primitive for ,X / ,@X ──────
7700    //
7701    // The `unquote_target_symbol(inner, form)?` primitive lifts the
7702    // inline `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(
7703    // form, inner))?` pattern that previously appeared at four call
7704    // sites (`compile_node` Unquote/UnquoteSplice + `substitute` Unquote
7705    // + `substitute` list-inner UnquoteSplice) behind ONE named
7706    // primitive. The tests below pin: (a) the Ok-arm borrows the
7707    // symbol name from `inner` for both UnquoteForm variants; (b) the
7708    // Err-arm routes through `non_symbol_unquote_target` and emits the
7709    // structural `LispError::NonSymbolUnquoteTarget` variant carrying
7710    // the typed `SexpWitness` (joint shape + display identity) for the
7711    // closed set of reachable non-symbol shapes (int / keyword / list /
7712    // nil); (c) the helper is path-uniform — the same Ok / Err
7713    // contracts hold regardless of which call site invokes it. A
7714    // regression that re-inlines the gate-1 projection at any of the
7715    // four call sites can no longer drift independent of the others —
7716    // the helper IS the gate.
7717
7718    #[test]
7719    fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
7720        // Positive control for the Ok-arm: `inner = Sexp::Symbol("xs")`
7721        // under `UnquoteForm::Unquote` projects through `as_symbol()`
7722        // to the borrowed `&str`. The returned slice's lifetime is
7723        // tied to `inner` so the caller can feed it directly into
7724        // `params.iter().position(...)` (`compile_node`) or
7725        // `bindings.get(...)` (`substitute`) without an intermediate
7726        // allocation. Fail-before/pass-after: this assert is meaningless
7727        // pre-lift because the helper does not exist; post-lift it
7728        // pins the typed gate-1 contract at the named primitive.
7729        let inner = Sexp::symbol("xs");
7730        let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7731            .expect("symbol inner must project to Ok");
7732        assert_eq!(name, "xs");
7733    }
7734
7735    #[test]
7736    fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
7737        // Sibling positive control: `UnquoteForm::Splice` shares the
7738        // gate-1 contract with `Unquote`. The helper is path-uniform
7739        // across both syntactic markers — a regression that bifurcates
7740        // the two arms (e.g., accepting non-symbols for `,@X` but not
7741        // `,X`) fails-loudly here. Pins that the closed-set
7742        // `UnquoteForm` enum's two variants share ONE projection
7743        // posture across the gate-1 boundary.
7744        let inner = Sexp::symbol("rest");
7745        let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
7746            .expect("symbol inner must project to Ok under Splice");
7747        assert_eq!(name, "rest");
7748    }
7749
7750    #[test]
7751    fn unquote_target_symbol_rejects_int_inner_under_unquote() {
7752        // Negative control for the Err-arm: `inner = Sexp::Int(5)` is
7753        // NOT a symbol — the gate-1 projection fires and routes through
7754        // `non_symbol_unquote_target` to the structural
7755        // `LispError::NonSymbolUnquoteTarget` variant. Pin the variant
7756        // identity AND the typed `SexpWitness` joint identity (shape +
7757        // display literal): a regression that drops the witness shape
7758        // or display fails-loudly here.
7759        let inner = Sexp::int(5);
7760        let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7761            .expect_err("int inner must error at gate-1");
7762        match err {
7763            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7764                assert_eq!(prefix, UnquoteForm::Unquote);
7765                assert_eq!(got.shape, crate::error::SexpShape::Int);
7766                assert_eq!(got.display, "5");
7767            }
7768            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7769        }
7770    }
7771
7772    #[test]
7773    fn unquote_target_symbol_rejects_list_inner_under_splice() {
7774        // Sibling negative control: `inner = (list 1 2)` is a list, not
7775        // a symbol — the gate-1 projection fires AND routes through
7776        // `non_symbol_unquote_target(UnquoteForm::Splice, inner)`. Pins
7777        // both the variant identity AND the typed witness's joint
7778        // shape (`SexpShape::List`) + display (`"(list 1 2)"`) so a
7779        // future shape drift fails-loudly. Sibling of the Int / Unquote
7780        // pin: closes the gate-1 contract across the closed-set
7781        // product of {Int, List, Keyword, …} × {Unquote, Splice}.
7782        let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
7783        let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
7784            .expect_err("list inner must error at gate-1");
7785        match err {
7786            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7787                assert_eq!(prefix, UnquoteForm::Splice);
7788                assert_eq!(got.shape, crate::error::SexpShape::List);
7789                assert_eq!(got.display, "(list 1 2)");
7790            }
7791            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7792        }
7793    }
7794
7795    #[test]
7796    fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
7797        // Pin a third reachable non-symbol shape: `Sexp::Keyword(":foo")`.
7798        // The gate-1 projection rejects keywords AS WELL as ints and
7799        // lists — closes the closed-set of "non-symbol shapes the gate
7800        // rejects" across one more reachable variant. The typed witness
7801        // carries `SexpShape::Keyword` + display `:foo` jointly so
7802        // authoring tools (REPL, LSP) bind on the structural shape
7803        // directly.
7804        let inner = Sexp::keyword("foo");
7805        let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7806            .expect_err("keyword inner must error at gate-1");
7807        match err {
7808            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7809                assert_eq!(prefix, UnquoteForm::Unquote);
7810                assert_eq!(got.shape, crate::error::SexpShape::Keyword);
7811                assert_eq!(got.display, ":foo");
7812            }
7813            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7814        }
7815    }
7816
7817    #[test]
7818    fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
7819        // Path-uniformity pin: end-to-end through ALL FOUR call sites
7820        // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7821        // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7822        // every non-symbol unquote target now routes through the SAME
7823        // `unquote_target_symbol(inner, form)?` helper. The four
7824        // end-to-end expansions below all reject with the SAME variant
7825        // (`NonSymbolUnquoteTarget`) — pins that the lift preserves the
7826        // path-uniform rejection contract `non_symbol_unquote_target`'s
7827        // prior lift established (and that drove the bytecode-vs-
7828        // substitute reunification in 0e9c… and successors). A
7829        // regression that re-inlines the gate-1 projection at one of
7830        // the four sites can drift the four call sites independent of
7831        // each other — this test would catch that drift.
7832        let cases: &[(&str, UnquoteForm)] = &[
7833            // compile_node Unquote (bytecode-path)
7834            ("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
7835            // compile_node UnquoteSplice (bytecode-path)
7836            ("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
7837        ];
7838        for (src, expected_form) in cases {
7839            let mut e = Expander::new();
7840            let err = e
7841                .expand_program(read(src).unwrap())
7842                .expect_err("non-symbol unquote target must error end-to-end");
7843            match err {
7844                LispError::NonSymbolUnquoteTarget { prefix, .. } => {
7845                    assert_eq!(prefix, *expected_form, "for src: {src}");
7846                }
7847                other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
7848            }
7849        }
7850        // substitute Unquote (substitute-only path) — sibling pin to
7851        // `non_symbol_unquote_in_substitute_emits_structural_variant`.
7852        let mut e_subst = Expander::new_substitute_only();
7853        let err = e_subst
7854            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7855            .expect_err("substitute Unquote must error end-to-end");
7856        assert!(
7857            matches!(
7858                err,
7859                LispError::NonSymbolUnquoteTarget {
7860                    prefix: UnquoteForm::Unquote,
7861                    ..
7862                }
7863            ),
7864            "expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
7865        );
7866        // substitute list-inner UnquoteSplice (substitute-only path) —
7867        // sibling pin to
7868        // `non_symbol_unquote_splice_inside_list_in_substitute_emits_…`.
7869        let mut e_subst2 = Expander::new_substitute_only();
7870        let err = e_subst2
7871            .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7872            .expect_err("substitute UnquoteSplice-in-list must error end-to-end");
7873        assert!(
7874            matches!(
7875                err,
7876                LispError::NonSymbolUnquoteTarget {
7877                    prefix: UnquoteForm::Splice,
7878                    ..
7879                }
7880            ),
7881            "expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
7882        );
7883    }
7884
7885    // ── Gate-2 (must-be-bound-in-scope) typed primitives ──────────────
7886    // Pins the contract of the two gate-2 helpers — `resolve_param_index`
7887    // (bytecode-template compile path) and `resolve_binding`
7888    // (substitute path) — that the four inline `<lookup>.ok_or_else(||
7889    // unbound_template_var(FORM, name, candidates))` projections at
7890    // `compile_node` Unquote/UnquoteSplice AND `substitute` Unquote/
7891    // UnquoteSplice-inside-list collapse behind. Tests pin: (a) Ok-arm
7892    // projection under both `UnquoteForm` variants — the helper returns
7893    // the resolved `usize` (compile path) or `&Sexp` (substitute path)
7894    // for in-scope names; (b) Err-arm projection routes through
7895    // `unbound_template_var` to the typed `LispError::UnboundTemplateVar`
7896    // variant with the correct `prefix` AND the suggest-driven `hint`;
7897    // (c) the helpers are path-uniform — both compile-path arms share
7898    // ONE `resolve_param_index`; both substitute-path arms share ONE
7899    // `resolve_binding`. A regression that re-inlines the gate-2
7900    // projection at any of the four call sites can no longer drift
7901    // independent of the others — the two helpers ARE the gate.
7902
7903    #[test]
7904    fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
7905        // Positive control for the Ok-arm: `name = "x"` against
7906        // `params = ["a", "x", "rest"]` projects through
7907        // `params.iter().position(|p| *p == name)` to `Some(1)`, which
7908        // the helper unwraps to `Ok(1)`. The returned index feeds
7909        // directly into `TemplateOp::Subst(idx)` at the compile site.
7910        let params = ["a", "x", "rest"];
7911        let idx = resolve_param_index("x", &params, UnquoteForm::Unquote)
7912            .expect("bound name must project to Ok at gate-2");
7913        assert_eq!(idx, 1);
7914    }
7915
7916    #[test]
7917    fn resolve_param_index_returns_position_for_bound_name_under_splice() {
7918        // Sibling positive control: `UnquoteForm::Splice` shares the
7919        // gate-2 contract with `Unquote`. The helper is path-uniform
7920        // across both syntactic markers on the compile path — a
7921        // regression that bifurcates the two arms fails-loudly here.
7922        let params = ["a", "x", "rest"];
7923        let idx = resolve_param_index("rest", &params, UnquoteForm::Splice)
7924            .expect("bound name must project to Ok at gate-2 under Splice");
7925        assert_eq!(idx, 2);
7926    }
7927
7928    #[test]
7929    fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
7930        // Negative control for the Err-arm: `name = "xs"` against
7931        // `params = ["x"]` — distance 1, bound 1 — routes through
7932        // `unbound_template_var` to the structural
7933        // `LispError::UnboundTemplateVar` variant with `hint = Some("x")`.
7934        // Pin the variant identity AND the prefix AND the suggest-driven
7935        // hint: a regression that drops the suggestion fails-loudly here.
7936        let params = ["x"];
7937        let err = resolve_param_index("xs", &params, UnquoteForm::Unquote)
7938            .expect_err("unbound name must error at gate-2");
7939        match err {
7940            LispError::UnboundTemplateVar { prefix, name, hint } => {
7941                assert_eq!(prefix, UnquoteForm::Unquote);
7942                assert_eq!(name, "xs");
7943                assert_eq!(hint.as_deref(), Some("x"));
7944            }
7945            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7946        }
7947    }
7948
7949    #[test]
7950    fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
7951        // Sibling negative control: `name = "wholly-unrelated"` against
7952        // `params = ["x"]` — past the bounded edit distance, so no hint.
7953        // Pin that the suggest-driven hint stays empty under Splice when
7954        // the substrate isn't confident — a wrong hint is worse than no
7955        // hint. Closes the closed-set product of {hint, no-hint} ×
7956        // {Unquote, Splice} on the compile-path gate-2.
7957        let params = ["x"];
7958        let err = resolve_param_index("wholly-unrelated", &params, UnquoteForm::Splice)
7959            .expect_err("unrelated unbound must error at gate-2");
7960        match err {
7961            LispError::UnboundTemplateVar { prefix, name, hint } => {
7962                assert_eq!(prefix, UnquoteForm::Splice);
7963                assert_eq!(name, "wholly-unrelated");
7964                assert_eq!(hint, None);
7965            }
7966            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7967        }
7968    }
7969
7970    #[test]
7971    fn resolve_binding_returns_value_for_bound_name_under_unquote() {
7972        // Positive control for the substitute-path Ok-arm: `name = "x"`
7973        // against a bindings map `{x: 42, y: "hi"}` projects through
7974        // `bindings.get(name)` to `Some(&Sexp::Int(42))`, which the
7975        // helper unwraps to `Ok(&Sexp::Int(42))`. The returned
7976        // `&Sexp` borrows from the bindings map — the top-level
7977        // `Sexp::Unquote(_)` substitute caller adds a single
7978        // `.cloned()` to satisfy its owned-`Sexp` return obligation.
7979        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7980        bindings.insert("x".to_string(), Sexp::int(42));
7981        bindings.insert("y".to_string(), Sexp::string("hi"));
7982        let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
7983            .expect("bound name must project to Ok at gate-2 (substitute)");
7984        assert_eq!(val, &Sexp::int(42));
7985    }
7986
7987    #[test]
7988    fn resolve_binding_returns_value_for_bound_name_under_splice() {
7989        // Sibling positive control: `UnquoteForm::Splice` shares the
7990        // gate-2 contract with `Unquote` on the substitute path too.
7991        // The bound value is a `Sexp::List` because the splice arm's
7992        // caller match expression expects `Sexp::List(items)` — but
7993        // the helper itself doesn't inspect the value's shape; it
7994        // just hands back the borrow. A regression that gate-checks
7995        // the value's shape inside `resolve_binding` (instead of at
7996        // the caller match arm) fails-loudly here.
7997        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7998        bindings.insert(
7999            "args".to_string(),
8000            Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
8001        );
8002        let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
8003            .expect("bound name must project to Ok at gate-2 under Splice");
8004        assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8005    }
8006
8007    #[test]
8008    fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
8009        // Negative control for the substitute-path Err-arm: `name =
8010        // "xs"` against bindings `{x: 1}` — distance 1, bound 1 —
8011        // routes through `unbound_template_var` to the structural
8012        // `LispError::UnboundTemplateVar` variant with `hint =
8013        // Some("x")`. The candidate set is drawn from
8014        // `bound_names(bindings)` — the live bindings' keys, never a
8015        // stale snapshot.
8016        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8017        bindings.insert("x".to_string(), Sexp::int(1));
8018        let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
8019            .expect_err("unbound name must error at gate-2 (substitute)");
8020        match err {
8021            LispError::UnboundTemplateVar { prefix, name, hint } => {
8022                assert_eq!(prefix, UnquoteForm::Unquote);
8023                assert_eq!(name, "xs");
8024                assert_eq!(hint.as_deref(), Some("x"));
8025            }
8026            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8027        }
8028    }
8029
8030    #[test]
8031    fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
8032        // Sibling negative control on the substitute path: past-bound
8033        // distance → no hint. Closes the closed-set product of
8034        // {hint, no-hint} × {Unquote, Splice} on the substitute-path
8035        // gate-2.
8036        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8037        bindings.insert("args".to_string(), Sexp::Nil);
8038        let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
8039            .expect_err("unrelated unbound must error at gate-2");
8040        match err {
8041            LispError::UnboundTemplateVar { prefix, name, hint } => {
8042                assert_eq!(prefix, UnquoteForm::Splice);
8043                assert_eq!(name, "wholly-unrelated");
8044                assert_eq!(hint, None);
8045            }
8046            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8047        }
8048    }
8049
8050    #[test]
8051    fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
8052        // Path-uniformity pin: end-to-end through ALL FOUR call sites
8053        // (`compile_node` Unquote, `compile_node` UnquoteSplice,
8054        // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
8055        // every unbound-template-var rejection now routes through one
8056        // of the TWO `resolve_param_index` / `resolve_binding` helpers
8057        // — `Expander::new()` runs the compile path, so its two arms
8058        // share `resolve_param_index`; `Expander::new_substitute_only()`
8059        // runs the substitute path, so its two arms share
8060        // `resolve_binding`. The four end-to-end expansions below all
8061        // reject with the SAME variant (`UnboundTemplateVar`) with the
8062        // expected `prefix` — pins that the lift preserves the
8063        // path-uniform rejection contract `unbound_template_var`'s
8064        // prior naming established. A regression that re-inlines the
8065        // gate-2 projection at one of the four sites can drift the
8066        // four call sites independent of each other — this test would
8067        // catch that drift.
8068        struct Case {
8069            src: &'static str,
8070            expander: fn() -> Expander,
8071            expected_form: UnquoteForm,
8072        }
8073        let cases: &[Case] = &[
8074            // compile_node Unquote (bytecode path) — uses resolve_param_index
8075            Case {
8076                src: "(defmacro w (x) `(list ,xs)) (w 1)",
8077                expander: Expander::new,
8078                expected_form: UnquoteForm::Unquote,
8079            },
8080            // compile_node UnquoteSplice (bytecode path) — uses resolve_param_index
8081            Case {
8082                src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8083                expander: Expander::new,
8084                expected_form: UnquoteForm::Splice,
8085            },
8086            // substitute Unquote (substitute-only path) — uses resolve_binding
8087            Case {
8088                src: "(defmacro w (x) `(list ,xs)) (w 1)",
8089                expander: Expander::new_substitute_only,
8090                expected_form: UnquoteForm::Unquote,
8091            },
8092            // substitute UnquoteSplice-in-list (substitute-only path) — uses resolve_binding
8093            Case {
8094                src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8095                expander: Expander::new_substitute_only,
8096                expected_form: UnquoteForm::Splice,
8097            },
8098        ];
8099        for case in cases {
8100            let mut e = (case.expander)();
8101            let err = e
8102                .expand_program(read(case.src).unwrap())
8103                .expect_err("unbound template var must error end-to-end");
8104            match err {
8105                LispError::UnboundTemplateVar { prefix, .. } => {
8106                    assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8107                }
8108                other => panic!(
8109                    "expected UnboundTemplateVar for {}, got: {other:?}",
8110                    case.src
8111                ),
8112            }
8113        }
8114    }
8115
8116    // ── resolve_unquote_in_params / _in_bindings: gate-1+gate-2 composition ─
8117
8118    #[test]
8119    fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
8120        // Ok-arm composition under `UnquoteForm::Unquote`: gate-1 projects
8121        // the symbol-inner to "x"; gate-2 looks "x" up in `params` and
8122        // returns its index. The combined helper returns the gate-2
8123        // result directly — pins that gate-1's Ok-arm threads into
8124        // gate-2's input without intermediate state.
8125        let inner = Sexp::symbol("x");
8126        let params = ["x", "y"];
8127        let idx = resolve_unquote_in_params(&inner, &params, UnquoteForm::Unquote)
8128            .expect("symbol-inner bound at index 0 must resolve");
8129        assert_eq!(idx, 0);
8130    }
8131
8132    #[test]
8133    fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
8134        // Sibling Ok-arm under `UnquoteForm::Splice`: pins that the
8135        // marker doesn't change the projection — only the rejection
8136        // path's `prefix` slot.
8137        let inner = Sexp::symbol("args");
8138        let params = ["f", "args"];
8139        let idx = resolve_unquote_in_params(&inner, &params, UnquoteForm::Splice)
8140            .expect("symbol-inner bound at index 1 must resolve");
8141        assert_eq!(idx, 1);
8142    }
8143
8144    #[test]
8145    fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
8146        // Err-arm at gate-1 (must-be-a-symbol): the inner is a list, not
8147        // a symbol, so gate-1 rejects via `non_symbol_unquote_target`
8148        // BEFORE gate-2's param lookup runs. Pins that the composition's
8149        // sequencing is gate-1-then-gate-2: a regression that runs
8150        // gate-2 first would attempt to look up "(list 1 2)" as a param
8151        // name and emit `LispError::UnboundTemplateVar { name: "(list 1
8152        // 2)", ... }` — a confusing diagnostic that would substring-grep
8153        // "unbound" instead of "expected symbol". This test pins the
8154        // structural floor: a non-symbol inner is rejected as a non-
8155        // symbol, never re-treated as a bound-name lookup key.
8156        let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
8157        let params = ["x"];
8158        let err = resolve_unquote_in_params(&inner, &params, UnquoteForm::Unquote)
8159            .expect_err("non-symbol inner must reject at gate-1");
8160        match err {
8161            LispError::NonSymbolUnquoteTarget { prefix, got } => {
8162                assert_eq!(prefix, UnquoteForm::Unquote);
8163                assert_eq!(got.display, "(list 1 2)");
8164            }
8165            other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8166        }
8167    }
8168
8169    #[test]
8170    fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
8171        // Err-arm at gate-2 (must-be-bound-in-scope): the inner IS a
8172        // symbol (gate-1 passes) but the name isn't in `params`, so
8173        // gate-2 rejects via `unbound_template_var`. Pins that gate-1
8174        // forwards its Ok-arm `&str` borrow into gate-2's lookup, and
8175        // that the marker `prefix` is threaded into gate-2's rejection
8176        // unchanged (a regression that hard-codes `UnquoteForm::Unquote`
8177        // at the composition boundary would fail this Splice-marker
8178        // assertion).
8179        let inner = Sexp::symbol("missing");
8180        let params = ["x", "y"];
8181        let err = resolve_unquote_in_params(&inner, &params, UnquoteForm::Splice)
8182            .expect_err("unbound symbol must reject at gate-2");
8183        match err {
8184            LispError::UnboundTemplateVar { prefix, name, .. } => {
8185                assert_eq!(prefix, UnquoteForm::Splice);
8186                assert_eq!(name, "missing");
8187            }
8188            other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8189        }
8190    }
8191
8192    #[test]
8193    fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
8194        // Substitute-path sibling of `resolve_unquote_in_params_returns_
8195        // index_for_symbol_inner_under_unquote`. The combined helper
8196        // composes gate-1 (project inner to symbol) THEN gate-2 (look
8197        // up name in bindings). The returned `&Sexp` borrows from
8198        // `bindings` so the list-inner caller threads it straight into
8199        // the splice-expansion match without an intermediate allocation.
8200        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8201        bindings.insert("v".to_string(), Sexp::int(42));
8202        let inner = Sexp::symbol("v");
8203        let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8204            .expect("symbol-inner bound to 42 must resolve");
8205        assert_eq!(val, &Sexp::int(42));
8206    }
8207
8208    #[test]
8209    fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
8210        // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8211        // non_symbol_inner_at_gate_1`. Pins the gate-1-then-gate-2
8212        // sequencing on the substitute path: a non-symbol inner is
8213        // rejected as a non-symbol BEFORE the bindings map is consulted.
8214        let bindings: HashMap<String, Sexp> = HashMap::new();
8215        let inner = Sexp::int(5);
8216        let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
8217            .expect_err("non-symbol inner must reject at gate-1");
8218        match err {
8219            LispError::NonSymbolUnquoteTarget { prefix, got } => {
8220                assert_eq!(prefix, UnquoteForm::Splice);
8221                assert_eq!(got.display, "5");
8222            }
8223            other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8224        }
8225    }
8226
8227    #[test]
8228    fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
8229        // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8230        // unbound_symbol_at_gate_2`. Pins the gate-2 rejection on the
8231        // substitute path with the marker threaded into the rejection's
8232        // `prefix` slot.
8233        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8234        bindings.insert("known".to_string(), Sexp::Nil);
8235        let inner = Sexp::symbol("missing");
8236        let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8237            .expect_err("unbound symbol must reject at gate-2");
8238        match err {
8239            LispError::UnboundTemplateVar { prefix, name, .. } => {
8240                assert_eq!(prefix, UnquoteForm::Unquote);
8241                assert_eq!(name, "missing");
8242            }
8243            other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8244        }
8245    }
8246
8247    #[test]
8248    fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
8249        // End-to-end pin: all FOUR call sites of the gate-1+gate-2
8250        // composition (compile_node Unquote, compile_node UnquoteSplice,
8251        // substitute Unquote, substitute list-inner UnquoteSplice) now
8252        // share TWO composed primitives — `resolve_unquote_in_params`
8253        // on the bytecode path, `resolve_unquote_in_bindings` on the
8254        // substitute path — and ALL four reject gate-1 failures (non-
8255        // symbol inner) with the SAME `LispError::NonSymbolUnquoteTarget`
8256        // variant carrying the expected `prefix` slot. Before the lift,
8257        // each site threaded `form` twice through two helper calls; this
8258        // test pins that the lift preserves the gate's rejection-shape
8259        // identity across all four sites for a non-symbol inner — i.e.
8260        // gate-1 fires identically across both expansion strategies.
8261        struct Case {
8262            src: &'static str,
8263            expander: fn() -> Expander,
8264            expected_form: UnquoteForm,
8265        }
8266        let cases: &[Case] = &[
8267            Case {
8268                src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8269                expander: Expander::new,
8270                expected_form: UnquoteForm::Unquote,
8271            },
8272            Case {
8273                src: "(defmacro w (x) `(outer ,@5)) (w 1)",
8274                expander: Expander::new,
8275                expected_form: UnquoteForm::Splice,
8276            },
8277            Case {
8278                src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8279                expander: Expander::new_substitute_only,
8280                expected_form: UnquoteForm::Unquote,
8281            },
8282            Case {
8283                src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
8284                expander: Expander::new_substitute_only,
8285                expected_form: UnquoteForm::Splice,
8286            },
8287        ];
8288        for case in cases {
8289            let mut e = (case.expander)();
8290            let err = e
8291                .expand_program(read(case.src).unwrap())
8292                .expect_err("non-symbol inner must error end-to-end");
8293            match err {
8294                LispError::NonSymbolUnquoteTarget { prefix, .. } => {
8295                    assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8296                }
8297                other => panic!(
8298                    "expected NonSymbolUnquoteTarget for {}, got: {other:?}",
8299                    case.src
8300                ),
8301            }
8302        }
8303    }
8304
8305    // ── Splice outside list: structural variant + path-uniform rejection ─
8306
8307    /// Helper for the splice-outside-list tests — pins the variant shape
8308    /// and carries the offending `got` field up to the assert site for
8309    /// legibility. Sibling of `unbound_var` and `non_symbol_target`.
8310    fn splice_outside_list_got(err: &LispError) -> &str {
8311        match err {
8312            LispError::SpliceOutsideList { got } => got.display.as_str(),
8313            other => panic!("expected SpliceOutsideList, got: {other:?}"),
8314        }
8315    }
8316
8317    #[test]
8318    fn splice_outside_list_in_substitute_emits_structural_variant() {
8319        // `,@xs` at the body's top level — there is no containing list to
8320        // splice into. Path: substitute (the `Expander::new_substitute_only`
8321        // path's top-level `Sexp::UnquoteSplice(_)` arm). Pins variant
8322        // identity AND the offending inner so a regression that re-inlines
8323        // the legacy `LispError::Compile` shape fails-loudly here.
8324        let mut e = Expander::new_substitute_only();
8325        let err = e
8326            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8327            .expect_err("splice outside list must error");
8328        assert_eq!(splice_outside_list_got(&err), "xs");
8329    }
8330
8331    #[test]
8332    fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
8333        // `,@(list 1 2)` at the body's top level — the inner is a literal
8334        // list, not a symbol. The structural variant carries the inner's
8335        // Sexp::Display projection so the operator sees the literal value
8336        // they wrote in the parenthetical.
8337        let mut e = Expander::new_substitute_only();
8338        let err = e
8339            .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8340            .expect_err("splice outside list must error");
8341        assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8342    }
8343
8344    #[test]
8345    fn splice_outside_list_in_compile_template_emits_structural_variant() {
8346        // The bytecode path's `compile_template` gate now rejects top-level
8347        // `,@X` bodies BEFORE walking — closing the prior silent-divergence
8348        // where the bytecode interpreter's outermost stack frame absorbed
8349        // the splice. Pins that the bytecode path emits the SAME structural
8350        // variant the substitute path emits — `,@-outside-list` is rejected
8351        // path-uniformly. Path: `Expander::new()` (compile_templates = true)
8352        // → `compile_template` gate.
8353        let mut e = Expander::new();
8354        let err = e
8355            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8356            .expect_err("compile-template splice outside list must error");
8357        assert_eq!(splice_outside_list_got(&err), "xs");
8358    }
8359
8360    #[test]
8361    fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
8362        // Same shape as the substitute test but routed through the bytecode
8363        // path's `compile_template` gate. Proves the gate fires on a
8364        // non-symbol inner too — the slot's contents are irrelevant; only
8365        // the syntactic position matters.
8366        let mut e = Expander::new();
8367        let err = e
8368            .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8369            .expect_err("compile-template splice outside list must error");
8370        assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8371    }
8372
8373    #[test]
8374    fn splice_outside_list_substitute_and_bytecode_paths_agree() {
8375        // Path-uniform rejection: the SAME source emits the SAME structural
8376        // variant (`SpliceOutsideList { got: "xs" }`) under both expansion
8377        // strategies. Before the `compile_template` gate, the bytecode path
8378        // silently produced a list while the substitute path errored —
8379        // expansion strategy was observable. After the gate, the gate is
8380        // strategy-uniform, so a macro that registers under one strategy
8381        // registers under the other.
8382        let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
8383        let mut subst = Expander::new_substitute_only();
8384        let mut bytecode = Expander::new();
8385        let err_subst = subst
8386            .expand_program(read(src).unwrap())
8387            .expect_err("substitute must error");
8388        let err_byte = bytecode
8389            .expand_program(read(src).unwrap())
8390            .expect_err("bytecode must error");
8391        assert_eq!(splice_outside_list_got(&err_subst), "xs");
8392        assert_eq!(splice_outside_list_got(&err_byte), "xs");
8393    }
8394
8395    #[test]
8396    fn splice_outside_list_position_is_none_today() {
8397        // Negative control for the future-spans move: until `Sexp` carries
8398        // source positions, `position()` returns `None` for this variant.
8399        // A future run that gives `Sexp` source spans adds `pos:
8400        // Option<usize>` to ONE place; this test gives that change a
8401        // deliberate fail-before/pass-after delta.
8402        let mut e = Expander::new_substitute_only();
8403        let err = e
8404            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8405            .expect_err("splice outside list must error");
8406        assert_eq!(err.position(), None);
8407    }
8408
8409    #[test]
8410    fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
8411        // End-to-end through the Display impl — pins the rendered diagnostic
8412        // a downstream tool sees today (REPL, tatara-check). The legacy
8413        // substring `"\`,@\` may only appear inside a list"` is preserved
8414        // verbatim AND the parenthetical `(got ,@xs)` names the offending
8415        // form; tools that pattern-match on the variant gain structural
8416        // binding to `got`.
8417        let mut e = Expander::new_substitute_only();
8418        let err = e
8419            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8420            .expect_err("splice outside list must error");
8421        let msg = format!("{err}");
8422        assert_eq!(
8423            msg,
8424            "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
8425        );
8426    }
8427
8428    #[test]
8429    fn splice_inside_list_still_succeeds_under_both_paths() {
8430        // Negative control: a well-positioned splice (`,@xs` INSIDE a list)
8431        // continues to succeed under both paths — the new gate only fires
8432        // when the splice is the entire body. Pins that the gate is scoped
8433        // to top-level only, not all `,@` occurrences. Uses a `&rest`-bound
8434        // list so `xs` is unambiguously a Sexp::List `(1 2)` rather than a
8435        // bare list-literal whose first symbol would also splice through.
8436        let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
8437        let mut subst = Expander::new_substitute_only();
8438        let mut bytecode = Expander::new();
8439        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8440        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8441        assert_eq!(out_subst, out_byte);
8442        assert_eq!(out_subst[0], parse("(outer 1 2)"));
8443    }
8444
8445    // ── splice_value_into: the shared splice-result coercion ──
8446
8447    #[test]
8448    fn splice_value_into_list_flattens_elements_into_builder() {
8449        // The canonical splice arm: a bound LIST contributes its elements
8450        // in order, preserving anything already in the builder.
8451        let mut builder = vec![Sexp::symbol("outer")];
8452        splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8453        assert_eq!(
8454            builder,
8455            vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
8456        );
8457    }
8458
8459    #[test]
8460    fn splice_value_into_nil_is_a_noop() {
8461        // Splicing the empty list (`Sexp::Nil`) contributes nothing —
8462        // the builder is unchanged.
8463        let mut builder = vec![Sexp::symbol("outer")];
8464        splice_value_into(&mut builder, &Sexp::Nil);
8465        assert_eq!(builder, vec![Sexp::symbol("outer")]);
8466    }
8467
8468    #[test]
8469    fn splice_value_into_scalar_pushes_single_element() {
8470        // A non-list, non-nil bound value degrades `,@x` to `,x`: it
8471        // splices as exactly one element. Pins the "free middle" coercion
8472        // every scalar shape (int, keyword, …) shares.
8473        let mut builder = vec![Sexp::symbol("outer")];
8474        splice_value_into(&mut builder, &Sexp::int(5));
8475        assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
8476        let mut other: Vec<Sexp> = vec![];
8477        splice_value_into(&mut other, &Sexp::keyword("k"));
8478        assert_eq!(other, vec![Sexp::keyword("k")]);
8479    }
8480
8481    #[test]
8482    fn splice_of_non_list_value_coerces_identically_under_both_paths() {
8483        // The point of the lift: the NON-list splice arms (scalar → single
8484        // element, nil → nothing) coerce identically under the substitute
8485        // AND bytecode strategies. Before the coercion was lifted to ONE
8486        // primitive these two arms lived inline at two sites; this test
8487        // pins that the two strategies cannot drift on the non-list arms.
8488        let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
8489        let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
8490        for src in [scalar, empty] {
8491            let mut subst = Expander::new_substitute_only();
8492            let mut bytecode = Expander::new();
8493            let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8494            let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8495            assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
8496        }
8497        let mut e = Expander::new();
8498        assert_eq!(
8499            e.expand_program(read(scalar).unwrap()).unwrap()[0],
8500            parse("(outer 5)")
8501        );
8502        let mut e2 = Expander::new();
8503        assert_eq!(
8504            e2.expand_program(read(empty).unwrap()).unwrap()[0],
8505            parse("(outer)")
8506        );
8507    }
8508
8509    // ── Missing macro arg: structural variant + path-uniform rejection ──
8510
8511    /// Helper for the missing-macro-arg tests — pins the variant shape
8512    /// and carries the failing macro's name + un-bound param up to the
8513    /// assert site for legibility. Sibling of `unbound_var`,
8514    /// `non_symbol_target`, and `splice_outside_list_got`.
8515    fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
8516        match err {
8517            LispError::MissingMacroArg { macro_name, param } => {
8518                (macro_name.as_str(), param.as_str())
8519            }
8520            other => panic!("expected MissingMacroArg, got: {other:?}"),
8521        }
8522    }
8523
8524    #[test]
8525    fn missing_macro_arg_in_compile_template_emits_structural_variant() {
8526        // `(need-two 1)` against `(need-two a b)` — `b` has no arg. Path:
8527        // `apply_compiled` (the bytecode-template path, default expander).
8528        // Pins variant identity AND macro_name AND the un-bound param so a
8529        // regression that re-inlines the legacy `LispError::Compile` shape
8530        // fails-loudly here.
8531        let mut e = Expander::new();
8532        let err = e
8533            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8534            .expect_err("missing required macro arg must error");
8535        let (macro_name, param) = missing_macro_arg_fields(&err);
8536        assert_eq!(macro_name, "need-two");
8537        assert_eq!(param, "b");
8538    }
8539
8540    #[test]
8541    fn missing_macro_arg_in_substitute_emits_structural_variant() {
8542        // Same shape as the bytecode test but routed through the
8543        // substitute-only expander → `bind_args` is the failing site.
8544        // Proves the substitute path emits the SAME structural variant the
8545        // bytecode path emits — `missing required arg` rejection is
8546        // path-uniform across both expansion strategies.
8547        let mut e = Expander::new_substitute_only();
8548        let err = e
8549            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8550            .expect_err("missing required macro arg must error");
8551        let (macro_name, param) = missing_macro_arg_fields(&err);
8552        assert_eq!(macro_name, "need-two");
8553        assert_eq!(param, "b");
8554    }
8555
8556    #[test]
8557    fn missing_macro_arg_first_position_is_named() {
8558        // `(f)` against `(f a b)` — `a` (the FIRST required param) has no
8559        // arg. The variant names `a`, not `b` — naming the LEFTMOST
8560        // un-bound param is the shape `bind_args` / `apply_compiled` both
8561        // emit (each iterates positionally and bails on the first missing
8562        // slot). Pins the leftmost-bail contract so a regression that
8563        // names the rightmost (or a surplus) param fails-loudly.
8564        let mut e = Expander::new();
8565        let err = e
8566            .expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
8567            .expect_err("missing first required arg must error");
8568        let (macro_name, param) = missing_macro_arg_fields(&err);
8569        assert_eq!(macro_name, "f");
8570        assert_eq!(param, "a");
8571    }
8572
8573    #[test]
8574    fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
8575        // Path-uniform rejection: the SAME source emits the SAME structural
8576        // variant under both expansion strategies. Negative control for
8577        // the divergence-closing posture: a future refactor that drifts
8578        // either path's rejection shape (or drops one path's rejection
8579        // entirely) fails-loudly here. Sibling of
8580        // `splice_outside_list_substitute_and_bytecode_paths_agree` —
8581        // both close `THEORY.md §II.1 invariant 2 — free middle` for one
8582        // failure mode each.
8583        let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
8584        let mut subst = Expander::new_substitute_only();
8585        let mut bytecode = Expander::new();
8586        let err_subst = subst
8587            .expand_program(read(src).unwrap())
8588            .expect_err("substitute must error");
8589        let err_byte = bytecode
8590            .expand_program(read(src).unwrap())
8591            .expect_err("bytecode must error");
8592        assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
8593        assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
8594    }
8595
8596    #[test]
8597    fn missing_macro_arg_position_is_none_today() {
8598        // Negative control for the future-spans move: until `Sexp` carries
8599        // source positions, `position()` returns `None` for this variant.
8600        // A future run that gives `Sexp` source spans adds `pos:
8601        // Option<usize>` to ONE place; this test gives that change a
8602        // deliberate fail-before/pass-after delta. Parallel to
8603        // `splice_outside_list_position_is_none_today`.
8604        let mut e = Expander::new();
8605        let err = e
8606            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8607            .expect_err("missing required macro arg must error");
8608        assert_eq!(err.position(), None);
8609    }
8610
8611    #[test]
8612    fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
8613        // End-to-end through the Display impl — pins the rendered diagnostic
8614        // a downstream tool sees today (REPL, tatara-check). The legacy
8615        // substring `"missing required arg: {param}"` is preserved verbatim
8616        // AND the head clause names the failing macro via `"call to
8617        // {macro_name}"`; tools that pattern-match on the variant gain
8618        // structural binding to `macro_name` / `param`.
8619        let mut e = Expander::new();
8620        let err = e
8621            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8622            .expect_err("missing required macro arg must error");
8623        assert_eq!(
8624            format!("{err}"),
8625            "compile error in call to need-two: missing required arg: b"
8626        );
8627    }
8628
8629    #[test]
8630    fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
8631        // Both `macro_name` (`wrap-twice`) and `param` (`notify-ref`)
8632        // round-trip through the variant unchanged. Pinning this contract
8633        // means a regression that camelCases or lowercases either side
8634        // fails-loudly here. Parallel to the
8635        // `unknown_kwarg_display_carries_kebab_case_keys_unchanged`
8636        // assertion for the kwarg-gate's symmetric surface.
8637        let mut e = Expander::new();
8638        let err = e
8639            .expand_program(
8640                read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
8641                    .unwrap(),
8642            )
8643            .expect_err("missing required macro arg must error");
8644        let (macro_name, param) = missing_macro_arg_fields(&err);
8645        assert_eq!(macro_name, "wrap-twice");
8646        assert_eq!(param, "body");
8647    }
8648
8649    #[test]
8650    fn rest_param_only_macro_with_no_args_still_succeeds() {
8651        // Negative control: a macro whose only param is `&rest` must NOT
8652        // error when called with zero args — the rest-param binds to the
8653        // empty list. The new structural variant fires only on REQUIRED
8654        // params; the `Param::Rest` arm in both `bind_args` and
8655        // `apply_compiled` continues to bind the empty tail. Pins that the
8656        // helper is scoped to required-param failure, not all
8657        // arity-mismatch shapes.
8658        let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
8659        let mut subst = Expander::new_substitute_only();
8660        let mut bytecode = Expander::new();
8661        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8662        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8663        assert_eq!(out_subst, out_byte);
8664        assert_eq!(out_subst[0], parse("(list)"));
8665    }
8666
8667    // ── TooManyMacroArgs: call-site mirror of RestParamTrailingTokens ──
8668    //
8669    // A rest-less param list has a FIXED maximum arity equal to
8670    // `required.len() + optional.len()`. Surplus call args have nowhere to
8671    // bind. Before this gate the surplus was silently truncated to the
8672    // slice the binder could consume — the typed-entry macro-call-gate
8673    // rejected too-few-args loudly (`MissingMacroArg`) but accepted
8674    // too-many silently, an asymmetry the definition-side `&rest <name>
8675    // extra` rejection (`RestParamTrailingTokens`) had no call-side dual.
8676    // After this gate the call-site arity surface is structurally
8677    // complete in both directions; the substitute + bytecode paths share
8678    // `MacroParams::bind`, so both inherit the rejection without drift.
8679
8680    /// Helper for the too-many-args tests — projects to (macro_name,
8681    /// expected, got) for legibility. Sibling of `missing_macro_arg_fields`.
8682    fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
8683        match err {
8684            LispError::TooManyMacroArgs {
8685                macro_name,
8686                expected,
8687                got,
8688            } => (macro_name.as_str(), *expected, *got),
8689            other => panic!("expected TooManyMacroArgs, got: {other:?}"),
8690        }
8691    }
8692
8693    #[test]
8694    fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
8695        // `(defmacro f (a b) ...)` called as `(f 1 2 3)` — `3` has
8696        // nowhere to bind. The rest-less binder rejects via
8697        // `TooManyMacroArgs { macro_name: "f", expected: 2, got: 3 }`,
8698        // NOT silently drops `3`. Pins both the variant identity AND the
8699        // structural fields the typed gate exposes for authoring-tool
8700        // quick-fixes ("you supplied 3 args; the macro takes at most
8701        // 2").
8702        let mut e = Expander::new();
8703        let err = e
8704            .expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8705            .expect_err("surplus arg on rest-less call must error");
8706        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8707        assert_eq!(macro_name, "f");
8708        assert_eq!(expected, 2);
8709        assert_eq!(got, 3);
8710    }
8711
8712    #[test]
8713    fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
8714        // The rest-less binder's fixed maximum arity is `required.len() +
8715        // optional.len()` — the optional section CONTRIBUTES to capacity.
8716        // `(defmacro f (a &optional b) ...)` accepts 1 OR 2 args; 3
8717        // args rejects with `expected: 2` (required + optional, NOT just
8718        // required). Pins the optional-counts-in-capacity contract so a
8719        // regression that omits optionals from the expected calculation
8720        // (and erroneously rejects 2-arg calls) fails-loudly here.
8721        let mut e = Expander::new();
8722        let err = e
8723            .expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8724            .expect_err("surplus arg beyond required+optional must error");
8725        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8726        assert_eq!(macro_name, "f");
8727        assert_eq!(expected, 2);
8728        assert_eq!(got, 3);
8729    }
8730
8731    #[test]
8732    fn too_many_macro_args_required_plus_two_optionals_arity_three() {
8733        // Larger optional section (capacity 1 + 2 = 3). 4 args rejects
8734        // with `expected: 3`. Pins that the capacity calculation scales
8735        // with optional.len(), not just at-most-one. Mixes a bare
8736        // optional with an optional carrying a default form — both shapes
8737        // contribute identically to capacity (the typed `OptionalParam`
8738        // entry's `default: Option<Sexp>` is irrelevant to the arity gate).
8739        let mut e = Expander::new();
8740        let err = e
8741            .expand_program(
8742                read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
8743            )
8744            .expect_err("surplus arg beyond required+two-optional must error");
8745        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8746        assert_eq!(macro_name, "f");
8747        assert_eq!(expected, 3);
8748        assert_eq!(got, 4);
8749    }
8750
8751    #[test]
8752    fn too_many_macro_args_does_not_fire_when_rest_is_present() {
8753        // Negative control: a rest-PRESENT param list has no maximum
8754        // arity — the `&rest` slot collects every trailing arg into a
8755        // `Sexp::List`. `(defmacro f (a &rest xs) ...)` called as
8756        // `(f 1 2 3 4)` MUST succeed; the new gate fires ONLY when
8757        // `MacroParams.rest` is `None`. Pins the rest-present-path
8758        // remains permissive — a regression that wrongly fires the
8759        // too-many gate for any surplus (including the rest-collecting
8760        // path) would break every `&rest`-using macro.
8761        let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
8762        let mut subst = Expander::new_substitute_only();
8763        let mut bytecode = Expander::new();
8764        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8765        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8766        assert_eq!(out_subst, out_byte);
8767        assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
8768    }
8769
8770    #[test]
8771    fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
8772        // Negative control: the rest-less gate fires STRICTLY when
8773        // `args.len() > expected` — at exact arity the binder accepts.
8774        // `(defmacro f (a &optional b) ...)` called as `(f 1 2)` binds
8775        // a=1, b=2 successfully (the optional takes its supplied arg,
8776        // not the default). Pins the boundary condition so a regression
8777        // that flips the comparison to `>=` (rejecting exact-arity
8778        // calls) fails-loudly here.
8779        let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
8780        let mut e = Expander::new();
8781        let out = e.expand_program(read(src).unwrap()).unwrap();
8782        assert_eq!(out[0], parse("(list 1 2)"));
8783    }
8784
8785    #[test]
8786    fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
8787        // Path-uniform rejection: the SAME source emits the SAME
8788        // structural variant under both expansion strategies. The
8789        // shared `MacroParams::bind` makes the rejection lands once and
8790        // both paths inherit it. Mirror of
8791        // `missing_macro_arg_substitute_and_bytecode_paths_agree` —
8792        // both close `THEORY.md §II.1 invariant 2 — free middle` for
8793        // one failure mode each.
8794        let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
8795        let mut subst = Expander::new_substitute_only();
8796        let mut bytecode = Expander::new();
8797        let err_subst = subst
8798            .expand_program(read(src).unwrap())
8799            .expect_err("substitute must error");
8800        let err_byte = bytecode
8801            .expand_program(read(src).unwrap())
8802            .expect_err("bytecode must error");
8803        assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
8804        assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
8805    }
8806
8807    #[test]
8808    fn too_many_macro_args_fires_after_missing_required_priority_held() {
8809        // Priority discipline: the required walk fires
8810        // `MissingMacroArg` BEFORE the rest-less surplus gate is
8811        // reached. `(defmacro f (a b c) …) (f 1)` is `MissingMacroArg
8812        // { param: "b" }`, NOT `TooManyMacroArgs` (and certainly not a
8813        // collision). The two failure modes are structurally disjoint:
8814        // too-few-required vs. too-many-with-no-rest. Pins the bail-on-
8815        // first-missing-required contract so a regression that swaps
8816        // the two gates' order would emit the wrong variant.
8817        let mut e = Expander::new();
8818        let err = e
8819            .expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
8820            .expect_err("missing required must error");
8821        assert!(
8822            matches!(err, LispError::MissingMacroArg { .. }),
8823            "expected MissingMacroArg (priority), got: {err:?}"
8824        );
8825    }
8826
8827    #[test]
8828    fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
8829        // Degenerate case: a nullary macro `(defmacro f () ...)` has
8830        // capacity 0; ANY supplied arg rejects with `expected: 0`. Pins
8831        // the gate fires even when the rest-less max-arity is zero —
8832        // i.e. the rejection is structural, not conditional on a
8833        // non-empty required+optional.
8834        let mut e = Expander::new();
8835        let err = e
8836            .expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
8837            .expect_err("nullary macro called with arg must error");
8838        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8839        assert_eq!(macro_name, "f");
8840        assert_eq!(expected, 0);
8841        assert_eq!(got, 1);
8842    }
8843
8844    #[test]
8845    fn too_many_macro_args_display_renders_legacy_compile_substring() {
8846        // The rendered Display matches the legacy `Compile`-shaped
8847        // diagnostic style — `"compile error in call to {macro_name}:
8848        // too many args: expected at most {expected}, got {got}"` — so
8849        // the existing `"compile error in call to"` substring authoring
8850        // tools' assertions key on stays unchanged. Pins the byte-level
8851        // rendered shape so a regression that drifts the prefix /
8852        // separator / labels fails-loudly here.
8853        let err = LispError::TooManyMacroArgs {
8854            macro_name: "pair".into(),
8855            expected: 2,
8856            got: 5,
8857        };
8858        assert_eq!(
8859            err.to_string(),
8860            "compile error in call to pair: too many args: expected at most 2, got 5"
8861        );
8862    }
8863
8864    #[test]
8865    fn too_many_macro_args_position_is_none_today() {
8866        // Negative control for the future-spans move: until `Sexp`
8867        // carries source positions, `position()` returns `None` for
8868        // this variant. A future run that gives `Sexp` source spans
8869        // adds `pos: Option<usize>` to ONE place; this test gives that
8870        // change a deliberate fail-before/pass-after delta. Parallel to
8871        // `missing_macro_arg_position_is_none_today`.
8872        let err = LispError::TooManyMacroArgs {
8873            macro_name: "pair".into(),
8874            expected: 2,
8875            got: 3,
8876        };
8877        assert_eq!(err.position(), None);
8878    }
8879
8880    /// Helper for the non-symbol-param tests — pins the variant shape and
8881    /// carries the failing position + offending element up to the assert
8882    /// site for legibility. Sibling of `missing_macro_arg_fields`.
8883    fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
8884        match err {
8885            LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
8886            other => panic!("expected NonSymbolParam, got: {other:?}"),
8887        }
8888    }
8889
8890    #[test]
8891    fn non_symbol_param_at_first_position_emits_structural_variant() {
8892        // `(defmacro f (5) ...)` — the first element of the param list is
8893        // an integer literal, not a symbol. Pins variant identity AND
8894        // that `position` is the loop index inside `parse_params` (0 for
8895        // the first slot) AND that `got` is the offending element via
8896        // `Sexp::Display` (`5`). A regression that re-inlines the legacy
8897        // `LispError::Compile` shape (which named neither the position
8898        // nor the offending element) fails-loudly here.
8899        let mut e = Expander::new();
8900        let err = e
8901            .expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
8902            .expect_err("non-symbol param must error");
8903        let (position, got) = non_symbol_param_fields(&err);
8904        assert_eq!(position, 0);
8905        assert_eq!(got, "5");
8906    }
8907
8908    #[test]
8909    fn non_symbol_param_at_second_position_emits_structural_variant() {
8910        // `(defmacro f (a 5) ...)` — `a` parses fine, `5` at position 1
8911        // misfires. Pins that `position` advances with the loop index, so
8912        // an LSP quick-fix that wants to point at "the second element of
8913        // your param list" gains the index as data, no source re-parse
8914        // required.
8915        let mut e = Expander::new();
8916        let err = e
8917            .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8918            .expect_err("non-symbol param must error");
8919        let (position, got) = non_symbol_param_fields(&err);
8920        assert_eq!(position, 1);
8921        assert_eq!(got, "5");
8922    }
8923
8924    #[test]
8925    fn non_symbol_param_carries_keyword_value_unchanged() {
8926        // `:k` at a param-list position. `Sexp::Display` for
8927        // `Atom::Keyword(s)` writes `:s`; pins that the variant's `got`
8928        // field round-trips the keyword form unchanged so an LSP that
8929        // surfaces "you wrote `:k` where a symbol was expected" gains
8930        // the literal keyword value as data, no re-parsing required.
8931        let mut e = Expander::new();
8932        let err = e
8933            .expand_program(read("(defmacro f (:k) `(list))").unwrap())
8934            .expect_err("non-symbol param must error");
8935        let (position, got) = non_symbol_param_fields(&err);
8936        assert_eq!(position, 0);
8937        assert_eq!(got, ":k");
8938    }
8939
8940    #[test]
8941    fn non_symbol_param_carries_nested_list_value_unchanged() {
8942        // A nested list at a param-list position. `Sexp::Display` for
8943        // `List(xs)` writes `(<x1> <x2> ...)`; pins that the variant's
8944        // `got` field carries the nested form's full Display projection
8945        // unchanged so the operator sees what they wrote.
8946        let mut e = Expander::new();
8947        let err = e
8948            .expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
8949            .expect_err("non-symbol param must error");
8950        let (position, got) = non_symbol_param_fields(&err);
8951        assert_eq!(position, 0);
8952        assert_eq!(got, "(nested)");
8953    }
8954
8955    #[test]
8956    fn non_symbol_param_in_defpoint_template_emits_same_variant() {
8957        // `defpoint-template` shares `parse_params` with `defmacro` (all
8958        // three head keywords route through `macro_def_from`). Pins that
8959        // the lift fires path-uniformly across the three head keywords
8960        // — `defmacro`, `defpoint-template`, `defcheck` — so the
8961        // structural-completeness floor holds for every defmacro-shaped
8962        // form, not just the one with the `defmacro` head literal.
8963        let mut e = Expander::new();
8964        let err = e
8965            .expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
8966            .expect_err("non-symbol param must error");
8967        let (position, got) = non_symbol_param_fields(&err);
8968        assert_eq!(position, 0);
8969        assert_eq!(got, "5");
8970    }
8971
8972    #[test]
8973    fn non_symbol_param_in_defcheck_emits_same_variant() {
8974        // Sibling of the defpoint-template test — `defcheck` is the
8975        // third head keyword `macro_def_from` recognizes. All three
8976        // route through the same `parse_params` and now reject
8977        // non-symbol params with the same structural variant.
8978        let mut e = Expander::new();
8979        let err = e
8980            .expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
8981            .expect_err("non-symbol param must error");
8982        let (position, got) = non_symbol_param_fields(&err);
8983        assert_eq!(position, 1);
8984        assert_eq!(got, "5");
8985    }
8986
8987    #[test]
8988    fn non_symbol_param_position_is_none_today() {
8989        // Negative control for the future-spans move: until `Sexp`
8990        // carries source positions, `position()` on `LispError` returns
8991        // `None` for this variant. A future run that gives `Sexp`
8992        // source spans adds `pos: Option<usize>` to ONE place; this
8993        // test gives that change a deliberate fail-before/pass-after
8994        // delta. Parallel to `missing_macro_arg_position_is_none_today`.
8995        let mut e = Expander::new();
8996        let err = e
8997            .expand_program(read("(defmacro f (5) `(list))").unwrap())
8998            .expect_err("non-symbol param must error");
8999        assert_eq!(err.position(), None);
9000    }
9001
9002    #[test]
9003    fn non_symbol_param_message_renders_legacy_substring_with_position() {
9004        // End-to-end through Display — pins the rendered diagnostic that
9005        // downstream tools (REPL, `tatara-check`) see today. Legacy
9006        // substrings `"defmacro params"` AND `"expected symbol"` are
9007        // preserved verbatim; the appended `at position {position}, got
9008        // {got}` clause is the new structural detail. Tools that
9009        // pattern-match on the variant gain structural binding to
9010        // `position` / `got`.
9011        let mut e = Expander::new();
9012        let err = e
9013            .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
9014            .expect_err("non-symbol param must error");
9015        assert_eq!(
9016            format!("{err}"),
9017            "compile error in defmacro params: \
9018             expected symbol at position 1, got 5"
9019        );
9020    }
9021
9022    #[test]
9023    fn non_symbol_param_substitute_and_bytecode_paths_agree() {
9024        // Path-uniform rejection: the SAME source emits the SAME
9025        // structural variant under both expansion strategies. The
9026        // defmacro-syntax-gate fires inside `macro_def_from` →
9027        // `parse_params`, BEFORE either strategy's expansion path runs;
9028        // so both `Expander::new()` (bytecode) and
9029        // `Expander::new_substitute_only()` (substitute) reject the
9030        // SAME malformed defmacro at the SAME gate. Sibling of
9031        // `missing_macro_arg_substitute_and_bytecode_paths_agree`.
9032        let src = "(defmacro f (a 5) `(,a))";
9033        let mut subst = Expander::new_substitute_only();
9034        let mut bytecode = Expander::new();
9035        let err_subst = subst
9036            .expand_program(read(src).unwrap())
9037            .expect_err("substitute must error");
9038        let err_byte = bytecode
9039            .expand_program(read(src).unwrap())
9040            .expect_err("bytecode must error");
9041        assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
9042        assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
9043    }
9044
9045    /// Helper for the rest-param-missing-name tests — pins the variant
9046    /// shape and carries the marker position + offending follower (or
9047    /// its absence) up to the assert site for legibility. Sibling of
9048    /// `non_symbol_param_fields`.
9049    fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
9050        match err {
9051            LispError::RestParamMissingName { rest_position, got } => {
9052                (*rest_position, got.as_ref().map(|w| w.display.as_str()))
9053            }
9054            other => panic!("expected RestParamMissingName, got: {other:?}"),
9055        }
9056    }
9057
9058    #[test]
9059    fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
9060        // `(defmacro f (&rest))` — the marker is the only param-list
9061        // element; nothing follows. Pins variant identity AND that
9062        // `rest_position == 0` (the first slot) AND that `got == None`
9063        // (no follower exists). A regression that re-inlines the legacy
9064        // `LispError::Compile` shape (which named neither field) fails-
9065        // loudly here.
9066        let mut e = Expander::new();
9067        let err = e
9068            .expand_program(read("(defmacro f (&rest) `(list))").unwrap())
9069            .expect_err("&rest with no follower must error");
9070        let (rest_position, got) = rest_param_missing_name_fields(&err);
9071        assert_eq!(rest_position, 0);
9072        assert_eq!(got, None);
9073    }
9074
9075    #[test]
9076    fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
9077        // `(defmacro f (a &rest))` — `a` parses fine, `&rest` at param-list
9078        // position 1 has no follower at all. Pins that `rest_position`
9079        // advances with the loop index, so an LSP quick-fix that wants to
9080        // point at "your `&rest` at position 1 has no name" gains the
9081        // marker position as data, no source re-parse required.
9082        let mut e = Expander::new();
9083        let err = e
9084            .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9085            .expect_err("&rest with no follower must error");
9086        let (rest_position, got) = rest_param_missing_name_fields(&err);
9087        assert_eq!(rest_position, 1);
9088        assert_eq!(got, None);
9089    }
9090
9091    #[test]
9092    fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
9093        // `(defmacro f (&rest 5))` — `&rest` at position 0 followed by
9094        // `5` (an integer literal, not a symbol). Pins that the variant's
9095        // `got` field is `Some` and carries the offending follower's
9096        // `Sexp::Display` projection; the bifurcation between "missing
9097        // entirely" and "present but non-symbol" is in the renderable
9098        // detail, not in what the gate rejects.
9099        let mut e = Expander::new();
9100        let err = e
9101            .expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
9102            .expect_err("&rest followed by non-symbol must error");
9103        let (rest_position, got) = rest_param_missing_name_fields(&err);
9104        assert_eq!(rest_position, 0);
9105        assert_eq!(got, Some("5"));
9106    }
9107
9108    #[test]
9109    fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
9110        // `(defmacro f (a &rest :foo))` — keyword follower at the rest-name
9111        // slot. `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pins
9112        // that the variant's `got` field round-trips the keyword form
9113        // unchanged so an LSP that surfaces "you wrote `:foo` where a
9114        // rest-name was expected" gains the literal keyword value as
9115        // data, no re-parsing required.
9116        let mut e = Expander::new();
9117        let err = e
9118            .expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
9119            .expect_err("&rest followed by keyword must error");
9120        let (rest_position, got) = rest_param_missing_name_fields(&err);
9121        assert_eq!(rest_position, 1);
9122        assert_eq!(got, Some(":foo"));
9123    }
9124
9125    #[test]
9126    fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
9127        // `(defmacro f (&rest (nested)))` — nested-list follower at the
9128        // rest-name slot. `Sexp::Display` for `List(xs)` writes
9129        // `(<x1> <x2> ...)`; pins that the variant's `got` field carries
9130        // the nested form's full Display projection unchanged so the
9131        // operator sees what they wrote.
9132        let mut e = Expander::new();
9133        let err = e
9134            .expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
9135            .expect_err("&rest followed by list must error");
9136        let (rest_position, got) = rest_param_missing_name_fields(&err);
9137        assert_eq!(rest_position, 0);
9138        assert_eq!(got, Some("(nested)"));
9139    }
9140
9141    #[test]
9142    fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
9143        // `defpoint-template` shares `parse_params` with `defmacro` (all
9144        // three head keywords route through `macro_def_from`). Pins that
9145        // the lift fires path-uniformly across the three head keywords —
9146        // a regression that handles `defpoint-template`'s param list
9147        // differently from `defmacro`'s would fail-loudly here.
9148        let mut e = Expander::new();
9149        let err = e
9150            .expand_program(read("(defpoint-template t (a &rest) `(,a))").unwrap())
9151            .expect_err("&rest with no follower must error");
9152        let (rest_position, got) = rest_param_missing_name_fields(&err);
9153        assert_eq!(rest_position, 1);
9154        assert_eq!(got, None);
9155    }
9156
9157    #[test]
9158    fn rest_param_missing_name_in_defcheck_emits_same_variant() {
9159        // Sibling for the `defcheck` head; rounds out the three-head-
9160        // keyword coverage so the lift is path-uniform across
9161        // `defmacro` / `defpoint-template` / `defcheck`. After this
9162        // test the defmacro-syntax-gate rejects `&rest`-without-name
9163        // identically across all three head keywords — the
9164        // typed-entry surface is single-shape across the cluster.
9165        let mut e = Expander::new();
9166        let err = e
9167            .expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
9168            .expect_err("&rest followed by non-symbol must error");
9169        let (rest_position, got) = rest_param_missing_name_fields(&err);
9170        assert_eq!(rest_position, 0);
9171        assert_eq!(got, Some("5"));
9172    }
9173
9174    #[test]
9175    fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
9176        // Path-uniform rejection: the SAME source emits the SAME
9177        // structural variant under both expansion strategies. The
9178        // defmacro-syntax-gate fires inside `macro_def_from` →
9179        // `parse_params`, BEFORE either strategy's expansion path
9180        // runs; so both `Expander::new()` (bytecode) and
9181        // `Expander::new_substitute_only()` (substitute) reject the
9182        // SAME malformed defmacro at the SAME gate. Sibling of
9183        // `non_symbol_param_substitute_and_bytecode_paths_agree`.
9184        let src = "(defmacro f (a &rest 5) `(,a))";
9185        let mut subst = Expander::new_substitute_only();
9186        let mut bytecode = Expander::new();
9187        let err_subst = subst
9188            .expand_program(read(src).unwrap())
9189            .expect_err("substitute must error");
9190        let err_byte = bytecode
9191            .expand_program(read(src).unwrap())
9192            .expect_err("bytecode must error");
9193        assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
9194        assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
9195    }
9196
9197    #[test]
9198    fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
9199        // End-to-end through Display — pins the rendered diagnostic
9200        // consumers see today (REPL, tatara-check) AND the new `(rest
9201        // marker at position {rest_position}, got {got})` clause. The
9202        // legacy `"&rest needs a name"` substring rides through
9203        // verbatim.
9204        let mut e = Expander::new();
9205        let err = e
9206            .expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
9207            .expect_err("&rest followed by non-symbol must error");
9208        assert_eq!(
9209            format!("{err}"),
9210            "compile error in defmacro params: &rest needs a name \
9211             (rest marker at position 1, got 5)"
9212        );
9213    }
9214
9215    #[test]
9216    fn rest_param_missing_name_message_renders_none_provided_when_follower_absent() {
9217        // Same as the prior test but for the "missing entirely" branch.
9218        // The renderable detail is `(rest marker at position
9219        // {rest_position}, none provided)` — naming the absence
9220        // structurally instead of an empty / partial parenthetical.
9221        let mut e = Expander::new();
9222        let err = e
9223            .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9224            .expect_err("&rest with no follower must error");
9225        assert_eq!(
9226            format!("{err}"),
9227            "compile error in defmacro params: &rest needs a name \
9228             (rest marker at position 1, none provided)"
9229        );
9230    }
9231
9232    #[test]
9233    fn rest_param_missing_name_position_is_none_today() {
9234        // Pins that `position()` returns `None` so the future `pos:
9235        // Option<usize>` add (once `Sexp` carries source spans) lands
9236        // as a deliberate fail-before/pass-after delta rather than a
9237        // silent default. Parallel to
9238        // `non_symbol_param_position_is_none_today` and
9239        // `missing_macro_arg_position_is_none_today`.
9240        let err_missing = LispError::RestParamMissingName {
9241            rest_position: 1,
9242            got: None,
9243        };
9244        assert_eq!(err_missing.position(), None);
9245        let err_got = LispError::RestParamMissingName {
9246            rest_position: 0,
9247            got: Some(crate::error::SexpWitness::new(
9248                crate::error::SexpShape::Int,
9249                "5",
9250            )),
9251        };
9252        assert_eq!(err_got.position(), None);
9253    }
9254
9255    // --- RestParamTrailingTokens: the parse_params gate's third (and
9256    // final) definition-site failure mode ---
9257    //
9258    // A `&rest <name>` absorbs every remaining call arg, so it is the LAST
9259    // thing a param list can name. Before this variant `parse_params`
9260    // returned the moment it bound the rest name, SILENTLY DROPPING any
9261    // trailing tokens — `(a &rest xs extra)` parsed as if `extra` weren't
9262    // there. These tests pin the loud rejection that replaces the silent
9263    // drop; the symbol `RestParamTrailingTokens` exists only after this
9264    // change, so the whole block is fail-before/pass-after by construction
9265    // (compile-time edge) and the end-to-end regression guard below pins
9266    // that the malformed defmacro no longer expands cleanly.
9267
9268    /// Helper mirroring `rest_param_missing_name_fields` — pins the variant
9269    /// shape and lifts the marker position, trailing count, and first
9270    /// offender's display up to the assert site.
9271    fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
9272        match err {
9273            LispError::RestParamTrailingTokens {
9274                rest_position,
9275                extra,
9276                first,
9277            } => (*rest_position, *extra, first.display.as_str()),
9278            other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
9279        }
9280    }
9281
9282    #[test]
9283    fn parse_params_rejects_single_trailing_token_after_rest_name() {
9284        // `(a &rest c extra)` — `&rest c` is well-formed, but `extra`
9285        // follows. The rest name is bound at position 2, the marker at 1;
9286        // the lone trailing token `extra` is reported (extra == 1, first ==
9287        // "extra"). Before this variant `parse_params` returned at the rest
9288        // name and `extra` vanished.
9289        let err = parse_params(&read("a &rest c extra").unwrap())
9290            .expect_err("a trailing token after the rest name must error");
9291        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9292    }
9293
9294    #[test]
9295    fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
9296        // `(&rest c x y z)` — three tokens follow the rest name. `extra`
9297        // counts ALL of them (3), `first` is the first (`x`), and the
9298        // marker is at position 0. A regression that reports only the
9299        // first trailing token's presence (extra hard-coded to 1) fails
9300        // loudly here.
9301        let err = parse_params(&read("&rest c x y z").unwrap())
9302            .expect_err("multiple trailing tokens must error");
9303        assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
9304    }
9305
9306    #[test]
9307    fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
9308        // `(a &rest c 5)` — the rest NAME `c` is a valid symbol, so this is
9309        // NOT a `RestParamMissingName`; the integer `5` is a trailing token
9310        // AFTER a well-formed `&rest c`. Pins that the two sibling failure
9311        // modes don't collide: a malformed rest-name is `RestParamMissingName`,
9312        // a well-formed rest-name followed by junk is
9313        // `RestParamTrailingTokens`. `first` round-trips `5` via the typed
9314        // witness's `Sexp::Display` projection.
9315        let err = parse_params(&read("a &rest c 5").unwrap())
9316            .expect_err("a trailing non-symbol after the rest name must error");
9317        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
9318    }
9319
9320    #[test]
9321    fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
9322        // The fidelity fix, end-to-end through `expand_program`: a defmacro
9323        // whose param list carries a stray token after `&rest <name>` now
9324        // ERRORS at the typed-entry gate instead of expanding as though the
9325        // stray token weren't there. This is the regression guard for the
9326        // silent-drop bug — before this change the same source expanded
9327        // cleanly and `extra` was discarded with no signal.
9328        let mut e = Expander::new();
9329        let err = e
9330            .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9331            .expect_err("trailing token after &rest name must error");
9332        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9333    }
9334
9335    #[test]
9336    fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
9337        // Path-uniform rejection: the gate fires inside `macro_def_from` →
9338        // `parse_params`, BEFORE either expansion strategy runs, so both
9339        // `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
9340        // (substitute) reject the SAME malformed defmacro at the SAME gate.
9341        // Sibling of `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9342        let src = "(defmacro f (a &rest xs extra) `(,a))";
9343        let mut subst = Expander::new_substitute_only();
9344        let mut bytecode = Expander::new();
9345        let err_subst = subst
9346            .expand_program(read(src).unwrap())
9347            .expect_err("substitute must error");
9348        let err_byte = bytecode
9349            .expand_program(read(src).unwrap())
9350            .expect_err("bytecode must error");
9351        assert_eq!(
9352            rest_param_trailing_tokens_fields(&err_subst),
9353            (1, 1, "extra")
9354        );
9355        assert_eq!(
9356            rest_param_trailing_tokens_fields(&err_byte),
9357            (1, 1, "extra")
9358        );
9359    }
9360
9361    #[test]
9362    fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
9363        // End-to-end through Display — pins the rendered diagnostic AND the
9364        // new `(rest marker at position {n}, {extra} trailing after name,
9365        // first: {first})` clause. The `compile error in defmacro params:`
9366        // prefix matches the sibling `&rest needs a name` rendering's shape.
9367        let mut e = Expander::new();
9368        let err = e
9369            .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9370            .expect_err("trailing token after &rest name must error");
9371        assert_eq!(
9372            format!("{err}"),
9373            "compile error in defmacro params: &rest name must be last \
9374             (rest marker at position 1, 1 trailing after name, first: extra)"
9375        );
9376    }
9377
9378    #[test]
9379    fn rest_param_trailing_tokens_position_is_none_today() {
9380        // Pins `position() == None` so the future `pos: Option<usize>` add
9381        // (once `Sexp` carries source spans) lands as a deliberate
9382        // fail-before/pass-after delta. Parallel to
9383        // `rest_param_missing_name_position_is_none_today`.
9384        let err = LispError::RestParamTrailingTokens {
9385            rest_position: 1,
9386            extra: 1,
9387            first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
9388        };
9389        assert_eq!(err.position(), None);
9390    }
9391
9392    // --- MacroDefHead enum (the closed-set lift) ---
9393    //
9394    // The next nine tests pin the typed-enum lift that closes the
9395    // three-times rule on the `head: &str → &'static str` projection
9396    // idiom previously inlined at FOUR sites (the `matches!` gate at
9397    // the top of `macro_def_from` plus the projection match inside
9398    // each of `defmacro_arity`, `defmacro_non_symbol_name`,
9399    // `defmacro_non_list_params`). Every test in this block names
9400    // `MacroDefHead` directly — the symbol exists only after the
9401    // lift, so the entire block is fail-before/pass-after by
9402    // construction (compile-time edge). Theory anchor: THEORY.md
9403    // §VI.1 — three-times rule; THEORY.md §V.1 — the closed set is
9404    // a TYPE rather than a `matches!` literal.
9405
9406    #[test]
9407    fn macro_def_head_from_keyword_recognizes_defmacro() {
9408        // Pins that `MacroDefHead::from_keyword("defmacro")` returns
9409        // `Some(MacroDefHead::Defmacro)` — the first of the three
9410        // canonical macro-definition head keywords. A regression that
9411        // re-inlines a `matches!`-only gate (without the typed-enum
9412        // projection) deletes `from_keyword` and fails-loudly here.
9413        assert_eq!(
9414            MacroDefHead::from_keyword("defmacro"),
9415            Some(MacroDefHead::Defmacro)
9416        );
9417    }
9418
9419    #[test]
9420    fn macro_def_head_from_keyword_recognizes_defpoint_template() {
9421        // Pins that `MacroDefHead::from_keyword("defpoint-template")`
9422        // returns `Some(MacroDefHead::DefpointTemplate)` — the second
9423        // of the three canonical head keywords. The `defpoint-template`
9424        // form is the K8s-as-processes authoring surface (see
9425        // tatara-process); `macro_def_from` must recognize it
9426        // identically to `defmacro` so the `(defpoint-template …)`
9427        // form's macro-style binding works the same way.
9428        assert_eq!(
9429            MacroDefHead::from_keyword("defpoint-template"),
9430            Some(MacroDefHead::DefpointTemplate)
9431        );
9432    }
9433
9434    #[test]
9435    fn macro_def_head_from_keyword_recognizes_defcheck() {
9436        // Pins that `MacroDefHead::from_keyword("defcheck")` returns
9437        // `Some(MacroDefHead::Defcheck)` — the third and final
9438        // canonical head keyword. The `defcheck` form is the
9439        // workspace-coherence authoring surface (see
9440        // tatara-reconciler/checks.lisp); `macro_def_from` must
9441        // recognize it identically to `defmacro` so user-defined
9442        // checks inherit the macro-style binding semantics.
9443        assert_eq!(
9444            MacroDefHead::from_keyword("defcheck"),
9445            Some(MacroDefHead::Defcheck)
9446        );
9447    }
9448
9449    #[test]
9450    fn macro_def_head_from_keyword_rejects_unknown() {
9451        // Pins that `MacroDefHead::from_keyword` returns `None` for
9452        // anything outside the closed set — a non-symbol keyword
9453        // (`"if"`), a near-miss spelling (`"defmacroo"`,
9454        // `"defcheckk"`), and the empty string. `macro_def_from`
9455        // depends on this `None` projection to mean "this form is
9456        // not a defmacro form" and walk past — a regression that
9457        // accidentally accepts a near-miss head (e.g. via a
9458        // lower-cased `EqualFold` match) would route `(defmacroo …)`
9459        // through the arity gate, which is wrong. Pins all four
9460        // canonical near-miss / non-canonical inputs.
9461        assert_eq!(MacroDefHead::from_keyword("if"), None);
9462        assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
9463        assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
9464        assert_eq!(MacroDefHead::from_keyword(""), None);
9465    }
9466
9467    #[test]
9468    fn macro_def_head_keyword_round_trips_each_variant() {
9469        // Pins that `MacroDefHead::keyword` returns the canonical
9470        // `&'static str` literal for each variant. Together with
9471        // `from_keyword` this closes the bidirectional projection:
9472        // for every canonical head keyword `s`, `MacroDefHead::
9473        // from_keyword(s).unwrap().keyword() == s`. The `&'static
9474        // str` lifetime on the return type is load-bearing — it's
9475        // what lets the `LispError::Defmacro*` variants carry
9476        // `head: &'static str` slots without an arbitrary owned
9477        // `String`. Pinning the `: &'static str` binding here
9478        // makes the lifetime requirement load-bearing in the test.
9479        let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
9480        let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
9481        let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
9482        assert_eq!(s_defmacro, "defmacro");
9483        assert_eq!(s_defpoint, "defpoint-template");
9484        assert_eq!(s_defcheck, "defcheck");
9485    }
9486
9487    #[test]
9488    fn macro_def_head_keyword_round_trips_through_from_keyword() {
9489        // Pins that the two halves of the projection compose to the
9490        // identity on the closed set: for every canonical head
9491        // keyword, projecting `&str → MacroDefHead → &'static str`
9492        // returns the original literal. Sibling of
9493        // `macro_def_head_keyword_round_trips_each_variant` —
9494        // together they pin both directions of the bidirection.
9495        for kw in ["defmacro", "defpoint-template", "defcheck"] {
9496            let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
9497            assert_eq!(head.keyword(), kw);
9498        }
9499    }
9500
9501    #[test]
9502    fn macro_def_head_threads_through_defmacro_arity_helper() {
9503        // Pins that `defmacro_arity` accepts a typed `MacroDefHead`
9504        // and threads it through to the variant's typed `head` slot
9505        // unchanged — no `&str` projection at the helper boundary
9506        // (the projection through `MacroDefHead::keyword()` happens at
9507        // Display rendering time inside the `#[error(...)]`
9508        // annotation). A regression that drops the `MacroDefHead`
9509        // parameter type (e.g. by reverting to `head: &str`) breaks
9510        // compilation here. Pinning each of the three variants gives
9511        // the typed-head threading the same path-uniformity edge the
9512        // existing `defmacro_arity_in_*_emits_same_variant` tests pin
9513        // for the call-site path through `macro_def_from`.
9514        for head in [
9515            MacroDefHead::Defmacro,
9516            MacroDefHead::DefpointTemplate,
9517            MacroDefHead::Defcheck,
9518        ] {
9519            let err = defmacro_arity(head, 2);
9520            match err {
9521                LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
9522                other => panic!("expected DefmacroArity, got: {other:?}"),
9523            }
9524        }
9525    }
9526
9527    #[test]
9528    fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
9529        // Sibling of the `defmacro_arity` threading test — pins that
9530        // `defmacro_non_symbol_name` accepts a typed `MacroDefHead`
9531        // and threads it through to the variant's typed `head` slot
9532        // unchanged. The `got: &Sexp` parameter rides through
9533        // `crate::domain::sexp_witness` into the variant's typed
9534        // `got: SexpWitness` slot so BOTH the structural shape AND
9535        // the rendered literal are preserved across the helper
9536        // boundary, parallel to how `non_symbol_param` and
9537        // `non_symbol_unquote_target` project their `&Sexp` arguments
9538        // through the same typed joint primitive.
9539        let got = parse("5");
9540        for head in [
9541            MacroDefHead::Defmacro,
9542            MacroDefHead::DefpointTemplate,
9543            MacroDefHead::Defcheck,
9544        ] {
9545            let err = defmacro_non_symbol_name(head, &got);
9546            match err {
9547                LispError::DefmacroNonSymbolName { head: h, got: g } => {
9548                    assert_eq!(h, head);
9549                    assert_eq!(g.shape, crate::error::SexpShape::Int);
9550                    assert_eq!(g.display, "5");
9551                }
9552                other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9553            }
9554        }
9555    }
9556
9557    #[test]
9558    fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
9559        // Sibling of the `defmacro_arity` and
9560        // `defmacro_non_symbol_name` threading tests — pins that
9561        // `defmacro_non_list_params` accepts a typed `MacroDefHead`
9562        // and threads it through to the variant's typed `head` slot
9563        // unchanged. Together the three threading tests close the
9564        // typed-enum lift across all three error helpers — every
9565        // call site that constructs a `LispError::Defmacro*` variant
9566        // takes its `head` from a `MacroDefHead`, never from a `&str`
9567        // match. The `got: &Sexp` parameter rides through
9568        // `crate::domain::sexp_witness` into the variant's typed
9569        // `got: SexpWitness` slot so BOTH the structural shape AND
9570        // the rendered literal are preserved across the helper
9571        // boundary, parallel to how `defmacro_non_symbol_name`,
9572        // `non_symbol_param`, and `non_symbol_unquote_target` project
9573        // their `&Sexp` arguments through the same typed joint
9574        // primitive.
9575        let got = parse("x");
9576        for head in [
9577            MacroDefHead::Defmacro,
9578            MacroDefHead::DefpointTemplate,
9579            MacroDefHead::Defcheck,
9580        ] {
9581            let err = defmacro_non_list_params(head, &got);
9582            match err {
9583                LispError::DefmacroNonListParams { head: h, got: g } => {
9584                    assert_eq!(h, head);
9585                    assert_eq!(g.shape, crate::error::SexpShape::Symbol);
9586                    assert_eq!(g.display, "x");
9587                }
9588                other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9589            }
9590        }
9591    }
9592
9593    /// Helper for the defmacro-arity tests — pins the variant shape and
9594    /// carries the head / arity up to the assert site for legibility.
9595    /// Sibling of `non_symbol_param_fields` and
9596    /// `rest_param_missing_name_fields`.
9597    fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
9598        match err {
9599            LispError::DefmacroArity { head, arity } => (*head, *arity),
9600            other => panic!("expected DefmacroArity, got: {other:?}"),
9601        }
9602    }
9603
9604    #[test]
9605    fn defmacro_arity_with_head_only_emits_structural_variant() {
9606        // `(defmacro)` — only the head, no name / params / body. Pins
9607        // variant identity AND that `arity == 1` (just the head
9608        // element) AND that `head == "defmacro"`. A regression that
9609        // re-inlines the legacy `LispError::Compile` shape (which
9610        // named neither field) fails-loudly here.
9611        let mut e = Expander::new();
9612        let err = e
9613            .expand_program(read("(defmacro)").unwrap())
9614            .expect_err("defmacro arity gate must error");
9615        let (head, arity) = defmacro_arity_fields(&err);
9616        assert_eq!(head, MacroDefHead::Defmacro);
9617        assert_eq!(arity, 1);
9618    }
9619
9620    #[test]
9621    fn defmacro_arity_with_head_and_name_emits_structural_variant() {
9622        // `(defmacro f)` — head + name, missing params + body. Pins
9623        // that `arity` advances with the actual form length (2 for
9624        // this case) so an LSP quick-fix that wants to surface "you
9625        // wrote 2 elements; need 4" gains the count as data, no
9626        // source re-parse required.
9627        let mut e = Expander::new();
9628        let err = e
9629            .expand_program(read("(defmacro f)").unwrap())
9630            .expect_err("defmacro arity gate must error");
9631        let (head, arity) = defmacro_arity_fields(&err);
9632        assert_eq!(head, MacroDefHead::Defmacro);
9633        assert_eq!(arity, 2);
9634    }
9635
9636    #[test]
9637    fn defmacro_arity_with_head_name_params_emits_structural_variant() {
9638        // `(defmacro f ())` — head + name + params, missing body
9639        // (the most-complete partial defmacro that still trips the
9640        // arity gate). Pins that `arity == 3` exactly so an LSP
9641        // quick-fix that wants to surface "your defmacro is one
9642        // element short — body is missing" gains the count as data.
9643        let mut e = Expander::new();
9644        let err = e
9645            .expand_program(read("(defmacro f ())").unwrap())
9646            .expect_err("defmacro arity gate must error");
9647        let (head, arity) = defmacro_arity_fields(&err);
9648        assert_eq!(head, MacroDefHead::Defmacro);
9649        assert_eq!(arity, 3);
9650    }
9651
9652    #[test]
9653    fn defmacro_arity_in_defpoint_template_emits_same_variant() {
9654        // `defpoint-template` shares `macro_def_from` with `defmacro`
9655        // (all three head keywords route through the same gate). Pins
9656        // that the lift fires path-uniformly across the three head
9657        // keywords AND that the variant's `head` slot carries the
9658        // actual head literal — `defpoint-template`, not `defmacro`
9659        // — so an LSP that wants to point at "your defpoint-template
9660        // form is missing elements" gains the head as data.
9661        let mut e = Expander::new();
9662        let err = e
9663            .expand_program(read("(defpoint-template t)").unwrap())
9664            .expect_err("defpoint-template arity gate must error");
9665        let (head, arity) = defmacro_arity_fields(&err);
9666        assert_eq!(head, MacroDefHead::DefpointTemplate);
9667        assert_eq!(arity, 2);
9668    }
9669
9670    #[test]
9671    fn defmacro_arity_in_defcheck_emits_same_variant() {
9672        // Sibling of the defpoint-template test — `defcheck` is the
9673        // third head keyword `macro_def_from` recognizes. All three
9674        // route through the same arity gate and now reject too-short
9675        // forms with the same structural variant.
9676        let mut e = Expander::new();
9677        let err = e
9678            .expand_program(read("(defcheck)").unwrap())
9679            .expect_err("defcheck arity gate must error");
9680        let (head, arity) = defmacro_arity_fields(&err);
9681        assert_eq!(head, MacroDefHead::Defcheck);
9682        assert_eq!(arity, 1);
9683    }
9684
9685    #[test]
9686    fn defmacro_arity_substitute_and_bytecode_paths_agree() {
9687        // Path-uniform rejection: the SAME source emits the SAME
9688        // structural variant under both expansion strategies. The
9689        // arity gate fires inside `macro_def_from` BEFORE either
9690        // strategy's expansion path runs; so both `Expander::new()`
9691        // (bytecode) and `Expander::new_substitute_only()`
9692        // (substitute) reject the SAME malformed defmacro at the
9693        // SAME gate. Sibling of
9694        // `non_symbol_param_substitute_and_bytecode_paths_agree` and
9695        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9696        let src = "(defmacro f)";
9697        let mut subst = Expander::new_substitute_only();
9698        let mut bytecode = Expander::new();
9699        let err_subst = subst
9700            .expand_program(read(src).unwrap())
9701            .expect_err("substitute must error");
9702        let err_byte = bytecode
9703            .expand_program(read(src).unwrap())
9704            .expect_err("bytecode must error");
9705        assert_eq!(
9706            defmacro_arity_fields(&err_subst),
9707            (MacroDefHead::Defmacro, 2)
9708        );
9709        assert_eq!(
9710            defmacro_arity_fields(&err_byte),
9711            (MacroDefHead::Defmacro, 2)
9712        );
9713    }
9714
9715    #[test]
9716    fn defmacro_arity_message_renders_legacy_substring_with_arity() {
9717        // End-to-end through Display — pins the rendered diagnostic
9718        // consumers see today (REPL, `tatara-check`) AND the new
9719        // `(got {arity} elements, need 4)` clause. The legacy
9720        // `"(defmacro name (params) body) required"` substring
9721        // rides through verbatim. Tools that pattern-match on the
9722        // variant gain structural binding to `head` / `arity`.
9723        let mut e = Expander::new();
9724        let err = e
9725            .expand_program(read("(defmacro f)").unwrap())
9726            .expect_err("defmacro arity gate must error");
9727        assert_eq!(
9728            format!("{err}"),
9729            "compile error in defmacro: (defmacro name (params) body) required \
9730             (got 2 elements, need 4)"
9731        );
9732    }
9733
9734    #[test]
9735    fn defmacro_arity_position_is_none_today() {
9736        // Negative control for the future-spans move: until `Sexp`
9737        // carries source positions, `position()` on `LispError`
9738        // returns `None` for this variant. A future run that gives
9739        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9740        // this test gives that change a deliberate fail-before/pass-
9741        // after delta. Parallel to
9742        // `non_symbol_param_position_is_none_today` and
9743        // `rest_param_missing_name_position_is_none_today`.
9744        let mut e = Expander::new();
9745        let err = e
9746            .expand_program(read("(defmacro)").unwrap())
9747            .expect_err("defmacro arity gate must error");
9748        assert_eq!(err.position(), None);
9749    }
9750
9751    #[test]
9752    fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
9753        // Negative control: a defmacro with exactly 4 elements (head
9754        // + name + params + body) passes the arity gate. Pins that
9755        // the lift is scoped to the arity-deficient case, not to
9756        // every defmacro form. After this test, a regression that
9757        // tightens the arity gate to >= 5 (e.g. spuriously requiring
9758        // a docstring slot) fails-loudly here.
9759        let mut e = Expander::new();
9760        let out = e
9761            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9762            .expect("well-formed defmacro must succeed");
9763        assert_eq!(out[0], Sexp::int(42));
9764    }
9765
9766    /// Helper for the defmacro-non-symbol-name tests — pins variant
9767    /// shape and carries the head / got up to the assert site for
9768    /// legibility. Sibling of `defmacro_arity_fields`,
9769    /// `non_symbol_param_fields`, and `rest_param_missing_name_fields`.
9770    fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
9771        match err {
9772            LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
9773            other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9774        }
9775    }
9776
9777    #[test]
9778    fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
9779        // `(defmacro 5 () body)` — the form passes the arity gate
9780        // (4 elements) but list[1] is `5`, not a symbol. Pins variant
9781        // identity AND that `head == "defmacro"` AND that `got ==
9782        // "5"`. A regression that re-inlines the legacy
9783        // `LispError::Compile { form: "defmacro", message: "expected
9784        // name symbol" }` shape (which named the failure mode but
9785        // not the offending element) fails-loudly here.
9786        let mut e = Expander::new();
9787        let err = e
9788            .expand_program(read("(defmacro 5 () body)").unwrap())
9789            .expect_err("defmacro non-symbol name gate must error");
9790        let (head, got) = defmacro_non_symbol_name_fields(&err);
9791        assert_eq!(head, MacroDefHead::Defmacro);
9792        assert_eq!(got, "5");
9793    }
9794
9795    #[test]
9796    fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
9797        // `(defmacro :foo () body)` — list[1] is the keyword `:foo`,
9798        // not a symbol. Pins that `Sexp::Display` for
9799        // `Atom::Keyword(s)` writes `:s` and the variant's `got` slot
9800        // carries the keyword form unchanged. An LSP that wants to
9801        // surface "you wrote `:foo` where a name symbol was expected"
9802        // gains the literal keyword value as data, no source re-parse
9803        // required.
9804        let mut e = Expander::new();
9805        let err = e
9806            .expand_program(read("(defmacro :foo () body)").unwrap())
9807            .expect_err("defmacro non-symbol name gate must error");
9808        let (head, got) = defmacro_non_symbol_name_fields(&err);
9809        assert_eq!(head, MacroDefHead::Defmacro);
9810        assert_eq!(got, ":foo");
9811    }
9812
9813    #[test]
9814    fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
9815        // `(defmacro "name" () body)` — list[1] is the string
9816        // literal `"name"`, not a symbol. Pins that `Sexp::Display`
9817        // for `Atom::String(s)` writes `"s"` (with quotes) and the
9818        // variant's `got` slot carries the quoted form unchanged.
9819        let mut e = Expander::new();
9820        let err = e
9821            .expand_program(read("(defmacro \"name\" () body)").unwrap())
9822            .expect_err("defmacro non-symbol name gate must error");
9823        let (head, got) = defmacro_non_symbol_name_fields(&err);
9824        assert_eq!(head, MacroDefHead::Defmacro);
9825        assert_eq!(got, "\"name\"");
9826    }
9827
9828    #[test]
9829    fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
9830        // `(defmacro (nested) () body)` — list[1] is a nested list,
9831        // not a symbol. Pins that `Sexp::Display` for a list writes
9832        // `(elements)` and the variant's `got` slot carries the
9833        // parenthesized form unchanged.
9834        let mut e = Expander::new();
9835        let err = e
9836            .expand_program(read("(defmacro (nested) () body)").unwrap())
9837            .expect_err("defmacro non-symbol name gate must error");
9838        let (head, got) = defmacro_non_symbol_name_fields(&err);
9839        assert_eq!(head, MacroDefHead::Defmacro);
9840        assert_eq!(got, "(nested)");
9841    }
9842
9843    #[test]
9844    fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
9845        // `defpoint-template` shares `macro_def_from` with `defmacro`
9846        // (all three head keywords route through the same gate).
9847        // Pins that the lift fires path-uniformly across the three
9848        // head keywords AND that the variant's `head` slot carries
9849        // the actual head literal — `defpoint-template`, not
9850        // `defmacro` — so an LSP that wants to point at "your
9851        // defpoint-template form's name slot isn't a symbol" gains
9852        // the head as data.
9853        let mut e = Expander::new();
9854        let err = e
9855            .expand_program(read("(defpoint-template 7 () body)").unwrap())
9856            .expect_err("defpoint-template non-symbol name gate must error");
9857        let (head, got) = defmacro_non_symbol_name_fields(&err);
9858        assert_eq!(head, MacroDefHead::DefpointTemplate);
9859        assert_eq!(got, "7");
9860    }
9861
9862    #[test]
9863    fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
9864        // Sibling for the `defcheck` head — third head keyword
9865        // `macro_def_from` recognizes. Rounds out the three-head-
9866        // keyword coverage so the lift is path-uniform across
9867        // `defmacro` / `defpoint-template` / `defcheck`.
9868        let mut e = Expander::new();
9869        let err = e
9870            .expand_program(read("(defcheck :k () body)").unwrap())
9871            .expect_err("defcheck non-symbol name gate must error");
9872        let (head, got) = defmacro_non_symbol_name_fields(&err);
9873        assert_eq!(head, MacroDefHead::Defcheck);
9874        assert_eq!(got, ":k");
9875    }
9876
9877    #[test]
9878    fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
9879        // Path-uniform rejection: the SAME source emits the SAME
9880        // structural variant under both expansion strategies. The
9881        // name-symbol gate fires inside `macro_def_from` BEFORE
9882        // either expansion strategy runs, so the gate is naturally
9883        // path-uniform; pinning it gives a regression that drifts
9884        // either strategy's handling of non-symbol-name defmacros (or
9885        // makes one strategy accept what the other rejects) a fail-
9886        // before/pass-after edge. Sibling of
9887        // `defmacro_arity_substitute_and_bytecode_paths_agree`,
9888        // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
9889        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9890        let src = "(defmacro 5 () body)";
9891        let mut subst = Expander::new_substitute_only();
9892        let mut bytecode = Expander::new();
9893        let err_subst = subst
9894            .expand_program(read(src).unwrap())
9895            .expect_err("substitute must error");
9896        let err_byte = bytecode
9897            .expand_program(read(src).unwrap())
9898            .expect_err("bytecode must error");
9899        assert_eq!(
9900            defmacro_non_symbol_name_fields(&err_subst),
9901            (MacroDefHead::Defmacro, "5")
9902        );
9903        assert_eq!(
9904            defmacro_non_symbol_name_fields(&err_byte),
9905            (MacroDefHead::Defmacro, "5")
9906        );
9907    }
9908
9909    #[test]
9910    fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
9911        // End-to-end through Display — pins the rendered diagnostic
9912        // consumers see today (REPL, `tatara-check`) AND the new
9913        // `, got {got}` clause. The legacy `"expected name symbol"`
9914        // substring rides through verbatim; the prefix matches the
9915        // legacy `Compile { form: "defmacro", message: "expected name
9916        // symbol" }` byte-for-byte. Tools that pattern-match on the
9917        // variant gain structural binding to `head` / `got`.
9918        let mut e = Expander::new();
9919        let err = e
9920            .expand_program(read("(defmacro 5 () body)").unwrap())
9921            .expect_err("defmacro non-symbol name gate must error");
9922        assert_eq!(
9923            format!("{err}"),
9924            "compile error in defmacro: expected name symbol, got 5"
9925        );
9926    }
9927
9928    #[test]
9929    fn defmacro_non_symbol_name_position_is_none_today() {
9930        // Negative control for the future-spans move: until `Sexp`
9931        // carries source positions, `position()` on `LispError`
9932        // returns `None` for this variant. A future run that gives
9933        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9934        // this test gives that change a deliberate fail-before/pass-
9935        // after delta. Parallel to
9936        // `defmacro_arity_position_is_none_today`,
9937        // `non_symbol_param_position_is_none_today`, and
9938        // `rest_param_missing_name_position_is_none_today`.
9939        let mut e = Expander::new();
9940        let err = e
9941            .expand_program(read("(defmacro 5 () body)").unwrap())
9942            .expect_err("defmacro non-symbol name gate must error");
9943        assert_eq!(err.position(), None);
9944    }
9945
9946    #[test]
9947    fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
9948        // Negative control: a defmacro whose name slot IS a symbol
9949        // passes the name-symbol gate. Pins that the lift is scoped
9950        // to the non-symbol-name case, not to every defmacro form.
9951        // After this test, a regression that tightens the gate to
9952        // reject e.g. kebab-cased names fails-loudly here.
9953        let mut e = Expander::new();
9954        let out = e
9955            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9956            .expect("well-formed defmacro must succeed");
9957        assert_eq!(out[0], Sexp::int(42));
9958    }
9959
9960    #[test]
9961    fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
9962        // Pins the gate ordering: a 4-element defmacro whose name
9963        // slot is non-symbol fires `DefmacroNonSymbolName`, NOT
9964        // `DefmacroArity`. The arity gate (>= 4 elements) admits
9965        // this form; the name-symbol gate is the next checkpoint.
9966        // A regression that swaps the gate ordering (e.g. checks
9967        // name-symbol before arity, so `(defmacro 5)` would emit
9968        // `DefmacroNonSymbolName` instead of `DefmacroArity`) fails-
9969        // loudly here.
9970        let mut e = Expander::new();
9971        let err = e
9972            .expand_program(read("(defmacro 5 () body)").unwrap())
9973            .expect_err("name-symbol gate must error");
9974        assert!(
9975            matches!(err, LispError::DefmacroNonSymbolName { .. }),
9976            "expected DefmacroNonSymbolName, got: {err:?}"
9977        );
9978
9979        let err_arity = e
9980            .expand_program(read("(defmacro 5)").unwrap())
9981            .expect_err("arity gate must error");
9982        assert!(
9983            matches!(err_arity, LispError::DefmacroArity { .. }),
9984            "expected DefmacroArity (arity < 4 short-circuits before name check), \
9985             got: {err_arity:?}"
9986        );
9987    }
9988
9989    /// Helper for the defmacro-non-list-params tests — pins variant
9990    /// shape and carries the head / got up to the assert site for
9991    /// legibility. Sibling of `defmacro_arity_fields`,
9992    /// `defmacro_non_symbol_name_fields`, `non_symbol_param_fields`,
9993    /// and `rest_param_missing_name_fields`.
9994    fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
9995        match err {
9996            LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
9997            other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9998        }
9999    }
10000
10001    #[test]
10002    fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
10003        // `(defmacro f x body)` — the form passes both the arity gate
10004        // (4 elements) AND the name-symbol gate (`f` is a symbol) but
10005        // list[2] is the symbol `x`, not a list. Pins variant identity
10006        // AND that `head == "defmacro"` AND that `got == "x"`. A
10007        // regression that re-inlines the legacy `LispError::Compile {
10008        // form: "defmacro", message: "expected param list" }` shape
10009        // (which named the failure mode but not the offending element)
10010        // fails-loudly here.
10011        let mut e = Expander::new();
10012        let err = e
10013            .expand_program(read("(defmacro f x body)").unwrap())
10014            .expect_err("defmacro non-list params gate must error");
10015        let (head, got) = defmacro_non_list_params_fields(&err);
10016        assert_eq!(head, MacroDefHead::Defmacro);
10017        assert_eq!(got, "x");
10018    }
10019
10020    #[test]
10021    fn defmacro_non_list_params_with_int_emits_structural_variant() {
10022        // `(defmacro f 5 body)` — list[2] is `5`, not a list. Pins
10023        // that `Sexp::Display` for `Atom::Int(n)` writes `n` and the
10024        // variant's `got` slot carries the integer form unchanged. An
10025        // LSP that surfaces "you wrote `5` where a param list was
10026        // expected" gains the literal value as data, no source
10027        // re-parse required.
10028        let mut e = Expander::new();
10029        let err = e
10030            .expand_program(read("(defmacro f 5 body)").unwrap())
10031            .expect_err("defmacro non-list params gate must error");
10032        let (head, got) = defmacro_non_list_params_fields(&err);
10033        assert_eq!(head, MacroDefHead::Defmacro);
10034        assert_eq!(got, "5");
10035    }
10036
10037    #[test]
10038    fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
10039        // `(defmacro f :foo body)` — list[2] is the keyword `:foo`,
10040        // not a list. Pins that `Sexp::Display` for `Atom::Keyword(s)`
10041        // writes `:s` and the variant's `got` slot carries the
10042        // keyword form unchanged.
10043        let mut e = Expander::new();
10044        let err = e
10045            .expand_program(read("(defmacro f :foo body)").unwrap())
10046            .expect_err("defmacro non-list params gate must error");
10047        let (head, got) = defmacro_non_list_params_fields(&err);
10048        assert_eq!(head, MacroDefHead::Defmacro);
10049        assert_eq!(got, ":foo");
10050    }
10051
10052    #[test]
10053    fn defmacro_non_list_params_with_string_emits_structural_variant() {
10054        // `(defmacro f "params" body)` — list[2] is the string literal
10055        // `"params"`, not a list. Pins that `Sexp::Display` for
10056        // `Atom::String(s)` writes `"s"` (with quotes) and the
10057        // variant's `got` slot carries the quoted form unchanged.
10058        let mut e = Expander::new();
10059        let err = e
10060            .expand_program(read("(defmacro f \"params\" body)").unwrap())
10061            .expect_err("defmacro non-list params gate must error");
10062        let (head, got) = defmacro_non_list_params_fields(&err);
10063        assert_eq!(head, MacroDefHead::Defmacro);
10064        assert_eq!(got, "\"params\"");
10065    }
10066
10067    #[test]
10068    fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
10069        // `defpoint-template` shares `macro_def_from` with `defmacro`
10070        // (all three head keywords route through the same gate).
10071        // Pins that the lift fires path-uniformly across the three
10072        // head keywords AND that the variant's `head` slot carries
10073        // the actual head literal — `defpoint-template`, not
10074        // `defmacro` — so an LSP that wants to point at "your
10075        // defpoint-template form's param-list slot isn't a list"
10076        // gains the head as data.
10077        let mut e = Expander::new();
10078        let err = e
10079            .expand_program(read("(defpoint-template t x body)").unwrap())
10080            .expect_err("defpoint-template non-list params gate must error");
10081        let (head, got) = defmacro_non_list_params_fields(&err);
10082        assert_eq!(head, MacroDefHead::DefpointTemplate);
10083        assert_eq!(got, "x");
10084    }
10085
10086    #[test]
10087    fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
10088        // Sibling for the `defcheck` head — third head keyword
10089        // `macro_def_from` recognizes. Rounds out the three-head-
10090        // keyword coverage so the lift is path-uniform across
10091        // `defmacro` / `defpoint-template` / `defcheck`.
10092        let mut e = Expander::new();
10093        let err = e
10094            .expand_program(read("(defcheck c 7 body)").unwrap())
10095            .expect_err("defcheck non-list params gate must error");
10096        let (head, got) = defmacro_non_list_params_fields(&err);
10097        assert_eq!(head, MacroDefHead::Defcheck);
10098        assert_eq!(got, "7");
10099    }
10100
10101    #[test]
10102    fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
10103        // Path-uniform rejection: the SAME source emits the SAME
10104        // structural variant under both expansion strategies. The
10105        // param-list gate fires inside `macro_def_from` BEFORE either
10106        // expansion strategy runs, so the gate is naturally path-
10107        // uniform; pinning it gives a regression that drifts either
10108        // strategy's handling of non-list-params defmacros (or makes
10109        // one strategy accept what the other rejects) a fail-before/
10110        // pass-after edge. Sibling of
10111        // `defmacro_arity_substitute_and_bytecode_paths_agree`,
10112        // `defmacro_non_symbol_name_substitute_and_bytecode_paths_agree`,
10113        // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
10114        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
10115        let src = "(defmacro f x body)";
10116        let mut subst = Expander::new_substitute_only();
10117        let mut bytecode = Expander::new();
10118        let err_subst = subst
10119            .expand_program(read(src).unwrap())
10120            .expect_err("substitute must error");
10121        let err_byte = bytecode
10122            .expand_program(read(src).unwrap())
10123            .expect_err("bytecode must error");
10124        assert_eq!(
10125            defmacro_non_list_params_fields(&err_subst),
10126            (MacroDefHead::Defmacro, "x")
10127        );
10128        assert_eq!(
10129            defmacro_non_list_params_fields(&err_byte),
10130            (MacroDefHead::Defmacro, "x")
10131        );
10132    }
10133
10134    #[test]
10135    fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
10136        // End-to-end through Display — pins the rendered diagnostic
10137        // consumers see today (REPL, `tatara-check`) AND the new
10138        // `, got {got}` clause. The legacy `"expected param list"`
10139        // substring rides through verbatim; the prefix matches the
10140        // legacy `Compile { form: "defmacro", message: "expected
10141        // param list" }` byte-for-byte. Tools that pattern-match on
10142        // the variant gain structural binding to `head` / `got`.
10143        let mut e = Expander::new();
10144        let err = e
10145            .expand_program(read("(defmacro f x body)").unwrap())
10146            .expect_err("defmacro non-list params gate must error");
10147        assert_eq!(
10148            format!("{err}"),
10149            "compile error in defmacro: expected param list, got x"
10150        );
10151    }
10152
10153    #[test]
10154    fn defmacro_non_list_params_position_is_none_today() {
10155        // Negative control for the future-spans move: until `Sexp`
10156        // carries source positions, `position()` on `LispError`
10157        // returns `None` for this variant. A future run that gives
10158        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
10159        // this test gives that change a deliberate fail-before/pass-
10160        // after delta. Parallel to
10161        // `defmacro_arity_position_is_none_today`,
10162        // `defmacro_non_symbol_name_position_is_none_today`,
10163        // `non_symbol_param_position_is_none_today`, and
10164        // `rest_param_missing_name_position_is_none_today`.
10165        let mut e = Expander::new();
10166        let err = e
10167            .expand_program(read("(defmacro f x body)").unwrap())
10168            .expect_err("defmacro non-list params gate must error");
10169        assert_eq!(err.position(), None);
10170    }
10171
10172    #[test]
10173    fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
10174        // Negative control: a defmacro whose param-list slot IS a
10175        // list passes the param-list gate. Pins that the lift is
10176        // scoped to the non-list-params case, not to every defmacro
10177        // form. After this test, a regression that tightens the gate
10178        // to reject e.g. empty param lists fails-loudly here.
10179        let mut e = Expander::new();
10180        let out = e
10181            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10182            .expect("well-formed defmacro must succeed");
10183        assert_eq!(out[0], Sexp::int(42));
10184    }
10185
10186    #[test]
10187    fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
10188        // Pins the gate ordering: a 4-element defmacro whose name
10189        // slot IS a symbol but whose param-list slot is non-list
10190        // fires `DefmacroNonListParams`, NOT `DefmacroNonSymbolName`.
10191        // The name-symbol gate admits this form; the param-list gate
10192        // is the next checkpoint. A regression that swaps the gate
10193        // ordering (e.g. checks param-list before name-symbol, so
10194        // `(defmacro 5 x body)` would emit `DefmacroNonListParams`
10195        // instead of `DefmacroNonSymbolName`) fails-loudly here.
10196        let mut e = Expander::new();
10197        let err = e
10198            .expand_program(read("(defmacro f x body)").unwrap())
10199            .expect_err("param-list gate must error");
10200        assert!(
10201            matches!(err, LispError::DefmacroNonListParams { .. }),
10202            "expected DefmacroNonListParams, got: {err:?}"
10203        );
10204
10205        let err_name = e
10206            .expand_program(read("(defmacro 5 x body)").unwrap())
10207            .expect_err("name-symbol gate must error");
10208        assert!(
10209            matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
10210            "expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
10211             got: {err_name:?}"
10212        );
10213    }
10214
10215    #[test]
10216    fn defmacro_non_list_params_fires_after_arity_gate_passes() {
10217        // Pins the full gate ordering: a 4-element defmacro whose
10218        // first three slots are head/symbol/non-list fires
10219        // `DefmacroNonListParams`, NOT `DefmacroArity`. The arity
10220        // gate (>= 4 elements) admits this form; the name-symbol
10221        // gate admits the symbol; the param-list gate is the third
10222        // checkpoint. A regression that drifts the gate sequence
10223        // (e.g. fires `DefmacroArity` for a 4-element form) fails-
10224        // loudly here. Parallel to
10225        // `defmacro_non_symbol_name_fires_after_arity_gate_passes`
10226        // — together they pin the full
10227        // arity → name-symbol → param-list ordering inside
10228        // `macro_def_from`.
10229        let mut e = Expander::new();
10230        let err = e
10231            .expand_program(read("(defmacro f x body)").unwrap())
10232            .expect_err("param-list gate must error");
10233        assert!(
10234            matches!(err, LispError::DefmacroNonListParams { .. }),
10235            "expected DefmacroNonListParams, got: {err:?}"
10236        );
10237
10238        let err_arity = e
10239            .expand_program(read("(defmacro f x)").unwrap())
10240            .expect_err("arity gate must error");
10241        assert!(
10242            matches!(err_arity, LispError::DefmacroArity { .. }),
10243            "expected DefmacroArity (arity < 4 short-circuits before param-list check), \
10244             got: {err_arity:?}"
10245        );
10246    }
10247
10248    #[test]
10249    fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
10250        // Negative control: `&rest` is a symbol (`Atom::Symbol("&rest")`)
10251        // at the parser level, so `as_symbol()` succeeds for it. The
10252        // `NonSymbolParam` variant does NOT fire on the `&rest` marker
10253        // itself; the dedicated `&rest needs a name` rejection (a
10254        // separate failure mode in this cluster) handles malformed
10255        // rest-param shapes. Pins that the lift is scoped to
10256        // non-symbol elements at param-list positions, not to
10257        // every malformed-param shape.
10258        let mut e = Expander::new();
10259        let out = e
10260            .expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
10261            .expect("&rest with name must succeed");
10262        assert_eq!(out[0], parse("(list 1 2 3)"));
10263    }
10264
10265    #[test]
10266    fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
10267        // End-to-end through the Display impl — pins the rendered diagnostic
10268        // a downstream tool sees today (REPL, tatara-check). The shape is
10269        // parallel to the existing `TypeMismatch` variant: form, expected
10270        // shape, offending literal — all three slots present.
10271        let mut e = Expander::new();
10272        let err = e
10273            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
10274            .expect_err("non-symbol target must error");
10275        assert_eq!(
10276            format!("{err}"),
10277            "compile error in ,: expected symbol, got (list 1 2)"
10278        );
10279    }
10280
10281    // ── template_invariant_violation: structural lift ───────────────
10282    //
10283    // The four byte-identical inline `LispError::Compile { form:
10284    // macro_name.into(), message: <invariant> }` triples in `apply_compiled`
10285    // (Subst-bad-index, Splice-bad-index, EndList-empty-stack,
10286    // final-no-value gates) were lifted to `template_invariant_violation`,
10287    // and the helper's emission was promoted from `LispError::Compile`-
10288    // shape to the structural `LispError::TemplateInvariant { macro_name,
10289    // kind: TemplateInvariantKind }` variant. The index payload of the
10290    // Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
10291    // / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
10292    // kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
10293    // is structurally unrepresentable. Display matches the legacy
10294    // `Compile`-shaped diagnostic byte-for-byte via the closed-set
10295    // `TemplateInvariantKind::message()` projection so authoring-tool
10296    // substring greps see no drift across the lift.
10297    //
10298    // The tests below pin: (a) the helper produces the structural
10299    // `LispError::TemplateInvariant` variant with `macro_name` and `kind`
10300    // first-class; (b) the Subst / Splice gates thread the bad index
10301    // through the typed variants `SubstBadIndex(usize)` / `SpliceBadIndex(usize)`
10302    // unchanged; (c) the two REACHABLE invariant-violation paths through
10303    // `apply_compiled` — Subst with out-of-bounds idx, Splice with
10304    // out-of-bounds idx — route through the helper end-to-end (the
10305    // EndList / no-value paths are guarded by `last_mut().unwrap()`
10306    // ahead of `pop().ok_or_else()` and are not reachable through any
10307    // single CompiledTemplate; they remain defensive against future
10308    // changes to the stack discipline); (d) the legacy Display
10309    // rendering matches byte-for-byte across the lift; (e) positive
10310    // controls: a well-formed CompiledTemplate routes PAST the helper
10311    // cleanly, and unrelated macro errors (missing-required-arg) do
10312    // NOT route through the helper.
10313
10314    #[test]
10315    fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
10316        // Direct unit test of the helper: a fixed macro_name and a
10317        // `TemplateInvariantKind` produce a `LispError::TemplateInvariant`
10318        // variant with the macro_name in the `macro_name` slot and the
10319        // kind passed through verbatim in the `kind` slot. A regression
10320        // that drifts the variant (e.g., back to `LispError::Compile`)
10321        // or swaps the slot positions fails-loudly here.
10322        let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
10323        match err {
10324            LispError::TemplateInvariant { macro_name, kind } => {
10325                assert_eq!(macro_name, "test-macro");
10326                assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10327            }
10328            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10329        }
10330    }
10331
10332    #[test]
10333    fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
10334        // The Subst gate's `usize` idx lives INSIDE the
10335        // `TemplateInvariantKind::SubstBadIndex(usize)` variant rather
10336        // than being substring-rendered into a free-form `message`
10337        // slot. Pin that the helper threads the bad index through the
10338        // typed variant unchanged; a regression that drops the index
10339        // payload (e.g., via a `usize -> ()` projection) fails here.
10340        let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
10341        match err {
10342            LispError::TemplateInvariant { macro_name, kind } => {
10343                assert_eq!(macro_name, "wrap");
10344                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10345            }
10346            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10347        }
10348    }
10349
10350    #[test]
10351    fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
10352        // Hand-crafted CompiledTemplate with a Subst(99) op against
10353        // an empty params list: `args_by_index` has length 0, so
10354        // `.get(99)` returns None and the `ok_or_else` triggers
10355        // through the helper. Fail-before-pass-after: this same input
10356        // pre-lift went through `LispError::Compile { form: macro_name,
10357        // message: format!("compiled template referenced bad param
10358        // index {idx}") }`; post-lift it routes through
10359        // `template_invariant_violation` and emits the structural
10360        // `TemplateInvariant { macro_name, kind: SubstBadIndex(99) }`
10361        // variant with the bad index threaded through as typed data.
10362        let tmpl = CompiledTemplate {
10363            ops: vec![TemplateOp::Subst(99)],
10364        };
10365        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10366            .expect_err("bad idx must error");
10367        match err {
10368            LispError::TemplateInvariant { macro_name, kind } => {
10369                assert_eq!(macro_name, "test-macro");
10370                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10371            }
10372            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10373        }
10374    }
10375
10376    #[test]
10377    fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
10378        // Hand-crafted CompiledTemplate with a Splice(42) op against
10379        // an empty params list. Sibling of the Subst-bad-idx test;
10380        // pins the Splice gate routes through the helper with the
10381        // typed `SpliceBadIndex(42)` kind carrying the bad index.
10382        let tmpl = CompiledTemplate {
10383            ops: vec![TemplateOp::Splice(42)],
10384        };
10385        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10386            .expect_err("bad splice idx must error");
10387        match err {
10388            LispError::TemplateInvariant { macro_name, kind } => {
10389                assert_eq!(macro_name, "call-macro");
10390                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10391            }
10392            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10393        }
10394    }
10395
10396    #[test]
10397    fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
10398        // End-to-end through the `LispError` Display impl — pins the
10399        // rendered diagnostic byte-for-byte: `"compile error in
10400        // test-macro: compiled template referenced bad param index 99"`.
10401        // Authoring tools that substring-grep the rendered diagnostic
10402        // (`tatara-check`'s diagnostic capture, REPL substring-greps)
10403        // see no drift across the lift. Parallel to how
10404        // `compile_named_non_symbol_name_renders_legacy_compile_shape`
10405        // pins the sibling-file (compile.rs) lift's Display contract.
10406        let tmpl = CompiledTemplate {
10407            ops: vec![TemplateOp::Subst(99)],
10408        };
10409        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10410            .expect_err("bad idx must error");
10411        assert_eq!(
10412            format!("{err}"),
10413            "compile error in test-macro: compiled template referenced bad param index 99"
10414        );
10415    }
10416
10417    #[test]
10418    fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
10419        // Sibling Display test for the Splice gate. Pins the message
10420        // byte-for-byte through the `LispError` Display impl: `"compile
10421        // error in call-macro: compiled template referenced bad splice
10422        // index 42"`.
10423        let tmpl = CompiledTemplate {
10424            ops: vec![TemplateOp::Splice(42)],
10425        };
10426        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10427            .expect_err("bad splice idx must error");
10428        assert_eq!(
10429            format!("{err}"),
10430            "compile error in call-macro: compiled template referenced bad splice index 42"
10431        );
10432    }
10433
10434    #[test]
10435    fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
10436        // Positive control: a CompiledTemplate produced by the
10437        // bytecode compiler (`compile_template`) for a well-formed
10438        // macro never references an out-of-bounds index nor
10439        // unbalances the stack, so `apply_compiled` routes PAST the
10440        // helper cleanly. A regression that fires the helper on
10441        // well-formed bytecode (e.g., off-by-one in the index
10442        // resolution) would fail here. End-to-end through the public
10443        // `Expander` surface so the test exercises the same code
10444        // path users see.
10445        let mut e = Expander::new();
10446        let out = e
10447            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10448            .expect("well-formed macro expansion must not fire template-invariant-violation");
10449        assert_eq!(out.len(), 1);
10450        assert_eq!(out[0], Sexp::int(42));
10451    }
10452
10453    #[test]
10454    fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
10455        // Negative control: the `missing_macro_arg` gate in the shared
10456        // positional binder (`MacroParams::bind`) fires BEFORE the bytecode
10457        // loop runs,
10458        // so a missing required arg routes through
10459        // `LispError::MissingMacroArg`, NOT through
10460        // `template_invariant_violation`. Pins the helper is
10461        // precisely scoped to bytecode-runtime invariant violations
10462        // (Subst / Splice / stack gates), not to macro-call arity
10463        // errors (the latter has its own structural variant). A
10464        // regression that conflates the two gate clusters would
10465        // route this case through `Compile { ... }` instead of
10466        // `MissingMacroArg` and fail-loudly here.
10467        let mut e = Expander::new();
10468        let err = e
10469            .expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
10470            .expect_err("missing required arg must error");
10471        assert!(
10472            matches!(err, LispError::MissingMacroArg { .. }),
10473            "expected MissingMacroArg, got: {err:?}"
10474        );
10475    }
10476
10477    // ── resolve_bound_arg: bytecode-runtime bound-arg-by-index lookup ──
10478    //
10479    // `resolve_bound_arg(args_by_index, idx, macro_name, kind)` lifts the
10480    // `args_by_index.get(*idx).ok_or_else(|| template_invariant_violation(
10481    // macro_name, KIND(*idx)))?` projection that recurred at BOTH the
10482    // `TemplateOp::Subst` and `TemplateOp::Splice` arms inside
10483    // `apply_compiled`. The arms differ in the kind constructor
10484    // (`SubstBadIndex` vs. `SpliceBadIndex`) and in their post-lookup
10485    // verb (clone+push vs. splice-coerce), but the lookup-and-reject
10486    // prelude is byte-identical modulo the constructor. These tests
10487    // pin the lifted helper's contract directly; the existing
10488    // `apply_compiled_*_bad_idx_*` tests are the path-uniformity
10489    // guards proving both production arms route through it without
10490    // behavior drift.
10491
10492    #[test]
10493    fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
10494        // For an in-range index, the helper returns `Ok(&args[idx])`
10495        // borrowed VERBATIM — same pointer as `args_by_index.get(idx)`.
10496        // Pins the borrow-not-clone contract: a regression that drifts
10497        // the helper to clone+return (`Result<Sexp>` instead of
10498        // `Result<&Sexp>`) would allocate per lookup at the production
10499        // `Subst`/`Splice` hot path. The kind constructor must NOT
10500        // fire on the success path (`FnOnce`'s lazy semantics) — pin
10501        // that the test passes a constructor that would panic if
10502        // called, asserting the helper short-circuits before invoking
10503        // it on the in-range arm.
10504        let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10505        let got = resolve_bound_arg(&args, 1, "m", |_| {
10506            panic!("kind constructor must not fire on the in-range path")
10507        })
10508        .expect("in-range lookup must succeed");
10509        assert!(
10510            std::ptr::eq(got, &args[1]),
10511            "resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
10512        );
10513        assert_eq!(*got, Sexp::int(2));
10514    }
10515
10516    #[test]
10517    fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
10518        // For an out-of-range index, the helper raises the structural
10519        // `LispError::TemplateInvariant` variant with the caller-
10520        // supplied `SubstBadIndex` kind constructor applied to the bad
10521        // index. Pins the post-lift emission shape (variant identity
10522        // + the kind constructor threaded with the actual idx); a
10523        // regression that drops the idx payload (e.g., via a `usize ->
10524        // ()` projection) or hard-codes a different kind at the helper
10525        // boundary fails-loudly here. Fail-before-pass-after: this
10526        // assert is contradicted by the pre-lift code path (which
10527        // never called `resolve_bound_arg` because it didn't exist),
10528        // ratifies the post-lift one.
10529        let args: Vec<Sexp> = Vec::new();
10530        let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
10531            .expect_err("out-of-range lookup must error");
10532        match err {
10533            LispError::TemplateInvariant { macro_name, kind } => {
10534                assert_eq!(macro_name, "test-macro");
10535                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10536            }
10537            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10538        }
10539    }
10540
10541    #[test]
10542    fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
10543        // Path-uniformity for the per-call-site kind constructor: the
10544        // SAME out-of-range idx via the `SpliceBadIndex` constructor
10545        // emits `kind: SpliceBadIndex(7)` — distinct from the sibling
10546        // `SubstBadIndex(7)` variant. Pins that the constructor is
10547        // chosen per call site (not hard-coded at the helper boundary),
10548        // closing the structural matrix `{Subst, Splice} × {in-range,
10549        // out-of-range}` the two production arms span across the
10550        // bytecode-runtime's bound-arg-by-index reads. A regression
10551        // that hard-codes a single kind at the helper boundary would
10552        // emit the same variant identity for both call sites and
10553        // fail-loudly here.
10554        let args: Vec<Sexp> = Vec::new();
10555        let err = resolve_bound_arg(
10556            &args,
10557            7,
10558            "test-macro",
10559            TemplateInvariantKind::SpliceBadIndex,
10560        )
10561        .expect_err("out-of-range lookup must error");
10562        match err {
10563            LispError::TemplateInvariant { macro_name, kind } => {
10564                assert_eq!(macro_name, "test-macro");
10565                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
10566            }
10567            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10568        }
10569    }
10570
10571    #[test]
10572    fn resolve_bound_arg_threads_macro_name_verbatim() {
10573        // Path-uniformity for the `macro_name` slot: the helper threads
10574        // the caller's borrow into the variant's owned `String` slot
10575        // verbatim. Pin two distinct macro names route through with no
10576        // mutual interference — a regression that hard-codes a single
10577        // macro_name at the helper boundary or swaps the parameter
10578        // ordering fails-loudly here. Same posture as
10579        // `compiler_spec_io_err_threads_each_stage_through_unchanged`
10580        // pins the typed `stage` slot in the disk-persistence sibling
10581        // lift.
10582        let args: Vec<Sexp> = Vec::new();
10583        for name in ["wrap", "call-macro", "obs"] {
10584            let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
10585                .expect_err("out-of-range lookup must error");
10586            match err {
10587                LispError::TemplateInvariant { macro_name, kind } => {
10588                    assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
10589                    assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10590                }
10591                other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10592            }
10593        }
10594    }
10595
10596    #[test]
10597    fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
10598        // Edge case: idx 0 with a single-element args_by_index returns
10599        // `Ok(&args[0])`. Pins the lower-bound of the in-range surface
10600        // — a regression that off-by-ones the lookup (e.g., `get(idx +
10601        // 1)` or `get(idx).filter(|_| idx > 0)`) would fail here.
10602        // Sibling to the upper-bound `resolve_bound_arg_out_of_range_
10603        // with_subst_kind_emits_typed_invariant` test.
10604        let args = vec![Sexp::int(42)];
10605        let got = resolve_bound_arg(&args, 0, "m", |_| {
10606            panic!("kind constructor must not fire on the in-range path")
10607        })
10608        .expect("idx-0 lookup must succeed");
10609        assert!(std::ptr::eq(got, &args[0]));
10610        assert_eq!(*got, Sexp::int(42));
10611    }
10612
10613    #[test]
10614    fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
10615        // Edge case: idx `len - 1` is the highest valid index. Pin
10616        // that it routes through the success arm (NOT the error arm),
10617        // closing the in-range surface end-to-end with the lower-
10618        // bound sibling. A regression that off-by-ones the upper
10619        // bound (e.g., `get(idx).filter(|_| idx < args.len() - 1)`)
10620        // would fail here.
10621        let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10622        let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
10623            panic!("kind constructor must not fire on the in-range path")
10624        })
10625        .expect("last-element lookup must succeed");
10626        assert!(std::ptr::eq(got, args.last().unwrap()));
10627        assert_eq!(*got, Sexp::int(3));
10628    }
10629
10630    #[test]
10631    fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
10632        // Boundary case: idx EQUAL to `args.len()` is out-of-range
10633        // (since `get` is 0-indexed). Pin that this routes through
10634        // the error arm with the kind constructor applied to the
10635        // EXACT idx that was tried. A regression that off-by-ones
10636        // the boundary (e.g., admits `idx == len`) would fail here.
10637        // This is the canonical off-by-one trap; the helper's
10638        // contract pins it at the variant-construction boundary.
10639        let args = vec![Sexp::int(1)];
10640        let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
10641            .expect_err("idx == len must error");
10642        match err {
10643            LispError::TemplateInvariant { kind, .. } => {
10644                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
10645            }
10646            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10647        }
10648    }
10649
10650    #[test]
10651    fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
10652        // Boundary case: an empty `args_by_index` slice rejects every
10653        // idx (including 0). Pin that the helper's emission shape is
10654        // uniform regardless of which out-of-range idx fires the
10655        // rejection — `SubstBadIndex(0)` for an empty slice is the
10656        // bytecode-runtime mirror of a zero-arity macro template
10657        // referencing the 0-th param.
10658        let args: Vec<Sexp> = Vec::new();
10659        let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
10660            .expect_err("empty slice rejects every idx");
10661        match err {
10662            LispError::TemplateInvariant { macro_name, kind } => {
10663                assert_eq!(macro_name, "zero-arity");
10664                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10665            }
10666            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10667        }
10668    }
10669
10670    #[test]
10671    fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
10672        // End-to-end path-uniformity: a `Subst(99)` op against a
10673        // zero-arity macro routes the bytecode-runtime's bound-arg
10674        // lookup through `resolve_bound_arg` with the
10675        // `SubstBadIndex` constructor, emitting the structural
10676        // variant with `kind: SubstBadIndex(99)`. The pre-lift
10677        // sibling test `apply_compiled_subst_bad_idx_routes_through_
10678        // template_invariant_violation` pins that the same input
10679        // routes through `template_invariant_violation`; this test
10680        // pins that BOTH still hold under the post-lift composition
10681        // — `resolve_bound_arg` calls `template_invariant_violation`
10682        // internally on the rejection arm. A regression that drifts
10683        // ONE arm's projection from the other (e.g., swaps the
10684        // constructor at one call site, or short-circuits the
10685        // composition) would fail here.
10686        let tmpl = CompiledTemplate {
10687            ops: vec![TemplateOp::Subst(99)],
10688        };
10689        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10690            .expect_err("bad idx must error");
10691        match err {
10692            LispError::TemplateInvariant { macro_name, kind } => {
10693                assert_eq!(macro_name, "test-macro");
10694                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10695            }
10696            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10697        }
10698    }
10699
10700    #[test]
10701    fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
10702        // Sibling end-to-end path-uniformity for the `Splice` arm:
10703        // the post-lift composition routes a `Splice(42)` op through
10704        // `resolve_bound_arg` with the `SpliceBadIndex` constructor,
10705        // emitting `kind: SpliceBadIndex(42)`. Together with the
10706        // `Subst` sibling test above, this pins the structural matrix
10707        // `{Subst, Splice} × resolve_bound_arg` end-to-end through
10708        // the public `apply_compiled` surface, so a regression that
10709        // drifts ONE arm's kind constructor (e.g., the `Splice` arm
10710        // accidentally emits `SubstBadIndex` after a copy-paste
10711        // refactor) fails-loudly here.
10712        let tmpl = CompiledTemplate {
10713            ops: vec![TemplateOp::Splice(42)],
10714        };
10715        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10716            .expect_err("bad splice idx must error");
10717        match err {
10718            LispError::TemplateInvariant { macro_name, kind } => {
10719                assert_eq!(macro_name, "call-macro");
10720                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10721            }
10722            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10723        }
10724    }
10725
10726    #[test]
10727    fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
10728        // Positive control: a `Subst(0)` op against a one-arg macro
10729        // routes through `resolve_bound_arg`'s success arm and the
10730        // `Subst` post-lookup verb (clone + push) emits the bound
10731        // value verbatim. Pin the post-lift composition's success
10732        // path: the clone-and-push semantics live at the call site
10733        // (NOT in `resolve_bound_arg`, which only borrows), and a
10734        // regression that drifts the borrow contract (e.g., the
10735        // helper clones internally + the call site clones again)
10736        // would still pass observationally but would regress the
10737        // hot-path allocation count.
10738        let params = MacroParams {
10739            required: vec!["x".into()],
10740            optional: Vec::new(),
10741            rest: None,
10742        };
10743        let tmpl = CompiledTemplate {
10744            ops: vec![TemplateOp::Subst(0)],
10745        };
10746        let out = apply_compiled("id", &params, &tmpl, &[Sexp::int(42)])
10747            .expect("in-range Subst must succeed");
10748        assert_eq!(out, Sexp::int(42));
10749    }
10750
10751    // ── current_builder_mut: the bytecode-runtime top-of-stack projection ──
10752    //
10753    // `current_builder_mut(stack)` lifts the `stack.last_mut().unwrap()`
10754    // projection that appeared at FOUR sites inside `apply_compiled`'s
10755    // op-loop (Literal, Subst, Splice, post-EndList parent-fold) into ONE
10756    // named primitive. The expect message names the bytecode-runtime
10757    // invariant ("at least one stack frame during op-loop") so a
10758    // regression that drifts the loop's frame management (a new op that
10759    // pops without pushing, an early-return that bypasses EndList's
10760    // stack-check) surfaces a NAMED panic rather than a silent unwrap.
10761    // These tests pin the projection's contract directly; the existing
10762    // `apply_compiled_*` tests + the cross-strategy `expansion_layers_
10763    // agree_on_output_and_cache_wins` benchmark are the path-uniformity
10764    // guards proving the four sites still emit the canonical bytecode-
10765    // runtime output across the lift.
10766
10767    #[test]
10768    fn current_builder_mut_returns_the_top_frame_reference() {
10769        // The simplest projection: on a single-frame stack, the helper
10770        // returns a `&mut Vec<Sexp>` pointing at THAT frame. Pin the
10771        // projection's identity end-to-end via a push that mutates
10772        // through the borrow and observe the original frame carries the
10773        // pushed value back.
10774        let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
10775        current_builder_mut(&mut stack).push(Sexp::int(42));
10776        assert_eq!(stack.len(), 1);
10777        assert_eq!(stack[0], vec![Sexp::int(42)]);
10778    }
10779
10780    #[test]
10781    fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
10782        // The projection MUST target the topmost frame, not the bottom
10783        // one — every `TemplateOp::BeginList` pushes a fresh frame the
10784        // subsequent ops emit into, and a regression that flipped the
10785        // projection to `first_mut` (or to a fixed bottom-frame
10786        // reference) would silently smear all op output into the
10787        // outermost result. Pin path-uniformity with the bytecode-
10788        // runtime's mid-list emission posture: with three frames on
10789        // the stack (one outer + two pending lists), the helper
10790        // returns a borrow into the third frame, leaving frames 0 and
10791        // 1 untouched.
10792        let mut stack: Vec<Vec<Sexp>> = vec![
10793            vec![Sexp::symbol("outer")],
10794            vec![Sexp::symbol("inner-a")],
10795            vec![Sexp::symbol("inner-b")],
10796        ];
10797        current_builder_mut(&mut stack).push(Sexp::int(99));
10798        assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10799        assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
10800        assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
10801    }
10802
10803    #[test]
10804    fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
10805        // Structural identity binding the lift to its pre-lift inline
10806        // shape: `current_builder_mut(&mut stack)` IS
10807        // `stack.last_mut().unwrap()` — the same `&mut Vec<Sexp>`,
10808        // pointing at the same allocation. Pin pointer equality via
10809        // `std::ptr::eq` on the projected slice's `as_ptr()` to rule
10810        // out any allocation-shape drift across the lift.
10811        let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
10812        let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
10813        let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
10814        assert!(
10815            std::ptr::eq(via_lift_ptr, via_inline_ptr),
10816            "current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
10817        );
10818    }
10819
10820    #[test]
10821    #[should_panic(
10822        expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
10823    )]
10824    fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
10825        // The bytecode-runtime invariant is encoded in the expect
10826        // message: an empty stack at the projection boundary is
10827        // structurally unreachable inside `apply_compiled`'s op-loop
10828        // (the outermost frame is seeded at entry and every BeginList
10829        // / EndList pair preserves the count >= 1). Pin that the
10830        // NAMED invariant fires on the failure path so a regression
10831        // that drifts the loop's frame management surfaces a
10832        // diagnostic-grade panic rather than a silent unwrap over
10833        // `None`. Authoring tools / future debug-mode hooks can
10834        // pattern-match on the named invariant string instead of
10835        // tracking down an unnamed unwrap site.
10836        let mut empty: Vec<Vec<Sexp>> = Vec::new();
10837        let _ = current_builder_mut(&mut empty);
10838    }
10839
10840    #[test]
10841    fn current_builder_mut_routes_apply_compiled_literal_emit() {
10842        // End-to-end path-uniformity guard: a single-op program
10843        // `TemplateOp::Literal(s)` routes its push through
10844        // `current_builder_mut(&mut stack)` and the literal lands in
10845        // the outermost frame. After the op-loop completes the outer
10846        // `stack.pop().FinalNoValue` gate sees a non-empty top frame
10847        // containing exactly one element, which `apply_compiled`'s
10848        // tail (`top.len() == 1 { top.remove(0) }`) projects back as
10849        // the bound value. Pre-lift the same emission ran through
10850        // `stack.last_mut().unwrap().push(s.clone())`; post-lift it
10851        // runs through `current_builder_mut(&mut stack).push(s.clone())`
10852        // — the byte-identical outcome pins that the Literal arm's
10853        // routing through the new projection preserves the bytecode-
10854        // runtime's emission shape.
10855        let tmpl = CompiledTemplate {
10856            ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
10857        };
10858        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10859            .expect("literal-only template must succeed");
10860        assert_eq!(out, Sexp::symbol("hello"));
10861    }
10862
10863    #[test]
10864    fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
10865        // End-to-end path-uniformity guard for the post-EndList
10866        // parent-fold push: `(BeginList, Literal(a), Literal(b),
10867        // EndList)` builds an inner frame `[a, b]`, pops it on
10868        // EndList, then pushes `Sexp::List([a, b])` into the parent
10869        // (outer) frame via `current_builder_mut`. The outermost
10870        // `stack.pop()` then surfaces that list as the bound result.
10871        // Pre-lift the parent-fold push ran through
10872        // `stack.last_mut().unwrap().push(Sexp::List(items))`; post-
10873        // lift it runs through `current_builder_mut(&mut stack).
10874        // push(Sexp::List(items))` — pin the byte-identical outcome
10875        // so a regression that drifts the parent-fold target (e.g.,
10876        // pushes onto the just-popped frame's pointer instead of the
10877        // new top) fails loudly here.
10878        let tmpl = CompiledTemplate {
10879            ops: vec![
10880                TemplateOp::BeginList,
10881                TemplateOp::Literal(Sexp::symbol("a")),
10882                TemplateOp::Literal(Sexp::symbol("b")),
10883                TemplateOp::EndList,
10884            ],
10885        };
10886        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10887            .expect("BeginList/EndList template must succeed");
10888        assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
10889    }
10890
10891    #[test]
10892    fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
10893        // End-to-end path-uniformity guard for BOTH index-reading
10894        // arms routing through the lifted projection: a one-required +
10895        // one-rest macro `(call f &rest args)` with template
10896        // `(BeginList, Subst(0), Splice(1), EndList)` exercises both
10897        // Subst's clone-and-push AND Splice's splice-value-into
10898        // emit-paths against the current builder via
10899        // `current_builder_mut(&mut stack)`. The composed result is
10900        // `(foo 1 2 3)` — Subst lands the bound `f = foo` and
10901        // Splice flattens `args = (1 2 3)` — and the byte-identical
10902        // outcome pins that BOTH Subst and Splice arms' emits route
10903        // through the SHARED projection. Sibling to
10904        // `apply_compiled_splice_in_range_routes_past_resolve_bound
10905        // _arg_into_splice_value_into` which already exercises this
10906        // shape end-to-end; the addition here is the path-uniformity
10907        // anchor for the `current_builder_mut` lift specifically.
10908        let params = MacroParams {
10909            required: vec!["f".into()],
10910            optional: Vec::new(),
10911            rest: Some("args".into()),
10912        };
10913        let tmpl = CompiledTemplate {
10914            ops: vec![
10915                TemplateOp::BeginList,
10916                TemplateOp::Subst(0),
10917                TemplateOp::Splice(1),
10918                TemplateOp::EndList,
10919            ],
10920        };
10921        let out = apply_compiled(
10922            "call",
10923            &params,
10924            &tmpl,
10925            &[
10926                Sexp::symbol("foo"),
10927                Sexp::int(1),
10928                Sexp::int(2),
10929                Sexp::int(3),
10930            ],
10931        )
10932        .expect("Subst + Splice template must succeed");
10933        assert_eq!(
10934            out,
10935            Sexp::List(vec![
10936                Sexp::symbol("foo"),
10937                Sexp::int(1),
10938                Sexp::int(2),
10939                Sexp::int(3),
10940            ])
10941        );
10942    }
10943
10944    #[test]
10945    fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
10946        // Positive control for the `Splice` arm: a `&rest` macro that
10947        // splices a bound list routes through `resolve_bound_arg`'s
10948        // success arm and the `Splice` post-lookup verb
10949        // (`splice_value_into`) flattens the bound list into the
10950        // builder. Pin the composition's success path end-to-end:
10951        // the bound `Sexp::List([1, 2, 3])` at idx 1 flattens into
10952        // the outer builder's `(call 1 2 3)` shape — the same output
10953        // `rest_param_splices_with_at` pins through the public
10954        // surface, here pinned with the bytecode-runtime composition
10955        // exposed directly.
10956        let params = MacroParams {
10957            required: vec!["f".into()],
10958            optional: Vec::new(),
10959            rest: Some("args".into()),
10960        };
10961        let tmpl = CompiledTemplate {
10962            ops: vec![
10963                TemplateOp::BeginList,
10964                TemplateOp::Subst(0),
10965                TemplateOp::Splice(1),
10966                TemplateOp::EndList,
10967            ],
10968        };
10969        let out = apply_compiled(
10970            "call",
10971            &params,
10972            &tmpl,
10973            &[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
10974        )
10975        .expect("in-range Splice must succeed");
10976        assert_eq!(
10977            out,
10978            Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
10979        );
10980    }
10981
10982    // ── pop_builder_frame: the bytecode-runtime stack-frame consume ─────
10983    //
10984    // `pop_builder_frame(stack, macro_name, kind)` lifts the
10985    // `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
10986    // kind))?` chain that recurred at two sites inside `apply_compiled`
10987    // (the `EndList` arm + the post-loop final pop) into ONE named
10988    // primitive on the bytecode-runtime's stack-frame algebra. Sibling of
10989    // `current_builder_mut` (the top-frame-borrow projection — the same
10990    // `&mut Vec<Vec<Sexp>>` consumed at the borrow face) and
10991    // `resolve_bound_arg` (the bound-arg-by-index lookup primitive that
10992    // also routes through `TemplateInvariantKind` as the per-call-site
10993    // rejection identity). These tests pin the primitive's contract
10994    // directly; the existing `apply_compiled_*` tests are the path-
10995    // uniformity guards proving the two sites route through it without
10996    // behavior drift.
10997
10998    #[test]
10999    fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
11000        // Happy path: a two-frame stack pops the topmost frame off and
11001        // returns it AS-IS while shrinking the stack by exactly one
11002        // element. Pin both the return value (the popped frame's
11003        // contents are byte-identical to what was pushed) AND the
11004        // mutation (`stack.len()` drops from 2 to 1).
11005        let mut stack: Vec<Vec<Sexp>> = vec![
11006            vec![Sexp::symbol("outer")],
11007            vec![Sexp::int(1), Sexp::int(2)],
11008        ];
11009        let popped =
11010            pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
11011                .expect("non-empty stack must pop cleanly");
11012        assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
11013        assert_eq!(stack.len(), 1);
11014        assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
11015    }
11016
11017    #[test]
11018    fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
11019        // Empty-stack rejection: an empty stack flows through the
11020        // `EndListEmptyStack` kind constructor into a structural
11021        // `LispError::TemplateInvariant { macro_name, kind:
11022        // EndListEmptyStack }` variant. Fail-before-pass-after: pre-lift
11023        // the same input would route through the inline
11024        // `stack.pop().ok_or_else(|| template_invariant_violation(_,
11025        // EndListEmptyStack))?` chain at the `EndList` arm; post-lift
11026        // it routes through ONE named primitive both pop-emitting
11027        // sites share, and a regression that drops the kind threading
11028        // (e.g. unifies both kinds into one constant) fails here.
11029        let mut empty: Vec<Vec<Sexp>> = Vec::new();
11030        let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
11031            .expect_err("empty stack must reject");
11032        match err {
11033            LispError::TemplateInvariant { macro_name, kind } => {
11034                assert_eq!(macro_name, "wrap");
11035                assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
11036            }
11037            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11038        }
11039    }
11040
11041    #[test]
11042    fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
11043        // Path-uniformity across the closed-set of pop-emitting kinds:
11044        // the same primitive threads `FinalNoValue` verbatim through
11045        // its `TemplateInvariantKind` slot, with the macro_name
11046        // identity preserved across the call boundary. Sibling of the
11047        // `EndListEmptyStack` test above — together they pin the
11048        // primitive's closed-set posture (BOTH reachable
11049        // pop-emitting kinds route through the same primitive, neither
11050        // is hard-coded), so a future `TemplateInvariantKind` variant
11051        // added for a new pop-emitting op (e.g. a hypothetical
11052        // `EndManyEmptyStack`) extends the primitive's reachability
11053        // mechanically by passing the new kind through the same slot.
11054        let mut empty: Vec<Vec<Sexp>> = Vec::new();
11055        let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
11056            .expect_err("empty stack must reject");
11057        match err {
11058            LispError::TemplateInvariant { macro_name, kind } => {
11059                assert_eq!(macro_name, "id");
11060                assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
11061            }
11062            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11063        }
11064    }
11065
11066    #[test]
11067    fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
11068        // Closed-set posture check: even kinds the production
11069        // `apply_compiled` op-loop ROUTES through `resolve_bound_arg`
11070        // rather than `pop_builder_frame` (the indexed `SubstBadIndex(_)`
11071        // / `SpliceBadIndex(_)` siblings) MUST compose correctly with
11072        // this primitive's typed slot — they are NOT reachable here
11073        // from the production loop, but `TemplateInvariantKind` does
11074        // not distinguish "indexed" kinds from "stack-gate" kinds at
11075        // the helper's signature, so a regression that special-cases
11076        // one kind family would be a silent type-narrowing the closed-
11077        // set typed enum was lifted to prevent. Pin the universal
11078        // routing: ANY kind variant feeds through the primitive's
11079        // `kind` slot identically. Sibling assertion to
11080        // `template_invariant_violation_threads_subst_idx_through_typed_variant`
11081        // — same compose-the-kind-into-the-variant contract, one
11082        // composition step further down the substrate stack.
11083        let mut empty: Vec<Vec<Sexp>> = Vec::new();
11084        let err = pop_builder_frame(
11085            &mut empty,
11086            "compose",
11087            TemplateInvariantKind::SubstBadIndex(42),
11088        )
11089        .expect_err("empty stack must reject regardless of kind family");
11090        match err {
11091            LispError::TemplateInvariant { macro_name, kind } => {
11092                assert_eq!(macro_name, "compose");
11093                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
11094            }
11095            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11096        }
11097    }
11098
11099    #[test]
11100    fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
11101        // Structural-identity binding the lift to its pre-lift inline
11102        // shape: `pop_builder_frame(stack, macro_name, kind)` IS
11103        // `stack.pop().ok_or_else(|| template_invariant_violation(
11104        // macro_name, kind))?` — both reachable arms (success +
11105        // failure) must produce byte-identical outcomes. The success
11106        // arm checks the popped Vec contents AND the post-pop stack
11107        // length match the inline path; the failure arm checks the
11108        // emitted variant's identity AND its `macro_name` / `kind`
11109        // slots match the inline path. A regression that drifts the
11110        // primitive's projection (e.g. a `stack.swap_remove(_)` typo,
11111        // or a kind-rewrite at the helper boundary) fails on at least
11112        // one of the two arms.
11113        // Success arm:
11114        let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11115        let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11116        let via_lift = pop_builder_frame(
11117            &mut stack_lift,
11118            "macro",
11119            TemplateInvariantKind::EndListEmptyStack,
11120        )
11121        .expect("non-empty stack pops cleanly through lift");
11122        let via_inline = stack_inline.pop().ok_or_else(|| {
11123            template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
11124        });
11125        assert_eq!(
11126            via_lift,
11127            via_inline.unwrap(),
11128            "popped frame must be byte-identical across lift vs inline"
11129        );
11130        assert_eq!(
11131            stack_lift.len(),
11132            stack_inline.len(),
11133            "post-pop stack length must be byte-identical across lift vs inline"
11134        );
11135        // Failure arm:
11136        let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
11137        let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
11138        let err_lift = pop_builder_frame(
11139            &mut empty_lift,
11140            "macro",
11141            TemplateInvariantKind::FinalNoValue,
11142        )
11143        .expect_err("empty stack rejects through lift");
11144        let err_inline = empty_inline
11145            .pop()
11146            .ok_or_else(|| {
11147                template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
11148            })
11149            .expect_err("empty stack rejects through inline");
11150        match (err_lift, err_inline) {
11151            (
11152                LispError::TemplateInvariant {
11153                    macro_name: m_lift,
11154                    kind: k_lift,
11155                },
11156                LispError::TemplateInvariant {
11157                    macro_name: m_inline,
11158                    kind: k_inline,
11159                },
11160            ) => {
11161                assert_eq!(m_lift, m_inline);
11162                assert_eq!(k_lift, k_inline);
11163            }
11164            (l, i) => panic!(
11165                "expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
11166            ),
11167        }
11168    }
11169
11170    #[test]
11171    fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
11172        // End-to-end path-uniformity guard for the `EndList` arm: a
11173        // `(BeginList, Literal(x), EndList)` program pushes a child
11174        // frame, populates it with one literal, then routes the child
11175        // frame OUT of the stack via `pop_builder_frame` (kind
11176        // `EndListEmptyStack` — unreachable on this valid input, but
11177        // the kind threads through the primitive identically). The
11178        // post-pop verb (`Sexp::List(items)` push into the parent via
11179        // `current_builder_mut`) yields the same `Sexp::List([x])`
11180        // shape the consumer projected pre-lift. Pin the byte-
11181        // identical outcome so a regression that drifts the EndList
11182        // arm's routing (e.g. swaps the kind constructor, or routes
11183        // through a different stack-mutating primitive) fails loudly
11184        // here. Sibling of
11185        // `current_builder_mut_routes_apply_compiled_end_list_parent_fold`
11186        // — that test pins the parent-fold PUSH; this test pins the
11187        // child-frame POP that immediately precedes it. Together the
11188        // two close the EndList arm's path-uniformity across the
11189        // pop-then-push composition.
11190        let tmpl = CompiledTemplate {
11191            ops: vec![
11192                TemplateOp::BeginList,
11193                TemplateOp::Literal(Sexp::symbol("only")),
11194                TemplateOp::EndList,
11195            ],
11196        };
11197        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11198            .expect("BeginList/EndList one-literal template must succeed");
11199        assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
11200    }
11201
11202    #[test]
11203    fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
11204        // End-to-end path-uniformity guard for the post-loop final
11205        // pop: a single-op `Literal(s)` program emits `s` into the
11206        // outermost (seed) frame, then the post-loop tail routes the
11207        // seed frame OUT via `pop_builder_frame` (kind `FinalNoValue`
11208        // — unreachable on this valid input). The post-pop arity gate
11209        // (`top.len() == 1 { top.remove(0) }`) projects the literal
11210        // back as the bound value. Pre-lift the same emission ran
11211        // through the inline `stack.pop().ok_or_else(|| template_
11212        // invariant_violation(_, FinalNoValue))?` chain at the
11213        // post-loop tail; post-lift it routes through ONE named
11214        // primitive the EndList arm ALSO routes through, and the
11215        // single-literal outcome is byte-identical across both code
11216        // paths. Sibling of
11217        // `current_builder_mut_routes_apply_compiled_literal_emit` —
11218        // that test pins the EMIT into the seed frame; this test pins
11219        // the POP of the same seed frame at the post-loop tail.
11220        // Together the two close the Literal-only program's path-
11221        // uniformity across the emit-then-consume composition.
11222        let tmpl = CompiledTemplate {
11223            ops: vec![TemplateOp::Literal(Sexp::int(123))],
11224        };
11225        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11226            .expect("literal-only template must succeed");
11227        assert_eq!(out, Sexp::int(123));
11228    }
11229
11230    // ── MacroParams: the typed param-list primitive ─────────────────────
11231    //
11232    // `parse_params` now yields a `MacroParams { required, optional, rest }`
11233    // whose shape makes the canonical lambda-list ordering (required →
11234    // optional → rest, "&rest is last + at-most-one", "&optional at most
11235    // once") structural rather than a construction discipline a `Vec<Param>`
11236    // only happened to uphold. These tests pin the parser's mapping into the
11237    // typed shape, the flat-index contract `names()` exposes to the template
11238    // bytecode, and the single positional binder `bind()` both expansion
11239    // strategies now route through. The end-to-end `rest_param_splices_with_at`
11240    // and `compiled_template_matches_substitute_path` tests above are the
11241    // path-uniformity guards proving both strategies still agree.
11242
11243    #[test]
11244    fn parse_params_maps_required_then_rest_into_typed_shape() {
11245        // `(a b &rest c)` — two required, one rest. The rest name lands in
11246        // the `Option`, never in `required`.
11247        let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
11248        assert_eq!(
11249            params,
11250            MacroParams {
11251                required: vec!["a".into(), "b".into()],
11252                optional: Vec::new(),
11253                rest: Some("c".into()),
11254            }
11255        );
11256    }
11257
11258    #[test]
11259    fn parse_params_rest_absent_leaves_none() {
11260        // `(x y)` — no `&rest`, so `rest` is structurally `None`. There is
11261        // no representation in which a rest-less list carries a stray rest.
11262        let params = parse_params(&read("x y").unwrap()).unwrap();
11263        assert_eq!(
11264            params,
11265            MacroParams {
11266                required: vec!["x".into(), "y".into()],
11267                optional: Vec::new(),
11268                rest: None,
11269            }
11270        );
11271    }
11272
11273    #[test]
11274    fn parse_params_maps_optional_section_between_required_and_rest() {
11275        // `(a &optional b c &rest d)` — the canonical lambda-list order. `a`
11276        // is required, `b`/`c` are optional, `d` is rest. The `&optional`
11277        // marker switches collection from `required` to `optional`; `&rest`
11278        // remains terminal.
11279        let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
11280        assert_eq!(
11281            params,
11282            MacroParams {
11283                required: vec!["a".into()],
11284                optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11285                rest: Some("d".into()),
11286            }
11287        );
11288    }
11289
11290    #[test]
11291    fn parse_params_optional_with_no_rest_leaves_rest_none() {
11292        // `(&optional x)` — a leading `&optional` (zero required) with no
11293        // rest. `required` is empty, `x` is the sole optional, `rest` None.
11294        let params = parse_params(&read("&optional x").unwrap()).unwrap();
11295        assert_eq!(
11296            params,
11297            MacroParams {
11298                required: Vec::new(),
11299                optional: vec![OptionalParam::bare("x")],
11300                rest: None,
11301            }
11302        );
11303    }
11304
11305    #[test]
11306    fn parse_params_rejects_repeated_optional_marker() {
11307        // `(a &optional b &optional c)` — a second `&optional` is
11308        // unrepresentable (one flat optional section), so the gate REJECTS
11309        // rather than binding args to a marker symbol named `&optional`. The
11310        // two marker positions (1 and 3) are named.
11311        let err = parse_params(&read("a &optional b &optional c").unwrap())
11312            .expect_err("repeated &optional must error");
11313        assert!(
11314            matches!(
11315                err,
11316                LispError::OptionalMarkerRepeated {
11317                    first_position: 1,
11318                    second_position: 3,
11319                }
11320            ),
11321            "expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
11322        );
11323    }
11324
11325    #[test]
11326    fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
11327        // `(&rest xs &optional y)` — `&rest <name>` is terminal, so the
11328        // `&optional y` tail is REJECTED as trailing tokens (not silently
11329        // dropped, and not a repeated-optional error: the rest gate fires
11330        // first). Pins the interaction the prior run (3627426) signposted.
11331        let err = parse_params(&read("&rest xs &optional y").unwrap())
11332            .expect_err("tokens after &rest <name> must error");
11333        assert!(
11334            matches!(err, LispError::RestParamTrailingTokens { .. }),
11335            "expected RestParamTrailingTokens, got: {err:?}"
11336        );
11337    }
11338
11339    #[test]
11340    fn names_are_required_then_optional_then_rest_in_flat_index_order() {
11341        // The flat-index contract the bytecode `Subst(idx)`/`Splice(idx)`
11342        // depends on: required names at 0.., then optional names, then the
11343        // rest name last.
11344        let params = MacroParams {
11345            required: vec!["a".into(), "b".into()],
11346            optional: vec![OptionalParam::bare("c")],
11347            rest: Some("d".into()),
11348        };
11349        assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
11350        // Optional names occupy the indices immediately after the required run.
11351        assert_eq!(params.names()[params.required.len()], "c");
11352        // The rest name is last, after required + optional — i.e. at the
11353        // structural `fixed_arity()` boundary the typed primitive names.
11354        assert_eq!(params.names()[params.fixed_arity()], "d");
11355    }
11356
11357    // ── `MacroParams::{REST_MARKER, OPTIONAL_MARKER,
11358    // LAMBDA_LIST_KEYWORD_LEAD}` — the typed CL lambda-list-keyword
11359    // marker algebra ────────────────────────────────────────────────
11360    //
11361    // The three `pub const`s close the Common-Lisp lambda-list keyword
11362    // family at the typed [`MacroParams`] algebra: `REST_MARKER` and
11363    // `OPTIONAL_MARKER` are the two `&'static str` markers the parser's
11364    // typed dispatch specialises on; `LAMBDA_LIST_KEYWORD_LEAD` is the
11365    // canonical `'&'` char shared as the LEAD byte of both markers.
11366    // Pre-lift the same two `&'static str` markers lived as two inline
11367    // `s == "..."` comparisons at `parse_params` — post-lift both
11368    // comparisons route through the typed constants so a delimiter swap
11369    // (e.g. a Racket-compat `#!rest` port, a Clojure-compat `&` port)
11370    // lands at ONE constant on the typed algebra.
11371    //
11372    // These pins cover: (a) the exact `&'static str` / `char` values,
11373    // (b) the structural round-trip law binding each `&'static str`
11374    // marker to its shared `char` LEAD byte, (c) the pairwise
11375    // disjointness of the two `&'static str` markers, (d) the cross-
11376    // axis disjointness of the `char` LEAD byte against every sibling
11377    // outer-marker `char` the substrate's other closed-set algebras
11378    // specialise on, and (e) the path-uniformity of `parse_params`
11379    // through the typed constants.
11380
11381    #[test]
11382    fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
11383        // Pins the constant's exact `&'static str` bytes so a typo
11384        // (`"&res"`, `"&Rest"`, `"&rst"`) or an accidental redefinition
11385        // surfaces immediately. Sibling-shape pin to
11386        // `macro_params_optional_marker_projects_canonical_ampersand_optional_str`
11387        // on the peer `&optional` axis.
11388        assert_eq!(
11389            MacroParams::REST_MARKER,
11390            "&rest",
11391            "MacroParams::REST_MARKER drifted from the substrate- \
11392             canonical CL lambda-list `&rest` marker — the parser's \
11393             `parse_params` rest-slot dispatch AND every downstream \
11394             authoring / rendering surface binds to this ONE typed \
11395             constant.",
11396        );
11397    }
11398
11399    #[test]
11400    fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
11401        // Pins the constant's exact `&'static str` bytes so a typo
11402        // (`"&opt"`, `"&Optional"`, `"&option"`) or an accidental
11403        // redefinition surfaces immediately. Sibling-shape pin to
11404        // `macro_params_rest_marker_projects_canonical_ampersand_rest_str`
11405        // on the peer `&rest` axis.
11406        assert_eq!(
11407            MacroParams::OPTIONAL_MARKER,
11408            "&optional",
11409            "MacroParams::OPTIONAL_MARKER drifted from the substrate- \
11410             canonical CL lambda-list `&optional` marker — the parser's \
11411             `parse_params` optional-section dispatch AND every \
11412             downstream authoring / rendering surface binds to this \
11413             ONE typed constant.",
11414        );
11415    }
11416
11417    #[test]
11418    fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
11419        // Pins the constant's exact `char` value so a typo (`'#'`,
11420        // `'@'`, `'!'`) or an accidental redefinition surfaces
11421        // immediately. Sibling-shape pin to
11422        // `atom_keyword_marker_lead_projects_canonical_colon_char`,
11423        // `atom_bool_literal_lead_projects_canonical_hash_char`,
11424        // `atom_str_delimiter_projects_canonical_double_quote_char`
11425        // on the peer per-role LEAD-byte axes across the substrate's
11426        // closed-set outer algebras.
11427        assert_eq!(
11428            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11429            '&',
11430            "LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
11431             canonical `&` LEAD byte — the CL lambda-list-keyword \
11432             family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
11433             typed constant as their common LEAD byte.",
11434        );
11435    }
11436
11437    #[test]
11438    fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
11439        // STRUCTURAL ROUND-TRIP CONTRACT: the `&'static str` marker
11440        // `MacroParams::REST_MARKER` starts with the `char` LEAD byte
11441        // `MacroParams::LAMBDA_LIST_KEYWORD_LEAD` — the projection law
11442        // binding the two typed constants on the [`MacroParams`]
11443        // algebra. A regression that renames the `&'static str` (e.g.
11444        // to Racket's `"#!rest"` keyword-args) OR the `char` (e.g. to
11445        // `'#'` for the `#!` shebang lead) without updating the other
11446        // fails HERE. Sibling-shape pin to
11447        // `atom_keyword_marker_lead_prefixes_keyword_marker` on the
11448        // peer `Atom`-algebra Keyword-prefix LEAD-byte axis.
11449        assert!(
11450            MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11451            "MacroParams::REST_MARKER `{}` does NOT start with \
11452             MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11453             typed constants have drifted apart on the [`MacroParams`] \
11454             algebra; the CL lambda-list-keyword family disjointness \
11455             contract can no longer bind to ONE shared LEAD byte.",
11456            MacroParams::REST_MARKER,
11457            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11458        );
11459    }
11460
11461    #[test]
11462    fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
11463        // STRUCTURAL ROUND-TRIP CONTRACT: peer to
11464        // `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
11465        // on the `&optional` axis. Both `&'static str` markers of the
11466        // CL lambda-list-keyword family MUST share the canonical `char`
11467        // LEAD byte so the typed-marker disjointness contract can bind
11468        // to ONE shared LEAD byte on the [`MacroParams`] algebra.
11469        assert!(
11470            MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11471            "MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
11472             MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11473             typed constants have drifted apart on the [`MacroParams`] \
11474             algebra; the CL lambda-list-keyword family disjointness \
11475             contract can no longer bind to ONE shared LEAD byte.",
11476            MacroParams::OPTIONAL_MARKER,
11477            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11478        );
11479    }
11480
11481    #[test]
11482    fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
11483        // PAIRWISE DISJOINTNESS PIN: the two `&'static str` markers on
11484        // the CL lambda-list-keyword algebra MUST differ so the
11485        // parser's typed dispatch cascade at `parse_params` (which
11486        // tests `REST_MARKER` FIRST, then `OPTIONAL_MARKER`) cannot
11487        // silently route both dispatches through the same arm. A
11488        // regression that aliases the two markers (e.g. both to
11489        // `"&rest"` after a typo) would silently drop the optional
11490        // section's structural distinction — every `&optional` name
11491        // would misclassify as a rest-slot marker.
11492        assert_ne!(
11493            MacroParams::REST_MARKER,
11494            MacroParams::OPTIONAL_MARKER,
11495            "REST_MARKER and OPTIONAL_MARKER collide — the parser's \
11496             typed dispatch cascade at `parse_params` can no longer \
11497             distinguish the rest-slot boundary from the optional- \
11498             section boundary.",
11499        );
11500    }
11501
11502    #[test]
11503    fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
11504        // CROSS-AXIS DISJOINTNESS PIN: `MacroParams::LAMBDA_LIST_KEYWORD_LEAD`
11505        // MUST NOT alias any sibling outer-marker `char` on the
11506        // substrate's other closed-set algebras — the Atom-payload
11507        // markers (`STR_DELIMITER`, `STR_ESCAPE_LEAD`,
11508        // `KEYWORD_MARKER_LEAD`, `BOOL_LITERAL_LEAD`), the paired list
11509        // delimiters (`Sexp::LIST_OPEN` / `Sexp::LIST_CLOSE`), the
11510        // paired line-comment delimiters (`Sexp::COMMENT_LEAD` /
11511        // `Sexp::COMMENT_TERM`), every `QuoteForm::lead_char`
11512        // projection, AND `QuoteForm::SPLICE_DISCRIMINATOR`. A
11513        // collision would silently break the reader's outer dispatch:
11514        // an `&`-prefixed bare atom `&rest` / `&optional` would collide
11515        // with whichever marker it aliased. Sibling-shape pin to
11516        // `atom_keyword_marker_lead_distinct_from_every_other_algebra_marker`
11517        // on the peer `Atom`-algebra Keyword-prefix LEAD-byte axis —
11518        // pins the SAME shape on the CL lambda-list-keyword LEAD-byte
11519        // axis. A future outer-marker extension that collided with
11520        // `'&'` fails HERE at the cross-axis enumeration.
11521        use crate::ast::{Atom, QuoteForm, Sexp};
11522
11523        assert_ne!(
11524            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11525            Atom::STR_DELIMITER,
11526            "LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
11527             bare `&rest` at a param-list position would ambiguously \
11528             begin a lambda-list keyword AND open a string.",
11529        );
11530        assert_ne!(
11531            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11532            Atom::STR_ESCAPE_LEAD,
11533            "LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
11534             the reader's Str-escape lead byte would alias the CL \
11535             lambda-list-keyword LEAD byte.",
11536        );
11537        assert_ne!(
11538            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11539            Atom::KEYWORD_MARKER_LEAD,
11540            "LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
11541             — a bare `&rest` at a param-list position would \
11542             ambiguously begin a lambda-list keyword AND begin an \
11543             `:foo` keyword.",
11544        );
11545        assert_ne!(
11546            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11547            Atom::BOOL_LITERAL_LEAD,
11548            "LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
11549             a bare `&rest` at a param-list position would ambiguously \
11550             begin a lambda-list keyword AND classify as a Bool prefix.",
11551        );
11552        assert_ne!(
11553            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11554            Sexp::LIST_OPEN,
11555            "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
11556             `&rest` at a param-list position would ambiguously begin \
11557             a lambda-list keyword AND open a list.",
11558        );
11559        assert_ne!(
11560            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11561            Sexp::LIST_CLOSE,
11562            "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
11563             `&rest` at a param-list position would ambiguously begin \
11564             a lambda-list keyword AND close a list.",
11565        );
11566        assert_ne!(
11567            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11568            Sexp::COMMENT_LEAD,
11569            "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
11570             bare `&rest` at a param-list position would ambiguously \
11571             begin a lambda-list keyword AND begin a comment.",
11572        );
11573        assert_ne!(
11574            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11575            Sexp::COMMENT_TERM,
11576            "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
11577             reader's line-comment discard loop would terminate on the \
11578             SAME byte the parser's lambda-list-keyword LEAD dispatch \
11579             binds to.",
11580        );
11581        for qf in QuoteForm::ALL {
11582            assert_ne!(
11583                MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11584                qf.lead_char(),
11585                "LAMBDA_LIST_KEYWORD_LEAD collides with \
11586                 QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
11587                 param-list position would ambiguously begin a lambda- \
11588                 list keyword AND begin a quote-family prefix.",
11589            );
11590        }
11591        assert_ne!(
11592            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11593            QuoteForm::SPLICE_DISCRIMINATOR,
11594            "LAMBDA_LIST_KEYWORD_LEAD collides with \
11595             SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
11596             peek byte would alias the CL lambda-list-keyword LEAD \
11597             byte.",
11598        );
11599    }
11600
11601    #[test]
11602    fn parse_params_recognizes_rest_marker_via_typed_constant() {
11603        // PATH-UNIFORMITY PIN: the parser's rest-slot dispatch at
11604        // `parse_params` MUST classify the typed constant
11605        // `MacroParams::REST_MARKER` as the rest-slot boundary —
11606        // authoring surfaces that assemble a param list through the
11607        // typed constant (rather than through an inline string literal
11608        // read from user source) must reach the SAME rest-slot binding
11609        // the reader-driven path does.
11610        //
11611        // The `read(REST_MARKER)` composition here is the load-bearing
11612        // structural link: we build the param-list source through the
11613        // typed constant, then parse it through the reader, then the
11614        // parser MUST bind `xs` at the `rest` slot. A regression that
11615        // drifts the parser's dispatch away from the typed constant
11616        // (e.g. re-inlines a string literal that goes stale against
11617        // the constant) fails HERE.
11618        let src = format!("a {} xs", MacroParams::REST_MARKER);
11619        let params = parse_params(&read(&src).unwrap()).unwrap();
11620        assert_eq!(
11621            params,
11622            MacroParams {
11623                required: vec!["a".into()],
11624                optional: Vec::new(),
11625                rest: Some("xs".into()),
11626            },
11627            "parse_params dispatch drifted away from REST_MARKER — the \
11628             typed constant no longer routes to the rest-slot arm.",
11629        );
11630    }
11631
11632    #[test]
11633    fn parse_params_recognizes_optional_marker_via_typed_constant() {
11634        // PATH-UNIFORMITY PIN: peer to
11635        // `parse_params_recognizes_rest_marker_via_typed_constant` on
11636        // the `&optional` axis. The parser's optional-section dispatch
11637        // at `parse_params` MUST classify the typed constant
11638        // `MacroParams::OPTIONAL_MARKER` as the section-switch
11639        // boundary that reroutes subsequent bare-symbol names from the
11640        // `required` bin to the `optional` bin.
11641        let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
11642        let params = parse_params(&read(&src).unwrap()).unwrap();
11643        assert_eq!(
11644            params,
11645            MacroParams {
11646                required: vec!["a".into()],
11647                optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11648                rest: None,
11649            },
11650            "parse_params dispatch drifted away from OPTIONAL_MARKER — \
11651             the typed constant no longer routes to the optional- \
11652             section arm.",
11653        );
11654    }
11655
11656    // ── `MacroParams::LAMBDA_LIST_KEYWORDS` + `is_lambda_list_keyword` —
11657    // the closed-set forced-arity ALL array + membership gate that closes
11658    // the CL lambda-list-keyword family at the typed [`MacroParams`]
11659    // algebra. Sibling posture to the closed set of `pub const ALL: [Self;
11660    // N]` forced-arity arrays across the substrate's other closed-set
11661    // outer algebras (`AtomKind::ALL`, `QuoteForm::ALL`, `SexpShape::ALL`,
11662    // `UnquoteForm::ALL`, `MacroDefHead::ALL`); a future third marker
11663    // (`&key`, `&aux`, `&body`) extends the array's arity + adds ONE `pub
11664    // const` for the marker and every downstream family-wide contract
11665    // sweep (LEAD-byte round-trip, pairwise disjointness, membership
11666    // gate) picks up the extension at ONE structural site.
11667    //
11668    // These pins cover: (a) the cardinality contract (the ALL array's
11669    // length matches the pre-lift per-role marker count), (b) the
11670    // ordering contract (each ALL entry matches its corresponding per-
11671    // role `pub const` by-index), (c) the family-wide structural round-
11672    // trip (every ALL element starts_with LEAD), (d) the family-wide
11673    // pairwise disjointness (every distinct ALL index pair yields
11674    // distinct markers), (e) the membership-gate acceptance side (every
11675    // ALL element classifies as `true`), and (f) the membership-gate
11676    // rejection side (the bare LEAD byte + unrecognised `&`-prefixed
11677    // names classify as `false`).
11678    //
11679    // Fail-before/pass-after: every test below references
11680    // `MacroParams::LAMBDA_LIST_KEYWORDS` or
11681    // `MacroParams::is_lambda_list_keyword`, which simply did not exist
11682    // on `MacroParams` before this lift — every assertion was a
11683    // compile-time error against the prior surface.
11684
11685    #[test]
11686    fn macro_params_lambda_list_keywords_has_expected_cardinality() {
11687        // CARDINALITY PIN: the ALL array closes the family at exactly TWO
11688        // entries — the two `&'static str` markers the parser's typed
11689        // dispatch specialises on today (`&rest` + `&optional`). A future
11690        // third marker (`&key`, `&aux`, `&body`) extends this arity to
11691        // 3 AND every downstream family-wide contract sweep picks up the
11692        // extension at ONE structural site. A regression that drops a
11693        // marker from the ALL array (silently narrowing the family) OR
11694        // aliases two markers to the same index (silently narrowing the
11695        // typed dispatch) fails HERE at the cardinality contract before
11696        // the structural round-trip / pairwise-disjointness contracts
11697        // even fire.
11698        assert_eq!(
11699            MacroParams::LAMBDA_LIST_KEYWORDS.len(),
11700            2,
11701            "LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
11702             lambda-list-keyword family closure now names a different \
11703             number of markers than the two the parser's typed dispatch \
11704             specialises on.",
11705        );
11706    }
11707
11708    #[test]
11709    fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
11710        // ORDERING PIN: each ALL entry matches its corresponding per-role
11711        // `pub const` by-index. A regression that swaps the ordering
11712        // (`[OPTIONAL_MARKER, REST_MARKER]` after a rebase) would keep
11713        // the cardinality + membership contracts intact but silently
11714        // reorder downstream consumers that iterate `LAMBDA_LIST_KEYWORDS`
11715        // in canonical order (a future authoring surface that renders
11716        // "supported CL lambda-list keywords: &rest, &optional" would
11717        // silently render "supported CL lambda-list keywords: &optional,
11718        // &rest" instead). Pins the array's declaration-order binding
11719        // structurally so a reorder fails HERE at each element rather
11720        // than only at consumer sites downstream.
11721        assert_eq!(
11722            MacroParams::LAMBDA_LIST_KEYWORDS[0],
11723            MacroParams::REST_MARKER,
11724            "LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
11725             array's declaration-order binding to the per-role `pub \
11726             const` broke at the rest-slot marker slot.",
11727        );
11728        assert_eq!(
11729            MacroParams::LAMBDA_LIST_KEYWORDS[1],
11730            MacroParams::OPTIONAL_MARKER,
11731            "LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
11732             ALL array's declaration-order binding to the per-role \
11733             `pub const` broke at the optional-section marker slot.",
11734        );
11735    }
11736
11737    #[test]
11738    fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
11739        // FAMILY-WIDE STRUCTURAL ROUND-TRIP PIN: every element of the ALL
11740        // array MUST start with the canonical LEAD byte. Where the two
11741        // per-marker `_prefixed_by_lambda_list_keyword_lead` pins each
11742        // named ONE marker inline as a duplicate 3-line
11743        // `assert!(A.starts_with(LEAD), ...)` shape, this family-wide
11744        // sweep routes the SAME contract through the closed-set ALL
11745        // array — a future third marker (`&key`, `&aux`, `&body`)
11746        // extends the array AND is automatically covered by this sweep
11747        // without adding a third `_prefixed_by_...` per-marker pin. The
11748        // per-marker pins stay as fine-grained fail-loud sites; this
11749        // pin is the algebra-wide closure the family-extending refactor
11750        // routes through.
11751        for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11752            assert!(
11753                m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11754                "LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
11755                 LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
11756                 list-keyword family's structural round-trip contract \
11757                 no longer binds every marker to its shared LEAD byte.",
11758                lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11759            );
11760        }
11761    }
11762
11763    #[test]
11764    fn macro_params_lambda_list_keywords_pairwise_distinct() {
11765        // FAMILY-WIDE PAIRWISE DISJOINTNESS PIN: every distinct index pair
11766        // `(i, j)` in the ALL array yields distinct markers. Where the
11767        // pre-lift `_rest_and_optional_markers_pairwise_disjoint` pin
11768        // named the TWO markers as a hand-rolled `assert_ne!` pair, this
11769        // family-wide sweep routes the SAME contract through the closed-
11770        // set ALL array so a future third marker automatically extends
11771        // the sweep to `(3 * 2) / 2 == 3` distinct pairs without re-
11772        // deriving per-marker `assert_ne!` calls. The pre-lift pin
11773        // stays as the fine-grained fail-loud site; this pin is the
11774        // family-wide closure the algebra-extending refactor routes
11775        // through.
11776        for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11777            for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11778                if i == j {
11779                    continue;
11780                }
11781                assert_ne!(
11782                    a, b,
11783                    "LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
11784                     LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
11785                     list-keyword family's pairwise disjointness \
11786                     contract no longer binds distinct index pairs \
11787                     to distinct markers.",
11788                );
11789            }
11790        }
11791    }
11792
11793    #[test]
11794    fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
11795        // ACCEPTANCE-SIDE MEMBERSHIP PIN: every element of the ALL array
11796        // MUST classify as `true` through the typed membership gate. The
11797        // gate is defined as `LAMBDA_LIST_KEYWORDS.contains(&s)`, so this
11798        // pin is the structural closure binding the gate's return value
11799        // to the ALL array's element set. A regression that specialises
11800        // the gate to a subset of the array (e.g. a `matches!(s,
11801        // "&rest")` inline that silently drops the `&optional` branch)
11802        // fails HERE at the ALL sweep. Sibling-shape acceptance sweep to
11803        // the closed-set `ClosedSet::parse_label` roundtrip pin — every
11804        // element of `Self::ALL` decodes through the projection back to
11805        // itself.
11806        for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11807            assert!(
11808                MacroParams::is_lambda_list_keyword(m),
11809                "is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
11810                 element `{m}` — the closed-set membership gate's \
11811                 acceptance side drifted from the ALL array.",
11812            );
11813        }
11814    }
11815
11816    #[test]
11817    fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
11818        // REJECTION-SIDE MEMBERSHIP PIN (LEAD byte alone): the bare LEAD
11819        // byte `"&"` MUST classify as `false`. A future Clojure-compat
11820        // port might land a bare `&` marker — but until such an
11821        // extension explicitly lands on the ALL array, the substrate
11822        // MUST NOT silently classify the LEAD byte alone as a
11823        // recognised CL lambda-list keyword. Pins the gate's
11824        // rejection-side contract against a plausible future
11825        // near-miss extension.
11826        let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
11827        assert!(
11828            !MacroParams::is_lambda_list_keyword(&bare_lead),
11829            "is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
11830             the closed-set membership gate silently classifies the bare `&` \
11831             LEAD byte as a recognised CL lambda-list keyword despite the ALL \
11832             array containing only the two suffixed markers.",
11833        );
11834    }
11835
11836    #[test]
11837    fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
11838        // REJECTION-SIDE MEMBERSHIP PIN (unrecognised near-misses): the
11839        // three plausible future CL lambda-list keywords (`&key` for
11840        // keyword-args, `&aux` for auxiliaries, `&body` for the
11841        // docstring-carrying tail of `defmacro` bodies) MUST classify as
11842        // `false` UNTIL they explicitly land on the ALL array. Pins the
11843        // gate's conservative rejection contract — the closed-set
11844        // membership gate is not a "starts with `&`" heuristic; it is a
11845        // typed enumeration over the currently-recognised markers. A
11846        // regression that silently loosens the gate to a `starts_with`
11847        // check (dropping the closed-set discipline) fails HERE.
11848        for candidate in ["&key", "&aux", "&body"] {
11849            assert!(
11850                !MacroParams::is_lambda_list_keyword(candidate),
11851                "is_lambda_list_keyword accepted the unrecognised \
11852                 `&`-prefixed name `{candidate}` — the closed-set \
11853                 membership gate silently loosened its acceptance beyond \
11854                 the two markers the ALL array names today.",
11855            );
11856        }
11857    }
11858
11859    #[test]
11860    fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
11861        // REJECTION-SIDE MEMBERSHIP PIN (bare identifiers + empty): a
11862        // bare identifier (`"a"`, `"xs"`, `"foo"`) and the empty string
11863        // MUST classify as `false`. These are the shapes the parser's
11864        // bare-symbol dispatch cascade sees at `parse_params` when
11865        // walking a param list, so pinning them here binds the
11866        // membership gate's rejection contract to the parser's most
11867        // common non-marker input shapes.
11868        for candidate in ["a", "xs", "foo", ""] {
11869            assert!(
11870                !MacroParams::is_lambda_list_keyword(candidate),
11871                "is_lambda_list_keyword accepted the bare non-marker \
11872                 input `{candidate}` — the closed-set membership gate's \
11873                 rejection side no longer excludes bare identifiers \
11874                 the parser routes through the fall-through cascade.",
11875            );
11876        }
11877    }
11878
11879    // ── fixed_arity: the rest-start / rest-less max-arity primitive ─────
11880    //
11881    // `fixed_arity()` lifts the `self.required.len() + self.optional.len()`
11882    // arithmetic that recurred three times inside `MacroParams::bind` — at
11883    // the `Vec::with_capacity` site (where it adds `usize::from(rest.is_some())`
11884    // to get the bound-values count), at the `rest_start` site (inside the
11885    // `if let Some(rest)` branch), and at the `expected` site (inside the
11886    // rest-less `else`). The latter two sites live in mutually-exclusive
11887    // branches yet name ONE structural concept; lifting them collapses the
11888    // arithmetic to one named primitive. These tests pin the primitive's
11889    // contract directly; the existing `bind_*` tests are the path-uniformity
11890    // guards proving `bind`'s sites route through the same value without
11891    // behavior drift.
11892    //
11893    // Fail-before/pass-after: every test below references
11894    // `params.fixed_arity()`, which simply did not exist on `MacroParams`
11895    // before this lift — every assertion's `expect: ___ == params.fixed_arity()`
11896    // line was a compile-time error against the prior surface.
11897
11898    #[test]
11899    fn fixed_arity_is_zero_for_the_empty_param_list() {
11900        // `()` — a nullary macro has fixed arity 0, the rest-less binder
11901        // boundary at which the FIRST surplus arg already rejects.
11902        let params = MacroParams::default();
11903        assert_eq!(params.fixed_arity(), 0);
11904    }
11905
11906    #[test]
11907    fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
11908        // `(a b c)` — three required, no optional, no rest. fixed_arity is
11909        // exactly the required length.
11910        let params = MacroParams {
11911            required: vec!["a".into(), "b".into(), "c".into()],
11912            optional: Vec::new(),
11913            rest: None,
11914        };
11915        assert_eq!(params.fixed_arity(), 3);
11916    }
11917
11918    #[test]
11919    fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
11920        // `(&optional x y)` — two optional, no required. fixed_arity is the
11921        // optional length; the optional section participates in the fixed
11922        // run because supplied positional args bind to it.
11923        let params = MacroParams {
11924            required: Vec::new(),
11925            optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
11926            rest: None,
11927        };
11928        assert_eq!(params.fixed_arity(), 2);
11929    }
11930
11931    #[test]
11932    fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
11933        // `(a b &optional c d e)` — two required + three optional, no rest.
11934        // fixed_arity is 5: the maximum arity a rest-less call can supply.
11935        let params = MacroParams {
11936            required: vec!["a".into(), "b".into()],
11937            optional: vec![
11938                OptionalParam::bare("c"),
11939                OptionalParam::bare("d"),
11940                OptionalParam::bare("e"),
11941            ],
11942            rest: None,
11943        };
11944        assert_eq!(params.fixed_arity(), 5);
11945    }
11946
11947    #[test]
11948    fn fixed_arity_ignores_rest_slot_by_construction() {
11949        // `(a &optional b &rest r)` and `(a &optional b)` — identical fixed
11950        // arity (2). The `&rest` slot has NO maximum and is structurally
11951        // excluded from `fixed_arity`. Naming this invariant pins that a
11952        // regression that drifts the primitive to "required + optional +
11953        // rest.is_some() as usize" fails loudly here — that drift would
11954        // collapse `fixed_arity` into `names().len()`, losing the rest-start
11955        // vs total-bound-values distinction the typed shape relies on.
11956        let with_rest = MacroParams {
11957            required: vec!["a".into()],
11958            optional: vec![OptionalParam::bare("b")],
11959            rest: Some("r".into()),
11960        };
11961        let without_rest = MacroParams {
11962            required: vec!["a".into()],
11963            optional: vec![OptionalParam::bare("b")],
11964            rest: None,
11965        };
11966        assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
11967        assert_eq!(with_rest.fixed_arity(), 2);
11968    }
11969
11970    #[test]
11971    fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
11972        // When `rest` is `Some`, `names()[fixed_arity()]` IS the rest name
11973        // — the rest-start reading of the primitive. Same arithmetic the
11974        // bytecode index would hit (`Subst(fixed_arity())` resolves to the
11975        // rest-bound `Sexp::List`).
11976        let params = MacroParams {
11977            required: vec!["a".into(), "b".into()],
11978            optional: vec![OptionalParam::bare("c")],
11979            rest: Some("r".into()),
11980        };
11981        assert_eq!(params.fixed_arity(), 3);
11982        assert_eq!(params.names()[params.fixed_arity()], "r");
11983    }
11984
11985    #[test]
11986    fn fixed_arity_equals_names_length_when_rest_is_absent() {
11987        // When `rest` is `None`, `names().len() == fixed_arity()` — there
11988        // is no rest-name slot to extend the flat run past the fixed
11989        // boundary. Pins the structural identity
11990        // `names().len() == fixed_arity() + usize::from(rest.is_some())`
11991        // for the rest-less case; the rest-present case is pinned by the
11992        // sibling test above (where the boundary is the rest-name index,
11993        // i.e. one short of `names().len()`).
11994        let params = MacroParams {
11995            required: vec!["a".into(), "b".into()],
11996            optional: vec![OptionalParam::bare("c")],
11997            rest: None,
11998        };
11999        assert_eq!(params.names().len(), params.fixed_arity());
12000        assert_eq!(params.names().len(), 3);
12001    }
12002
12003    #[test]
12004    fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
12005        // The `expected` field of `TooManyMacroArgs` IS `fixed_arity()` —
12006        // the rest-less binder rejects iff `args.len() > fixed_arity()`.
12007        // This pin is the path-uniformity guard binding the typed primitive
12008        // to the binder's rejection contract: a regression that drifts
12009        // `bind`'s `expected` arithmetic from `fixed_arity()` would silently
12010        // surface a different boundary in the diagnostic without touching
12011        // the primitive — and this assertion fails loudly. Mirror of the
12012        // sibling rest-less surplus pin (`bind_rest_less_params_reject_
12013        // surplus_args`); this test pins WHAT the `expected` slot's value
12014        // structurally IS, that pin checks the variant SHAPE.
12015        let params = MacroParams {
12016            required: vec!["a".into(), "b".into()],
12017            optional: vec![OptionalParam::bare("c")],
12018            rest: None,
12019        };
12020        assert_eq!(params.fixed_arity(), 3);
12021        let err = params
12022            .bind(
12023                "m",
12024                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12025            )
12026            .expect_err("4 args against fixed_arity 3 must reject");
12027        match err {
12028            LispError::TooManyMacroArgs {
12029                expected,
12030                got,
12031                macro_name,
12032            } => {
12033                assert_eq!(expected, params.fixed_arity());
12034                assert_eq!(got, 4);
12035                assert_eq!(macro_name, "m");
12036            }
12037            other => panic!("expected TooManyMacroArgs, got {other:?}"),
12038        }
12039    }
12040
12041    #[test]
12042    fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
12043        // When `rest` is `Some`, `bind` collects `args[fixed_arity()..]`
12044        // into the rest's `Sexp::List`. Pin that the rest list contents
12045        // are exactly the suffix beginning at `fixed_arity()` — the
12046        // primitive's rest-start reading IS the slice index the binder
12047        // consumes. A regression that drifts `bind`'s rest-collection
12048        // slice from `fixed_arity()` would surface as a misaligned rest
12049        // list (off-by-one in either direction) and this assertion fails
12050        // loudly. Sibling of `fixed_arity_is_the_rest_less_surplus_
12051        // rejection_boundary` on the rest-PRESENT branch.
12052        let params = MacroParams {
12053            required: vec!["a".into()],
12054            optional: vec![OptionalParam::bare("b")],
12055            rest: Some("r".into()),
12056        };
12057        assert_eq!(params.fixed_arity(), 2);
12058        let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
12059        let vals = params.bind("m", &args).unwrap();
12060        // Bound vec: [a=1, b=2, r=(3 4)] — the rest list IS args[fixed_arity()..].
12061        let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
12062        assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
12063    }
12064
12065    #[test]
12066    fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
12067        // Exactly-saturated rest-present call: `args.len() == fixed_arity()`.
12068        // The rest slot collects the empty slice; bind succeeds, never
12069        // misaligning to an off-by-one underflow. Pin the boundary on the
12070        // rest-PRESENT path — the rest-less mirror is
12071        // `too_many_macro_args_does_not_fire_at_exact_max_arity` above.
12072        let params = MacroParams {
12073            required: vec!["a".into()],
12074            optional: vec![OptionalParam::bare("b")],
12075            rest: Some("r".into()),
12076        };
12077        assert_eq!(params.fixed_arity(), 2);
12078        let vals = params
12079            .bind("m", &[Sexp::int(1), Sexp::int(2)])
12080            .expect("rest-present at exact fixed_arity must bind cleanly");
12081        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
12082    }
12083
12084    #[test]
12085    fn bind_threads_required_positionally_and_collects_rest_as_list() {
12086        // `(a b &rest c)` bound to `1 2 3 4`: a=1, b=2, c=(3 4). The bound
12087        // vec is parallel to `names()`, so the rest list sits at the rest's
12088        // flat index.
12089        let params = MacroParams {
12090            required: vec!["a".into(), "b".into()],
12091            optional: Vec::new(),
12092            rest: Some("c".into()),
12093        };
12094        let vals = params
12095            .bind(
12096                "m",
12097                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12098            )
12099            .unwrap();
12100        assert_eq!(
12101            vals,
12102            vec![
12103                Sexp::int(1),
12104                Sexp::int(2),
12105                Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12106            ]
12107        );
12108    }
12109
12110    #[test]
12111    fn bind_supplied_optional_takes_its_positional_arg() {
12112        // `(a &optional b)` bound to `1 2`: a=1, b=2. A supplied optional
12113        // behaves exactly like a positional — only its ABSENCE differs.
12114        let params = MacroParams {
12115            required: vec!["a".into()],
12116            optional: vec![OptionalParam::bare("b")],
12117            rest: None,
12118        };
12119        let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
12120        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
12121    }
12122
12123    #[test]
12124    fn bind_unsupplied_optional_defaults_to_nil() {
12125        // `(a &optional b c)` bound to just `1`: a=1, then b and c run out of
12126        // args and bind to `Sexp::Nil` — CL's default for an `&optional` with
12127        // no supplied default-form. The bound vec is still parallel to
12128        // `names()`, so the template's `,b` / `,c` resolve to nil, not a
12129        // missing-arg error.
12130        let params = MacroParams {
12131            required: vec!["a".into()],
12132            optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
12133            rest: None,
12134        };
12135        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12136        assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
12137    }
12138
12139    #[test]
12140    fn bind_rest_collects_args_beyond_required_and_optional() {
12141        // `(a &optional b &rest c)` bound to `1 2 3 4`: a=1, b=2 (supplied),
12142        // c=(3 4). The rest starts AFTER the required+optional run, so the
12143        // optional's supplied arg is not swept into the rest.
12144        let params = MacroParams {
12145            required: vec!["a".into()],
12146            optional: vec![OptionalParam::bare("b")],
12147            rest: Some("c".into()),
12148        };
12149        let vals = params
12150            .bind(
12151                "m",
12152                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12153            )
12154            .unwrap();
12155        assert_eq!(
12156            vals,
12157            vec![
12158                Sexp::int(1),
12159                Sexp::int(2),
12160                Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12161            ]
12162        );
12163    }
12164
12165    #[test]
12166    fn bind_unsupplied_optional_then_empty_rest() {
12167        // `(a &optional b &rest c)` bound to just `1`: a=1, b=nil (absent),
12168        // c=() (nothing left). Both the optional default AND the empty-rest
12169        // contract hold in the same bind.
12170        let params = MacroParams {
12171            required: vec!["a".into()],
12172            optional: vec![OptionalParam::bare("b")],
12173            rest: Some("c".into()),
12174        };
12175        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12176        assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
12177    }
12178
12179    #[test]
12180    fn bind_rest_with_no_remaining_args_is_the_empty_list() {
12181        // Exactly-saturated required args + a rest that captures nothing →
12182        // the rest binds to the empty list, never errors. Mirrors the
12183        // splice contract `,@()` contributes nothing.
12184        let params = MacroParams {
12185            required: vec!["a".into()],
12186            optional: Vec::new(),
12187            rest: Some("c".into()),
12188        };
12189        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12190        assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
12191    }
12192
12193    #[test]
12194    fn bind_missing_required_errors_before_any_rest_collection() {
12195        // A required name with no arg at its position is a
12196        // `MissingMacroArg` — the gate fires during the required walk,
12197        // before the rest is ever collected.
12198        let params = MacroParams {
12199            required: vec!["a".into(), "b".into()],
12200            optional: Vec::new(),
12201            rest: Some("c".into()),
12202        };
12203        let err = params
12204            .bind("m", &[Sexp::int(1)])
12205            .expect_err("missing required `b` must error");
12206        assert!(
12207            matches!(err, LispError::MissingMacroArg { .. }),
12208            "expected MissingMacroArg, got: {err:?}"
12209        );
12210    }
12211
12212    #[test]
12213    fn bind_missing_required_errors_even_with_optional_present() {
12214        // An absent REQUIRED arg errors even when the param list has an
12215        // optional section: the required walk fires `MissingMacroArg` before
12216        // the optional arm (which would otherwise default to nil) is reached.
12217        // Required absence is an error; optional absence is a nil default.
12218        let params = MacroParams {
12219            required: vec!["a".into(), "b".into()],
12220            optional: vec![OptionalParam::bare("c")],
12221            rest: None,
12222        };
12223        let err = params
12224            .bind("m", &[Sexp::int(1)])
12225            .expect_err("missing required `b` must error before optional defaulting");
12226        assert!(
12227            matches!(err, LispError::MissingMacroArg { .. }),
12228            "expected MissingMacroArg, got: {err:?}"
12229        );
12230    }
12231
12232    #[test]
12233    fn bind_rest_less_params_reject_surplus_args() {
12234        // The rest-less binder REJECTS surplus call args via the
12235        // structural `TooManyMacroArgs { macro_name, expected, got }`
12236        // rejection — the call-site mirror of `RestParamTrailingTokens`
12237        // (the definition-site rejection lifted at the parse_params
12238        // boundary). Closes the asymmetry where the typed-entry
12239        // macro-call-gate rejected too-few-args loudly
12240        // (`MissingMacroArg`) but silently truncated too-many. `expected`
12241        // is the rest-less binder's fixed maximum arity
12242        // (`required.len() + optional.len()`); `got` is the actual
12243        // call-site arg count.
12244        let params = MacroParams {
12245            required: vec!["a".into()],
12246            optional: Vec::new(),
12247            rest: None,
12248        };
12249        let err = params
12250            .bind("m", &[Sexp::int(1), Sexp::int(2)])
12251            .expect_err("rest-less surplus must error");
12252        match err {
12253            LispError::TooManyMacroArgs {
12254                macro_name,
12255                expected,
12256                got,
12257            } => {
12258                assert_eq!(macro_name, "m");
12259                assert_eq!(expected, 1);
12260                assert_eq!(got, 2);
12261            }
12262            other => panic!("expected TooManyMacroArgs, got: {other:?}"),
12263        }
12264    }
12265
12266    // ── OptionalParam: per-param default forms — `&optional (x DEFAULT)` ──
12267    //
12268    // The `&optional` section now admits both bare-symbol entries (`x`) AND
12269    // list-form entries (`(x DEFAULT)`). The typed `OptionalParam.default:
12270    // Option<Sexp>` slot makes the per-param default a FIELD on each
12271    // optional entry, not a discipline a sibling `Vec<Sexp>` would have had
12272    // to maintain in lock-step with `Vec<String>`. These tests pin: the
12273    // parser admits both shapes side-by-side; the four malformed list-spec
12274    // shapes (empty / missing-default / extra-elements / non-symbol-name)
12275    // are rejected via `OptionalParamMalformed` with the typed
12276    // `OptionalParamMalformedReason`; the binder consults the default form
12277    // when the arg is absent and ignores it when supplied; and the end-to-
12278    // end expansion agrees between the bytecode and substitute strategies
12279    // (invariant 2 — free middle).
12280
12281    #[test]
12282    fn parse_params_admits_optional_list_spec_with_default() {
12283        // `(a &optional (b 5))` — one bare optional becomes
12284        // `OptionalParam { name: "b", default: Some(Int(5)) }`. The
12285        // surrounding `MacroParams` shape is otherwise identical.
12286        let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
12287        assert_eq!(
12288            params,
12289            MacroParams {
12290                required: vec!["a".into()],
12291                optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12292                rest: None,
12293            }
12294        );
12295    }
12296
12297    #[test]
12298    fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
12299        // `(a &optional b (c "x") d (e 9) &rest r)` — the optional section
12300        // interleaves bare and list-form specs. Each lands in its own
12301        // `OptionalParam` entry; `names()` still yields the flat
12302        // required-then-optional-then-rest order.
12303        let params =
12304            parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
12305        assert_eq!(
12306            params,
12307            MacroParams {
12308                required: vec!["a".into()],
12309                optional: vec![
12310                    OptionalParam::bare("b"),
12311                    OptionalParam::with_default("c", Sexp::string("x")),
12312                    OptionalParam::bare("d"),
12313                    OptionalParam::with_default("e", Sexp::int(9)),
12314                ],
12315                rest: Some("r".into()),
12316            }
12317        );
12318        assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
12319    }
12320
12321    #[test]
12322    fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
12323        // `(&optional (x (list 1 2)))` — the default form is itself a list.
12324        // Without an evaluator, the literal Sexp is parked verbatim into
12325        // `default`; the binder produces it for any absent call.
12326        let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
12327        let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12328        assert_eq!(
12329            params,
12330            MacroParams {
12331                required: Vec::new(),
12332                optional: vec![OptionalParam::with_default("x", want_default)],
12333                rest: None,
12334            }
12335        );
12336    }
12337
12338    #[test]
12339    fn parse_params_rejects_empty_list_optional_spec() {
12340        // `(&optional ())` — a zero-element list is the empty-list rejection.
12341        // Without the gate the loop would `as_symbol()` on a `Sexp::List` and
12342        // fall through to `NonSymbolParam`, which mis-classifies the failure
12343        // (this is a malformed DEFAULT-FORM spec, not a "param must be a
12344        // symbol" rejection).
12345        let err = parse_params(&read("&optional ()").unwrap())
12346            .expect_err("empty list optional spec must error");
12347        assert!(
12348            matches!(
12349                err,
12350                LispError::OptionalParamMalformed {
12351                    position: 1,
12352                    reason: crate::error::OptionalParamMalformedReason::EmptyList,
12353                    ..
12354                }
12355            ),
12356            "expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
12357        );
12358    }
12359
12360    #[test]
12361    fn parse_params_rejects_one_element_optional_list_as_missing_default() {
12362        // `(&optional (x))` — a one-element list. REJECTED with reason
12363        // `MissingDefault` rather than reinterpreted as `&optional x`, because
12364        // a parenthesized single-element spec is structurally ambiguous and
12365        // the bare-symbol form `x` IS the canonical "no default" shape.
12366        let err = parse_params(&read("&optional (x)").unwrap())
12367            .expect_err("one-element list optional spec must error");
12368        assert!(
12369            matches!(
12370                err,
12371                LispError::OptionalParamMalformed {
12372                    position: 1,
12373                    reason: crate::error::OptionalParamMalformedReason::MissingDefault,
12374                    ..
12375                }
12376            ),
12377            "expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
12378        );
12379    }
12380
12381    #[test]
12382    fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
12383        // `(&optional (x 5 6))` — a three-element list. CL's `(name default
12384        // supplied-p)` shape is not yet supported (no evaluator → no
12385        // supplied-p variable binding), so the third element is structurally
12386        // surplus. REJECTED with reason `ExtraElements{length: 3}`.
12387        let err = parse_params(&read("&optional (x 5 6)").unwrap())
12388            .expect_err("three-element list optional spec must error");
12389        assert!(
12390            matches!(
12391                err,
12392                LispError::OptionalParamMalformed {
12393                    position: 1,
12394                    reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
12395                    ..
12396                }
12397            ),
12398            "expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
12399        );
12400    }
12401
12402    #[test]
12403    fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
12404        // `(&optional (5 default))` — the name slot must be a symbol; a
12405        // numeric literal is REJECTED with reason `NonSymbolName`. Without
12406        // this branch the gate would silently populate
12407        // `OptionalParam.name` from a stringified non-symbol value (`"5"`),
12408        // breaking the invariant that param names are symbols.
12409        let err = parse_params(&read("&optional (5 default)").unwrap())
12410            .expect_err("non-symbol-name optional spec must error");
12411        assert!(
12412            matches!(
12413                err,
12414                LispError::OptionalParamMalformed {
12415                    position: 1,
12416                    reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
12417                    ..
12418                }
12419            ),
12420            "expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
12421        );
12422    }
12423
12424    #[test]
12425    fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
12426        // `((a 5))` — a list in the REQUIRED section is NOT a default-form
12427        // spec; default forms are an optional-section affordance. The gate
12428        // must fall through to `NonSymbolParam` (parity with the prior
12429        // behavior on lists in the required section), not silently admit
12430        // the list as a default-form spec.
12431        let err =
12432            parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
12433        assert!(
12434            matches!(err, LispError::NonSymbolParam { position: 0, .. }),
12435            "expected NonSymbolParam{{position: 0}}, got: {err:?}"
12436        );
12437    }
12438
12439    #[test]
12440    fn bind_unsupplied_optional_with_default_takes_the_default() {
12441        // `(a &optional (b 5))` bound to just `1`: a=1, b=5 (the declared
12442        // default), not nil. The default form is consulted ONLY when the
12443        // call ran out of args.
12444        let params = MacroParams {
12445            required: vec!["a".into()],
12446            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12447            rest: None,
12448        };
12449        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12450        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
12451    }
12452
12453    #[test]
12454    fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
12455        // `(&optional (b 5))` bound to `42`: b=42, NOT the default. A
12456        // supplied optional ALWAYS takes its arg; the default is the
12457        // absence-only fallback. Pins that the default form does not
12458        // shadow a supplied call arg.
12459        let params = MacroParams {
12460            required: Vec::new(),
12461            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12462            rest: None,
12463        };
12464        let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12465        assert_eq!(vals, vec![Sexp::int(42)]);
12466    }
12467
12468    #[test]
12469    fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
12470        // `(a &optional (b 5) c (d "z"))` bound to just `1`: a=1, b=5
12471        // (default), c=nil (bare floor), d="z" (default). The three
12472        // absence cases coexist in one bind: per-default fill, nil floor,
12473        // and a tail with a literal-string default.
12474        let params = MacroParams {
12475            required: vec!["a".into()],
12476            optional: vec![
12477                OptionalParam::with_default("b", Sexp::int(5)),
12478                OptionalParam::bare("c"),
12479                OptionalParam::with_default("d", Sexp::string("z")),
12480            ],
12481            rest: None,
12482        };
12483        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12484        assert_eq!(
12485            vals,
12486            vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
12487        );
12488    }
12489
12490    // ── OptionalParam::resolved_default: the absent-call binder accessor ──
12491    //
12492    // `resolved_default` lifts the `param.default.clone().unwrap_or(Sexp::Nil)`
12493    // two-arm fallback that previously inlined at `MacroParams::bind`'s
12494    // optional arm into ONE named accessor on the typed `OptionalParam`.
12495    // The constructor pair `bare` / `with_default` defines the typed
12496    // shapes of the `default` slot; this accessor names the symmetric
12497    // bound-value projection both shapes yield at the absence boundary.
12498    // Tests pin: (a) `bare(name).resolved_default()` is the `Sexp::Nil`
12499    // no-default floor; (b) `with_default(name, d).resolved_default()` is
12500    // `d.clone()`; (c) the projection is `Clone`-stable across repeated
12501    // calls (the typed `default` field is not consumed); (d) path-
12502    // uniformity at the binder — `bind`'s optional arm routes through
12503    // `resolved_default` for both shapes; (e) end-to-end through both
12504    // expansion strategies, the absent-call binding agrees.
12505
12506    #[test]
12507    fn resolved_default_is_nil_for_bare_optional() {
12508        // `OptionalParam::bare(name).default` is `None`, so
12509        // `resolved_default()` projects to `Sexp::Nil` — the CL
12510        // `&optional` no-default-form floor. Fail-before/pass-after: this
12511        // assert is meaningless pre-lift because the helper does not
12512        // exist; post-lift it pins the typed accessor's `Sexp::Nil` arm
12513        // at the named primitive. Sibling of `bare` itself: the
12514        // constructor defines the shape (`default: None`); the accessor
12515        // names the bound-value projection of that shape.
12516        let p = OptionalParam::bare("x");
12517        assert_eq!(p.resolved_default(), Sexp::Nil);
12518    }
12519
12520    #[test]
12521    fn resolved_default_clones_declared_default_for_with_default_optional() {
12522        // `OptionalParam::with_default(name, d).default` is `Some(d)`, so
12523        // `resolved_default()` projects to `d.clone()` — the declared
12524        // default form. Sibling of the bare-floor pin: the closed-set
12525        // `default: Option<Sexp>` slot's two shapes correspond 1:1 with
12526        // the two arms of `resolved_default`. Pins the closed-set
12527        // exhaustive coverage of `Option<Sexp>` × `{Some, None}`.
12528        let p = OptionalParam::with_default("x", Sexp::int(5));
12529        assert_eq!(p.resolved_default(), Sexp::int(5));
12530    }
12531
12532    #[test]
12533    fn resolved_default_clones_arbitrary_sexp_default_form() {
12534        // The declared default can be any `Sexp` — a literal list, a
12535        // keyword, a string, a quasi-quoted form — because v0 has no
12536        // evaluator and the typed slot parks the literal verbatim. Pin
12537        // that `resolved_default()` is faithful to the parked literal
12538        // regardless of shape: a regression that special-cases an arm
12539        // (e.g., projecting `Sexp::List(_)` to `Sexp::Nil`, or "normalizing"
12540        // a `Sexp::Quote`) fails here. The accessor is exactly
12541        // `default.clone()` for the `Some` arm — no shape rewriting.
12542        let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12543        let p = OptionalParam::with_default("x", arbitrary.clone());
12544        assert_eq!(p.resolved_default(), arbitrary);
12545    }
12546
12547    #[test]
12548    fn resolved_default_is_clone_stable_across_repeated_calls() {
12549        // The accessor takes `&self` and projects through `Clone`, so
12550        // repeated calls yield IDENTICAL values — the typed `default`
12551        // field is not consumed. Pins that the accessor is idempotent
12552        // for the same `OptionalParam`, which is the contract the binder
12553        // relies on across multiple `bind` invocations of the same
12554        // macro: every call that leaves the optional unfilled yields
12555        // the SAME bound value, never a partially-consumed shape. A
12556        // regression that converted the accessor to `self.default.take()`
12557        // (consuming the field) would still type-check at the call site
12558        // but would silently desync repeated absent-call bindings; this
12559        // test catches that drift.
12560        let p = OptionalParam::with_default("x", Sexp::string("hi"));
12561        let first = p.resolved_default();
12562        let second = p.resolved_default();
12563        assert_eq!(first, second);
12564        assert_eq!(first, Sexp::string("hi"));
12565    }
12566
12567    #[test]
12568    fn resolved_default_is_the_binders_absent_optional_projection() {
12569        // Path-uniformity pin at the binder boundary: `MacroParams::bind`'s
12570        // optional arm consults `param.resolved_default()` for any
12571        // absent slot. Two-arm coverage: a bare optional (`b`) binds to
12572        // `Sexp::Nil` via the `None` arm; a with-default optional
12573        // (`(c 5)`) binds to `Sexp::int(5)` via the `Some` arm. The
12574        // single bind call exercises both arms in one walk, and the
12575        // bound values vec is parallel to `names()` so position
12576        // checking pins the arm-to-slot mapping. A regression that
12577        // re-inlines the two-arm fallback at the binder, drifting it
12578        // independently from the accessor, would still type-check but
12579        // a future shape change to `resolved_default` (e.g., adding a
12580        // typed `&supplied-p` companion slot) would silently desync
12581        // the binder from the accessor — this test catches that drift.
12582        let params = MacroParams {
12583            required: Vec::new(),
12584            optional: vec![
12585                OptionalParam::bare("b"),
12586                OptionalParam::with_default("c", Sexp::int(5)),
12587            ],
12588            rest: None,
12589        };
12590        // Empty args → both optionals are absent → both arms fire.
12591        let vals = params.bind("m", &[]).unwrap();
12592        assert_eq!(vals.len(), 2);
12593        assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
12594        assert_eq!(
12595            vals[1],
12596            OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
12597        );
12598        // And the absolute identities pin the projection's arms:
12599        assert_eq!(vals[0], Sexp::Nil);
12600        assert_eq!(vals[1], Sexp::int(5));
12601    }
12602
12603    #[test]
12604    fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
12605        // End-to-end path-uniformity: a macro with both an `&optional
12606        // (g "hi")` (with-default) and an `&optional h` (bare) param
12607        // expands the same way under bytecode AND substitute
12608        // strategies, because both strategies route through the SHARED
12609        // `MacroParams::bind` which now consults `resolved_default`
12610        // for absent slots. Pins that the accessor's contract is
12611        // structurally observable at the strategy boundary — a
12612        // regression that bifurcated the accessor's behavior between
12613        // the two paths (impossible, since they share `bind`) would
12614        // surface here.
12615        let src = r#"
12616            (defmacro greet (n &optional (g "hi") h)
12617              `(list ,g ,n ,h))
12618            (greet world)
12619        "#;
12620        let expected = vec![Sexp::List(vec![
12621            Sexp::symbol("list"),
12622            Sexp::string("hi"),
12623            Sexp::symbol("world"),
12624            Sexp::Nil,
12625        ])];
12626        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12627        let substitute = Expander::new_substitute_only()
12628            .expand_program(read(src).unwrap())
12629            .unwrap();
12630        assert_eq!(
12631            bytecode, expected,
12632            "bytecode resolved_default expansion drifted"
12633        );
12634        assert_eq!(
12635            substitute, expected,
12636            "substitute resolved_default expansion drifted"
12637        );
12638        assert_eq!(
12639            bytecode, substitute,
12640            "the two strategies disagree on resolved_default expansion"
12641        );
12642    }
12643
12644    #[test]
12645    fn resolved_default_supplied_optional_does_not_consult_accessor() {
12646        // A SUPPLIED optional binds to its CALL ARG, never to the
12647        // accessor's projection. Pins the contract: `resolved_default`
12648        // is the absence-only fallback; a present arg shadows the
12649        // accessor at the binder. Sibling negative control to
12650        // `resolved_default_is_the_binders_absent_optional_projection`:
12651        // that test exercises the absence arm at every slot; this test
12652        // exercises the presence arm at every slot and proves the
12653        // accessor's `Sexp::int(5)` projection is NOT consulted when
12654        // the optional is supplied with `Sexp::int(42)`. A regression
12655        // that wired the binder to always consult the accessor (the
12656        // wrong direction — the default would shadow supplied args) is
12657        // caught here.
12658        let params = MacroParams {
12659            required: Vec::new(),
12660            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12661            rest: None,
12662        };
12663        let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12664        assert_eq!(vals, vec![Sexp::int(42)]);
12665        // And the accessor's would-be projection IS NOT the bound value:
12666        let p = OptionalParam::with_default("b", Sexp::int(5));
12667        assert_ne!(vals[0], p.resolved_default());
12668    }
12669
12670    #[test]
12671    fn optional_default_macro_expands_end_to_end_under_both_strategies() {
12672        // The end-to-end path: a macro with `&optional (g "hi")` expands to
12673        // the default literal when unsupplied, and to the supplied arg when
12674        // present. Both the bytecode and substitute strategies must agree
12675        // (invariant 2 — free middle); they share `MacroParams::bind`, so
12676        // the default arm lands once in `bind` and both strategies inherit
12677        // it unable to drift. This is the test the prior run (611a682)
12678        // signposted as the next-change-that-benefits.
12679        let src = r#"
12680            (defmacro greet (n &optional (g "hi"))
12681              `(list ,g ,n))
12682            (greet world)
12683            (greet world there)
12684        "#;
12685        let expected = vec![
12686            Sexp::List(vec![
12687                Sexp::symbol("list"),
12688                Sexp::string("hi"),
12689                Sexp::symbol("world"),
12690            ]),
12691            Sexp::List(vec![
12692                Sexp::symbol("list"),
12693                Sexp::symbol("there"),
12694                Sexp::symbol("world"),
12695            ]),
12696        ];
12697        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12698        let substitute = Expander::new_substitute_only()
12699            .expand_program(read(src).unwrap())
12700            .unwrap();
12701        assert_eq!(
12702            bytecode, expected,
12703            "bytecode optional-default expansion drifted"
12704        );
12705        assert_eq!(
12706            substitute, expected,
12707            "substitute optional-default expansion drifted"
12708        );
12709        assert_eq!(
12710            bytecode, substitute,
12711            "the two strategies disagree on optional-default expansion"
12712        );
12713    }
12714
12715    #[test]
12716    fn optional_macro_expands_end_to_end_under_both_strategies() {
12717        // The end-to-end path: a macro with an `&optional` param expands
12718        // correctly whether the optional is supplied or defaulted, and the
12719        // bytecode and substitute strategies agree (invariant 2 — free
12720        // middle). `,b` resolves to the supplied arg when present, to
12721        // `Sexp::Nil` when absent (CL's `&optional` default).
12722        let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
12723        // (cons 1 2) — optional supplied; (cons 3 <Nil>) — optional defaulted.
12724        // The defaulted slot is the canonical `Sexp::Nil`, distinct in the AST
12725        // from a reader-produced empty list `()` even though both Display as
12726        // `()`.
12727        let expected = vec![
12728            Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
12729            Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
12730        ];
12731        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12732        let substitute = Expander::new_substitute_only()
12733            .expand_program(read(src).unwrap())
12734            .unwrap();
12735        assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
12736        assert_eq!(
12737            substitute, expected,
12738            "substitute optional expansion drifted"
12739        );
12740        assert_eq!(
12741            bytecode, substitute,
12742            "the two strategies disagree on optional expansion"
12743        );
12744    }
12745
12746    // ── MacroDef::template_body: the shared body-projection primitive ──
12747    //
12748    // `template_body` lifts the `match &def.body { Sexp::Quasiquote(inner)
12749    // => inner.as_ref(), other => other }` inline peel — present
12750    // byte-identically at the bytecode (`compile_template`) AND substitute
12751    // (`apply`'s fallback) path entries — into ONE named projection both
12752    // strategies share. The existing `compiled_template_matches_substitute
12753    // _path` and `expansion_layers_agree_on_output_and_cache_wins` tests
12754    // are the path-uniformity guards covering the SHARED-PROJECTION shape;
12755    // the four tests below pin the projection's contract DIRECTLY:
12756    // (a) a quasi-quoted body unwraps to the inner; (b) a non-quasi-quoted
12757    // body returns the body verbatim; (c) the borrow is rooted in the
12758    // body field (single-level peel); (d) the projection is the same
12759    // `&Sexp` both strategies route on (so a regression that drifts the
12760    // body-peel from one strategy to the other becomes a type-level
12761    // change at this helper, not a silent two-site divergence).
12762
12763    #[test]
12764    fn template_body_unwraps_outer_quasiquote_to_inner() {
12765        // The canonical authoring shape: `(defmacro f (a) `(list ,a))` —
12766        // the reader wraps the `` ` `` form into `Sexp::Quasiquote(inner)`,
12767        // and `template_body` peels the outer marker. The returned `&Sexp`
12768        // is the inner walker-payload both expansion strategies consume.
12769        let inner = Sexp::List(vec![
12770            Sexp::symbol("list"),
12771            Sexp::Unquote(Box::new(Sexp::symbol("a"))),
12772        ]);
12773        let def = MacroDef {
12774            name: "f".into(),
12775            params: MacroParams::default(),
12776            body: Sexp::Quasiquote(Box::new(inner.clone())),
12777        };
12778        assert_eq!(def.template_body(), &inner);
12779    }
12780
12781    #[test]
12782    fn template_body_returns_non_quasiquote_body_verbatim() {
12783        // A body authored WITHOUT the outer `` ` `` affordance — a bare
12784        // `Sexp::List` body — returns verbatim. The "other" arm of the
12785        // legacy match. Pin parity with the pre-lift code path so a
12786        // regression that drifts the body-peel into "always peel
12787        // something" (which would break literal-body macros) fails here.
12788        let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
12789        let def = MacroDef {
12790            name: "f".into(),
12791            params: MacroParams::default(),
12792            body: body.clone(),
12793        };
12794        assert_eq!(def.template_body(), &body);
12795        // Atom bodies too — the projection is a single-arm match, not a
12796        // recursive descent. A `Sexp::Atom` body is its own template payload.
12797        let atom_def = MacroDef {
12798            name: "g".into(),
12799            params: MacroParams::default(),
12800            body: Sexp::symbol("nil-template"),
12801        };
12802        assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
12803    }
12804
12805    #[test]
12806    fn template_body_peels_single_level_only() {
12807        // A nested `` ``form `` body — `Sexp::Quasiquote(Box::new(
12808        // Sexp::Quasiquote(...)))` — unwraps ONE outer quasi-quote and
12809        // returns the inner `Sexp::Quasiquote(...)` as-is. The v0 module
12810        // preamble declares "Nested quasi-quotes: Not yet supported"; the
12811        // single-level peel matches the legacy inline match's posture
12812        // (which only matched ONE outer `Sexp::Quasiquote(_)` arm, not a
12813        // recursive loop). A regression that drifts to a recursive peel
12814        // would project too far and the inner `Sexp::Quasiquote` marker —
12815        // which the substitute walker treats as an atomic leaf returned
12816        // verbatim (line ~830, `Sexp::Quote(_) | Sexp::Quasiquote(_)
12817        // => Ok(form.clone())`) — would silently disappear from the
12818        // expansion's emitted form. Pin the single-level contract here.
12819        let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
12820        let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
12821        let def = MacroDef {
12822            name: "nested".into(),
12823            params: MacroParams::default(),
12824            body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
12825        };
12826        // Outer peel returns the INNER quasi-quote, NOT its inner payload.
12827        assert_eq!(def.template_body(), &inner_qq);
12828        assert_ne!(def.template_body(), &inner_payload);
12829    }
12830
12831    #[test]
12832    fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
12833        // A `Sexp::Quote(_)` body — not a `Sexp::Quasiquote(_)` — returns
12834        // verbatim through the "other" arm. The two close-cousin shapes
12835        // share an outer-marker character (`'` vs `` ` ``) at the reader
12836        // boundary but differ semantically: a `Quote` body is a literal
12837        // template (no substitution semantics), a `Quasiquote` body is a
12838        // substitution-walker entry. A regression that conflated the two
12839        // — peeling Quote as if it were Quasiquote — would silently turn
12840        // every quoted-body macro into a template-walked macro. Pin the
12841        // discrimination.
12842        let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
12843        let body = Sexp::Quote(Box::new(inner.clone()));
12844        let def = MacroDef {
12845            name: "quoted".into(),
12846            params: MacroParams::default(),
12847            body: body.clone(),
12848        };
12849        assert_eq!(def.template_body(), &body);
12850        // The inner is NOT what comes back — only Quasiquote-bodied macros
12851        // would peel to the inner.
12852        assert_ne!(def.template_body(), &inner);
12853    }
12854
12855    #[test]
12856    fn template_body_is_the_shared_projection_both_strategies_walk() {
12857        // End-to-end path-uniformity at the projection boundary: a macro
12858        // authored with the canonical quasi-quoted body expands
12859        // IDENTICALLY under bytecode and substitute strategies because
12860        // both route their walker's body through `template_body()` — the
12861        // SAME `&Sexp` projection. Sibling of
12862        // `compiled_template_matches_substitute_path` (which observes
12863        // agreement on the EMITTED form); this test pins agreement on
12864        // the projection ENTRY: `compile_template` and the substitute
12865        // fallback now consume the same `&Sexp` (`def.template_body()`),
12866        // never a divergent inline match the two paths could regress
12867        // independently.
12868        let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
12869        let expected = vec![Sexp::List(vec![
12870            Sexp::symbol("list"),
12871            Sexp::int(5),
12872            Sexp::int(5),
12873        ])];
12874        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12875        let substitute = Expander::new_substitute_only()
12876            .expand_program(read(src).unwrap())
12877            .unwrap();
12878        assert_eq!(bytecode, expected, "bytecode body-projection drifted");
12879        assert_eq!(substitute, expected, "substitute body-projection drifted");
12880        assert_eq!(
12881            bytecode, substitute,
12882            "the two strategies disagree on the body-projection's emission"
12883        );
12884    }
12885
12886    // ── Expander::expand: macro-call dispatch routes through `as_call_to_any` ──
12887    //
12888    // `expand` lifts its macro-call recognition to route through the
12889    // substrate's typed-decoded call decomposition: `as_call_to_any(|h|
12890    // self.macros.get(h))` answers "is this form an invocation of any
12891    // registered macro?" in ONE structural query on the Sexp algebra,
12892    // and a HashMap-backed lookup as its classifier. Sibling consumer to
12893    // `macro_def_from` (the typed-macro-definition dispatcher already
12894    // routing through `as_call_to_any(MacroDefHead::from_keyword)` with
12895    // a closed-set enum classifier). With both in place, BOTH dispatch
12896    // sites in the macro expander project through the SAME family
12897    // primitive — each binding the classifier that fits its candidate
12898    // set. The tests below pin the consumer's path-uniformity contract
12899    // at the new boundary: a hand-rolled `as_call_to_any(|h| macros.get
12900    // (h))` dispatch observes the SAME `(def, args)` decomposition the
12901    // `Expander::expand` consumer routes through.
12902
12903    #[test]
12904    fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
12905        // Structural identity: on a registered-macro call, the consumer's
12906        // expansion is observably equivalent to: classify the form via
12907        // `as_call_to_any(|h| macros.get(h))` → some `(def, args)` →
12908        // apply the def to args. Pin path-uniformity: a hand-rolled
12909        // `as_call_to_any` lookup against the same registry the expander
12910        // walks produces the SAME `MacroDef` reference for the SAME
12911        // input form. A regression that drifts the consumer back to an
12912        // inline `as_list + as_call + macros.get` chain (which would
12913        // fragment the family adoption) is caught structurally — the
12914        // hand-rolled `as_call_to_any` and the consumer's dispatch must
12915        // observe the same decomposition.
12916        let mut e = Expander::new();
12917        e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12918            .unwrap();
12919        let call_form = parse("(wrap 42)");
12920
12921        // Hand-rolled family-primitive lookup mirrors the lifted consumer.
12922        let (def_via_family, args_via_family) = call_form
12923            .as_call_to_any(|h| e.macros.get(h))
12924            .expect("registered macro call must decompose via as_call_to_any");
12925        assert_eq!(def_via_family.name, "wrap");
12926        assert_eq!(args_via_family, &[Sexp::int(42)]);
12927
12928        // Consumer's expand observes the SAME decomposition: the expanded
12929        // form is `(list 42 42)`, derived from the SAME def + args the
12930        // hand-rolled lookup found. Path-uniform with the family
12931        // primitive at the dispatch boundary.
12932        let expanded = e.expand(&call_form).unwrap();
12933        assert_eq!(
12934            expanded,
12935            Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
12936        );
12937    }
12938
12939    #[test]
12940    fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
12941        // Path-uniformity for the non-registered-head path: `as_call_to_any
12942        // (|h| macros.get(h))` returns `None` for a call whose head ISN'T
12943        // a registered macro, and the consumer falls through to the
12944        // children-walk (which expands any nested macro calls). Pin
12945        // both halves: the hand-rolled lookup returns `None`, AND the
12946        // consumer's expand walks into the children. A regression that
12947        // accidentally short-circuits the children-walk for non-macro
12948        // calls (e.g. by treating `as_call_to_any` = `None` as
12949        // "non-expandable" globally) would fail here.
12950        let mut e = Expander::new();
12951        e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12952            .unwrap();
12953        let outer = parse("(foo (wrap 5))");
12954
12955        // Hand-rolled family-primitive lookup rejects the outer head.
12956        assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
12957
12958        // Consumer walks children — the inner `(wrap 5)` IS a macro call
12959        // and expands to `(list 5 5)`; the outer `foo` head is preserved.
12960        let expanded = e.expand(&outer).unwrap();
12961        assert_eq!(
12962            expanded,
12963            Sexp::List(vec![
12964                Sexp::symbol("foo"),
12965                Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
12966            ])
12967        );
12968    }
12969
12970    #[test]
12971    fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
12972        // Path-uniformity for the non-call path: every shape `as_call`
12973        // rejects (atoms across all 6 kinds, Nil, Quote-family wrappers)
12974        // ALSO routes past `as_call_to_any` into the `as_list()`
12975        // fallthrough, where the not-a-list arm returns `form.clone()`
12976        // verbatim. Pin both halves: the hand-rolled lookup rejects
12977        // every non-call shape regardless of decoder, AND the consumer
12978        // preserves each shape unchanged. A regression that drifts the
12979        // consumer's dispatch order (e.g. checking `as_list()` BEFORE
12980        // `as_call_to_any` in a way that mis-handles Quote-family
12981        // wrappers) would fail here.
12982        let e = Expander::new();
12983        let shapes = [
12984            Sexp::symbol("foo"),
12985            Sexp::int(5),
12986            Sexp::keyword("k"),
12987            Sexp::string("s"),
12988            Sexp::boolean(true),
12989            Sexp::float(1.5),
12990            Sexp::Nil,
12991            Sexp::Quote(Box::new(Sexp::symbol("x"))),
12992            Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
12993            Sexp::Unquote(Box::new(Sexp::symbol("x"))),
12994            Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
12995        ];
12996        for s in &shapes {
12997            // Hand-rolled family-primitive lookup rejects non-call shapes
12998            // even for a promiscuous decoder — the call-shape gate fires
12999            // BEFORE the decoder runs.
13000            assert!(
13001                s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
13002                "non-call shape must yield None for as_call_to_any: {s}"
13003            );
13004            // Consumer preserves the shape verbatim — the not-a-list
13005            // arm at the fallthrough returns `form.clone()`.
13006            assert_eq!(
13007                e.expand(s).unwrap(),
13008                s.clone(),
13009                "non-call shape must round-trip unchanged through expand: {s}"
13010            );
13011        }
13012    }
13013
13014    #[test]
13015    fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
13016        // The empty list `()` has no operator and no children. Pin that
13017        // `as_call_to_any` rejects it (no head to feed the decoder), the
13018        // consumer falls through to `as_list()` which returns `Some(&[])`,
13019        // and the children-walk emits `Sexp::List(vec![])` (an empty
13020        // list, not `form.clone()` of `Sexp::List(vec![])` — both happen
13021        // to be observationally identical, but the path is the
13022        // children-walk arm, NOT the not-a-list arm). Path-uniformity
13023        // gate for the singleton-list edge case the `compile_named_from_
13024        // forms` rejection chain relies on `as_call_to(KEYWORD)` to yield
13025        // `Some(&[])` for — same posture, different family member.
13026        let e = Expander::new();
13027        let empty = Sexp::List(vec![]);
13028
13029        // Hand-rolled family-primitive lookup rejects the empty list.
13030        assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
13031
13032        // Consumer walks children (zero of them) — output is the empty
13033        // list, same as input.
13034        assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
13035    }
13036
13037    // ── Expander::expand_and_collect_calls_to: program-level walk ───────
13038    //
13039    // `expand_and_collect_calls_to(forms, keyword, project)` composes the
13040    // expander's program-level expansion (`expand_program`) with the
13041    // substrate's slice-side typed-keyword projection (`iter_calls_to`)
13042    // and a caller-supplied per-form mapper into ONE method on the
13043    // `Expander` surface. Both `compile_typed::<T>` and
13044    // `compile_named_from_forms::<T>` (compile.rs) route through it; the
13045    // tests below pin its contract directly. The existing compile.rs
13046    // dispatch tests are the path-uniformity guards proving the two
13047    // production sites route through it without behavior drift.
13048
13049    #[test]
13050    fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
13051        // Three forms — two match `defmonitor`, one matches `defalert`.
13052        // The mapper records `args.len()` from each matching form's tail.
13053        // Pin: only the two `defmonitor` matches flow through `project`,
13054        // in source order (so the recorded lengths are 2, 1).
13055        let forms = vec![
13056            Sexp::List(vec![
13057                Sexp::symbol("defmonitor"),
13058                Sexp::keyword("name"),
13059                Sexp::string("first"),
13060            ]),
13061            Sexp::List(vec![
13062                Sexp::symbol("defalert"),
13063                Sexp::keyword("name"),
13064                Sexp::string("not-a-match"),
13065            ]),
13066            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
13067        ];
13068        let mut e = Expander::new();
13069        let lengths: Vec<usize> = e
13070            .expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
13071            .expect("matching forms must compose");
13072        assert_eq!(lengths, vec![2, 1]);
13073    }
13074
13075    #[test]
13076    fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13077        // Path-uniformity for the soft-projection posture inherited from
13078        // `iter_calls_to`: non-matching forms are skipped silently, and
13079        // `project` is never invoked on them. Pin via a counter the
13080        // closure increments per invocation — the counter is the
13081        // observable that distinguishes "skipped silently" from
13082        // "projected to a no-op".
13083        let forms = vec![
13084            Sexp::symbol("bare-atom"),
13085            Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
13086            Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
13087            Sexp::List(vec![]),
13088            Sexp::Nil,
13089        ];
13090        let mut count = 0usize;
13091        let mut e = Expander::new();
13092        let out: Vec<()> = e
13093            .expand_and_collect_calls_to(forms, "defmonitor", |_args| {
13094                count += 1;
13095                Ok(())
13096            })
13097            .expect("non-matching-only slice must collect to empty Vec");
13098        assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13099        assert_eq!(count, 0, "project must never run for non-matching forms");
13100    }
13101
13102    #[test]
13103    fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13104        // Three matching forms; the closure errors on the SECOND. Pin: the
13105        // collect's `Result<Vec<_>, _>` short-circuit fires at the second
13106        // form's error, the third form's `project` is NEVER invoked, and
13107        // the returned error is exactly the one the closure raised. This
13108        // mirrors `compile_named_from_forms`'s short-circuit on the first
13109        // `NamedFormMissingName` / `NamedFormNonSymbolName` / typed-entry
13110        // kwargs rejection.
13111        let forms = vec![
13112            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13113            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
13114            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13115        ];
13116        let mut seen = Vec::new();
13117        let mut e = Expander::new();
13118        let err = e
13119            .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
13120                let n = args[0].as_int().expect("test args are ints");
13121                seen.push(n);
13122                if n == 2 {
13123                    Err(LispError::Compile {
13124                        form: "test".to_string(),
13125                        message: "stop at two".to_string(),
13126                    })
13127                } else {
13128                    Ok(())
13129                }
13130            })
13131            .expect_err("project's error must short-circuit collect");
13132        // Only the first two forms were ever projected — the third
13133        // never reached `project`.
13134        assert_eq!(seen, vec![1, 2]);
13135        assert!(
13136            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13137            "short-circuit must propagate the project's error verbatim, got {err:?}"
13138        );
13139    }
13140
13141    #[test]
13142    fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
13143        // The `expand_program` step runs BEFORE `iter_calls_to` walks
13144        // anything — so an `expand_program` error (e.g., a malformed
13145        // `defmacro` head) must short-circuit the entire composition
13146        // and `project` must NEVER run. Pin via the closure-running
13147        // counter: an error from the FIRST step never invokes `project`.
13148        // `(defmacro 5 (x) `,x)` — the macro NAME slot is an int, not a
13149        // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13150        // fires during `expand_program`.
13151        let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
13152        let mut count = 0usize;
13153        let mut e = Expander::new();
13154        let err = e
13155            .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
13156                count += 1;
13157                Ok(())
13158            })
13159            .expect_err("expand_program error must short-circuit before project");
13160        assert_eq!(
13161            count, 0,
13162            "project must never run when expand_program errors"
13163        );
13164        // Sanity: the error IS the expand_program-stage rejection, NOT
13165        // a project-stage error (path-uniformity for the ordering).
13166        let rendered = format!("{err}");
13167        assert!(
13168            rendered.contains("NAME") || rendered.contains("symbol"),
13169            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13170        );
13171    }
13172
13173    #[test]
13174    fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
13175        // Boundary: empty forms slice yields zero items regardless of
13176        // keyword. Pin the degenerate boundary — empty in, empty out —
13177        // and the closure is never invoked. Sibling of
13178        // `iter_calls_to_yields_nothing_for_empty_slice` in ast.rs,
13179        // one level up the composition: the program-level walk is
13180        // fused-empty when `expand_program` yields an empty `Vec<Sexp>`.
13181        let mut count = 0usize;
13182        let mut e = Expander::new();
13183        let out: Vec<()> = e
13184            .expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
13185                count += 1;
13186                Ok(())
13187            })
13188            .expect("empty forms is not an error");
13189        assert!(out.is_empty());
13190        assert_eq!(count, 0);
13191    }
13192
13193    #[test]
13194    fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13195        // Critical ordering: `expand_program` runs BEFORE the keyword
13196        // filter, so a `defmacro` whose expansion produces a form with
13197        // a matching head IS visible to `project`. Pin the pipeline
13198        // order: `(defmacro emit-monitor (n) `(defmonitor :name ,n))
13199        //         (emit-monitor "alpha") (defmonitor :name "beta")`
13200        // expands the macro call to `(defmonitor :name "alpha")` and
13201        // the keyword walk then sees TWO `defmonitor` forms (the
13202        // macro-emitted one AND the directly-authored one). A
13203        // pre-expansion filter would miss the macro-emitted form.
13204        let forms = read(
13205            "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13206             (emit-monitor \"alpha\")
13207             (defmonitor :name \"beta\")",
13208        )
13209        .unwrap();
13210        let mut e = Expander::new();
13211        let names: Vec<String> = e
13212            .expand_and_collect_calls_to(forms, "defmonitor", |args| {
13213                // Each defmonitor form's tail is `(:name "X")`; project
13214                // the second element's string to capture the name.
13215                Ok(args[1].as_string().unwrap().to_string())
13216            })
13217            .expect("macroexpanded + directly-authored forms must both flow");
13218        assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13219    }
13220
13221    #[test]
13222    fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13223        // The `keyword` argument flows straight into `iter_calls_to`'s
13224        // head-comparison gate — a regression that drifts the keyword
13225        // (e.g., uses `T::KEYWORD` from a different `T` at the helper,
13226        // or lowercases / trims the keyword en route) fails here. Pin
13227        // by passing TWO different keywords against the SAME forms
13228        // input and asserting each picks up only its matching forms.
13229        let forms = vec![
13230            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13231            Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
13232            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13233            Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
13234        ];
13235
13236        let mut e = Expander::new();
13237        let monitors: Vec<i64> = e
13238            .expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
13239                Ok(args[0].as_int().unwrap())
13240            })
13241            .unwrap();
13242        assert_eq!(monitors, vec![1, 3]);
13243
13244        let mut e2 = Expander::new();
13245        let alerts: Vec<i64> = e2
13246            .expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
13247                Ok(args[0].as_int().unwrap())
13248            })
13249            .unwrap();
13250        assert_eq!(alerts, vec![2]);
13251
13252        // A keyword nothing matches collects to the empty Vec — same
13253        // shape as the empty-forms case at the slice level.
13254        let mut e3 = Expander::new();
13255        let none: Vec<i64> = e3
13256            .expand_and_collect_calls_to(forms, "missing-keyword", |args| {
13257                Ok(args[0].as_int().unwrap())
13258            })
13259            .unwrap();
13260        assert!(none.is_empty());
13261    }
13262
13263    #[test]
13264    fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
13265        // Structural identity: for any (forms, keyword, project) triple
13266        // whose pieces succeed, the method's result equals the inlined
13267        // pre-lift pipeline `expand_program + iter_calls_to + map +
13268        // collect`. Pin shape AND ordering on a mixed forms input
13269        // (matching + non-matching + macroexpand-emitted matches) so a
13270        // regression that drifts the method's pipeline from the inlined
13271        // closed-form fails here. This is the lift's load-bearing
13272        // path-uniformity gate — the SAME assertion that holds for
13273        // both production consumers (compile.rs) holds at the
13274        // primitive's contract level.
13275        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13276                   (foo :idx 1)
13277                   (emit-foo 2)
13278                   (bar :idx 99)
13279                   (foo :idx 3)";
13280        let forms = read(src).unwrap();
13281
13282        // Inlined pre-lift pipeline.
13283        let mut exp_inline = Expander::new();
13284        let expanded = exp_inline.expand_program(forms.clone()).unwrap();
13285        let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
13286            .map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
13287            .collect::<Result<Vec<_>>>()
13288            .unwrap();
13289
13290        // Method pipeline.
13291        let mut exp_method = Expander::new();
13292        let via_method: Vec<i64> = exp_method
13293            .expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13294            .unwrap();
13295
13296        assert_eq!(via_inline, via_method);
13297        assert_eq!(via_inline, vec![1, 2, 3]);
13298    }
13299
13300    // ── Expander::expand_and_collect_calls_to_any: typed-decoded sibling ──
13301    //
13302    // The typed-decoded classifier sibling of `expand_and_collect_calls_to`:
13303    // composes `expand_program` with `iter_calls_to_any` and a per-form
13304    // mapper that receives BOTH the typed witness AND the args tail.
13305    // Closes the (keyword, classifier) 2×2 of expander-surface
13306    // compose-on-iter projections at the typed-decoded corner the prior
13307    // runs' slice-side `iter_calls_to_any` lift left open. The keyword
13308    // sibling now ROUTES through this primitive via a constant-classifier
13309    // composition — the tests below pin both the typed-decoded primitive's
13310    // contract directly AND the routing identity binding the keyword
13311    // sibling to it.
13312    //
13313    // The existing `expand_and_collect_calls_to_*` tests above remain
13314    // load-bearing — they pin the keyword sibling's contract through
13315    // its ROUTED implementation, so a regression that drifts the routing
13316    // composition surfaces at the existing keyword tests too.
13317
13318    #[test]
13319    fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
13320    {
13321        // The typed-decoded primitive's happy path: a closed-set
13322        // classifier decodes head symbols to a typed enum, and the
13323        // per-form projection receives `(decoded, args)` for every
13324        // matched form in source order. Sweep across THREE distinct
13325        // classifier outcomes — `Foo` / `Bar` / `Baz` — interleaved with
13326        // a non-matching form to pin both the typed-witness threading
13327        // AND the source-order yield AND the rejection of non-classifier
13328        // forms in ONE assertion. The decoder runs once per call form
13329        // (`FnMut`); the projection runs once per matched form.
13330        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
13331        enum Op {
13332            Foo,
13333            Bar,
13334            Baz,
13335        }
13336        let src = "(foo 1)
13337                   (bar 2)
13338                   (other 99)
13339                   (baz 3)
13340                   (foo 4)";
13341        let forms = read(src).unwrap();
13342        let mut e = Expander::new();
13343        let pairs: Vec<(Op, i64)> = e
13344            .expand_and_collect_calls_to_any(
13345                forms,
13346                |h| match h {
13347                    "foo" => Some(Op::Foo),
13348                    "bar" => Some(Op::Bar),
13349                    "baz" => Some(Op::Baz),
13350                    _ => None,
13351                },
13352                |op, args| Ok((op, args[0].as_int().unwrap())),
13353            )
13354            .unwrap();
13355        assert_eq!(
13356            pairs,
13357            vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
13358        );
13359    }
13360
13361    #[test]
13362    fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13363        // The soft-projection contract — every shape `iter_calls_to_any`
13364        // rejects (non-list, empty list, list whose head is not a
13365        // symbol, list whose head decodes through the classifier to
13366        // `None`) skips the projection silently. Pin a deliberately-
13367        // panicking projection so the assertion fires loudly if a
13368        // non-matching form reaches it. The decoder accepts only
13369        // `"defmonitor"`; every other shape (atom, list whose head is a
13370        // keyword, list whose head is a symbol the decoder rejects)
13371        // short-circuits before the projection runs.
13372        let src = r#":bare-keyword
13373                     "bare-string"
13374                     42
13375                     ()
13376                     (foo bar)
13377                     (defmonitor :name "matches")"#;
13378        let forms = read(src).unwrap();
13379        let mut e = Expander::new();
13380        let lengths: Vec<usize> = e
13381            .expand_and_collect_calls_to_any(
13382                forms,
13383                |h| (h == "defmonitor").then_some(()),
13384                |(), args| {
13385                    // The projection must run EXACTLY once — for the
13386                    // single matched form. Any non-matching form that
13387                    // reaches the projection panics here.
13388                    assert_eq!(args.len(), 2, "projection ran on non-matching form");
13389                    Ok(args.len())
13390                },
13391            )
13392            .unwrap();
13393        assert_eq!(lengths, vec![2]);
13394    }
13395
13396    #[test]
13397    fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13398        // The Result projection's short-circuit contract — when the
13399        // projection returns `Err` on a matched form, the walk
13400        // short-circuits and subsequent matched forms are NOT projected.
13401        // Pin the source-order short-circuit via a counter the
13402        // projection increments before deciding to fail: the counter
13403        // sits at exactly the index of the failing form (the second
13404        // match), proving the third match never reached the projection.
13405        let src = "(foo 1) (foo 2) (foo 3)";
13406        let forms = read(src).unwrap();
13407        let mut count = 0usize;
13408        let mut e = Expander::new();
13409        let err = e
13410            .expand_and_collect_calls_to_any::<i64, _, _, ()>(
13411                forms,
13412                |h| (h == "foo").then_some(()),
13413                |(), args| {
13414                    count += 1;
13415                    let v = args[0].as_int().unwrap();
13416                    if v == 2 {
13417                        return Err(crate::error::LispError::Missing("test-failure"));
13418                    }
13419                    Ok(v)
13420                },
13421            )
13422            .expect_err("projection must short-circuit on first Err");
13423        assert_eq!(
13424            count, 2,
13425            "projection must have run on first AND failing form, then stopped",
13426        );
13427        // Sanity: the error is the project-stage rejection — the same
13428        // typed `LispError` the projection returned, propagated verbatim.
13429        assert!(
13430            matches!(err, crate::error::LispError::Missing("test-failure")),
13431            "expected the projection's typed Err verbatim, got {err:?}",
13432        );
13433    }
13434
13435    #[test]
13436    fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
13437        // Ordering contract — `expand_program` runs BEFORE
13438        // `iter_calls_to_any` walks the slice. A `(defmacro …)` form
13439        // that emits a classifier-decoded body must have its expanded
13440        // body decoded by the classifier (not the unexpanded macro-call
13441        // head). Pin the ordering with a macro `(emit-foo n)` that
13442        // expands to `(foo :idx n)` — the classifier sees the expanded
13443        // `foo` head, not `emit-foo`. Mirrors the parallel keyword test
13444        // `expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword`
13445        // — the SAME composition contract holds on the typed-decoded
13446        // sibling.
13447        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13448                   (foo :idx 1)
13449                   (emit-foo 2)
13450                   (bar :idx 99)
13451                   (foo :idx 3)";
13452        let forms = read(src).unwrap();
13453        let mut e = Expander::new();
13454        // Classifier returns `()` for "foo" only; the keyword sibling's
13455        // routing identity is the same composition we exercise here
13456        // directly.
13457        let idxs: Vec<i64> = e
13458            .expand_and_collect_calls_to_any(
13459                forms,
13460                |h| (h == "foo").then_some(()),
13461                |(), args| Ok(args[1].as_int().unwrap()),
13462            )
13463            .unwrap();
13464        // 1 from the literal call, 2 from the macro-emitted call, 3 from
13465        // the final literal call. The macro-emitted `(foo :idx 2)` is
13466        // present BECAUSE `expand_program` ran first.
13467        assert_eq!(idxs, vec![1, 2, 3]);
13468    }
13469
13470    #[test]
13471    fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
13472        // The `FnMut` constraint — the slice walk calls the classifier
13473        // once per call form, and a decoder that captures mutable state
13474        // (a counter, a registry cache, a visited-set) maintains that
13475        // state across the walk. Pin the `FnMut` contract with a counter
13476        // closure that increments per shape-gate-passing form (calls
13477        // whose head is a symbol), asserting the counter equals the
13478        // total call count regardless of whether the classifier accepts
13479        // each form. Mirrors the parallel slice-side test
13480        // `iter_calls_to_any_admits_fnmut_classifier_maintaining_state_across_batch_walk`
13481        // — the SAME `FnMut` contract holds on the expander surface.
13482        let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
13483        let forms = read(src).unwrap();
13484        let mut e = Expander::new();
13485        let mut classifier_calls = 0usize;
13486        let projected: Vec<i64> = e
13487            .expand_and_collect_calls_to_any(
13488                forms,
13489                |h| {
13490                    classifier_calls += 1;
13491                    (h == "foo").then_some(())
13492                },
13493                |(), args| Ok(args[0].as_int().unwrap()),
13494            )
13495            .unwrap();
13496        assert_eq!(
13497            classifier_calls, 5,
13498            "classifier must run once per call form (5 forms, all calls with symbol heads)",
13499        );
13500        assert_eq!(
13501            projected,
13502            vec![1, 3, 5],
13503            "projection must run only for classifier-accepted forms in source order",
13504        );
13505    }
13506
13507    #[test]
13508    fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
13509    ) {
13510        // Structural identity: the keyword sibling
13511        // `expand_and_collect_calls_to` ROUTES through the typed-decoded
13512        // primitive via a constant-classifier composition — the
13513        // post-lift composition law binding the two siblings:
13514        //
13515        //   expand_and_collect_calls_to(forms, k, p) ==
13516        //       expand_and_collect_calls_to_any(forms,
13517        //           |h| (h == k).then_some(()),
13518        //           |(), args| p(args))
13519        //
13520        // Pin shape AND ordering across THREE representative keyword
13521        // values: one that matches multiple forms, one that matches
13522        // none, one that matches exactly one form. Same mixed input
13523        // (literal + macroexpand-emitted + non-matching) the keyword
13524        // path-uniformity test exercises, so the routing identity holds
13525        // on the SAME input shape the keyword sibling's contract pins.
13526        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13527                   (foo :idx 1)
13528                   (emit-foo 2)
13529                   (bar :idx 99)
13530                   (foo :idx 3)";
13531        let forms = read(src).unwrap();
13532
13533        for keyword in ["foo", "bar", "absent"] {
13534            let mut exp_keyword = Expander::new();
13535            let via_keyword: Vec<i64> = exp_keyword
13536                .expand_and_collect_calls_to(forms.clone(), keyword, |args| {
13537                    Ok(args[1].as_int().unwrap())
13538                })
13539                .unwrap();
13540            let mut exp_classifier = Expander::new();
13541            let via_classifier: Vec<i64> = exp_classifier
13542                .expand_and_collect_calls_to_any(
13543                    forms.clone(),
13544                    |h| (h == keyword).then_some(()),
13545                    |(), args| Ok(args[1].as_int().unwrap()),
13546                )
13547                .unwrap();
13548            assert_eq!(
13549                via_keyword, via_classifier,
13550                "routing identity drifted for keyword {keyword:?}",
13551            );
13552        }
13553    }
13554
13555    #[test]
13556    fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
13557    {
13558        // The expand_program-stage short-circuit contract — when
13559        // `expand_program` rejects (e.g. a `(defmacro …)` form whose
13560        // param list is malformed), the walk short-circuits BEFORE the
13561        // classifier or projection runs. Pin the ordering with a
13562        // deliberately-panicking classifier AND projection: any
13563        // post-expand-program execution fires the panic. Mirrors the
13564        // parallel keyword test
13565        // `expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs`
13566        // — the SAME composition contract holds on the typed-decoded
13567        // sibling.
13568        // `(defmacro 5 (x) `,x)` — macro NAME slot is an int, not a
13569        // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13570        // fires during `expand_program`. Same rejection shape the
13571        // parallel keyword test uses, so the ordering contract observed
13572        // here is structurally identical to the keyword sibling's.
13573        let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
13574        let mut e = Expander::new();
13575        let err = e
13576            .expand_and_collect_calls_to_any::<(), _, _, ()>(
13577                forms,
13578                |_h| -> Option<()> {
13579                    panic!("classifier must not run when expand_program errors");
13580                },
13581                |(), _args| {
13582                    panic!("project must not run when expand_program errors");
13583                },
13584            )
13585            .expect_err("expand_program error must short-circuit before classifier or project");
13586        // Sanity: the error IS an expand_program-stage rejection — not a
13587        // classifier or projection error — so the ordering is observable.
13588        let rendered = format!("{err}");
13589        assert!(
13590            rendered.contains("NAME") || rendered.contains("symbol"),
13591            "expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
13592        );
13593    }
13594
13595    // ── Expander::expand_source_and_collect_calls_to: from-source walk ──
13596    //
13597    // The from-source sibling of `expand_and_collect_calls_to`: composes
13598    // `crate::reader::read(src)?` with the from-forms primitive in ONE
13599    // method on the `Expander` surface. All four typed-program dispatchers
13600    // (free-function `compile_typed` / `compile_named`, preloaded-expander
13601    // `RealizedCompiler::compile_typed` / `compile_named`) route through
13602    // it; the tests below pin its contract directly. The existing
13603    // compile.rs + compiler_spec.rs dispatch tests are the path-uniformity
13604    // guards proving the four production sites route through it without
13605    // behavior drift.
13606
13607    #[test]
13608    fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
13609        // Happy path: a source string with three top-level forms (two
13610        // matching `defmonitor`, one matching `defalert`) flows through
13611        // `read` then through the from-forms primitive. Pin the SAME
13612        // emission shape the from-forms test pins for the matching
13613        // sub-set, sourced from a `&str` rather than a `Vec<Sexp>`.
13614        let src = r#"(defmonitor :name "first")
13615                     (defalert :name "not-a-match")
13616                     (defmonitor :solo)"#;
13617        let mut e = Expander::new();
13618        let lengths: Vec<usize> = e
13619            .expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
13620            .expect("matching forms must compose");
13621        assert_eq!(lengths, vec![2, 1]);
13622    }
13623
13624    #[test]
13625    fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
13626        // The reader runs BEFORE `expand_program` — so a reader error (an
13627        // unterminated string, an unbalanced paren, an unknown escape)
13628        // must short-circuit the entire composition and `expand_program`
13629        // / the keyword filter / `project` must NEVER run. Pin the
13630        // ordering: an unbalanced open-paren is rejected by the reader
13631        // at parse time, never reaches the from-forms primitive.
13632        let mut count = 0usize;
13633        let mut e = Expander::new();
13634        let err = e
13635            .expand_source_and_collect_calls_to::<(), _>(
13636                "(defmonitor :name \"unbalanced",
13637                "defmonitor",
13638                |_args| {
13639                    count += 1;
13640                    Ok(())
13641                },
13642            )
13643            .expect_err("reader error must short-circuit before expand_program");
13644        assert_eq!(
13645            count, 0,
13646            "project must never run when reader errors at parse time"
13647        );
13648        // Sanity: the error IS a reader-stage rejection — the rendered
13649        // diagnostic mentions the lexer's gate, not the expander's or
13650        // projector's (path-uniformity for the read-then-walk ordering).
13651        let rendered = format!("{err}");
13652        assert!(
13653            rendered.to_lowercase().contains("string")
13654                || rendered.to_lowercase().contains("paren")
13655                || rendered.to_lowercase().contains("eof")
13656                || rendered.to_lowercase().contains("unexpected")
13657                || rendered.to_lowercase().contains("unterminated")
13658                || rendered.to_lowercase().contains("unclosed"),
13659            "error must be the reader-stage rejection, got: {rendered}"
13660        );
13661    }
13662
13663    #[test]
13664    fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
13665    ) {
13666        // Reader succeeds, but `expand_program` rejects a malformed
13667        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13668        // ordering: `expand_program` rejects BEFORE the keyword filter
13669        // walks anything, `project` must NEVER run. Sibling of the
13670        // from-forms test of the same name — one level up the
13671        // composition (sourced from `&str` rather than `Vec<Sexp>`).
13672        let mut count = 0usize;
13673        let mut e = Expander::new();
13674        let err = e
13675            .expand_source_and_collect_calls_to::<(), _>(
13676                "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13677                "defmonitor",
13678                |_args| {
13679                    count += 1;
13680                    Ok(())
13681                },
13682            )
13683            .expect_err("expand_program error must short-circuit before project");
13684        assert_eq!(
13685            count, 0,
13686            "project must never run when expand_program errors"
13687        );
13688        let rendered = format!("{err}");
13689        assert!(
13690            rendered.contains("NAME") || rendered.contains("symbol"),
13691            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13692        );
13693    }
13694
13695    #[test]
13696    fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13697        // Reader + `expand_program` both succeed; the per-form `project`
13698        // errors on the SECOND matched form. Pin: the collect's short-
13699        // circuit fires at the second match's error, the third match's
13700        // `project` is NEVER invoked, and the returned error is exactly
13701        // the one the closure raised. Mirrors `compile_named`'s
13702        // short-circuit on the first `NamedFormMissingName` /
13703        // `NamedFormNonSymbolName` / typed-entry rejection — sourced
13704        // from `&str`.
13705        let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
13706        let mut seen = Vec::new();
13707        let mut e = Expander::new();
13708        let err = e
13709            .expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
13710                let n = args[1].as_int().expect("test args are ints");
13711                seen.push(n);
13712                if n == 2 {
13713                    Err(LispError::Compile {
13714                        form: "test".to_string(),
13715                        message: "stop at two".to_string(),
13716                    })
13717                } else {
13718                    Ok(())
13719                }
13720            })
13721            .expect_err("project's error must short-circuit collect");
13722        assert_eq!(seen, vec![1, 2]);
13723        assert!(
13724            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13725            "short-circuit must propagate the project's error verbatim, got {err:?}"
13726        );
13727    }
13728
13729    #[test]
13730    fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
13731        // Boundary: empty source string yields zero items regardless of
13732        // keyword. Pin the degenerate boundary — empty in, empty out —
13733        // and the closure is never invoked. Sibling of the from-forms
13734        // test of the same name, one level up the composition: the
13735        // from-source posture is fused-empty when `read` yields an
13736        // empty `Vec<Sexp>` (which, fed into `expand_program`, yields
13737        // an empty `Vec<Sexp>`, which iter_calls_to walks to zero
13738        // matches).
13739        let mut count = 0usize;
13740        let mut e = Expander::new();
13741        let out: Vec<()> = e
13742            .expand_source_and_collect_calls_to("", "anything", |_args| {
13743                count += 1;
13744                Ok(())
13745            })
13746            .expect("empty source is not an error");
13747        assert!(out.is_empty());
13748        assert_eq!(count, 0);
13749    }
13750
13751    #[test]
13752    fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13753        // Critical ordering preserved across the from-source posture:
13754        // `defmacro` source registers in `expand_program`, its call
13755        // sites expand to the matching keyword, AND the keyword walk
13756        // sees both the macro-emitted form and any directly-authored
13757        // matches in source order. Sibling of the from-forms test of
13758        // the same name, sourced from `&str`.
13759        let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13760                   (emit-monitor \"alpha\")
13761                   (defmonitor :name \"beta\")";
13762        let mut e = Expander::new();
13763        let names: Vec<String> = e
13764            .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13765                Ok(args[1].as_string().unwrap().to_string())
13766            })
13767            .expect("macroexpanded + directly-authored forms must both flow");
13768        assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13769    }
13770
13771    #[test]
13772    fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
13773        // Structural identity: for any (src, keyword, project) triple
13774        // whose pieces succeed, the from-source method's result equals
13775        // the inlined pre-lift pipeline `read + expand_and_collect_
13776        // calls_to`. Pin shape AND ordering on a mixed source
13777        // (matching + non-matching + macroexpand-emitted matches) so a
13778        // regression that drifts the method's pipeline from the
13779        // inlined closed-form fails here. This is the lift's
13780        // load-bearing path-uniformity gate — the SAME assertion that
13781        // holds for all four production consumers (free-function
13782        // `compile_typed`/`compile_named` + `RealizedCompiler::compile_typed`/`compile_named`)
13783        // holds at the primitive's contract level.
13784        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13785                   (foo :idx 1)
13786                   (emit-foo 2)
13787                   (bar :idx 99)
13788                   (foo :idx 3)";
13789
13790        // Inlined pre-lift pipeline.
13791        let mut exp_inline = Expander::new();
13792        let inline_forms = read(src).unwrap();
13793        let via_inline: Vec<i64> = exp_inline
13794            .expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13795            .unwrap();
13796
13797        // From-source method pipeline.
13798        let mut exp_method = Expander::new();
13799        let via_method: Vec<i64> = exp_method
13800            .expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
13801            .unwrap();
13802
13803        assert_eq!(via_inline, via_method);
13804        assert_eq!(via_inline, vec![1, 2, 3]);
13805    }
13806
13807    #[test]
13808    fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13809        // The `keyword` argument flows straight into the from-forms
13810        // primitive's head-comparison gate, which inherits from
13811        // `iter_calls_to` — a regression that drifts the keyword (e.g.,
13812        // lowercases / trims it en route through the read step) fails
13813        // here. Pin by passing TWO different keywords against the SAME
13814        // source and asserting each picks up only its matching forms.
13815        let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
13816
13817        let mut e1 = Expander::new();
13818        let monitors: Vec<i64> = e1
13819            .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13820                Ok(args[1].as_int().unwrap())
13821            })
13822            .unwrap();
13823        assert_eq!(monitors, vec![1, 3]);
13824
13825        let mut e2 = Expander::new();
13826        let alerts: Vec<i64> = e2
13827            .expand_source_and_collect_calls_to(src, "defalert", |args| {
13828                Ok(args[1].as_int().unwrap())
13829            })
13830            .unwrap();
13831        assert_eq!(alerts, vec![2]);
13832
13833        let mut e3 = Expander::new();
13834        let none: Vec<i64> = e3
13835            .expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
13836                Ok(args[1].as_int().unwrap())
13837            })
13838            .unwrap();
13839        assert!(none.is_empty());
13840    }
13841
13842    #[test]
13843    fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13844        // Path-uniformity for the soft-projection posture inherited
13845        // from `iter_calls_to`: non-matching forms in the source are
13846        // skipped silently, and `project` is never invoked on them.
13847        // Sibling of the from-forms test of the same name, sourced
13848        // from `&str` with a mix of atoms-and-lists that the reader
13849        // emits at the top level.
13850        let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
13851        let mut count = 0usize;
13852        let mut e = Expander::new();
13853        let out: Vec<()> = e
13854            .expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
13855                count += 1;
13856                Ok(())
13857            })
13858            .expect("non-matching-only source must collect to empty Vec");
13859        assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13860        assert_eq!(count, 0, "project must never run for non-matching forms");
13861    }
13862
13863    // ── Expander::expand_source_and_collect_calls_to_any — from-source × classifier ─
13864    //
13865    // The from-source posture of the typed-decoded classifier sibling on
13866    // the `Expander` surface — closes the (from-forms, from-source) ×
13867    // (keyword, classifier) 2×2 of compose-on-iter projections at the
13868    // from-source classifier corner the prior runs left open. The
13869    // pre-existing `expand_source_and_collect_calls_to` (from-source ×
13870    // keyword) is the constant-classifier projection of this primitive:
13871    // its body composes this function with a `|h| (h == keyword).
13872    // then_some(())` decoder, so the from-source pipeline lives at ONE
13873    // site. These tests pin both the typed-decoded primitive's contract
13874    // directly AND the post-lift routing of
13875    // `expand_source_and_collect_calls_to` through it.
13876
13877    #[test]
13878    fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
13879    ) {
13880        // Happy path: a source string with four top-level forms (two
13881        // matching "foo", one matching "bar", one rejected-head
13882        // "defmonitor") flows through `read` → `expand_program` → the
13883        // typed-decoded classifier walk. The closed-set classifier
13884        // `Op::from_keyword` admits "foo" and "bar" but rejects
13885        // "defmonitor". Pin the typed-decoded yield shape (decoded
13886        // witness alongside args tail) AND source-order preservation
13887        // sourced from `&str` rather than a pre-parsed `Vec<Sexp>`.
13888        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13889        enum Op {
13890            Foo,
13891            Bar,
13892        }
13893        fn op_from_keyword(head: &str) -> Option<Op> {
13894            match head {
13895                "foo" => Some(Op::Foo),
13896                "bar" => Some(Op::Bar),
13897                _ => None,
13898            }
13899        }
13900        let src = r#"(foo :idx 1)
13901                     (defmonitor :idx 99)
13902                     (bar :idx 2)
13903                     (foo :idx 3)"#;
13904        let mut e = Expander::new();
13905        let yielded: Vec<(Op, i64)> = e
13906            .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
13907                Ok((op, args[1].as_int().expect("test args are ints")))
13908            })
13909            .expect("matching forms must compose through from-source classifier walk");
13910        assert_eq!(
13911            yielded,
13912            vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
13913            "yields must be in source order, decoded witness paired with per-form mapper output"
13914        );
13915    }
13916
13917    #[test]
13918    fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
13919    ) {
13920        // The reader runs BEFORE `expand_program` AND BEFORE the
13921        // classifier — so a reader error (an unterminated string)
13922        // short-circuits the entire composition and both the classifier
13923        // AND `project` must NEVER run. Pin the ordering with BOTH
13924        // decoder and projection explicitly panicking — any post-reader
13925        // execution fires the panic. Sibling of
13926        // `expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program`
13927        // one corner over in the 2×2 (the classifier version of the
13928        // same ordering pin).
13929        let mut e = Expander::new();
13930        let err = e
13931            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13932                "(defmonitor :name \"unbalanced",
13933                |_h: &str| -> Option<()> {
13934                    panic!("classifier must not run when reader errors at parse time")
13935                },
13936                |(), _args| -> Result<()> { panic!("project must not run when reader errors") },
13937            )
13938            .expect_err("reader error must short-circuit before classifier");
13939        // Sanity: the error is a reader-stage rejection.
13940        let rendered = format!("{err}");
13941        assert!(
13942            rendered.to_lowercase().contains("string")
13943                || rendered.to_lowercase().contains("paren")
13944                || rendered.to_lowercase().contains("eof")
13945                || rendered.to_lowercase().contains("unexpected")
13946                || rendered.to_lowercase().contains("unterminated")
13947                || rendered.to_lowercase().contains("unclosed"),
13948            "error must be the reader-stage rejection, got: {rendered}"
13949        );
13950    }
13951
13952    #[test]
13953    fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
13954    ) {
13955        // Reader succeeds, but `expand_program` rejects a malformed
13956        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13957        // ordering: `expand_program` rejects BEFORE the classifier walks
13958        // anything, both decoder AND `project` must NEVER run. Sibling
13959        // of the keyword-from-source variant — one corner over in the
13960        // 2×2 with both classifier and projection explicitly panicking.
13961        let mut e = Expander::new();
13962        let err = e
13963            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13964                "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13965                |_h: &str| -> Option<()> {
13966                    panic!("classifier must not run when expand_program errors")
13967                },
13968                |(), _args| -> Result<()> {
13969                    panic!("project must not run when expand_program errors")
13970                },
13971            )
13972            .expect_err("expand_program error must short-circuit before classifier");
13973        let rendered = format!("{err}");
13974        assert!(
13975            rendered.contains("NAME") || rendered.contains("symbol"),
13976            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13977        );
13978    }
13979
13980    #[test]
13981    fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13982        // Path-uniformity for the soft-projection posture inherited
13983        // from `iter_calls_to_any`: forms whose head the classifier
13984        // rejects are skipped silently, and `project` is never invoked
13985        // on them. Pin via an explicitly-panicking projection across a
13986        // mix of forms (atom, list-with-non-symbol-head, list with
13987        // unrecognized-by-classifier symbol head). A regression that
13988        // wrongly invokes `project` on a rejected form fires the panic.
13989        let src = r#"bare-atom
13990                     (5 :not-a-symbol-head)
13991                     (defmonitor :name "decoder-rejects-me")"#;
13992        let mut e = Expander::new();
13993        // The closed-set classifier admits NOTHING in this source.
13994        let out: Vec<()> = e
13995            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13996                src,
13997                |_h: &str| -> Option<()> { None },
13998                |(), _args| -> Result<()> {
13999                    panic!("project must not run for classifier-rejected forms")
14000                },
14001            )
14002            .expect("classifier-rejects-all source must collect to empty Vec");
14003        assert!(
14004            out.is_empty(),
14005            "no classifier-accepted forms — empty Vec, got {out:?}"
14006        );
14007    }
14008
14009    #[test]
14010    fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
14011        // Reader + `expand_program` + classifier all succeed; the
14012        // per-form `project` errors on the SECOND matched form. Pin: the
14013        // collect's short-circuit fires at the second match's error, the
14014        // third match's `project` is NEVER invoked, and the returned
14015        // error is exactly the one the closure raised. Sibling of the
14016        // keyword-from-source variant — one corner over in the 2×2.
14017        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14018        enum Op {
14019            Foo,
14020        }
14021        fn op_from_keyword(head: &str) -> Option<Op> {
14022            (head == "foo").then_some(Op::Foo)
14023        }
14024        let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
14025        let mut seen = Vec::new();
14026        let mut e = Expander::new();
14027        let err = e
14028            .expand_source_and_collect_calls_to_any::<(), _, _, _>(
14029                src,
14030                op_from_keyword,
14031                |_op: Op, args: &[Sexp]| -> Result<()> {
14032                    let n = args[1].as_int().expect("test args are ints");
14033                    seen.push(n);
14034                    if n == 2 {
14035                        Err(LispError::Compile {
14036                            form: "test".to_string(),
14037                            message: "stop at two".to_string(),
14038                        })
14039                    } else {
14040                        Ok(())
14041                    }
14042                },
14043            )
14044            .expect_err("project's error must short-circuit collect");
14045        assert_eq!(seen, vec![1, 2], "third match's project must never run");
14046        assert!(
14047            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
14048            "short-circuit must propagate the project's error verbatim, got {err:?}"
14049        );
14050    }
14051
14052    #[test]
14053    fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
14054    ) {
14055        // The post-lift composition law binding the keyword-from-source
14056        // sibling to the typed-decoded primitive:
14057        //   `expand_source_and_collect_calls_to(src, k, project) ==
14058        //    expand_source_and_collect_calls_to_any(src,
14059        //        |h| (h == k).then_some(()),
14060        //        |(), args| project(args))`
14061        // Pin shape AND ordering across THREE representative keywords
14062        // (matching some, matching exactly one, matching none) on the
14063        // SAME mixed source the keyword path-uniformity test exercises.
14064        // A regression that re-implements the keyword-from-source filter
14065        // as a parallel `read + expand_and_collect_calls_to` pipeline
14066        // (rather than routing through the typed-decoded primitive)
14067        // fails the value pin.
14068        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
14069                   (foo :idx 1)
14070                   (emit-foo 2)
14071                   (bar :idx 99)
14072                   (foo :idx 3)";
14073        for k in ["foo", "bar", "missing"] {
14074            let mut e_keyword = Expander::new();
14075            let via_keyword: Vec<i64> = e_keyword
14076                .expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
14077                .unwrap();
14078
14079            let mut e_classifier = Expander::new();
14080            let via_classifier: Vec<i64> = e_classifier
14081                .expand_source_and_collect_calls_to_any(
14082                    src,
14083                    |h: &str| (h == k).then_some(()),
14084                    |(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
14085                )
14086                .unwrap();
14087
14088            assert_eq!(
14089                via_keyword, via_classifier,
14090                "keyword from-source path must equal classifier from-source path for {k:?}"
14091            );
14092        }
14093    }
14094
14095    #[test]
14096    fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
14097        // Critical ordering preserved across the typed-decoded
14098        // from-source posture: `defmacro` source registers in
14099        // `expand_program`, its call sites expand to a classifier-
14100        // accepted form, AND the classifier walk sees both the
14101        // macro-emitted form and any directly-authored matches in
14102        // source order. Sibling of the keyword-from-source ordering
14103        // test, one corner over in the 2×2.
14104        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14105        enum Op {
14106            Foo,
14107        }
14108        fn op_from_keyword(head: &str) -> Option<Op> {
14109            (head == "foo").then_some(Op::Foo)
14110        }
14111        let src = "(defmacro emit-foo (n) `(foo :name ,n))
14112                   (emit-foo \"alpha\")
14113                   (foo :name \"beta\")";
14114        let mut e = Expander::new();
14115        let names: Vec<(Op, String)> = e
14116            .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
14117                Ok((op, args[1].as_string().unwrap().to_string()))
14118            })
14119            .expect("macroexpanded + directly-authored forms must both flow through classifier");
14120        assert_eq!(
14121            names,
14122            vec![
14123                (Op::Foo, "alpha".to_string()),
14124                (Op::Foo, "beta".to_string()),
14125            ]
14126        );
14127    }
14128
14129    // ── expand_source_program — from-source × yield-all primitive ───
14130    //
14131    // Closes the 2×2 program-level walk family on the `Expander` surface:
14132    // from-forms × yield-all (`expand_program`), from-forms × keyword-
14133    // projected (`expand_and_collect_calls_to`), from-source × keyword-
14134    // projected (`expand_source_and_collect_calls_to`), AND now
14135    // from-source × yield-all (`expand_source_program`). Pins (a) the
14136    // happy path (mixed forms flow through reader then expand_program),
14137    // (b) reader-stage short-circuit (no expand_program side-effects),
14138    // (c) expand_program-stage short-circuit (defmacro-NAME-not-symbol
14139    // is rejected at its offending form), (d) defmacro absorption (the
14140    // primitive's side-effect on `self.macros` matches the from-forms
14141    // posture's side-effect verbatim), (e) the empty-source boundary,
14142    // and (f) structural identity vs. the inlined pre-lift `read +
14143    // expand_program` pipeline (path-uniformity gate).
14144
14145    #[test]
14146    fn expand_source_program_routes_through_reader_then_expand_program() {
14147        // Happy path: a source string with mixed forms (a `defmacro`
14148        // definition, a `defmonitor` form, a plain symbol literal) flows
14149        // through `read` then through the from-forms primitive. Pin the
14150        // SAME emission shape the from-forms test pins, sourced from a
14151        // `&str` rather than a `Vec<Sexp>`.
14152        let src = r#"(defmacro id (x) `,x)
14153                     (defmonitor :name "alpha")
14154                     bare-symbol"#;
14155        let mut e = Expander::new();
14156        let out = e
14157            .expand_source_program(src)
14158            .expect("mixed forms must compose");
14159        // `(defmacro id …)` is consumed as a side-effect (registers `id`
14160        // in `e.macros`), so the returned `Vec<Sexp>` contains the
14161        // remaining two top-level forms in source order.
14162        assert_eq!(out.len(), 2);
14163        assert_eq!(
14164            out[0].as_call_to("defmonitor").map(<[_]>::len),
14165            Some(2),
14166            "first surviving form is the defmonitor with two args, got: {:?}",
14167            out[0]
14168        );
14169        assert_eq!(
14170            out[1].as_symbol(),
14171            Some("bare-symbol"),
14172            "second surviving form is the bare symbol literal, got: {:?}",
14173            out[1]
14174        );
14175        // The defmacro side-effect landed in the expander's macro table.
14176        assert!(
14177            e.has("id"),
14178            "defmacro must register `id` in the expander's macro table"
14179        );
14180    }
14181
14182    #[test]
14183    fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
14184        // The reader runs BEFORE `expand_program` — so a reader error (an
14185        // unterminated string, an unbalanced paren) must short-circuit
14186        // the entire composition and `expand_program` must NEVER run.
14187        // Pin the ordering: a `defmacro` BEFORE the unterminated string
14188        // is NOT registered, because the reader fails at parse time
14189        // before `expand_program` sees any form.
14190        let mut e = Expander::new();
14191        let err = e
14192            .expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
14193            .expect_err("reader error must short-circuit before expand_program");
14194        // Sanity: the error IS a reader-stage rejection.
14195        let rendered = format!("{err}").to_lowercase();
14196        assert!(
14197            rendered.contains("string")
14198                || rendered.contains("paren")
14199                || rendered.contains("eof")
14200                || rendered.contains("unexpected")
14201                || rendered.contains("unterminated")
14202                || rendered.contains("unclosed"),
14203            "error must be the reader-stage rejection, got: {err}"
14204        );
14205        // Path-uniformity gate: the `defmacro` lexically BEFORE the
14206        // unterminated string must NOT have been absorbed — `read`'s
14207        // failure short-circuits the entire pipeline before
14208        // `expand_program` walks any forms.
14209        assert!(
14210            !e.has("should-not-register"),
14211            "reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
14212        );
14213    }
14214
14215    #[test]
14216    fn expand_source_program_short_circuits_on_expand_program_error() {
14217        // Reader succeeds, but `expand_program` rejects a malformed
14218        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14219        // ordering: the expand_program-stage rejection bubbles up
14220        // verbatim, sourced from `&str` rather than `Vec<Sexp>`.
14221        let mut e = Expander::new();
14222        let err = e
14223            .expand_source_program("(defmacro 5 (x) `,x)")
14224            .expect_err("expand_program error must propagate");
14225        let rendered = format!("{err}");
14226        assert!(
14227            rendered.contains("NAME") || rendered.contains("symbol"),
14228            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14229        );
14230    }
14231
14232    #[test]
14233    fn expand_source_program_yields_empty_vec_for_empty_source() {
14234        // Boundary: empty source string yields zero items. Pin the
14235        // degenerate boundary — empty in, empty out — sibling of the
14236        // from-source × keyword-projected test of the same name, one
14237        // level down the composition (no keyword filter).
14238        let mut e = Expander::new();
14239        let out = e
14240            .expand_source_program("")
14241            .expect("empty source is not an error");
14242        assert!(
14243            out.is_empty(),
14244            "empty source must yield empty Vec, got: {out:?}"
14245        );
14246    }
14247
14248    #[test]
14249    fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
14250        // Critical pipeline order: a `defmacro` in the source registers
14251        // AND a subsequent call to it expands AND the expanded form
14252        // surfaces in the returned Vec. Sourced from `&str`, the
14253        // from-source posture preserves the side-effect-then-expansion
14254        // semantics of the from-forms primitive verbatim.
14255        let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
14256                     (emit-monitor "alpha")
14257                     (emit-monitor "beta")"#;
14258        let mut e = Expander::new();
14259        let out = e
14260            .expand_source_program(src)
14261            .expect("defmacro absorption then expansion must compose");
14262        // Two surviving forms (the `defmacro` itself is consumed as a
14263        // side-effect), each expanded to a `(defmonitor :name "<n>")`.
14264        assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
14265        for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
14266            let args = out[i]
14267                .as_call_to("defmonitor")
14268                .unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
14269            assert_eq!(args[0].as_keyword(), Some("name"));
14270            assert_eq!(args[1].as_string(), Some(expected_name));
14271        }
14272        // The macro is now in the table — a subsequent call would
14273        // expand against this SAME expander.
14274        assert!(e.has("emit-monitor"));
14275    }
14276
14277    #[test]
14278    fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
14279        // Structural identity: for any `src` whose pieces succeed, the
14280        // from-source method's result equals the inlined pre-lift
14281        // pipeline `read + expand_program`. Pin shape AND ordering on a
14282        // mixed source (defmacro definition + macro call + plain form)
14283        // so a regression that drifts the method's pipeline from the
14284        // inlined closed-form fails here. This is the lift's
14285        // load-bearing path-uniformity gate — the SAME assertion holds
14286        // for both production consumers (`RealizedCompiler::compile` +
14287        // `realize_in_memory`'s `:macros` loop) at the primitive's
14288        // contract level.
14289        let src = r#"(defmacro id (x) `,x)
14290                     (id (foo 1 2))
14291                     (bar)"#;
14292
14293        // Inlined pre-lift pipeline.
14294        let mut e_inline = Expander::new();
14295        let inline_forms = read(src).unwrap();
14296        let via_inline = e_inline
14297            .expand_program(inline_forms)
14298            .expect("inlined pipeline must succeed");
14299
14300        // From-source method pipeline.
14301        let mut e_method = Expander::new();
14302        let via_method = e_method
14303            .expand_source_program(src)
14304            .expect("from-source method pipeline must succeed");
14305
14306        assert_eq!(
14307            via_inline, via_method,
14308            "from-source method must emit byte-identical result to inlined read+expand_program"
14309        );
14310        // Both pipelines registered the same defmacro side-effect.
14311        assert_eq!(e_inline.has("id"), e_method.has("id"));
14312        assert!(e_method.has("id"));
14313    }
14314
14315    #[test]
14316    fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
14317        // Per-`&mut self` absorption posture pin: a `defmacro` registered
14318        // in call 1 SURVIVES into call 2 on the SAME `Expander`. This is
14319        // the load-bearing semantic `realize_in_memory`'s per-spec-macro
14320        // build-up depends on — every iteration's `expand_source_program(
14321        // macro_src)?` lands its `defmacro` heads into the SAME mutable
14322        // `preloaded` expander, so a follow-up `:macros` source can
14323        // invoke macros defined in earlier ones.
14324        let mut e = Expander::new();
14325        let _ = e
14326            .expand_source_program("(defmacro outer (n) `(inner ,n))")
14327            .unwrap();
14328        assert!(e.has("outer"));
14329        // Call 2: defines `inner` AND invokes `outer`, which expands to
14330        // `(inner <n>)`. The expansion proves call 1's `outer` survived
14331        // into call 2's expansion.
14332        let out = e
14333            .expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
14334            .unwrap();
14335        assert_eq!(out.len(), 1, "call 2 yields one expanded form");
14336        // (outer 42) → (inner 42) → (wrapped 42)
14337        let args = out[0]
14338            .as_call_to("wrapped")
14339            .expect("nested expansion must reach `wrapped`");
14340        assert_eq!(args[0].as_int(), Some(42));
14341    }
14342
14343    // ── Expander::register_macro_def: the macro-registration primitive ──
14344    //
14345    // `register_macro_def(&mut self, def: MacroDef) -> Result<()>` lifts
14346    // the byte-identical two-step block —
14347    //
14348    //   if self.compile_templates {
14349    //       self.templates.insert(def.name.clone(), compile_template(&def)?);
14350    //   }
14351    //   self.macros.insert(def.name.clone(), def);
14352    //
14353    // — that lived inline at `with_macros` (the bulk-from-iterator
14354    // constructor) AND `expand_program`'s `(defmacro …)`-head arm (the
14355    // program-walk-time registration site) into ONE named method on the
14356    // `Expander` surface. The tests below pin:
14357    //
14358    //   (a) the bytecode-default expander (`Expander::new()`) populates
14359    //       BOTH `macros` AND `templates` keyed by `def.name`,
14360    //   (b) the substitute-only expander (`Expander::new_substitute_only()`)
14361    //       populates `macros` but SKIPS `templates` — the
14362    //       `compile_templates: false` gate fires structurally,
14363    //   (c) a template that fails to compile (`,foo` against `params:
14364    //       []`) short-circuits BEFORE `self.macros.insert` runs, so the
14365    //       failed registration leaves both tables untouched — no
14366    //       partial-write window in which `self.macros.has(name)` is
14367    //       true but `self.templates.has(name)` is missing,
14368    //   (d) `with_macros([def])` and `register_macro_def(def)` on a fresh
14369    //       expander produce structurally identical state (both tables'
14370    //       sets of keys + the same `MacroDef` body under each key),
14371    //   (e) `expand_program` of one `(defmacro …)` form and
14372    //       `register_macro_def` of the parsed `MacroDef` produce
14373    //       structurally identical state — closing path-uniformity
14374    //       across the lift's two consumers AT the registration site.
14375    //
14376    // The tests bind on `Expander::has(name)` / `Expander::len()` for
14377    // the macros table and on bytecode-vs-substitute output equivalence
14378    // (a registered macro must expand to the SAME result regardless of
14379    // whether the bytecode path's `self.templates` entry exists) for the
14380    // templates table.
14381
14382    fn macro_def_id() -> MacroDef {
14383        // `(defmacro id (x) `,x)` — the simplest well-formed MacroDef:
14384        // one required param, a single-symbol unquote body. Compiles to
14385        // a valid `CompiledTemplate` (no unbound vars), so
14386        // `register_macro_def` succeeds on every Expander posture.
14387        MacroDef {
14388            name: "id".into(),
14389            params: MacroParams {
14390                required: vec!["x".into()],
14391                optional: vec![],
14392                rest: None,
14393            },
14394            // ` ` quasi-quoted `,x` — `Quasiquote(Unquote(Symbol("x")))`.
14395            body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14396        }
14397    }
14398
14399    fn macro_def_bad_template() -> MacroDef {
14400        // `(defmacro bad () `,unbound)` — a quasi-quote body with `,unbound`
14401        // against an EMPTY required-params list. `compile_template`
14402        // rejects it with `UnboundTemplateVar` because `unbound` is not
14403        // in `params.names()`. Used to exercise the
14404        // `compile_template`-failed-but-self.macros-still-pristine
14405        // path-uniformity pin.
14406        MacroDef {
14407            name: "bad".into(),
14408            params: MacroParams::default(),
14409            body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
14410        }
14411    }
14412
14413    #[test]
14414    fn register_macro_def_bytecode_default_populates_macros_and_templates() {
14415        // The bytecode-default `Expander::new()` carries
14416        // `compile_templates: true`; `register_macro_def` MUST populate
14417        // BOTH `self.macros` (the substitute path's registry) AND
14418        // `self.templates` (the bytecode path's pre-compiled bytecode
14419        // index) keyed by `def.name`. Fail-before-pass-after: this
14420        // assert requires the new method to exist AND to compose the
14421        // two side-effects in canonical order on a single-call
14422        // registration — pre-lift `register_macro_def` did not exist.
14423        let mut e = Expander::new();
14424        e.register_macro_def(macro_def_id())
14425            .expect("well-formed MacroDef must register");
14426        assert!(
14427            e.has("id"),
14428            "self.macros must carry the registered name after register_macro_def"
14429        );
14430        assert!(
14431            e.templates.contains_key("id"),
14432            "self.templates must carry the compiled bytecode under the bytecode-default posture"
14433        );
14434        // The registered macro must expand correctly through the
14435        // bytecode strategy — proves the inserted template is the right
14436        // bytecode for the body.
14437        let out = e
14438            .expand_program(read("(id 42)").unwrap())
14439            .expect("registered macro must expand");
14440        assert_eq!(out.len(), 1);
14441        assert_eq!(out[0], Sexp::int(42));
14442    }
14443
14444    #[test]
14445    fn register_macro_def_substitute_only_skips_templates() {
14446        // `Expander::new_substitute_only()` carries `compile_templates:
14447        // false`; `register_macro_def` MUST populate `self.macros` for
14448        // the substitute path but SKIP `self.templates` (no bytecode
14449        // pre-compile). Pin the `compile_templates: false` gate fires
14450        // structurally — a regression that drifts the conditional from
14451        // the registration primitive (e.g. a future emitter that
14452        // unconditionally pre-compiles) would silently double the
14453        // benchmark baseline's allocation footprint.
14454        let mut e = Expander::new_substitute_only();
14455        e.register_macro_def(macro_def_id())
14456            .expect("well-formed MacroDef must register under substitute-only");
14457        assert!(
14458            e.has("id"),
14459            "self.macros must carry the registered name even under substitute-only"
14460        );
14461        assert!(
14462            !e.templates.contains_key("id"),
14463            "self.templates MUST be empty under compile_templates: false — the gate fires"
14464        );
14465        // The registered macro must still expand correctly through the
14466        // substitute strategy — proves the inserted MacroDef is the
14467        // right body for substitute-walking.
14468        let out = e
14469            .expand_program(read("(id 42)").unwrap())
14470            .expect("registered macro must expand via substitute path");
14471        assert_eq!(out.len(), 1);
14472        assert_eq!(out[0], Sexp::int(42));
14473    }
14474
14475    #[test]
14476    fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
14477        // The structural ordering pin: when `compile_template(&def)?`
14478        // rejects (e.g. `,unbound` against empty params), the `?` MUST
14479        // short-circuit BEFORE `self.macros.insert(def.name.clone(),
14480        // def)` runs, so a failed registration leaves BOTH tables
14481        // exactly as they were. Without this ordering a future
14482        // bytecode-strategy lookup would resolve `self.macros.get(name)`
14483        // (the body was inserted) AND find `self.templates.get(name)`
14484        // returning `None` (the pre-compile failed), silently coercing
14485        // the macro onto the substitute fallback path despite
14486        // `compile_templates: true`. The lift preserves the pre-lift
14487        // ordering (`compile_template` precedes `self.macros.insert`)
14488        // structurally — the test pins it across the lift.
14489        let mut e = Expander::new();
14490        let err = e
14491            .register_macro_def(macro_def_bad_template())
14492            .expect_err("unbound-template body must reject");
14493        // The rejection is the structural variant the bytecode
14494        // strategy's template-gate emits.
14495        assert!(
14496            matches!(err, LispError::UnboundTemplateVar { .. }),
14497            "expected UnboundTemplateVar, got: {err:?}"
14498        );
14499        // Both tables MUST be pristine — no partial write.
14500        assert!(
14501            !e.has("bad"),
14502            "self.macros must be untouched after compile_template failure"
14503        );
14504        assert!(
14505            !e.templates.contains_key("bad"),
14506            "self.templates must be untouched after compile_template failure"
14507        );
14508    }
14509
14510    #[test]
14511    fn with_macros_routes_through_register_macro_def_path_uniformity() {
14512        // Path-uniformity pin across the bulk-constructor consumer:
14513        // `with_macros([def])` MUST produce the same final state as
14514        // `Expander::new()` + `register_macro_def(def)`. A regression
14515        // that drifts the constructor's per-MacroDef inline block from
14516        // the registration primitive (e.g. a future emitter that
14517        // re-inlines the two-step block at `with_macros` rather than
14518        // delegating) would fail loudly here.
14519        let mut via_register = Expander::new();
14520        via_register
14521            .register_macro_def(macro_def_id())
14522            .expect("register must succeed");
14523        let mut via_with_macros =
14524            Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
14525        assert_eq!(via_register.len(), via_with_macros.len());
14526        assert!(via_register.has("id"));
14527        assert!(via_with_macros.has("id"));
14528        // Both tables key on the same set across both postures.
14529        assert_eq!(
14530            via_register.templates.contains_key("id"),
14531            via_with_macros.templates.contains_key("id"),
14532            "self.templates key-presence must agree across with_macros and register_macro_def"
14533        );
14534        // Both registered expanders expand the registered macro
14535        // identically — the strongest behavioral parity.
14536        let out_a = via_register
14537            .expand_program(read("(id 99)").unwrap())
14538            .unwrap();
14539        let out_b = via_with_macros
14540            .expand_program(read("(id 99)").unwrap())
14541            .unwrap();
14542        assert_eq!(out_a, out_b);
14543        assert_eq!(out_a, vec![Sexp::int(99)]);
14544    }
14545
14546    #[test]
14547    fn expand_program_routes_through_register_macro_def_path_uniformity() {
14548        // Path-uniformity pin across the program-walk consumer:
14549        // `expand_program` of `(defmacro id (x) `,x)` MUST produce the
14550        // same final state as a direct
14551        // `register_macro_def(macro_def_id())`. A regression that
14552        // drifts `expand_program`'s `(defmacro …)`-head arm from the
14553        // registration primitive (e.g. a future emitter that re-inlines
14554        // the two-step block at `expand_program` rather than delegating)
14555        // would fail loudly here.
14556        let mut via_register = Expander::new();
14557        via_register
14558            .register_macro_def(macro_def_id())
14559            .expect("register must succeed");
14560        let mut via_expand_program = Expander::new();
14561        let yielded = via_expand_program
14562            .expand_program(read("(defmacro id (x) `,x)").unwrap())
14563            .expect("expand_program of one defmacro must succeed");
14564        // expand_program drops the (defmacro …) form from its returned
14565        // Vec<Sexp> (defmacro is a definition, not a program form), so
14566        // the yielded list is empty — pin the side-effect-only posture.
14567        assert!(
14568            yielded.is_empty(),
14569            "(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
14570        );
14571        assert!(via_register.has("id"));
14572        assert!(via_expand_program.has("id"));
14573        assert_eq!(via_register.len(), via_expand_program.len());
14574        // Both tables key on the same set across both postures.
14575        assert_eq!(
14576            via_register.templates.contains_key("id"),
14577            via_expand_program.templates.contains_key("id"),
14578            "self.templates key-presence must agree across expand_program and register_macro_def"
14579        );
14580        // Both registered expanders expand the registered macro
14581        // identically — the strongest behavioral parity, closing
14582        // path-uniformity across BOTH consumers (with_macros above,
14583        // expand_program here) at ONE primitive.
14584        let out_a = via_register
14585            .expand_program(read("(id 7)").unwrap())
14586            .unwrap();
14587        let out_b = via_expand_program
14588            .expand_program(read("(id 7)").unwrap())
14589            .unwrap();
14590        assert_eq!(out_a, out_b);
14591        assert_eq!(out_a, vec![Sexp::int(7)]);
14592    }
14593
14594    // ── as_unquote: path-uniformity across the three substitute / compile_node sites ──
14595    //
14596    // The new typed-marker projection `Sexp::as_unquote` lifts the
14597    // (Sexp::Unquote/UnquoteSplice variant, UnquoteForm::Unquote/Splice
14598    // literal) pair into ONE structural query. These tests pin behavioral
14599    // parity end-to-end across BOTH expansion strategies:
14600    //   * `compile_node` — the bytecode-template strategy's typed marker
14601    //     dispatch routes through as_unquote at the compile step.
14602    //   * `substitute` (top-level + list-inner) — the substitute-walker
14603    //     fallback strategy's typed marker dispatch routes through
14604    //     as_unquote at both per-form sites.
14605    // The structural invariant the prior runs' `expansion_layers_agree_on_
14606    // output_and_cache_wins` benchmark observes — bytecode AND substitute
14607    // produce byte-identical output — is anchored here at the macro-template
14608    // level for every distinct unquote-family shape the substitute and
14609    // bytecode strategies discriminate.
14610
14611    #[test]
14612    fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
14613        // A template body whose only marker is a top-level `,x`. Both
14614        // expansion strategies route through `as_unquote` (compile_node for
14615        // bytecode, substitute for the fallback walker), each pairing
14616        // Sexp::Unquote ↔ UnquoteForm::Unquote at ONE typed projection.
14617        // Pin byte-identical output across both strategies — the
14618        // structural-invariant the new projection's lift was designed to
14619        // make load-bearing structural rather than per-site discipline.
14620        let src = "(defmacro id (x) ,x) (id 42)";
14621        let mut bc = Expander::new();
14622        let mut sub = Expander::new_substitute_only();
14623        let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14624        let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14625        assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
14626        assert_eq!(out_bc, vec![Sexp::int(42)]);
14627    }
14628
14629    #[test]
14630    fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
14631        // A template body whose marker is a list-inner `,@xs`. The substitute
14632        // strategy's list-inner Splice arm routes through `as_unquote`
14633        // matching Some((UnquoteForm::Splice, inner)); the bytecode strategy's
14634        // compile_node Splice arm routes through `as_unquote` matching the
14635        // same. Pin byte-identical output across both strategies for the
14636        // splice path — the projection's typed-marker pairing
14637        // Sexp::UnquoteSplice ↔ UnquoteForm::Splice is structurally
14638        // identical at every consumer post-lift.
14639        let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
14640        let mut bc = Expander::new();
14641        let mut sub = Expander::new_substitute_only();
14642        let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14643        let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14644        assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
14645        let expected = Sexp::List(vec![
14646            Sexp::symbol("list"),
14647            Sexp::int(0),
14648            Sexp::int(1),
14649            Sexp::int(2),
14650            Sexp::int(3),
14651            Sexp::int(99),
14652        ]);
14653        assert_eq!(out_bc, vec![expected]);
14654    }
14655
14656    #[test]
14657    fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
14658        // The substitute strategy's top-level `,@x` arm rejects with
14659        // LispError::SpliceOutsideList (a splice marker with no containing
14660        // list to flatten into). Pre-lift the arm was
14661        // `Sexp::UnquoteSplice(inner) => Err(splice_outside_list(inner))`;
14662        // post-lift the arm routes through `as_unquote` matching
14663        // Some((UnquoteForm::Splice, inner)) and dispatches on the typed
14664        // marker. Pin path-uniformity: the rejection MUST fire identically
14665        // through the new projection. The substitute-only strategy bypasses
14666        // the bytecode path, exposing this gate directly.
14667        //
14668        // `(defmacro bad (xs) ,@xs) (bad (1 2 3))` — the body is a bare
14669        // `,@xs` at top level (NOT wrapped in a containing list), so the
14670        // substitute fallback's top-level Splice arm fires.
14671        let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
14672        let mut sub = Expander::new_substitute_only();
14673        let err = sub.expand_program(read(src).unwrap()).unwrap_err();
14674        assert!(
14675            matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14676            "expected SpliceOutsideList through as_unquote, got: {err:?}"
14677        );
14678    }
14679
14680    #[test]
14681    fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
14682        // A `,unbound` template body — the inner symbol isn't a param —
14683        // fires gate-2 (must-be-bound-in-scope). The typed marker
14684        // `UnquoteForm::Unquote` MUST thread through `as_unquote` →
14685        // `resolve_unquote_in_params(inner, params, form)` → gate-2's
14686        // rejection-builder. Pre-lift the marker was bound at the per-arm
14687        // literal `UnquoteForm::Unquote`; post-lift it's bound at the
14688        // typed projection. Pin: the rejection's `prefix` slot is
14689        // UnquoteForm::Unquote, structurally derived from the typed
14690        // projection's typed marker, NOT a literal at the arm body.
14691        let src = "(defmacro bad (x) ,unbound)";
14692        let mut bc = Expander::new();
14693        let err = bc.expand_program(read(src).unwrap()).unwrap_err();
14694        match err {
14695            crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14696                assert_eq!(
14697                    prefix,
14698                    UnquoteForm::Unquote,
14699                    "typed marker drifted from UnquoteForm::Unquote at gate-2"
14700                );
14701            }
14702            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14703        }
14704        // Sibling negative control: `,@unbound` inside a containing list
14705        // threads UnquoteForm::Splice through the same projection. The
14706        // splice MUST be inside a list — `compile_template` rejects
14707        // top-level `,@X` bodies with SpliceOutsideList before compile_node
14708        // runs, so the typed-marker dispatch on Splice is observable only
14709        // for well-positioned splice bodies. Closes path-uniformity across
14710        // BOTH typed marker variants at the compile path.
14711        let src_splice = "(defmacro bad (x) (foo ,@unbound))";
14712        let mut bc2 = Expander::new();
14713        let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
14714        match err_splice {
14715            crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14716                assert_eq!(
14717                    prefix,
14718                    UnquoteForm::Splice,
14719                    "typed marker drifted from UnquoteForm::Splice at gate-2"
14720                );
14721            }
14722            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14723        }
14724    }
14725
14726    // ── compile_template + contains_unquote path-uniformity through as_unquote ──
14727    //
14728    // The prior `as_unquote` projection lift (eb00684) routed `compile_node`'s
14729    // two arms and `substitute`'s top-level + list-inner arms through the
14730    // typed-marker projection but LEFT `compile_template`'s top-level
14731    // splice-outside-list gate and `contains_unquote`'s family check inline
14732    // matching `Sexp::UnquoteSplice(inner)` / `Sexp::Unquote(_) |
14733    // Sexp::UnquoteSplice(_)`. After this lift both production sites route
14734    // through `as_unquote`: `compile_template` matches `Some((UnquoteForm::
14735    // Splice, inner))` (same shape as `substitute`'s list-inner Splice arm),
14736    // `contains_unquote` uses `as_unquote().is_some()` for the family check.
14737    // Every production-site recognizer of an unquote-family wrapper now
14738    // shares ONE typed-marker projection — the (Sexp variant, UnquoteForm
14739    // variant) pairing for `,@-outside-list` is bound at ONE structural
14740    // query across all FOUR reachable splice-recognition sites, and a future
14741    // regression that drifts the marker pairing at any production site
14742    // becomes a type error at the helper boundary rather than a silent
14743    // per-site divergence.
14744
14745    #[test]
14746    fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
14747        // The bytecode path's `compile_template` gate pre-rejects top-level
14748        // `,@X` bodies via `as_unquote()` matching `Some((UnquoteForm::Splice,
14749        // inner))`. Pre-lift the arm was `if let Sexp::UnquoteSplice(inner) =
14750        // body` — the LAST production-site inline `Sexp::UnquoteSplice(_)`
14751        // match outside the projection itself. Post-lift the (Sexp variant,
14752        // UnquoteForm variant) pairing for the splice-outside-list gate is
14753        // bound at ONE projection function across all three reachable
14754        // emission sites (compile_template top-level, substitute top-level,
14755        // substitute list-inner). Path-uniformity guard at the new boundary:
14756        // the bytecode-path compile-time gate now routes through the same
14757        // shape `substitute`'s list-inner Splice arm uses.
14758        //
14759        // `(defmacro bad (xs) \`,@xs)` — the body's outer quasi-quote is
14760        // peeled by `template_body`, leaving `,@xs` at top level for
14761        // `compile_template` to gate before `compile_node` walks anything.
14762        // The bytecode strategy is default-on (`Expander::new()` sets
14763        // `compile_templates: true`), so the gate fires at
14764        // `register_macro_def` time.
14765        let mut e = Expander::new();
14766        let err = e
14767            .expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
14768            .expect_err("compile_template must reject top-level ,@X via as_unquote");
14769        assert!(
14770            matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14771            "expected SpliceOutsideList through as_unquote, got: {err:?}"
14772        );
14773    }
14774
14775    #[test]
14776    fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
14777        // Negative control on the typed-marker dispatch at `compile_template`'s
14778        // top-level gate. A top-level `,X` body (Unquote, NOT Splice) is a
14779        // valid template — `compile_node` lowers it to a single `Subst(idx)`
14780        // op. The new `Some((UnquoteForm::Splice, inner))` pattern at the
14781        // gate MUST NOT fire on the Unquote variant — only Splice — so the
14782        // typed marker is observably load-bearing: a regression that drifts
14783        // the pattern from `UnquoteForm::Splice` to the wider
14784        // `Some((_, inner))` would mis-reject `(defmacro id (x) ,x)` here.
14785        //
14786        // `(defmacro id (x) ,x) (id 42)` — `id`'s body is bare `,x` at top
14787        // level; `compile_template` admits it via the typed-marker gate,
14788        // `compile_node` emits `Subst(0)`, and the call expands to `42`.
14789        let src = "(defmacro id (x) ,x) (id 42)";
14790        let mut e = Expander::new();
14791        let expanded = e
14792            .expand_program(read(src).unwrap())
14793            .expect("top-level ,X body must compile through as_unquote-typed gate");
14794        assert_eq!(expanded, vec![Sexp::int(42)]);
14795    }
14796
14797    #[test]
14798    fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
14799        // The fast-path optimizer in `compile_node` short-circuits on
14800        // `contains_unquote(node)` — true iff the subtree carries an
14801        // unquote-family wrapper. Pre-lift the family check inlined
14802        // `Sexp::Unquote(_) | Sexp::UnquoteSplice(_) => true`; post-lift it
14803        // routes through `as_unquote().is_some()`. Pin path-uniformity at
14804        // the family-recognition boundary: every production-site unquote
14805        // recognizer (compile_template's gate, compile_node's per-arm
14806        // dispatch, substitute's top-level + list-inner arms, AND this
14807        // fast-path predicate) now shares ONE typed-marker projection.
14808        //
14809        // Both variants must trigger the fast-path bail — the optimizer's
14810        // gate keys on the family, not the inner. The recursion into
14811        // Quote/Quasiquote/List subtrees ALSO observes the same family
14812        // gate at every level, so a `,@xs` buried under a `\`...` outer
14813        // wrapper still fires it.
14814        let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14815        let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
14816        let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
14817            Sexp::symbol("foo"),
14818            Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
14819        ])));
14820        // The projection's `is_some()` face MUST agree with the pre-lift
14821        // `matches!()` discriminant on every variant — closed-set guarantee
14822        // shared with `as_unquote`'s contract pin in `ast.rs`.
14823        assert!(super::contains_unquote(&bare_unquote));
14824        assert!(super::contains_unquote(&bare_splice));
14825        assert!(super::contains_unquote(&nested));
14826        // Negative control: shapes the projection rejects (Nil, Atom,
14827        // bare List, Quote-family without inner unquote) must NOT trigger
14828        // the fast-path bail — the projection's None face stays observably
14829        // distinct from its Some face.
14830        assert!(!super::contains_unquote(&Sexp::Nil));
14831        assert!(!super::contains_unquote(&Sexp::symbol("plain")));
14832        assert!(!super::contains_unquote(&Sexp::int(5)));
14833        assert!(!super::contains_unquote(&Sexp::List(vec![
14834            Sexp::symbol("plain"),
14835            Sexp::int(1),
14836        ])));
14837        assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
14838            Sexp::symbol("inert")
14839        ))));
14840    }
14841
14842    #[test]
14843    fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
14844        // Pin the lift: `contains_unquote`'s quote-family recognition
14845        // now routes through `Sexp::as_quote_form` (the 4-of-4 wrapper
14846        // projection) AND `QuoteForm::as_unquote_form` (the 2-of-4
14847        // substitution-subset gate) at ONE site — not through the pair
14848        // of `as_unquote().is_some()` + inline
14849        // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm. The
14850        // path-uniformity assertion: for every quote-family wrapper, the
14851        // function's behavior agrees with the manual composition through
14852        // the two typed-marker projections. A regression that re-inlines
14853        // either arm (e.g. drops the `as_unquote_form().is_some()` gate
14854        // and just returns true for any `as_quote_form().is_some()`, or
14855        // restores the per-variant `Quote | Quasiquote` arm) drifts from
14856        // this composition and surfaces here.
14857        use crate::ast::QuoteForm;
14858
14859        // Sweep every QuoteForm variant under both axes:
14860        //   * unquote-only-inner (inner = symbol):
14861        //       Unquote(s)        → true via `as_unquote_form() == Some`
14862        //       UnquoteSplice(s)  → true via `as_unquote_form() == Some`
14863        //       Quote(s)          → false (inner has no unquote, recurses to false)
14864        //       Quasiquote(s)     → false (same)
14865        //   * unquote-inner-inside-quote-wrapper:
14866        //       Quote(Unquote(s)) → true (Quote arm recurses via `as_quote_form`,
14867        //                                 inner `Unquote` returns true through the
14868        //                                 SAME projection — both arms share the
14869        //                                 ONE typed-marker site post-lift)
14870        let inner_plain = Sexp::symbol("x");
14871        let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14872
14873        for qf in QuoteForm::ALL {
14874            let wrapped_plain = match qf {
14875                QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
14876                QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
14877                QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
14878                QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
14879            };
14880            // Path-uniformity: behavior derives from the manual two-marker
14881            // composition through `as_quote_form` + `as_unquote_form`.
14882            // The substitution-subset gate (`as_unquote_form().is_some()`)
14883            // returns true for Unquote/UnquoteSplice and false for
14884            // Quote/Quasiquote — directly encoding the 2-of-4 partition.
14885            let via_manual =
14886                qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
14887            assert_eq!(
14888                super::contains_unquote(&wrapped_plain),
14889                via_manual,
14890                "contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
14891            );
14892
14893            let wrapped_unquote = match qf {
14894                QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
14895                QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
14896                QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
14897                QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
14898            };
14899            // EVERY quote-family wrapper around an inner `Unquote` must
14900            // contain unquote — either the outer projects as `Some` on
14901            // the substitution subset (Unquote/UnquoteSplice arms), OR
14902            // the outer is Quote/Quasiquote and the recursion into the
14903            // inner re-fires the projection. The pre-lift Quote arm
14904            // recursed via the inline `Sexp::Quote(inner)` match;
14905            // post-lift the SAME recursion fires via `as_quote_form`'s
14906            // typed projection. Both yield `true`.
14907            assert!(
14908                super::contains_unquote(&wrapped_unquote),
14909                "contains_unquote missed an inner Unquote under {qf:?} — \
14910                 the quote-family recursion through as_quote_form drifted"
14911            );
14912        }
14913
14914        // Negative control: a non-quote-family wrapper (List, Atom,
14915        // Nil) must NOT route through `as_quote_form` at all. The
14916        // `if let Some(...) = node.as_quote_form()` gate stays `None`
14917        // and falls through to the List/_  arms. This is the structural
14918        // partition: the projection's `None` face MUST agree with
14919        // `!matches!(node, Sexp::Quote(_) | Sexp::Quasiquote(_) |
14920        // Sexp::Unquote(_) | Sexp::UnquoteSplice(_))`.
14921        let nested_in_list = Sexp::List(vec![
14922            Sexp::symbol("outer"),
14923            Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14924        ]);
14925        assert!(
14926            super::contains_unquote(&nested_in_list),
14927            "contains_unquote failed to descend into a List subtree containing a \
14928             Quasiquote(Unquote(_)) — list recursion arm drifted"
14929        );
14930    }
14931
14932    // ── expand_and_collect_named_calls_to_any (from-forms × named ────
14933    // × typed-decoded classifier) ─────────────────────────────────────
14934    //
14935    // The (named NAME-then-kwargs × typed-decoded classifier) cell of
14936    // the dispatcher matrix on the `Expander` surface — pre-lift the
14937    // matrix had the bare-kwargs row closed (expand_and_collect_calls_to
14938    // + expand_and_collect_calls_to_any) AND the constant-keyword
14939    // column closed on the named axis (expand_to_named +
14940    // expand_source_to_named) but the (named × classifier) cell was
14941    // open — every consumer that wanted to walk a program by typed-
14942    // decoded classifier AND extract the NAME slot per matched form
14943    // had to compose `expand_and_collect_calls_to_any` with
14944    // `split_name_slot` inline. Post-lift the cell is ONE named primitive
14945    // on the `Expander` surface, and the constant-`T::KEYWORD` named
14946    // sibling routes through it as the (constant-classifier × named)
14947    // specialization.
14948    //
14949    // The tests below pin: (a) the happy path yields the decoded triple
14950    // `(T, &str, &[Sexp])` for every matched form in source order; (b)
14951    // non-matching forms are skipped (soft-projection contract inherited
14952    // from the typed-decoded primitive); (c) the `NamedFormMissingName`
14953    // variant fires for matched forms with no NAME slot, threading the
14954    // classifier-supplied `&'static str` keyword; (d) the
14955    // `NamedFormNonSymbolName` variant fires for matched forms with a
14956    // non-symbol-or-string NAME slot, again threading the classifier-
14957    // supplied keyword; (e) the projection's `Err` short-circuits at
14958    // the first failing matched form; (f) `expand_program` runs BEFORE
14959    // the classifier filter walks — a `(defmacro …)` emitting a named
14960    // form is decoded by the classifier on the EXPANDED head, not the
14961    // macro-call head; (g) the from-source sibling agrees with `read +
14962    // from-forms` on the same source.
14963
14964    #[test]
14965    fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
14966    ) {
14967        // The typed-decoded named primitive's happy path: a closed-set
14968        // classifier decodes head symbols to a typed enum PAIRED with
14969        // its canonical static keyword, and the per-form projection
14970        // receives `(decoded, name, spec_args)` for every matched form
14971        // in source order. Sweep across TWO distinct classifier outcomes
14972        // — `Monitor` / `Notify` — interleaved with a non-matching form
14973        // to pin the typed-witness threading AND the NAME-slot
14974        // projection AND the source-order yield AND the rejection of
14975        // non-classifier forms in ONE assertion.
14976        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14977        enum Kind {
14978            Monitor,
14979            Notify,
14980        }
14981        let src = r#"(defmonitor cpu :threshold 80)
14982                     (other-form 99)
14983                     (defnotify email :to "ops@example.com")
14984                     (defmonitor mem :threshold 90)"#;
14985        let forms = read(src).unwrap();
14986        let mut e = Expander::new();
14987        let rows: Vec<(Kind, String, usize)> = e
14988            .expand_and_collect_named_calls_to_any(
14989                forms,
14990                |h| match h {
14991                    "defmonitor" => Some((Kind::Monitor, "defmonitor")),
14992                    "defnotify" => Some((Kind::Notify, "defnotify")),
14993                    _ => None,
14994                },
14995                |kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
14996            )
14997            .unwrap();
14998        assert_eq!(
14999            rows,
15000            vec![
15001                (Kind::Monitor, "cpu".to_string(), 2),
15002                (Kind::Notify, "email".to_string(), 2),
15003                (Kind::Monitor, "mem".to_string(), 2),
15004            ],
15005        );
15006    }
15007
15008    #[test]
15009    fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
15010        // Soft-projection contract — every shape the typed-decoded
15011        // primitive rejects (non-list, empty list, list whose head is
15012        // not a symbol, list whose head decodes to `None`) skips the
15013        // projection silently. The named gate inside the wrapper
15014        // projection runs ONLY for classifier-matched forms; a non-
15015        // matching form that reaches the projection panics here.
15016        let src = r#":bare-keyword
15017                     "bare-string"
15018                     42
15019                     ()
15020                     (foo bar)
15021                     (defmonitor cpu :threshold 80)"#;
15022        let forms = read(src).unwrap();
15023        let mut e = Expander::new();
15024        let names: Vec<String> = e
15025            .expand_and_collect_named_calls_to_any(
15026                forms,
15027                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15028                |(), name, _args| Ok(name.to_string()),
15029            )
15030            .unwrap();
15031        assert_eq!(names, vec!["cpu".to_string()]);
15032    }
15033
15034    #[test]
15035    fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
15036    ) {
15037        // `(defmonitor)` matches the classifier but has no NAME slot —
15038        // `split_name_slot` fires `NamedFormMissingName { keyword:
15039        // "defmonitor" }`. The classifier's `&'static str` keyword is
15040        // threaded VERBATIM through the named gate; a regression that
15041        // hardcodes a different keyword (e.g. the matched head text or
15042        // a default) would fail the structural variant identity
15043        // assertion. Fail-before-pass-after: pre-lift the primitive did
15044        // not exist; the inline `expand_and_collect_calls_to_any +
15045        // split_name_slot` composition at the consumer site was the
15046        // only path, with the keyword bound at the consumer's call
15047        // boundary rather than at the named primitive's body.
15048        let forms = read("(defmonitor)").unwrap();
15049        let mut e = Expander::new();
15050        let err = e
15051            .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15052                forms,
15053                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15054                |(), _name, _args| Ok(()),
15055            )
15056            .unwrap_err();
15057        assert!(
15058            matches!(
15059                err,
15060                crate::error::LispError::NamedFormMissingName {
15061                    keyword: "defmonitor"
15062                }
15063            ),
15064            "expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
15065        );
15066    }
15067
15068    #[test]
15069    fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
15070    ) {
15071        // `(defmonitor 42 :threshold 80)` matches the classifier but
15072        // the NAME slot is an int — `split_name_slot` fires
15073        // `NamedFormNonSymbolName { keyword: "defmonitor", got:
15074        // SexpShape::Int }`. The classifier's `&'static str` keyword is
15075        // threaded through the structural rejection variant verbatim,
15076        // and the typed `SexpShape` projection on the offending slot
15077        // is preserved across the named primitive.
15078        let forms = read("(defmonitor 42 :threshold 80)").unwrap();
15079        let mut e = Expander::new();
15080        let err = e
15081            .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15082                forms,
15083                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15084                |(), _name, _args| Ok(()),
15085            )
15086            .unwrap_err();
15087        assert!(
15088            matches!(
15089                err,
15090                crate::error::LispError::NamedFormNonSymbolName {
15091                    keyword: "defmonitor",
15092                    got: crate::error::SexpShape::Int,
15093                }
15094            ),
15095            "expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
15096        );
15097    }
15098
15099    #[test]
15100    fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
15101        // Result projection's short-circuit contract — when the
15102        // projection returns `Err` on a matched named form, the walk
15103        // stops and subsequent matched forms are NOT projected. Pin
15104        // source-order short-circuit via a counter the projection
15105        // increments before deciding to fail: the counter sits at
15106        // exactly the index of the failing form (second match),
15107        // proving the third match never reached the projection.
15108        let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
15109        let mut count = 0usize;
15110        let mut e = Expander::new();
15111        let err = e
15112            .expand_and_collect_named_calls_to_any::<String, _, _, ()>(
15113                forms,
15114                |h| (h == "defmon").then_some(((), "defmon")),
15115                |(), name, _args| {
15116                    count += 1;
15117                    if name == "b" {
15118                        return Err(crate::error::LispError::Missing("test-failure"));
15119                    }
15120                    Ok(name.to_string())
15121                },
15122            )
15123            .expect_err("projection must short-circuit on first Err");
15124        assert_eq!(
15125            count, 2,
15126            "projection must have run on first matched form AND the failing form, then stopped"
15127        );
15128        assert!(
15129            matches!(err, crate::error::LispError::Missing("test-failure")),
15130            "expected the projection's typed Err verbatim, got: {err:?}"
15131        );
15132    }
15133
15134    #[test]
15135    fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
15136        // Ordering contract — `expand_program` runs BEFORE the
15137        // typed-decoded named walk. A `(defmacro …)` form that emits a
15138        // classifier-decoded named form must have its EXPANDED head
15139        // decoded by the classifier, not the macro-call head. Pin the
15140        // ordering with a macro `(emit-mon n thr)` expanding to
15141        // `(defmonitor ,n :threshold ,thr)` — the classifier sees the
15142        // expanded `defmonitor` head and the NAME slot's symbol value
15143        // from the macro arg.
15144        let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15145                     (defmonitor cpu :threshold 80)
15146                     (emit-mon mem 90)
15147                     (other-form 99)
15148                     (emit-mon disk 70)"#;
15149        let forms = read(src).unwrap();
15150        let mut e = Expander::new();
15151        let names: Vec<String> = e
15152            .expand_and_collect_named_calls_to_any(
15153                forms,
15154                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15155                |(), name, _args| Ok(name.to_string()),
15156            )
15157            .unwrap();
15158        // `cpu` from the literal call, `mem` from the first macro
15159        // emit, `disk` from the second macro emit. The macro-emitted
15160        // forms are present BECAUSE `expand_program` ran first.
15161        assert_eq!(names, vec!["cpu", "mem", "disk"]);
15162    }
15163
15164    #[test]
15165    fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
15166        // From-source posture parity: feeding source through
15167        // `expand_source_and_collect_named_calls_to_any` is byte-
15168        // identical to feeding `read(src)?` through the from-forms
15169        // sibling on a fresh expander, because the from-source posture
15170        // is `read(src)? + from-forms` by construction. A regression
15171        // that drifts the from-source posture's pipeline from the
15172        // from-forms posture's pipeline would fail here.
15173        let src = r#"(defmonitor cpu :threshold 80)
15174                     (defmonitor mem :threshold 90)"#;
15175        let via_src: Vec<String> = Expander::new()
15176            .expand_source_and_collect_named_calls_to_any(
15177                src,
15178                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15179                |(), name, _args| Ok(name.to_string()),
15180            )
15181            .unwrap();
15182        let forms = read(src).unwrap();
15183        let via_forms: Vec<String> = Expander::new()
15184            .expand_and_collect_named_calls_to_any(
15185                forms,
15186                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15187                |(), name, _args| Ok(name.to_string()),
15188            )
15189            .unwrap();
15190        assert_eq!(via_src, via_forms);
15191        assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15192    }
15193
15194    #[test]
15195    fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
15196    ) {
15197        // `?`-routing through `read` short-circuits BEFORE the
15198        // classifier or the named gate runs. Unbalanced paren — reader
15199        // error fires; classifier panic-decoder MUST NOT execute,
15200        // proving the read step gates the entire pipeline.
15201        let mut e = Expander::new();
15202        let err = e
15203            .expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
15204                "(defmonitor cpu :threshold 80",
15205                |_h| panic!("classifier must not run when reader fails"),
15206                |(), _name, _args| Ok(()),
15207            )
15208            .unwrap_err();
15209        // The named gate's variants MUST NOT fire — the reader error
15210        // is structurally distinct.
15211        assert!(
15212            !matches!(
15213                err,
15214                crate::error::LispError::NamedFormMissingName { .. }
15215                    | crate::error::LispError::NamedFormNonSymbolName { .. }
15216            ),
15217            "expected reader error, not named-gate variant; got: {err:?}"
15218        );
15219    }
15220
15221    // ── expand_and_collect_named_calls_to (from-forms × named × ──────
15222    // constant-keyword × untyped R) ─────────────────────────────────
15223    //
15224    // The (named NAME-then-kwargs × constant-keyword × untyped `R`)
15225    // cell of the `Expander` typed-walk family — pre-lift the cell was
15226    // reachable ONLY through `expand_to_named<T>` with the
15227    // `T: TataraDomain` parameter baking BOTH the `T::KEYWORD` filter
15228    // AND the `T::compile_from_args`-based projection through
15229    // `expand_and_collect_calls_to(forms, T::KEYWORD,
15230    // named_form_projection::<T>)`. Post-lift the cell surfaces as ONE
15231    // method that takes the keyword and the `(name, args) -> R`
15232    // projection as caller-supplied parameters, AND
15233    // `expand_to_named<T>` routes through it as the typed
15234    // `T::KEYWORD`-constant specialization — so the named-form
15235    // `split_name_slot` composition lives at ONE site
15236    // (`expand_and_collect_named_calls_to_any` body) post-lift rather
15237    // than at TWO sites pre-lift (the bare-kwargs path through
15238    // `named_form_projection<T>` AND the classifier path).
15239    //
15240    // The tests below pin: (a) the happy path yields the `(name, args)`
15241    // pair for every matched form in source order; (b) non-matching
15242    // forms are skipped (soft-projection contract inherited from the
15243    // named typed-decoded primitive); (c) the `NamedFormMissingName`
15244    // variant fires for matched forms with no NAME slot, threading the
15245    // primitive's `&'static str` keyword; (d) the
15246    // `NamedFormNonSymbolName` variant fires for matched forms with a
15247    // non-symbol-or-string NAME slot, again threading the primitive's
15248    // keyword; (e) the projection's `Err` short-circuits at the first
15249    // failing matched form; (f) `expand_program` runs BEFORE the
15250    // keyword filter walks — a `(defmacro …)` emitting a named form
15251    // has its EXPANDED head matched by the constant-keyword filter,
15252    // not the macro-call head; (g) the from-source sibling agrees with
15253    // `read + from-forms` on the same source; (h) the constant-
15254    // classifier composition law binds the runtime-keyword cell
15255    // (`expand_and_collect_named_calls_to`) to the typed-decoded
15256    // classifier cell (`expand_and_collect_named_calls_to_any`) via a
15257    // constant-classifier decoder.
15258
15259    #[test]
15260    fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
15261    ) {
15262        // The constant-keyword named primitive's happy path: a runtime
15263        // `&'static str` keyword filters matched forms, and the per-
15264        // form projection receives `(name, spec_args)` for every
15265        // matched form in source order. Three matched forms
15266        // interleaved with a non-matcher pin the source-order yield,
15267        // the NAME-slot projection, AND the rejection of non-keyword
15268        // forms in ONE assertion.
15269        let src = r#"(defmonitor cpu :threshold 80)
15270                     (other-form 99)
15271                     (defmonitor mem :threshold 90 :unit "MiB")
15272                     (defmonitor disk :threshold 70)"#;
15273        let forms = read(src).unwrap();
15274        let mut e = Expander::new();
15275        let rows: Vec<(String, usize)> = e
15276            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
15277                Ok((name.to_string(), spec_args.len()))
15278            })
15279            .unwrap();
15280        assert_eq!(
15281            rows,
15282            vec![
15283                ("cpu".to_string(), 2),
15284                ("mem".to_string(), 4),
15285                ("disk".to_string(), 2),
15286            ],
15287        );
15288    }
15289
15290    #[test]
15291    fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
15292        // Soft-projection contract — every shape the named typed-
15293        // decoded primitive rejects (non-list, empty list, list whose
15294        // head is not a symbol, list whose head doesn't match the
15295        // constant keyword) skips the projection silently. The named
15296        // gate inside the wrapper projection runs ONLY for matched
15297        // forms; a non-matching form that reaches the projection
15298        // panics here.
15299        let src = r#":bare-keyword
15300                     "bare-string"
15301                     42
15302                     ()
15303                     (foo bar)
15304                     (defmonitor cpu :threshold 80)"#;
15305        let forms = read(src).unwrap();
15306        let mut e = Expander::new();
15307        let names: Vec<String> = e
15308            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15309                Ok(name.to_string())
15310            })
15311            .unwrap();
15312        assert_eq!(names, vec!["cpu".to_string()]);
15313    }
15314
15315    #[test]
15316    fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
15317        // The named-form gate's missing-NAME variant must thread the
15318        // primitive's `&'static str` keyword (NOT a hardcoded literal,
15319        // NOT the projection's own copy) — the `NamedFormMissingName
15320        // { keyword }` slot inside `split_name_slot` is bound at the
15321        // call site BY THE PRIMITIVE itself, via the constant-
15322        // classifier decoder threading `((), keyword)` into
15323        // `expand_and_collect_named_calls_to_any`.
15324        let src = r#"(defmonitor)"#;
15325        let forms = read(src).unwrap();
15326        let mut e = Expander::new();
15327        let err = e
15328            .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15329            .unwrap_err();
15330        assert!(
15331            matches!(
15332                err,
15333                crate::error::LispError::NamedFormMissingName {
15334                    keyword: "defmonitor"
15335                }
15336            ),
15337            "expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
15338        );
15339    }
15340
15341    #[test]
15342    fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
15343    ) {
15344        // The named-form gate's non-symbol-NAME variant must thread
15345        // the primitive's `&'static str` keyword AND the typed-shape
15346        // witness (`SexpShape::Int` for an integer NAME slot). The
15347        // shape projection through `split_name_slot`'s
15348        // `as_symbol_or_string()` gate fires on the first matched
15349        // form whose NAME slot is not a symbol or string.
15350        let src = r#"(defmonitor 42 :threshold 80)"#;
15351        let forms = read(src).unwrap();
15352        let mut e = Expander::new();
15353        let err = e
15354            .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15355            .unwrap_err();
15356        match err {
15357            crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
15358                assert_eq!(keyword, "defmonitor");
15359                assert_eq!(got, crate::error::SexpShape::Int);
15360            }
15361            other => panic!(
15362                "expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
15363            ),
15364        }
15365    }
15366
15367    #[test]
15368    fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
15369        // `Iterator::collect::<Result<Vec<R>, _>>()` short-circuits at
15370        // the first failing projection. The second matched form's
15371        // projection error MUST fire AND the third matched form's
15372        // projection MUST NOT run. Pin both halves with a counter +
15373        // a fail-on-call sentinel.
15374        let src = r#"(defmonitor cpu :threshold 80)
15375                     (defmonitor mem :threshold 90)
15376                     (defmonitor disk :threshold 70)"#;
15377        let forms = read(src).unwrap();
15378        let mut count: usize = 0;
15379        let mut e = Expander::new();
15380        let err = e
15381            .expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
15382                count += 1;
15383                if name == "mem" {
15384                    Err(crate::error::LispError::Compile {
15385                        form: "test".into(),
15386                        message: format!("rejecting at NAME={name}"),
15387                    })
15388                } else {
15389                    Ok(name.to_string())
15390                }
15391            })
15392            .unwrap_err();
15393        assert_eq!(count, 2, "third matched form must not be projected");
15394        match err {
15395            crate::error::LispError::Compile { message, .. } => {
15396                assert!(message.contains("NAME=mem"));
15397            }
15398            other => panic!("expected projection-driven Compile error; got: {other:?}"),
15399        }
15400    }
15401
15402    #[test]
15403    fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
15404        // Ordering contract — `expand_program` runs BEFORE the
15405        // constant-keyword named walk. A `(defmacro …)` form that
15406        // emits a matched named form must have its EXPANDED head
15407        // matched by the constant-keyword filter, not the macro-call
15408        // head. Pin the ordering with a macro `(emit-mon n thr)`
15409        // expanding to `(defmonitor ,n :threshold ,thr)` — the
15410        // constant-keyword filter matches the expanded `defmonitor`
15411        // head and the NAME slot projects the symbol-author value.
15412        let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15413                     (defmonitor cpu :threshold 80)
15414                     (emit-mon mem 90)
15415                     (other-form 99)
15416                     (emit-mon disk 70)"#;
15417        let forms = read(src).unwrap();
15418        let mut e = Expander::new();
15419        let names: Vec<String> = e
15420            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15421                Ok(name.to_string())
15422            })
15423            .unwrap();
15424        // `cpu` from the literal call, `mem` from the first macro
15425        // emit, `disk` from the second macro emit. The macro-emitted
15426        // forms are present BECAUSE `expand_program` ran first.
15427        assert_eq!(names, vec!["cpu", "mem", "disk"]);
15428    }
15429
15430    #[test]
15431    fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
15432        // From-source posture parity: feeding source through
15433        // `expand_source_and_collect_named_calls_to` is byte-identical
15434        // to feeding `read(src)?` through the from-forms sibling on a
15435        // fresh expander, because the from-source posture is
15436        // `read(src)? + from-forms` by construction. A regression that
15437        // drifts the from-source posture's pipeline from the from-
15438        // forms posture's pipeline would fail here.
15439        let src = r#"(defmonitor cpu :threshold 80)
15440                     (defmonitor mem :threshold 90)"#;
15441        let via_src: Vec<String> = Expander::new()
15442            .expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
15443                Ok(name.to_string())
15444            })
15445            .unwrap();
15446        let forms = read(src).unwrap();
15447        let via_forms: Vec<String> = Expander::new()
15448            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15449                Ok(name.to_string())
15450            })
15451            .unwrap();
15452        assert_eq!(via_src, via_forms);
15453        assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15454    }
15455
15456    #[test]
15457    fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
15458    ) {
15459        // `?`-routing through `read` short-circuits BEFORE the
15460        // constant-keyword filter, the named gate, or the projection
15461        // runs. Unbalanced paren — reader error fires; the projection
15462        // panic-payload MUST NOT execute, proving the read step gates
15463        // the entire pipeline.
15464        let mut e = Expander::new();
15465        let err = e
15466            .expand_source_and_collect_named_calls_to::<(), _>(
15467                "(defmonitor cpu :threshold 80",
15468                "defmonitor",
15469                |_name, _args| panic!("projection must not run when reader fails"),
15470            )
15471            .unwrap_err();
15472        // The named gate's variants MUST NOT fire — the reader error
15473        // is structurally distinct.
15474        assert!(
15475            !matches!(
15476                err,
15477                crate::error::LispError::NamedFormMissingName { .. }
15478                    | crate::error::LispError::NamedFormNonSymbolName { .. }
15479            ),
15480            "expected reader error, not named-gate variant; got: {err:?}",
15481        );
15482    }
15483
15484    #[test]
15485    fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
15486    ) {
15487        // Composition-identity test pinning the runtime-keyword named
15488        // cell (`expand_and_collect_named_calls_to`) to the typed-
15489        // decoded named-classifier cell
15490        // (`expand_and_collect_named_calls_to_any`) via the constant-
15491        // classifier decoder shape. Post-lift the identity:
15492        //
15493        //   expand_and_collect_named_calls_to(forms, kw, project) ==
15494        //       expand_and_collect_named_calls_to_any(forms,
15495        //           |h| (h == kw).then_some(((), kw)),
15496        //           |(), name, args| project(name, args))
15497        //
15498        // Pinning the identity here makes the typed-decoded named-
15499        // classifier primitive the CANONICAL composition point the
15500        // runtime-keyword sibling routes through — parallel to how
15501        // `expand_and_collect_calls_to` routes through
15502        // `expand_and_collect_calls_to_any` via a `|h| (h == k).
15503        // then_some(())` decoder on the bare-kwargs axis. A future
15504        // regression that drifts ONE cell's NAME-slot rejection chain
15505        // from the other becomes loudly visible at this assertion.
15506        let src = r#"(defmonitor cpu :threshold 80)
15507                     (other-form 99)
15508                     (defmonitor mem :threshold 90)"#;
15509        let forms = read(src).unwrap();
15510        let via_constant_keyword: Vec<(String, usize)> = Expander::new()
15511            .expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
15512                Ok((name.to_string(), args.len()))
15513            })
15514            .unwrap();
15515        let via_classifier: Vec<(String, usize)> = Expander::new()
15516            .expand_and_collect_named_calls_to_any(
15517                forms,
15518                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15519                |(), name, args| Ok((name.to_string(), args.len())),
15520            )
15521            .unwrap();
15522        assert_eq!(via_constant_keyword, via_classifier);
15523        assert_eq!(
15524            via_constant_keyword,
15525            vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
15526        );
15527    }
15528
15529    #[test]
15530    fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
15531    ) {
15532        // End-to-end path-uniformity: `Expander::expand_to_named<T>`
15533        // (the typed constant-keyword named cell) must yield the same
15534        // payload as `expand_and_collect_named_calls_to(forms,
15535        // T::KEYWORD, |name, args| { T::compile_from_args(args)? +
15536        // NamedDefinition })` — the lift's structural identity.
15537        //
15538        // Pre-lift `expand_to_named<T>` routed through
15539        // `expand_and_collect_calls_to(forms, T::KEYWORD,
15540        // named_form_projection::<T>)` (the bare-kwargs constant
15541        // primitive with `named_form_projection<T>` doing the NAME
15542        // extraction internally). Post-lift `expand_to_named<T>`
15543        // routes through the named constant-keyword primitive (which
15544        // itself routes through the classifier primitive via a
15545        // constant-classifier decoder) — so the `split_name_slot`
15546        // composition lives at ONE site (the `_any` primitive body)
15547        // post-lift rather than at TWO sites.
15548        use crate::compile::NamedDefinition;
15549        use crate::compiler_spec::CompilerSpec;
15550        let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
15551                     (defcompiler beta-compiler  :name "y" :dialect "standard")"#;
15552        let forms = read(src).unwrap();
15553        let via_expand_to_named = Expander::new()
15554            .expand_to_named::<CompilerSpec>(forms.clone())
15555            .unwrap();
15556        let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
15557            .expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
15558                let spec =
15559                    <CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
15560                Ok(NamedDefinition {
15561                    name: name.to_string(),
15562                    spec,
15563                })
15564            })
15565            .unwrap();
15566        assert_eq!(via_expand_to_named.len(), 2);
15567        assert_eq!(via_expand_to_named.len(), via_named_constant.len());
15568        for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
15569            assert_eq!(a.name, b.name, "NAME slot must agree across cells");
15570            assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
15571        }
15572        assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
15573        assert_eq!(via_expand_to_named[0].spec.name, "x");
15574        assert_eq!(via_expand_to_named[1].name, "beta-compiler");
15575        assert_eq!(via_expand_to_named[1].spec.name, "y");
15576    }
15577
15578    // ── ExpansionDepthExceeded: runaway macro rejected instead of aborting ──
15579
15580    #[test]
15581    fn expander_default_max_expansion_depth_is_the_module_constant() {
15582        // Both constructors seed `max_expansion_depth` from the module
15583        // constant — a regression that drifts one constructor's default
15584        // from the constant (e.g. a hardcoded `256` at ONE site with the
15585        // other left uninitialized after a `Default` derive addition)
15586        // fails this pin.
15587        assert_eq!(
15588            Expander::new().max_expansion_depth(),
15589            DEFAULT_MAX_EXPANSION_DEPTH
15590        );
15591        assert_eq!(
15592            Expander::new_substitute_only().max_expansion_depth(),
15593            DEFAULT_MAX_EXPANSION_DEPTH
15594        );
15595        assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
15596    }
15597
15598    #[test]
15599    fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
15600        let mut e = Expander::new();
15601        e.set_max_expansion_depth(7);
15602        assert_eq!(e.max_expansion_depth(), 7);
15603    }
15604
15605    #[test]
15606    fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
15607        // `(defmacro loop (x) `(loop ,x))` applied to `(loop hello)`
15608        // pre-lift stack-overflowed the process — a below-floor abort.
15609        // Post-lift the expander returns a typed
15610        // `LispError::ExpansionDepthExceeded` at the ceiling with
15611        // `macro_name` populated from the offending call's head.
15612        let mut e = Expander::new();
15613        e.set_max_expansion_depth(4);
15614        let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15615        let err = e.expand_program(forms).unwrap_err();
15616        match err {
15617            LispError::ExpansionDepthExceeded { macro_name, limit } => {
15618                assert_eq!(macro_name, "loop");
15619                assert_eq!(limit, 4);
15620            }
15621            other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15622        }
15623    }
15624
15625    #[test]
15626    fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
15627        // The substitute strategy shares the same `expand` outer walker
15628        // with the bytecode strategy, so the ceiling fires at the SAME
15629        // gate regardless of which apply-layer is active. The pin makes
15630        // that shared-outer-walker property structural: a regression
15631        // that ever branches the depth guard on strategy (e.g. moves it
15632        // into `apply_compiled` alone) fails this test on the
15633        // substitute expander.
15634        let mut e = Expander::new_substitute_only();
15635        e.set_max_expansion_depth(4);
15636        let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15637        let err = e.expand_program(forms).unwrap_err();
15638        match err {
15639            LispError::ExpansionDepthExceeded { macro_name, limit } => {
15640                assert_eq!(macro_name, "loop");
15641                assert_eq!(limit, 4);
15642            }
15643            other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15644        }
15645    }
15646
15647    #[test]
15648    fn expand_lawful_nested_macros_within_ceiling_succeed() {
15649        // Lawful nested macros (`(twice (twice hey))`) accrete depth in
15650        // the low single digits because the tree-child arm doesn't
15651        // count — only the post-apply re-expansion path bumps the
15652        // counter. A ceiling of 3 is enough for a two-deep nested
15653        // expansion. This pin is the fail-before/pass-after negative
15654        // control for the recursive-macro rejection tests above: the
15655        // depth counter must NOT reject lawful macro-nesting within
15656        // the ceiling, and only reject the runaway shape.
15657        let mut e = Expander::new();
15658        e.set_max_expansion_depth(3);
15659        let forms = read(
15660            "(defmacro twice (x) `(list ,x ,x))
15661             (twice (twice hey))",
15662        )
15663        .unwrap();
15664        let out = e.expand_program(forms).unwrap();
15665        assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
15666    }
15667
15668    #[test]
15669    fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
15670        // A macro-free lawful deep tree — five levels of plain nesting —
15671        // is expanded at a ceiling well below the tree's height. The
15672        // tree-child arm does not accrete depth, so tree height is
15673        // orthogonal to the ceiling; only macro re-expansion counts.
15674        // A regression that ever bumps depth on tree-child descent
15675        // fails this pin.
15676        let mut e = Expander::new();
15677        e.set_max_expansion_depth(2);
15678        let forms = read("(a (b (c (d (e f)))))").unwrap();
15679        let out = e.expand_program(forms).unwrap();
15680        assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
15681    }
15682
15683    #[test]
15684    fn expansion_depth_exceeded_position_is_none() {
15685        // The variant joins the non-positional cohort — the offending
15686        // macro's byte offset is not what a runaway diagnostic anchors
15687        // to; the OFFENDING MACRO NAME (`macro_name`) is. This pin
15688        // keeps the variant in the `position() -> None` cohort so a
15689        // future span-carrying edit lands the field in ONE place
15690        // across every non-positional variant simultaneously.
15691        let err = LispError::ExpansionDepthExceeded {
15692            macro_name: "loop".to_string(),
15693            limit: 256,
15694        };
15695        assert_eq!(err.position(), None);
15696    }
15697
15698    #[test]
15699    fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
15700        // Display projection carries both `macro_name` and `limit`, so
15701        // authoring surfaces that substring-grep the rendered
15702        // diagnostic (`tatara-check`, REPL, LSP) see BOTH halves at
15703        // the variant boundary and never need to substring-parse
15704        // free-form text to recover the identity of the offending
15705        // macro or the ceiling it breached.
15706        let err = LispError::ExpansionDepthExceeded {
15707            macro_name: "loop".to_string(),
15708            limit: 256,
15709        };
15710        let rendered = err.to_string();
15711        assert!(rendered.contains("loop"), "rendered: {rendered}");
15712        assert!(rendered.contains("256"), "rendered: {rendered}");
15713        assert!(
15714            rendered.contains("macro expansion depth exceeded"),
15715            "rendered: {rendered}"
15716        );
15717    }
15718
15719    // ── max_cache_entries: bounded-memoization ceiling ──────────────────
15720
15721    #[test]
15722    fn expander_default_max_cache_entries_is_the_module_constant() {
15723        // Both cache-enabling constructors seed `max_cache_entries` from
15724        // the module constant — a regression that drifts one
15725        // constructor's default from the constant (e.g. a hardcoded
15726        // `8192` at ONE site with the other left uninitialized after a
15727        // `Default` derive addition) fails this pin. The peer
15728        // constructor `new_substitute_only` seeds the same ceiling so
15729        // the field stays initialized under `#[derive(Clone)]` (a
15730        // zero-initialized clone would be a silent bifurcation of the
15731        // cache-ceiling surface).
15732        assert_eq!(
15733            Expander::new().max_cache_entries(),
15734            DEFAULT_MAX_CACHE_ENTRIES
15735        );
15736        assert_eq!(
15737            Expander::new_substitute_only().max_cache_entries(),
15738            DEFAULT_MAX_CACHE_ENTRIES
15739        );
15740        assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
15741    }
15742
15743    #[test]
15744    fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
15745        // Setter mirrors accessor — a regression that ever forgets to
15746        // write through to the field, or writes to a shadow field the
15747        // accessor does not read, fails this pin.
15748        let mut e = Expander::new();
15749        e.set_max_cache_entries(7);
15750        assert_eq!(e.max_cache_entries(), 7);
15751    }
15752
15753    #[test]
15754    fn expand_cache_size_is_bounded_by_max_cache_entries() {
15755        // Five distinct `(name, args)` pairs expanded against a cache
15756        // ceiling of `2` populate the cache with EXACTLY the first two
15757        // insertions and skip the remaining three. Every expansion
15758        // still returns the correct result — the cache is a pure
15759        // PERFORMANCE optimization; the ceiling gates memoization,
15760        // never correctness. LOAD-BEARING true-arm catch on the
15761        // bounded-cache contract: a regression that ignored the
15762        // ceiling (unbounded insert) would land `cache_size() == 5`
15763        // here; a regression that skipped insertion entirely (broken
15764        // cache) would still see correct expansion output but would
15765        // fail the interior `cache_size() == 2` pin.
15766        let mut e = Expander::new();
15767        e.set_max_cache_entries(2);
15768        let src = "
15769            (defmacro id (x) `,x)
15770            (id one)
15771            (id two)
15772            (id three)
15773            (id four)
15774            (id five)
15775        ";
15776        let out = e.expand_program(read(src).unwrap()).unwrap();
15777        assert_eq!(out.len(), 5);
15778        assert_eq!(out[0], parse("one"));
15779        assert_eq!(out[1], parse("two"));
15780        assert_eq!(out[2], parse("three"));
15781        assert_eq!(out[3], parse("four"));
15782        assert_eq!(out[4], parse("five"));
15783        assert_eq!(
15784            e.cache_size(),
15785            2,
15786            "cache grew past the max_cache_entries ceiling",
15787        );
15788    }
15789
15790    #[test]
15791    fn expand_zero_cache_ceiling_disables_caching_effectively() {
15792        // Zero ceiling admits every fresh `(name, args)` pair through
15793        // the compute path and never grows the cache — the same
15794        // observable behavior as `set_cache_enabled(false)` but
15795        // reached through the ceiling knob. Correctness is preserved:
15796        // the miss path always recomputes the same value the cache
15797        // would have returned, so an operator that dials the ceiling
15798        // to `0` (e.g. to hunt a cache-related regression) never
15799        // bifurcates the output surface. Peer to
15800        // `set_max_expansion_depth(0)` one RESOURCE axis over — both
15801        // ceilings admit the zero endpoint as a well-defined
15802        // extremum.
15803        let mut e = Expander::new();
15804        e.set_max_cache_entries(0);
15805        let src = "
15806            (defmacro id (x) `,x)
15807            (id one)
15808            (id two)
15809        ";
15810        let out = e.expand_program(read(src).unwrap()).unwrap();
15811        assert_eq!(out.len(), 2);
15812        assert_eq!(out[0], parse("one"));
15813        assert_eq!(out[1], parse("two"));
15814        assert_eq!(
15815            e.cache_size(),
15816            0,
15817            "zero cache ceiling grew the cache — the ceiling must be respected on every insert",
15818        );
15819    }
15820
15821    #[test]
15822    fn expand_cached_hits_survive_past_the_ceiling() {
15823        // Once a `(name, args)` pair sits IN the cache under the
15824        // ceiling, subsequent calls hit it — the ceiling only gates
15825        // NEW-KEY inserts, never lookups. This test seeds the cache
15826        // with two entries under a ceiling of `2`, then re-issues the
15827        // SAME calls to prove the cached hits still fire (cache_size
15828        // stays at `2` because no new keys are inserted, and the
15829        // expansion output is stable). LOAD-BEARING true-arm catch
15830        // that the ceiling gates the INSERT path alone — a regression
15831        // that ever refused cache LOOKUPS at the ceiling would
15832        // silently bifurcate correctness on the second call.
15833        let mut e = Expander::new();
15834        e.set_max_cache_entries(2);
15835        let src = "
15836            (defmacro id (x) `,x)
15837            (id one)
15838            (id two)
15839            (id one)
15840            (id two)
15841        ";
15842        let out = e.expand_program(read(src).unwrap()).unwrap();
15843        assert_eq!(out.len(), 4);
15844        assert_eq!(out[0], parse("one"));
15845        assert_eq!(out[1], parse("two"));
15846        assert_eq!(out[2], parse("one"));
15847        assert_eq!(out[3], parse("two"));
15848        assert_eq!(
15849            e.cache_size(),
15850            2,
15851            "cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
15852        );
15853    }
15854
15855    #[test]
15856    fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
15857        // After the cache reaches its ceiling and subsequent inserts
15858        // are skipped, `clear_cache` empties the memo and RE-OPENS the
15859        // insert path — the next expansion populates the freshly-
15860        // empty cache without operator intervention on the ceiling
15861        // itself. The pin binds the operator-facing recovery path
15862        // documented on [`DEFAULT_MAX_CACHE_ENTRIES`]: "skip cache
15863        // insertion until the operator clears the cache via
15864        // [`Expander::clear_cache`]".
15865        let mut e = Expander::new();
15866        e.set_max_cache_entries(1);
15867        let src_fill = "
15868            (defmacro id (x) `,x)
15869            (id one)
15870            (id two)
15871        ";
15872        let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
15873        assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
15874        e.clear_cache();
15875        assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
15876        let out = e
15877            .expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
15878            .unwrap();
15879        assert_eq!(out.len(), 1);
15880        assert_eq!(out[0], parse("three"));
15881        assert_eq!(
15882            e.cache_size(),
15883            1,
15884            "clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
15885        );
15886    }
15887
15888    // ── max_expansion_size: bounded-output ceiling ─────────────────────
15889
15890    #[test]
15891    fn expander_default_max_expansion_size_is_the_module_constant() {
15892        // Both constructors seed `max_expansion_size` from the module
15893        // constant — a regression that drifts one constructor's
15894        // default from the constant (e.g. a hardcoded `65_536` at
15895        // ONE site with the other left uninitialized after a
15896        // `Default` derive addition) fails this pin. Peer to the
15897        // `max_expansion_depth` + `max_cache_entries` default-
15898        // coherence pins one RESOURCE-DIMENSION axis over.
15899        assert_eq!(
15900            Expander::new().max_expansion_size(),
15901            DEFAULT_MAX_EXPANSION_SIZE
15902        );
15903        assert_eq!(
15904            Expander::new_substitute_only().max_expansion_size(),
15905            DEFAULT_MAX_EXPANSION_SIZE
15906        );
15907        assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
15908    }
15909
15910    #[test]
15911    fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
15912        // Setter mirrors accessor — a regression that ever forgets to
15913        // write through to the field, or writes to a shadow field the
15914        // accessor does not read, fails this pin. Peer to the
15915        // setter/accessor coherence pins on the depth + cache axes.
15916        let mut e = Expander::new();
15917        e.set_max_expansion_size(64);
15918        assert_eq!(e.max_expansion_size(), 64);
15919    }
15920
15921    #[test]
15922    fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
15923        // `(defmacro bomb (x) `(list ,x ,x ,x ,x))` applied to
15924        // `(bomb hey)` produces `(list hey hey hey hey)` — 6 nodes
15925        // (outer List + 5 children). With a ceiling of `4` this
15926        // crosses the OUTPUT-SIZE threshold at the first apply and
15927        // the expander returns a typed
15928        // `LispError::ExpansionSizeExceeded` with `macro_name`,
15929        // `size`, and `limit` all populated. Pre-lift the runaway
15930        // just accumulated in a growing tree until the process
15931        // heap ran out; post-lift the rejection sits at the same
15932        // call boundary as every other macro-expansion failure.
15933        let mut e = Expander::new();
15934        e.set_max_expansion_size(4);
15935        let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15936        let err = e.expand_program(forms).unwrap_err();
15937        match err {
15938            LispError::ExpansionSizeExceeded {
15939                macro_name,
15940                size,
15941                limit,
15942            } => {
15943                assert_eq!(macro_name, "bomb");
15944                assert_eq!(size, 6);
15945                assert_eq!(limit, 4);
15946            }
15947            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15948        }
15949    }
15950
15951    #[test]
15952    fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
15953        // The substitute strategy shares the same `expand_with_depth`
15954        // outer walker with the bytecode strategy, so the ceiling
15955        // fires at the SAME gate regardless of which apply-layer is
15956        // active. The pin makes that shared-outer-walker property
15957        // structural: a regression that ever branches the size guard
15958        // on strategy (e.g. moves it into `apply_compiled` alone)
15959        // fails this test on the substitute expander. Peer to
15960        // `expand_recursive_macro_rejects_at_depth_limit_substitute_path`
15961        // one RESOURCE axis over.
15962        let mut e = Expander::new_substitute_only();
15963        e.set_max_expansion_size(4);
15964        let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15965        let err = e.expand_program(forms).unwrap_err();
15966        match err {
15967            LispError::ExpansionSizeExceeded {
15968                macro_name,
15969                size,
15970                limit,
15971            } => {
15972                assert_eq!(macro_name, "bomb");
15973                assert_eq!(size, 6);
15974                assert_eq!(limit, 4);
15975            }
15976            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15977        }
15978    }
15979
15980    #[test]
15981    fn expand_lawful_output_within_size_ceiling_succeeds() {
15982        // `(defmacro twice (x) `(list ,x ,x))` applied to `(twice hey)`
15983        // produces `(list hey hey)` — 4 nodes. With a ceiling of `4`
15984        // the output sits AT the ceiling (`<=` admits equality; only
15985        // `>` rejects), so the expansion succeeds. Fail-before/pass-
15986        // after negative control for the bomb rejection tests: a
15987        // regression that ever rejected at-ceiling output would fail
15988        // this pin. Peer to `expand_lawful_nested_macros_within_ceiling_succeed`
15989        // one RESOURCE axis over.
15990        let mut e = Expander::new();
15991        e.set_max_expansion_size(4);
15992        let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
15993        let out = e.expand_program(forms).unwrap();
15994        assert_eq!(out[0], parse("(list hey hey)"));
15995    }
15996
15997    #[test]
15998    fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
15999        // A macro-free lawful large tree is NOT gated by the size
16000        // ceiling — the ceiling gates macro-`apply` outputs alone,
16001        // not tree-child descents through non-macro forms. A `usize::MAX`
16002        // ceiling would trivially admit anything; this pin uses a
16003        // deliberately small ceiling of `4` on a tree with 6 nodes
16004        // to catch a regression that ever bumps the ceiling on tree
16005        // descent. A tree-only walk should NEVER fire the ceiling.
16006        // Peer to `expand_depth_ceiling_ignores_lawful_tree_nesting_depth`
16007        // one RESOURCE axis over.
16008        let mut e = Expander::new();
16009        e.set_max_expansion_size(4);
16010        // `(a (b (c d)))` — 6 nodes, no macros. Passes cleanly.
16011        let forms = read("(a (b (c d)))").unwrap();
16012        let out = e.expand_program(forms).unwrap();
16013        assert_eq!(out[0], parse("(a (b (c d)))"));
16014    }
16015
16016    #[test]
16017    fn expansion_size_exceeded_position_is_none() {
16018        // The variant joins the non-positional cohort — the offending
16019        // macro's byte offset is not what an expansion-bomb diagnostic
16020        // anchors to; the (offending macro name, observed size,
16021        // configured ceiling) triple is. This pin keeps the variant
16022        // in the `position() -> None` cohort so a future span-carrying
16023        // edit lands the field in ONE place across every non-positional
16024        // variant simultaneously. Peer to
16025        // `expansion_depth_exceeded_position_is_none` one RESOURCE
16026        // axis over.
16027        let err = LispError::ExpansionSizeExceeded {
16028            macro_name: "bomb".to_string(),
16029            size: 512,
16030            limit: 256,
16031        };
16032        assert_eq!(err.position(), None);
16033    }
16034
16035    #[test]
16036    fn expansion_size_exceeded_display_matches_typed_variant_shape() {
16037        // Display projection carries `macro_name`, `size`, AND
16038        // `limit`, so authoring surfaces that substring-grep the
16039        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16040        // THREE halves at the variant boundary and never need to
16041        // substring-parse free-form text to recover the identity of
16042        // the offending macro, the observed size, or the ceiling it
16043        // breached. Peer to `expansion_depth_exceeded_display_matches_typed_variant_shape`
16044        // one RESOURCE axis over.
16045        let err = LispError::ExpansionSizeExceeded {
16046            macro_name: "bomb".to_string(),
16047            size: 512,
16048            limit: 256,
16049        };
16050        let rendered = err.to_string();
16051        assert!(rendered.contains("bomb"), "rendered: {rendered}");
16052        assert!(rendered.contains("512"), "rendered: {rendered}");
16053        assert!(rendered.contains("256"), "rendered: {rendered}");
16054        assert!(
16055            rendered.contains("macro expansion output size exceeded"),
16056            "rendered: {rendered}"
16057        );
16058    }
16059
16060    #[test]
16061    fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
16062        // A lawful outer macro that expands to a call of a bomb inner
16063        // macro rejects at the INNER call — `macro_name` at the
16064        // variant boundary is the name of the macro whose `apply`
16065        // output crossed the ceiling, not the outermost caller.
16066        // Load-bearing name-provenance pin: an operator whose bomb
16067        // sits N levels deep in a compose chain needs to see the
16068        // INNER macro's name in the diagnostic, not the wrapping
16069        // shape. A regression that ever populates `macro_name` from
16070        // the outermost caller's head fails this pin.
16071        let mut e = Expander::new();
16072        e.set_max_expansion_size(4);
16073        // `wrapper` re-expands into `bomb`; `bomb` produces a size-6
16074        // output that crosses the ceiling. Expected: the diagnostic
16075        // names `bomb`, not `wrapper`.
16076        let src = "
16077            (defmacro bomb (x) `(list ,x ,x ,x ,x))
16078            (defmacro wrapper (x) `(bomb ,x))
16079            (wrapper hey)
16080        ";
16081        let err = e.expand_program(read(src).unwrap()).unwrap_err();
16082        match err {
16083            LispError::ExpansionSizeExceeded {
16084                macro_name,
16085                size,
16086                limit,
16087            } => {
16088                assert_eq!(macro_name, "bomb");
16089                assert_eq!(size, 6);
16090                assert_eq!(limit, 4);
16091            }
16092            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16093        }
16094    }
16095
16096    // ── max_macro_body_size: REGISTRATION-time body-size ceiling ──────
16097    // Peer to depth (recursion length) + cache (memoization width) +
16098    // expansion size (single-`apply` OUTPUT size) one PIPELINE-STAGE
16099    // axis over — the three prior guards fire at EXPAND time; this
16100    // one fires at REGISTER time. Closes the expander's RESOURCE
16101    // surface at a fourth typed dimension across two pipeline stages.
16102
16103    #[test]
16104    fn expander_default_max_macro_body_size_is_the_module_constant() {
16105        // Both constructors seed `max_macro_body_size` from the
16106        // module constant — a regression that drifts one
16107        // constructor's default from the constant (e.g. a hardcoded
16108        // `16_384` at ONE site with the other left uninitialized
16109        // after a `Default` derive addition) fails this pin. Peer
16110        // to the `expander_default_max_expansion_{depth,size}_is_the_module_constant`
16111        // pins one PIPELINE-STAGE axis over.
16112        assert_eq!(
16113            Expander::new().max_macro_body_size(),
16114            DEFAULT_MAX_MACRO_BODY_SIZE
16115        );
16116        assert_eq!(
16117            Expander::new_substitute_only().max_macro_body_size(),
16118            DEFAULT_MAX_MACRO_BODY_SIZE
16119        );
16120        assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
16121    }
16122
16123    #[test]
16124    fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
16125        // Setter mirrors accessor — a regression that ever forgets to
16126        // write through to the field, or writes to a shadow field the
16127        // accessor does not read, fails this pin. Peer to the
16128        // setter/accessor coherence pins on the depth + cache +
16129        // expansion-size axes.
16130        let mut e = Expander::new();
16131        e.set_max_macro_body_size(64);
16132        assert_eq!(e.max_macro_body_size(), 64);
16133    }
16134
16135    #[test]
16136    fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
16137        // `(defmacro huge (x) `(list a b c d e))` has body
16138        // `` `(list a b c d e) `` — a `Sexp::Quasiquote` wrapping a
16139        // list of 6 nodes (outer List + 5 children); the outer
16140        // quasi-quote wrapper adds one → 8 nodes total for
16141        // `def.body.node_count()`. With a ceiling of `4` this
16142        // crosses the REGISTRATION-time BODY-SIZE threshold and the
16143        // expander returns a typed `LispError::MacroBodySizeExceeded`
16144        // with `macro_name`, `size`, and `limit` all populated. Peer
16145        // to `expand_expansion_bomb_rejects_at_size_limit_bytecode_path`
16146        // one PIPELINE-STAGE axis over.
16147        let mut e = Expander::new();
16148        e.set_max_macro_body_size(4);
16149        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16150        let err = e.expand_program(forms).unwrap_err();
16151        match err {
16152            LispError::MacroBodySizeExceeded {
16153                macro_name,
16154                size,
16155                limit,
16156            } => {
16157                assert_eq!(macro_name, "huge");
16158                assert_eq!(size, 8);
16159                assert_eq!(limit, 4);
16160            }
16161            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16162        }
16163    }
16164
16165    #[test]
16166    fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
16167        // The substitute strategy shares the same
16168        // `register_macro_def` outer registration entry with the
16169        // bytecode strategy, so the REGISTRATION-time BODY-SIZE
16170        // ceiling fires at the SAME gate regardless of which
16171        // apply-layer is active. The pin makes that shared-registration
16172        // property structural: a regression that ever branches the
16173        // body-size guard on strategy (e.g. moves it into the
16174        // `compile_templates` arm alone) fails this test on the
16175        // substitute expander. Peer to
16176        // `expand_expansion_bomb_rejects_at_size_limit_substitute_path`
16177        // one PIPELINE-STAGE axis over.
16178        let mut e = Expander::new_substitute_only();
16179        e.set_max_macro_body_size(4);
16180        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16181        let err = e.expand_program(forms).unwrap_err();
16182        match err {
16183            LispError::MacroBodySizeExceeded {
16184                macro_name,
16185                size,
16186                limit,
16187            } => {
16188                assert_eq!(macro_name, "huge");
16189                assert_eq!(size, 8);
16190                assert_eq!(limit, 4);
16191            }
16192            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16193        }
16194    }
16195
16196    #[test]
16197    fn register_macro_body_at_ceiling_admits() {
16198        // `(defmacro twice (x) `(list ,x))` has body
16199        // `` `(list ,x) `` — `Sexp::Quasiquote` wrapping a 3-node
16200        // list (List + `list` atom + `,x` unquote which is
16201        // 1 + inner atom = 2 nodes) = 1 + (1 + 1 + 2) = 5 nodes.
16202        // With a ceiling of `5` the body sits AT the ceiling
16203        // (`<=` admits equality; only `>` rejects), so the
16204        // registration succeeds and the subsequent expansion
16205        // proceeds normally. Fail-before/pass-after negative
16206        // control for the body-bomb rejection tests: a regression
16207        // that ever rejected at-ceiling bodies would fail this pin.
16208        let mut e = Expander::new();
16209        e.set_max_macro_body_size(5);
16210        let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
16211        let out = e.expand_program(forms).unwrap();
16212        assert_eq!(out[0], parse("(list hey)"));
16213    }
16214
16215    #[test]
16216    fn register_failure_leaves_both_tables_pristine() {
16217        // A rejected registration MUST leave BOTH `self.macros`
16218        // AND `self.templates` exactly as they were — no
16219        // partial-write window in which one table carries the
16220        // over-ceiling entry while the other does not, or in
16221        // which the template pre-compile already ran. This pin
16222        // catches a regression that ever moves the body-size
16223        // gate below `self.templates.insert` or below
16224        // `self.macros.insert`.
16225        let mut e = Expander::new();
16226        e.set_max_macro_body_size(4);
16227        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16228        assert!(e.expand_program(forms).is_err());
16229        assert!(
16230            !e.has("huge"),
16231            "macros table must stay pristine after rejection"
16232        );
16233        assert_eq!(e.len(), 0);
16234    }
16235
16236    #[test]
16237    fn macro_body_size_exceeded_position_is_none() {
16238        // The variant joins the non-positional cohort — the
16239        // offending macro's byte offset is not what a body-bomb
16240        // diagnostic anchors to; the (offending macro name,
16241        // observed body size, configured ceiling) triple is.
16242        // This pin keeps the variant in the `position() -> None`
16243        // cohort so a future span-carrying edit lands the field
16244        // in ONE place across every non-positional variant
16245        // simultaneously. Peer to
16246        // `expansion_{depth,size}_exceeded_position_is_none` one
16247        // PIPELINE-STAGE axis over.
16248        let err = LispError::MacroBodySizeExceeded {
16249            macro_name: "huge".to_string(),
16250            size: 512,
16251            limit: 256,
16252        };
16253        assert_eq!(err.position(), None);
16254    }
16255
16256    #[test]
16257    fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
16258        // Display projection carries `macro_name`, `size`, AND
16259        // `limit`, so authoring surfaces that substring-grep the
16260        // rendered diagnostic (`tatara-check`, REPL, LSP) see
16261        // ALL THREE halves at the variant boundary and never need
16262        // to substring-parse free-form text to recover the
16263        // identity of the offending macro, the observed body
16264        // size, or the ceiling it breached. Peer to
16265        // `expansion_size_exceeded_display_matches_typed_variant_shape`
16266        // one PIPELINE-STAGE axis over.
16267        let err = LispError::MacroBodySizeExceeded {
16268            macro_name: "huge".to_string(),
16269            size: 512,
16270            limit: 256,
16271        };
16272        let rendered = err.to_string();
16273        assert!(rendered.contains("huge"), "rendered: {rendered}");
16274        assert!(rendered.contains("512"), "rendered: {rendered}");
16275        assert!(rendered.contains("256"), "rendered: {rendered}");
16276        assert!(
16277            rendered.contains("macro body size exceeded"),
16278            "rendered: {rendered}"
16279        );
16280    }
16281
16282    #[test]
16283    fn lawful_macro_body_within_ceiling_registers_and_expands() {
16284        // A lawful hand-authored macro body sits well below any
16285        // realistic ceiling — the default `DEFAULT_MAX_MACRO_BODY_SIZE`
16286        // is deliberately generous so operators never brush it on
16287        // real authoring workloads. This pin locks the default
16288        // ceiling's non-interference posture: a regression that
16289        // ever lowered the default enough to reject the reference
16290        // `(defmacro when (cond x) `(if ,cond ,x))` shape would
16291        // fail here.
16292        let mut e = Expander::new();
16293        let forms = read(
16294            "(defmacro when (cond x) `(if ,cond ,x))
16295             (when #t hey)",
16296        )
16297        .unwrap();
16298        let out = e.expand_program(forms).unwrap();
16299        assert_eq!(out[0], parse("(if #t hey)"));
16300    }
16301
16302    #[test]
16303    fn register_macro_def_direct_call_respects_body_size_ceiling() {
16304        // `register_macro_def` is the substrate primitive both
16305        // `expand_program`'s `defmacro` recognition AND
16306        // `with_macros`'s bulk load route through. The body-size
16307        // ceiling fires on the primitive itself, not on any one
16308        // consumer — a regression that ever moved the gate into
16309        // `expand_program`'s arm alone would fail this pin.
16310        let mut e = Expander::new();
16311        e.set_max_macro_body_size(4);
16312        let def = MacroDef {
16313            name: "huge".to_string(),
16314            params: MacroParams {
16315                required: vec!["x".to_string()],
16316                optional: Vec::new(),
16317                rest: None,
16318            },
16319            body: Sexp::List(vec![
16320                Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
16321                Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
16322                Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
16323                Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
16324                Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
16325            ]),
16326        };
16327        // Body node_count: List(5 atoms) = 1 + 5 = 6 nodes.
16328        let err = e.register_macro_def(def).unwrap_err();
16329        match err {
16330            LispError::MacroBodySizeExceeded {
16331                macro_name,
16332                size,
16333                limit,
16334            } => {
16335                assert_eq!(macro_name, "huge");
16336                assert_eq!(size, 6);
16337                assert_eq!(limit, 4);
16338            }
16339            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16340        }
16341        assert!(!e.has("huge"));
16342    }
16343
16344    // ── max_registered_macros: REGISTRATION-time table-count ceiling ──
16345    // Peer to body-size (REGISTER-time SIZE) one RESOURCE-DIMENSION
16346    // axis over — where body-size bounds a per-registration SIZE,
16347    // this ceiling bounds cumulative registration COUNT. Peer to
16348    // cache-entries (EXPAND-time COUNT) one PIPELINE-STAGE axis over
16349    // — the two entry-count guards close the two pipeline stages
16350    // symmetrically. Closes the expander's RESOURCE surface at a
16351    // FIFTH typed dimension across two pipeline stages: the
16352    // REGISTER-time face is now a two-dimensional (size × count)
16353    // closure symmetric with the EXPAND-time (depth + count + size)
16354    // triple.
16355
16356    #[test]
16357    fn expander_default_max_registered_macros_is_the_module_constant() {
16358        // Both constructors seed `max_registered_macros` from the
16359        // module constant — a regression that drifts one
16360        // constructor's default from the constant (e.g. a hardcoded
16361        // `4096` at ONE site with the other left uninitialized
16362        // after a `Default` derive addition) fails this pin. Peer
16363        // to the four prior default-coherence pins on depth,
16364        // cache-entries, expansion-size, and body-size.
16365        assert_eq!(
16366            Expander::new().max_registered_macros(),
16367            DEFAULT_MAX_REGISTERED_MACROS
16368        );
16369        assert_eq!(
16370            Expander::new_substitute_only().max_registered_macros(),
16371            DEFAULT_MAX_REGISTERED_MACROS
16372        );
16373        assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
16374    }
16375
16376    #[test]
16377    fn set_max_registered_macros_takes_effect_on_subsequent_register() {
16378        // Setter mirrors accessor — a regression that ever forgets
16379        // to write through to the field, or writes to a shadow
16380        // field the accessor does not read, fails this pin. Peer
16381        // to the four prior setter/accessor coherence pins.
16382        let mut e = Expander::new();
16383        e.set_max_registered_macros(64);
16384        assert_eq!(e.max_registered_macros(), 64);
16385    }
16386
16387    #[test]
16388    fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
16389        // With a ceiling of `2`, the first two fresh registrations
16390        // land in the table and the third fresh key rejects with
16391        // `LispError::RegisteredMacrosExceeded { macro_name: "c",
16392        // count: 2, limit: 2 }`. `count` is the pre-insert table
16393        // length (which equals `limit` at rejection in the common
16394        // case); `macro_name` is the OFFENDING call's name (the
16395        // fresh key we could not admit), not the outermost caller.
16396        let mut e = Expander::new();
16397        e.set_max_registered_macros(2);
16398        let forms = read(
16399            "(defmacro a (x) `(list ,x))
16400             (defmacro b (x) `(list ,x))
16401             (defmacro c (x) `(list ,x))",
16402        )
16403        .unwrap();
16404        let err = e.expand_program(forms).unwrap_err();
16405        match err {
16406            LispError::RegisteredMacrosExceeded {
16407                macro_name,
16408                count,
16409                limit,
16410            } => {
16411                assert_eq!(macro_name, "c");
16412                assert_eq!(count, 2);
16413                assert_eq!(limit, 2);
16414            }
16415            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16416        }
16417        // First two admitted; third rejected leaves the table at
16418        // its pre-third-call size.
16419        assert!(e.has("a"));
16420        assert!(e.has("b"));
16421        assert!(!e.has("c"));
16422        assert_eq!(e.len(), 2);
16423    }
16424
16425    #[test]
16426    fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
16427        // The substitute strategy shares the same
16428        // `register_macro_def` outer registration entry with the
16429        // bytecode strategy, so the REGISTRATION-time TABLE-COUNT
16430        // ceiling fires at the SAME gate regardless of which
16431        // apply-layer is active. The pin makes that shared-
16432        // registration property structural: a regression that ever
16433        // branches the table-count guard on strategy (e.g. moves
16434        // it into the `compile_templates` arm alone) fails this
16435        // test on the substitute expander. Peer to the four prior
16436        // shared-registration pins.
16437        let mut e = Expander::new_substitute_only();
16438        e.set_max_registered_macros(2);
16439        let forms = read(
16440            "(defmacro a (x) `(list ,x))
16441             (defmacro b (x) `(list ,x))
16442             (defmacro c (x) `(list ,x))",
16443        )
16444        .unwrap();
16445        let err = e.expand_program(forms).unwrap_err();
16446        match err {
16447            LispError::RegisteredMacrosExceeded {
16448                macro_name,
16449                count,
16450                limit,
16451            } => {
16452                assert_eq!(macro_name, "c");
16453                assert_eq!(count, 2);
16454                assert_eq!(limit, 2);
16455            }
16456            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16457        }
16458        assert!(e.has("a"));
16459        assert!(e.has("b"));
16460        assert!(!e.has("c"));
16461        assert_eq!(e.len(), 2);
16462    }
16463
16464    #[test]
16465    fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
16466        // OVERWRITES (a re-registration of an already-registered
16467        // key) do not grow the table and MUST be admitted at any
16468        // ceiling — operators redefining a macro at capacity is a
16469        // legitimate authoring pattern. This pin catches a
16470        // regression that ever treats overwrites the same as fresh
16471        // keys (a `contains_key` check that inverts, or a
16472        // `set_max_registered_macros(0)` that would reject even
16473        // pre-registered overwrites).
16474        let mut e = Expander::new();
16475        // Land two macros with a permissive ceiling.
16476        e.expand_program(
16477            read(
16478                "(defmacro a (x) `(list ,x))
16479                 (defmacro b (x) `(list ,x))",
16480            )
16481            .unwrap(),
16482        )
16483        .unwrap();
16484        assert_eq!(e.len(), 2);
16485        // Drop the ceiling to exactly the current size — a fresh
16486        // third key would now reject, but an overwrite of `a` MUST
16487        // admit.
16488        e.set_max_registered_macros(2);
16489        let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
16490        e.expand_program(forms).unwrap();
16491        assert_eq!(e.len(), 2, "overwrite must not grow the table");
16492        // Re-expand a call to `a` — the NEW definition (two
16493        // required params) is active, and the OLD definition (one
16494        // required param) is gone. Under the new definition,
16495        // `(a 1 2)` expands to `(pair 1 2)`.
16496        let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
16497        assert_eq!(expanded[0], parse("(pair foo bar)"));
16498    }
16499
16500    #[test]
16501    fn register_at_table_ceiling_admits_up_to_but_not_past() {
16502        // Fail-before/pass-after negative control on the exact
16503        // boundary: with a ceiling of `3`, exactly three fresh keys
16504        // admit and the fourth rejects. A regression that ever
16505        // shifted the boundary by one (e.g. keyed on `>` instead of
16506        // `>=`, or checked `len() > cap` before insert instead of
16507        // `len() >= cap`) would fail one of the two arms — either
16508        // the third admission or the fourth rejection.
16509        let mut e = Expander::new();
16510        e.set_max_registered_macros(3);
16511        // Three fresh keys admit.
16512        e.expand_program(
16513            read(
16514                "(defmacro a (x) `(list ,x))
16515                 (defmacro b (x) `(list ,x))
16516                 (defmacro c (x) `(list ,x))",
16517            )
16518            .unwrap(),
16519        )
16520        .unwrap();
16521        assert_eq!(e.len(), 3);
16522        // Fourth fresh key rejects.
16523        let err = e
16524            .expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
16525            .unwrap_err();
16526        match err {
16527            LispError::RegisteredMacrosExceeded {
16528                macro_name,
16529                count,
16530                limit,
16531            } => {
16532                assert_eq!(macro_name, "d");
16533                assert_eq!(count, 3);
16534                assert_eq!(limit, 3);
16535            }
16536            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16537        }
16538        // Table stays at pre-rejection size — no partial write.
16539        assert_eq!(e.len(), 3);
16540        assert!(!e.has("d"));
16541    }
16542
16543    #[test]
16544    fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
16545        // A rejected registration MUST leave BOTH `self.macros` AND
16546        // `self.templates` exactly as they were — no partial-write
16547        // window in which one table carries the over-ceiling entry
16548        // while the other does not, or in which the template
16549        // pre-compile already ran. Table-count gate fires BEFORE
16550        // body-size gate walks the body AND BEFORE any insert lands,
16551        // so a regression that ever moved the table-count gate
16552        // below `self.templates.insert` or below
16553        // `self.macros.insert` would fail this pin. Peer to
16554        // `register_failure_leaves_both_tables_pristine` on the
16555        // body-size axis.
16556        let mut e = Expander::new();
16557        e.set_max_registered_macros(1);
16558        // Land one macro at ceiling.
16559        e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16560            .unwrap();
16561        assert!(e.has("a"));
16562        // Reject a fresh second key.
16563        let err = e
16564            .expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
16565            .unwrap_err();
16566        assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
16567        // Table stays at pre-rejection state — `b` is NOT present
16568        // in either self.macros OR self.templates.
16569        assert_eq!(e.len(), 1);
16570        assert!(e.has("a"));
16571        assert!(!e.has("b"));
16572        // Expanding a call to `a` still works (templates table for
16573        // `a` untouched by the failed `b` registration).
16574        let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
16575        assert_eq!(expanded[0], parse("(list hey)"));
16576    }
16577
16578    #[test]
16579    fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
16580        // Zero ceiling is the "contract-test that this expander
16581        // accepts no new macros" posture — every fresh key rejects
16582        // at the first `defmacro` because `0 >= 0`. Peer to the
16583        // depth-ceiling zero-admits-and-rejects-every-macro-call
16584        // pin one RESOURCE axis over.
16585        let mut e = Expander::new();
16586        e.set_max_registered_macros(0);
16587        let err = e
16588            .expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16589            .unwrap_err();
16590        match err {
16591            LispError::RegisteredMacrosExceeded {
16592                macro_name,
16593                count,
16594                limit,
16595            } => {
16596                assert_eq!(macro_name, "a");
16597                assert_eq!(count, 0);
16598                assert_eq!(limit, 0);
16599            }
16600            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16601        }
16602        assert!(e.is_empty());
16603    }
16604
16605    #[test]
16606    fn registered_macros_exceeded_position_is_none() {
16607        // The variant joins the non-positional cohort — the
16608        // offending macro's byte offset is not what a table-count
16609        // diagnostic anchors to; the (offending fresh macro name,
16610        // observed table count, configured ceiling) triple is.
16611        // This pin keeps the variant in the `position() -> None`
16612        // cohort so a future span-carrying edit lands the field
16613        // in ONE place across every non-positional variant
16614        // simultaneously. Peer to the four prior
16615        // `*_exceeded_position_is_none` pins on the resource-
16616        // ceiling cohort.
16617        let err = LispError::RegisteredMacrosExceeded {
16618            macro_name: "fresh-1000".to_string(),
16619            count: 4096,
16620            limit: 4096,
16621        };
16622        assert_eq!(err.position(), None);
16623    }
16624
16625    #[test]
16626    fn registered_macros_exceeded_display_matches_typed_variant_shape() {
16627        // Display projection carries `macro_name`, `count`, AND
16628        // `limit`, so authoring surfaces that substring-grep the
16629        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16630        // THREE halves at the variant boundary and never need to
16631        // substring-parse free-form text to recover the identity
16632        // of the offending fresh key, the observed table count, or
16633        // the ceiling it breached. Peer to the four prior
16634        // `*_exceeded_display_matches_typed_variant_shape` pins.
16635        let err = LispError::RegisteredMacrosExceeded {
16636            macro_name: "fresh-1000".to_string(),
16637            count: 4096,
16638            limit: 4096,
16639        };
16640        let rendered = err.to_string();
16641        assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
16642        assert!(rendered.contains("4096"), "rendered: {rendered}");
16643        assert!(
16644            rendered.contains("registered macros count exceeded"),
16645            "rendered: {rendered}"
16646        );
16647    }
16648
16649    #[test]
16650    fn lawful_typescape_within_ceiling_registers_and_expands() {
16651        // A lawful hand-authored typescape (a handful of macros)
16652        // sits well below any realistic ceiling — the default
16653        // `DEFAULT_MAX_REGISTERED_MACROS` is deliberately generous
16654        // so operators never brush it on real authoring workloads.
16655        // This pin locks the default ceiling's non-interference
16656        // posture: a regression that ever lowered the default
16657        // enough to reject a small typescape would fail here. Peer
16658        // to `lawful_macro_body_within_ceiling_registers_and_expands`.
16659        let mut e = Expander::new();
16660        let forms = read(
16661            "(defmacro when (cond x) `(if ,cond ,x))
16662             (defmacro twice (x) `(list ,x ,x))
16663             (defmacro pair (x y) `(list ,x ,y))
16664             (when #t (twice hey))",
16665        )
16666        .unwrap();
16667        let out = e.expand_program(forms).unwrap();
16668        assert_eq!(out[0], parse("(if #t (list hey hey))"));
16669        assert_eq!(e.len(), 3);
16670    }
16671
16672    #[test]
16673    fn register_macro_def_direct_call_respects_table_ceiling() {
16674        // `register_macro_def` is the substrate primitive both
16675        // `expand_program`'s `defmacro` recognition AND
16676        // `with_macros`'s bulk load route through. The table-count
16677        // ceiling fires on the primitive itself, not on any one
16678        // consumer — a regression that ever moved the gate into
16679        // `expand_program`'s arm alone would fail this pin. Peer
16680        // to `register_macro_def_direct_call_respects_body_size_ceiling`
16681        // one RESOURCE-DIMENSION axis over.
16682        let mut e = Expander::new();
16683        e.set_max_registered_macros(1);
16684        let def_a = MacroDef {
16685            name: "a".to_string(),
16686            params: MacroParams {
16687                required: vec!["x".to_string()],
16688                optional: Vec::new(),
16689                rest: None,
16690            },
16691            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16692        };
16693        let def_b = MacroDef {
16694            name: "b".to_string(),
16695            params: MacroParams {
16696                required: vec!["x".to_string()],
16697                optional: Vec::new(),
16698                rest: None,
16699            },
16700            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16701        };
16702        // First admits — table sits at ceiling.
16703        e.register_macro_def(def_a).unwrap();
16704        assert!(e.has("a"));
16705        assert_eq!(e.len(), 1);
16706        // Second rejects — fresh key past ceiling.
16707        let err = e.register_macro_def(def_b).unwrap_err();
16708        match err {
16709            LispError::RegisteredMacrosExceeded {
16710                macro_name,
16711                count,
16712                limit,
16713            } => {
16714                assert_eq!(macro_name, "b");
16715                assert_eq!(count, 1);
16716                assert_eq!(limit, 1);
16717            }
16718            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16719        }
16720        assert!(!e.has("b"));
16721        assert_eq!(e.len(), 1);
16722    }
16723
16724    #[test]
16725    fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
16726        // Direct `register_macro_def` calls must ALSO admit
16727        // overwrites at ceiling — the overwrite discipline lives
16728        // on the primitive, not on `expand_program`'s arm alone.
16729        // Sibling of the source-level overwrite admission test.
16730        let mut e = Expander::new();
16731        e.set_max_registered_macros(1);
16732        let def_a_v1 = MacroDef {
16733            name: "a".to_string(),
16734            params: MacroParams {
16735                required: vec!["x".to_string()],
16736                optional: Vec::new(),
16737                rest: None,
16738            },
16739            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16740        };
16741        let def_a_v2 = MacroDef {
16742            name: "a".to_string(),
16743            params: MacroParams {
16744                required: vec!["x".to_string(), "y".to_string()],
16745                optional: Vec::new(),
16746                rest: None,
16747            },
16748            body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
16749        };
16750        e.register_macro_def(def_a_v1).unwrap();
16751        assert_eq!(e.len(), 1);
16752        // Overwrite of `a` at ceiling admits — no growth.
16753        e.register_macro_def(def_a_v2).unwrap();
16754        assert_eq!(e.len(), 1);
16755    }
16756
16757    // ── `MacroParams::total_arity` — the total-slots projection ──
16758    //
16759    // `total_arity()` names `required.len() + optional.len() +
16760    // rest.is_some() as usize` on the typed `MacroParams` algebra —
16761    // the `Vec`-free peer of `names().len()` AND the axis both
16762    // `MacroParams::bind`'s `Vec::with_capacity` hint AND the
16763    // REGISTER-time arity-ceiling gate consult. These pins cover:
16764    // structural identity vs. `names().len()`, the
16765    // `fixed_arity + rest.is_some() as usize` decomposition, and the
16766    // three canonical shapes (empty, all-required, mixed with rest).
16767    #[test]
16768    fn macro_params_total_arity_matches_names_len() {
16769        // The structural identity `total_arity() == names().len()`
16770        // — the `Vec`-free primitive's projection equals the flat-
16771        // index list's length. A regression that ever miscounts
16772        // one slot (drops rest, double-counts optional) fails this
16773        // pin. Sibling to `fixed_arity`'s peer-arithmetic pins.
16774        let params = MacroParams {
16775            required: vec!["a".into(), "b".into()],
16776            optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
16777            rest: Some("e".into()),
16778        };
16779        assert_eq!(params.total_arity(), params.names().len());
16780        assert_eq!(params.total_arity(), 5);
16781    }
16782
16783    #[test]
16784    fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
16785        // The `fixed_arity + rest.is_some() as usize` decomposition
16786        // — pins the composition on the [`MacroParams`] algebra so
16787        // a future consumer that wants "total slots" via
16788        // `fixed_arity + rest?` binds to the same arithmetic
16789        // `total_arity` names.
16790        let rest_none = MacroParams {
16791            required: vec!["a".into()],
16792            optional: vec![OptionalParam::bare("b")],
16793            rest: None,
16794        };
16795        assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
16796        assert_eq!(rest_none.total_arity(), 2);
16797        let rest_some = MacroParams {
16798            required: vec!["a".into()],
16799            optional: vec![OptionalParam::bare("b")],
16800            rest: Some("r".into()),
16801        };
16802        assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
16803        assert_eq!(rest_some.total_arity(), 3);
16804    }
16805
16806    #[test]
16807    fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
16808        // Nullary macro edge: `(defmacro nullary () BODY)` has
16809        // `total_arity() == 0`. Under a `set_max_macro_arity(0)`
16810        // ceiling the arity gate keys on `>` (equality admits), so
16811        // this nullary registration succeeds while any non-nullary
16812        // one rejects — the exact contract the arity-gate
16813        // zero-admits-nullary test below relies on.
16814        let params = MacroParams::default();
16815        assert_eq!(params.total_arity(), 0);
16816    }
16817
16818    // ── max_macro_arity: REGISTRATION-time param-count ceiling ──
16819    // Peer to body-size (REGISTER-time BODY SIZE) one RESOURCE-
16820    // DIMENSION axis over — where body-size bounds a per-registration
16821    // BODY node count, this ceiling bounds a per-registration PARAM
16822    // slot count. Peer to registered-macros (REGISTER-time TABLE
16823    // COUNT) one RESOURCE-DIMENSION axis over — where
16824    // registered-macros bounds cumulative registration COUNT, this
16825    // ceiling bounds a per-registration PARAM-LIST WIDTH. Together
16826    // the three REGISTER-time ceilings close the (per-body SIZE,
16827    // per-body ARITY, per-table COUNT) three-corner surface.
16828
16829    #[test]
16830    fn expander_default_max_macro_arity_is_the_module_constant() {
16831        // Both constructors seed `max_macro_arity` from the module
16832        // constant — a regression that drifts one constructor's
16833        // default from the constant (e.g. a hardcoded `128` at ONE
16834        // site with the other left uninitialized after a `Default`
16835        // derive addition) fails this pin. Peer to the five prior
16836        // default-coherence pins on depth, cache-entries,
16837        // expansion-size, body-size, and registered-macros.
16838        assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
16839        assert_eq!(
16840            Expander::new_substitute_only().max_macro_arity(),
16841            DEFAULT_MAX_MACRO_ARITY
16842        );
16843        assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
16844    }
16845
16846    #[test]
16847    fn set_max_macro_arity_takes_effect_on_subsequent_register() {
16848        // Setter mirrors accessor — a regression that ever forgets to
16849        // write through to the field, or writes to a shadow field the
16850        // accessor does not read, fails this pin. Peer to the five
16851        // prior setter/accessor coherence pins.
16852        let mut e = Expander::new();
16853        e.set_max_macro_arity(16);
16854        assert_eq!(e.max_macro_arity(), 16);
16855    }
16856
16857    #[test]
16858    fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
16859        // `(defmacro huge (a b c) `,a)` declares 3 required params.
16860        // Under `set_max_macro_arity(2)` the arity gate fires:
16861        // 3 > 2 → `LispError::MacroArityExceeded { macro_name: "huge",
16862        // arity: 3, limit: 2 }`. `arity` is the observed
16863        // `def.params.total_arity()`, NOT `required.len()` alone —
16864        // even an optional-only or rest-only overrun trips the gate
16865        // (pinned separately). Peer to the register-time body-size
16866        // rejection tests one RESOURCE-DIMENSION axis over.
16867        let mut e = Expander::new();
16868        e.set_max_macro_arity(2);
16869        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16870        let err = e.expand_program(forms).unwrap_err();
16871        match err {
16872            LispError::MacroArityExceeded {
16873                macro_name,
16874                arity,
16875                limit,
16876            } => {
16877                assert_eq!(macro_name, "huge");
16878                assert_eq!(arity, 3);
16879                assert_eq!(limit, 2);
16880            }
16881            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16882        }
16883        assert!(!e.has("huge"));
16884    }
16885
16886    #[test]
16887    fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
16888        // The substitute strategy shares the same
16889        // `register_macro_def` outer registration entry with the
16890        // bytecode strategy, so the REGISTRATION-time ARITY ceiling
16891        // fires at the SAME gate regardless of which apply-layer is
16892        // active. Peer to
16893        // `register_arity_bomb_rejects_at_arity_limit_bytecode_path`.
16894        let mut e = Expander::new_substitute_only();
16895        e.set_max_macro_arity(2);
16896        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16897        let err = e.expand_program(forms).unwrap_err();
16898        match err {
16899            LispError::MacroArityExceeded {
16900                macro_name,
16901                arity,
16902                limit,
16903            } => {
16904                assert_eq!(macro_name, "huge");
16905                assert_eq!(arity, 3);
16906                assert_eq!(limit, 2);
16907            }
16908            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16909        }
16910        assert!(!e.has("huge"));
16911    }
16912
16913    #[test]
16914    fn register_arity_gate_counts_optional_slots() {
16915        // `(defmacro foo (a &optional b c) `,a)` has 1 required + 2
16916        // optional + 0 rest = arity 3 via `total_arity`. Under a
16917        // ceiling of `2` the gate rejects, matching the required-only
16918        // arity-bomb — the arity axis is `total_arity()`, not
16919        // `required.len()`. A regression that drops the optional slots
16920        // from the arity computation would ADMIT this shape and fail
16921        // this pin.
16922        let mut e = Expander::new();
16923        e.set_max_macro_arity(2);
16924        let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
16925        let err = e.expand_program(forms).unwrap_err();
16926        match err {
16927            LispError::MacroArityExceeded {
16928                macro_name,
16929                arity,
16930                limit,
16931            } => {
16932                assert_eq!(macro_name, "foo");
16933                assert_eq!(arity, 3);
16934                assert_eq!(limit, 2);
16935            }
16936            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16937        }
16938    }
16939
16940    #[test]
16941    fn register_arity_gate_counts_rest_slot() {
16942        // `(defmacro spread (a b &rest r) `,a)` has 2 required + 0
16943        // optional + 1 rest = arity 3 via `total_arity`. Under a
16944        // ceiling of `2` the gate rejects, matching the required-only
16945        // arity-bomb — the arity axis includes the rest slot as ONE
16946        // named param, not zero. A regression that drops the rest slot
16947        // from the arity computation would ADMIT this shape and fail
16948        // this pin.
16949        let mut e = Expander::new();
16950        e.set_max_macro_arity(2);
16951        let forms = read("(defmacro spread (a b &rest r) `,a)").unwrap();
16952        let err = e.expand_program(forms).unwrap_err();
16953        match err {
16954            LispError::MacroArityExceeded {
16955                macro_name,
16956                arity,
16957                limit,
16958            } => {
16959                assert_eq!(macro_name, "spread");
16960                assert_eq!(arity, 3);
16961                assert_eq!(limit, 2);
16962            }
16963            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16964        }
16965    }
16966
16967    #[test]
16968    fn register_arity_at_ceiling_admits() {
16969        // `(defmacro pair (x y) `(list ,x ,y))` has 2 required = arity
16970        // 2 via `total_arity`. Under a ceiling of `2` the arity gate
16971        // keys on `>` (equality admits; only strict overrun rejects),
16972        // so the registration succeeds and the expansion proceeds
16973        // normally. Fail-before/pass-after negative control on the
16974        // exact boundary: a regression that ever shifted the boundary
16975        // by one (e.g. keyed on `>=` instead of `>`) would fail this
16976        // pin. Peer to `register_macro_body_at_ceiling_admits` on the
16977        // body-size axis.
16978        let mut e = Expander::new();
16979        e.set_max_macro_arity(2);
16980        let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
16981        let out = e.expand_program(forms).unwrap();
16982        assert_eq!(out[0], parse("(list a b)"));
16983        assert_eq!(e.len(), 1);
16984    }
16985
16986    #[test]
16987    fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
16988        // A rejected arity-gated registration MUST leave BOTH
16989        // `self.macros` AND `self.templates` exactly as they were —
16990        // no partial-write window in which one table carries the
16991        // over-ceiling entry while the other does not, or in which
16992        // the template pre-compile already ran. Arity gate fires
16993        // BEFORE the body-size gate walks the body AND BEFORE any
16994        // insert lands, so a regression that ever moved the arity
16995        // gate below `self.templates.insert` or below
16996        // `self.macros.insert` would fail this pin. Peer to
16997        // `register_failure_leaves_both_tables_pristine` (body-size)
16998        // and `register_table_ceiling_leaves_both_tables_pristine_on_rejection`
16999        // (table-count).
17000        let mut e = Expander::new();
17001        e.set_max_macro_arity(1);
17002        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17003        assert!(e.expand_program(forms).is_err());
17004        assert!(
17005            !e.has("huge"),
17006            "macros table must stay pristine after rejection"
17007        );
17008        assert_eq!(e.len(), 0);
17009        // Expanding a NEW lawful macro afterwards still works — the
17010        // failed arity-gate rejection has not corrupted expander
17011        // state.
17012        let out = e
17013            .expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
17014            .unwrap();
17015        assert_eq!(out[0], parse("hey"));
17016    }
17017
17018    #[test]
17019    fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
17020        // Zero ceiling is the "contract-test that this expander
17021        // accepts only nullary macros" posture — every fresh
17022        // non-nullary key rejects at the arity gate because
17023        // `total_arity() > 0`, while nullary `(defmacro nullary ()
17024        // BODY)` still admits (its arity is exactly 0, and only
17025        // strict overrun rejects). Peer to the body-size zero-rejects-
17026        // every-non-empty-body pin one RESOURCE-DIMENSION axis over
17027        // (though body-size zero rejects even nullary-body shapes,
17028        // since `Sexp::Nil::node_count() == 1 > 0`; arity zero
17029        // preserves the nullary registration surface).
17030        let mut e = Expander::new();
17031        e.set_max_macro_arity(0);
17032        // Nullary admits — arity 0 <= 0.
17033        e.expand_program(read("(defmacro nullary () `unit)").unwrap())
17034            .unwrap();
17035        assert!(e.has("nullary"));
17036        // Non-nullary rejects — arity 1 > 0.
17037        let err = e
17038            .expand_program(read("(defmacro id (x) `,x)").unwrap())
17039            .unwrap_err();
17040        match err {
17041            LispError::MacroArityExceeded {
17042                macro_name,
17043                arity,
17044                limit,
17045            } => {
17046                assert_eq!(macro_name, "id");
17047                assert_eq!(arity, 1);
17048                assert_eq!(limit, 0);
17049            }
17050            other => panic!("expected MacroArityExceeded, got: {other:?}"),
17051        }
17052        assert!(!e.has("id"));
17053    }
17054
17055    #[test]
17056    fn macro_arity_exceeded_position_is_none() {
17057        // The variant joins the non-positional cohort — the offending
17058        // macro's byte offset is not what an arity-bomb diagnostic
17059        // anchors to; the (macro name, observed arity, configured
17060        // ceiling) triple is. This pin keeps the variant in the
17061        // `position() -> None` cohort so a future span-carrying edit
17062        // lands the field in ONE place across every non-positional
17063        // variant simultaneously. Peer to the five prior
17064        // `*_exceeded_position_is_none` pins on the resource-ceiling
17065        // cohort.
17066        let err = LispError::MacroArityExceeded {
17067            macro_name: "huge".to_string(),
17068            arity: 512,
17069            limit: 128,
17070        };
17071        assert_eq!(err.position(), None);
17072    }
17073
17074    #[test]
17075    fn macro_arity_exceeded_display_matches_typed_variant_shape() {
17076        // Display projection carries `macro_name`, `arity`, AND
17077        // `limit`, so authoring surfaces that substring-grep the
17078        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
17079        // THREE halves at the variant boundary and never need to
17080        // substring-parse free-form text to recover the identity of
17081        // the offending macro, the observed arity, or the ceiling it
17082        // breached. Peer to the five prior
17083        // `*_exceeded_display_matches_typed_variant_shape` pins.
17084        let err = LispError::MacroArityExceeded {
17085            macro_name: "huge".to_string(),
17086            arity: 512,
17087            limit: 128,
17088        };
17089        let rendered = err.to_string();
17090        assert!(rendered.contains("huge"), "rendered: {rendered}");
17091        assert!(rendered.contains("512"), "rendered: {rendered}");
17092        assert!(rendered.contains("128"), "rendered: {rendered}");
17093        assert!(
17094            rendered.contains("macro arity exceeded"),
17095            "rendered: {rendered}"
17096        );
17097    }
17098
17099    #[test]
17100    fn lawful_macro_arity_within_ceiling_registers_and_expands() {
17101        // A lawful hand-authored macro arity sits well below any
17102        // realistic ceiling — the default `DEFAULT_MAX_MACRO_ARITY`
17103        // is deliberately generous so operators never brush it on
17104        // real authoring workloads. This pin locks the default
17105        // ceiling's non-interference posture: a regression that
17106        // ever lowered the default enough to reject the reference
17107        // multi-slot `(defmacro when (cond x) …)` shape would fail
17108        // here. Peer to `lawful_macro_body_within_ceiling_registers_and_expands`
17109        // and `lawful_typescape_within_ceiling_registers_and_expands`.
17110        let mut e = Expander::new();
17111        let forms = read(
17112            "(defmacro when (cond x) `(if ,cond ,x))
17113             (defmacro triple (x y z) `(list ,x ,y ,z))
17114             (when #t (triple a b c))",
17115        )
17116        .unwrap();
17117        let out = e.expand_program(forms).unwrap();
17118        assert_eq!(out[0], parse("(if #t (list a b c))"));
17119    }
17120
17121    #[test]
17122    fn register_macro_def_direct_call_respects_arity_ceiling() {
17123        // `register_macro_def` is the substrate primitive both
17124        // `expand_program`'s `defmacro` recognition AND `with_macros`'s
17125        // bulk load route through. The arity ceiling fires on the
17126        // primitive itself, not on any one consumer — a regression
17127        // that ever moved the gate into `expand_program`'s arm alone
17128        // would fail this pin. Peer to
17129        // `register_macro_def_direct_call_respects_body_size_ceiling`
17130        // AND `register_macro_def_direct_call_respects_table_ceiling`
17131        // one RESOURCE-DIMENSION axis over.
17132        let mut e = Expander::new();
17133        e.set_max_macro_arity(2);
17134        let def = MacroDef {
17135            name: "huge".to_string(),
17136            params: MacroParams {
17137                required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
17138                optional: Vec::new(),
17139                rest: None,
17140            },
17141            body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
17142        };
17143        // Arity 3 > ceiling 2 — rejected before body-size gate.
17144        let err = e.register_macro_def(def).unwrap_err();
17145        match err {
17146            LispError::MacroArityExceeded {
17147                macro_name,
17148                arity,
17149                limit,
17150            } => {
17151                assert_eq!(macro_name, "huge");
17152                assert_eq!(arity, 3);
17153                assert_eq!(limit, 2);
17154            }
17155            other => panic!("expected MacroArityExceeded, got: {other:?}"),
17156        }
17157        assert!(!e.has("huge"));
17158    }
17159
17160    #[test]
17161    fn arity_gate_fires_before_body_size_gate() {
17162        // ORDERING PIN — the arity gate fires BEFORE the body-size
17163        // gate. With both `set_max_macro_arity(1)` AND
17164        // `set_max_macro_body_size(2)` set below a shape that violates
17165        // BOTH ceilings — `(defmacro dual (a b) `(list ,a ,b))` has
17166        // arity 2 AND body node count > 2 — the arity gate's
17167        // rejection wins, so the returned variant is
17168        // `MacroArityExceeded`, not `MacroBodySizeExceeded`. A
17169        // regression that ever swapped the two gates' order would fail
17170        // this pin. The ordering isn't cosmetic: arity is O(1)
17171        // (three-arm addition on `def.params` fields), body-size is
17172        // O(body_nodes) (`def.body.node_count()` walks the AST), so
17173        // the O(1)-first order minimises the cost of rejecting the
17174        // worst case (a large-body large-arity macro).
17175        let mut e = Expander::new();
17176        e.set_max_macro_arity(1);
17177        e.set_max_macro_body_size(2);
17178        let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
17179        let err = e.expand_program(forms).unwrap_err();
17180        assert!(
17181            matches!(err, LispError::MacroArityExceeded { .. }),
17182            "arity gate must fire before body-size gate; got: {err:?}"
17183        );
17184    }
17185
17186    #[test]
17187    fn arity_gate_fires_after_table_count_gate() {
17188        // ORDERING PIN — the table-count gate fires BEFORE the arity
17189        // gate. Under `set_max_registered_macros(1)` at capacity AND
17190        // `set_max_macro_arity(1)`, a fresh over-arity registration
17191        // trips the table-count gate FIRST (its `HashMap::len`
17192        // comparison is O(1) cache-friendly and gates cumulative
17193        // registration cost); the arity gate is O(1) but on `def`,
17194        // not on shared state, so it sits AFTER the table-count gate
17195        // on the O(1)-first ordering of the REGISTER-time chain. A
17196        // regression that ever swapped the two would fail this pin.
17197        let mut e = Expander::new();
17198        e.set_max_registered_macros(1);
17199        e.set_max_macro_arity(1);
17200        // Land one macro at table ceiling.
17201        e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
17202            .unwrap();
17203        assert_eq!(e.len(), 1);
17204        // Fresh registration with BOTH violations — table-count wins.
17205        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17206        let err = e.expand_program(forms).unwrap_err();
17207        assert!(
17208            matches!(err, LispError::RegisteredMacrosExceeded { .. }),
17209            "table-count gate must fire before arity gate; got: {err:?}"
17210        );
17211    }
17212
17213    // ── ResourceLimits: bundled resource-ceiling snapshot ────────────
17214
17215    #[test]
17216    fn default_resource_limits_binds_each_field_to_matching_module_constant() {
17217        // Pin the (constant × aggregation) corner: the const-form
17218        // aggregate MUST bind every field to its individually-defined
17219        // module constant. A re-tuning of ONE module constant
17220        // (e.g. `DEFAULT_MAX_EXPANSION_DEPTH` raised from 256 to 512)
17221        // has to propagate through the aggregate at ONE site; this
17222        // test fires the divergence loudly if a future edit sets a
17223        // module constant to a fresh value but forgets to update the
17224        // matching aggregate slot. Field-by-field equality carries
17225        // the message better than one deep-equality assertion.
17226        assert_eq!(
17227            DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
17228            DEFAULT_MAX_EXPANSION_DEPTH
17229        );
17230        assert_eq!(
17231            DEFAULT_RESOURCE_LIMITS.max_cache_entries,
17232            DEFAULT_MAX_CACHE_ENTRIES
17233        );
17234        assert_eq!(
17235            DEFAULT_RESOURCE_LIMITS.max_expansion_size,
17236            DEFAULT_MAX_EXPANSION_SIZE
17237        );
17238        assert_eq!(
17239            DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
17240            DEFAULT_MAX_MACRO_BODY_SIZE
17241        );
17242        assert_eq!(
17243            DEFAULT_RESOURCE_LIMITS.max_registered_macros,
17244            DEFAULT_MAX_REGISTERED_MACROS
17245        );
17246        assert_eq!(
17247            DEFAULT_RESOURCE_LIMITS.max_macro_arity,
17248            DEFAULT_MAX_MACRO_ARITY
17249        );
17250    }
17251
17252    #[test]
17253    fn resource_limits_default_impl_matches_const_form() {
17254        // The two projections of the shipped posture — the `const`
17255        // form [`DEFAULT_RESOURCE_LIMITS`] and the runtime
17256        // `Default::default()` form — must reach the SAME value. Pins
17257        // that a future edit which flips one to a new default cannot
17258        // silently drift the other.
17259        assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
17260    }
17261
17262    #[test]
17263    fn expander_new_resource_limits_matches_shipped_defaults() {
17264        // Fresh-expander posture on the bundled-getter surface — the
17265        // six ceilings [`Expander::new`] seeds MUST snapshot to
17266        // [`DEFAULT_RESOURCE_LIMITS`] verbatim. Peer of the six
17267        // individual `expander_default_max_*_is_the_module_constant`
17268        // pins one AGGREGATION axis over — where each individual
17269        // pin catches ONE field drift, this pin catches ANY of the
17270        // six drifting AND the snapshot projection itself losing a
17271        // field.
17272        assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
17273    }
17274
17275    #[test]
17276    fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
17277        // Substitute-only expander seeds the SAME six ceilings from
17278        // the SAME module constants as [`Expander::new`] — the two
17279        // constructors share the resource-ceiling posture even
17280        // though they diverge on `compile_templates` +
17281        // `cache_enabled`. Pins that a future constructor which
17282        // forgets to seed one of the six ceilings from its module
17283        // constant fails loudly through the bundled getter.
17284        assert_eq!(
17285            Expander::new_substitute_only().resource_limits(),
17286            DEFAULT_RESOURCE_LIMITS
17287        );
17288    }
17289
17290    #[test]
17291    fn resource_limits_snapshot_reflects_each_individual_setter() {
17292        // Coherence pin between the six individual setters and the
17293        // bundled getter — flipping ONE ceiling via its individual
17294        // setter must project through [`Expander::resource_limits`]
17295        // to the SAME value the individual getter returns. A
17296        // regression that leaves ONE field out of the snapshot
17297        // projection (a copy-paste omission in the struct literal
17298        // constructor) fires here. Distinct low-non-default values
17299        // so a stray field carrying the default fails loudly.
17300        let mut e = Expander::new();
17301        e.set_max_expansion_depth(3);
17302        e.set_max_cache_entries(5);
17303        e.set_max_expansion_size(7);
17304        e.set_max_macro_body_size(11);
17305        e.set_max_registered_macros(13);
17306        e.set_max_macro_arity(17);
17307        let snap = e.resource_limits();
17308        assert_eq!(snap.max_expansion_depth, 3);
17309        assert_eq!(snap.max_cache_entries, 5);
17310        assert_eq!(snap.max_expansion_size, 7);
17311        assert_eq!(snap.max_macro_body_size, 11);
17312        assert_eq!(snap.max_registered_macros, 13);
17313        assert_eq!(snap.max_macro_arity, 17);
17314        // And the individual getters see the same values — the
17315        // snapshot is a projection, not a divergent copy.
17316        assert_eq!(e.max_expansion_depth(), 3);
17317        assert_eq!(e.max_cache_entries(), 5);
17318        assert_eq!(e.max_expansion_size(), 7);
17319        assert_eq!(e.max_macro_body_size(), 11);
17320        assert_eq!(e.max_registered_macros(), 13);
17321        assert_eq!(e.max_macro_arity(), 17);
17322    }
17323
17324    #[test]
17325    fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
17326        // Sibling pin to `resource_limits_snapshot_reflects_...` on the
17327        // WRITE axis — bulk-setting via [`Expander::set_resource_limits`]
17328        // must reach every individual `max_*` getter. A regression
17329        // that drops one field from the destructuring assignment in
17330        // the bulk setter fires here. Distinct low-non-default values
17331        // so a stray field carrying the previous value fails loudly.
17332        let mut e = Expander::new();
17333        e.set_resource_limits(ResourceLimits {
17334            max_expansion_depth: 2,
17335            max_cache_entries: 4,
17336            max_expansion_size: 8,
17337            max_macro_body_size: 16,
17338            max_registered_macros: 32,
17339            max_macro_arity: 64,
17340        });
17341        assert_eq!(e.max_expansion_depth(), 2);
17342        assert_eq!(e.max_cache_entries(), 4);
17343        assert_eq!(e.max_expansion_size(), 8);
17344        assert_eq!(e.max_macro_body_size(), 16);
17345        assert_eq!(e.max_registered_macros(), 32);
17346        assert_eq!(e.max_macro_arity(), 64);
17347    }
17348
17349    #[test]
17350    fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
17351        // Round-trip theorem — for any starting expander, the
17352        // composition `set_resource_limits(resource_limits())` is
17353        // the identity on the resource-ceiling surface. This is the
17354        // ONE typed statement that pins the (getter, setter) pair as
17355        // strict projections of the same six-field surface, no
17356        // hidden lossy field, no ordering asymmetry between the
17357        // struct literal and the destructuring pattern.
17358        let mut e = Expander::new();
17359        e.set_max_expansion_depth(9);
17360        e.set_max_cache_entries(19);
17361        e.set_max_expansion_size(29);
17362        e.set_max_macro_body_size(39);
17363        e.set_max_registered_macros(49);
17364        e.set_max_macro_arity(59);
17365        let snap = e.resource_limits();
17366        // Reset to defaults so any leftover state would show up in
17367        // the post-round-trip snapshot as the default rather than
17368        // the intended value.
17369        e.set_resource_limits(ResourceLimits::default());
17370        assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
17371        // Apply the captured snapshot — the expander must land at
17372        // the same six values `snap` recorded.
17373        e.set_resource_limits(snap);
17374        assert_eq!(e.resource_limits(), snap);
17375    }
17376
17377    #[test]
17378    fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
17379        // The `Copy` posture on [`ResourceLimits`] gives callers the
17380        // cheap ergonomic of a struct-update literal — take the
17381        // shipped posture and override ONE ceiling — that a chain of
17382        // six independent setters cannot express in ONE typed expression.
17383        // Pins the intended use-shape so a future refactor which
17384        // removes `Copy` from the struct fails to compile at THIS
17385        // site rather than at every downstream consumer.
17386        let mut e = Expander::new();
17387        e.set_resource_limits(ResourceLimits {
17388            max_macro_arity: 4,
17389            ..DEFAULT_RESOURCE_LIMITS
17390        });
17391        // Overridden field carries the fresh value.
17392        assert_eq!(e.max_macro_arity(), 4);
17393        // Every other field carries the shipped default — the
17394        // struct-update literal is a full projection, not a
17395        // selective mutation.
17396        assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17397        assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17398        assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17399        assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17400        assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17401    }
17402
17403    #[test]
17404    fn expander_default_derives_resource_limits_from_bundled_field_default() {
17405        // Post the [`Expander::limits`]-field consolidation the derived
17406        // `Default` on [`Expander`] projects through [`ResourceLimits`]'s
17407        // own `Default` — which returns [`DEFAULT_RESOURCE_LIMITS`] — so
17408        // `Expander::default().resource_limits()` lands at the shipped
17409        // ceiling posture verbatim. Pre-consolidation the six independent
17410        // `max_*: usize` fields defaulted to the bare `usize` zero, so
17411        // `Expander::default()` yielded a below-floor posture with every
17412        // ceiling clamped to `0` — a shape no consumer wanted and every
17413        // test path avoided by routing through [`Expander::new`] instead.
17414        // The consolidation is what turned that latent below-floor
17415        // derived-`Default` shape into a lawful shipped posture on the
17416        // structural axis. Peer of
17417        // `expander_new_resource_limits_matches_shipped_defaults` one
17418        // CONSTRUCTOR axis over — where that test pins the explicit
17419        // [`Expander::new`] constructor, this test pins the derived-
17420        // [`Default`] constructor route to the SAME six values through
17421        // the SAME bundled projection. Sibling of
17422        // `resource_limits_default_impl_matches_const_form` one
17423        // TYPE-LEVEL axis over — that test pins [`ResourceLimits::default`]
17424        // to [`DEFAULT_RESOURCE_LIMITS`]; this test pins [`Expander`]'s
17425        // derived `Default` to project through it.
17426        assert_eq!(
17427            Expander::default().resource_limits(),
17428            DEFAULT_RESOURCE_LIMITS
17429        );
17430    }
17431
17432    #[test]
17433    fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
17434        // Behavioral pin — the bundled setter is not a decorative
17435        // getter/setter pair; the six ceilings it propagates must
17436        // reach the SAME check sites the individual setters do. Set
17437        // `max_macro_arity` to 1 via the bulk setter and confirm the
17438        // arity gate fires on a two-arg macro registration. A
17439        // regression that wired the bulk setter to a shadow field
17440        // rather than the actual `Expander` state fails here.
17441        let mut e = Expander::new();
17442        e.set_resource_limits(ResourceLimits {
17443            max_macro_arity: 1,
17444            ..DEFAULT_RESOURCE_LIMITS
17445        });
17446        let forms = read("(defmacro two (a b) `,a)").unwrap();
17447        let err = e.expand_program(forms).unwrap_err();
17448        assert!(
17449            matches!(
17450                err,
17451                LispError::MacroArityExceeded {
17452                    arity: 2,
17453                    limit: 1,
17454                    ..
17455                }
17456            ),
17457            "bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
17458        );
17459    }
17460
17461    // ── Expander::with_limits — at-construction resource-posture ───────
17462
17463    #[test]
17464    fn expander_with_limits_seeds_the_provided_resource_posture() {
17465        // Round-trip pin at CONSTRUCTION time — the bundle the caller
17466        // passes into [`Expander::with_limits`] MUST project back
17467        // through [`Expander::resource_limits`] verbatim. Peer of the
17468        // `set_resource_limits(l); resource_limits() == l` round-trip
17469        // one CONSTRUCTOR-STAGE axis over — that pin covers the
17470        // POST-construction assignment; this pin covers the
17471        // AT-construction assignment. A regression that dropped the
17472        // `limits` argument on the floor (a copy-paste that left
17473        // `limits: DEFAULT_RESOURCE_LIMITS` in the struct literal
17474        // instead of `limits`) fires here on every non-default field.
17475        // Distinct low-non-default values so a stray field defaulting
17476        // fails loudly.
17477        let want = ResourceLimits {
17478            max_expansion_depth: 3,
17479            max_cache_entries: 5,
17480            max_expansion_size: 7,
17481            max_macro_body_size: 11,
17482            max_registered_macros: 13,
17483            max_macro_arity: 17,
17484        };
17485        assert_eq!(Expander::with_limits(want).resource_limits(), want);
17486    }
17487
17488    #[test]
17489    fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
17490        // Delegation identity — [`Expander::new`] delegates to
17491        // [`Expander::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`] so
17492        // the two constructors MUST agree on the resource-ceiling
17493        // projection. Pins the "no drift between the two paths to a
17494        // fresh default-posture expander" theorem: a future refactor
17495        // that breaks the delegation (an inline field literal reappearing
17496        // in [`Expander::new`], an accidental hardcoded const at ONE
17497        // site with the other unchanged) fires here. Peer of
17498        // `resource_limits_default_impl_matches_const_form` one
17499        // TYPE-LEVEL axis over — that test pins the two projections of
17500        // the shipped posture on [`ResourceLimits`] itself; this test
17501        // pins the two constructor paths on [`Expander`] to reach the
17502        // SAME six values through the SAME bundle.
17503        assert_eq!(
17504            Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
17505            Expander::new().resource_limits()
17506        );
17507    }
17508
17509    #[test]
17510    fn expander_with_limits_composes_with_struct_update_override() {
17511        // The `Copy` posture on [`ResourceLimits`] lets a caller compose
17512        // "the shipped posture with ONE ceiling overridden" as ONE
17513        // struct-update literal AND thread that composition through
17514        // [`Expander::with_limits`] as ONE typed call — the ergonomic
17515        // shape that six independent setter invocations cannot express
17516        // in ONE expression. Overridden ceiling carries the fresh value,
17517        // every other ceiling carries its shipped default. A future
17518        // refactor that removes `Copy` from [`ResourceLimits`] fails to
17519        // compile at THIS site rather than at every downstream consumer.
17520        let e = Expander::with_limits(ResourceLimits {
17521            max_macro_arity: 4,
17522            ..DEFAULT_RESOURCE_LIMITS
17523        });
17524        assert_eq!(e.max_macro_arity(), 4);
17525        assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17526        assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17527        assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17528        assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17529        assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17530    }
17531
17532    #[test]
17533    fn expander_with_limits_carries_the_new_execution_strategy() {
17534        // The (compile_templates, cache_enabled) execution-strategy pair
17535        // MUST carry the [`Expander::new`] default posture — bytecode +
17536        // cache both on — regardless of which resource posture the
17537        // caller passes in. Cache-count observation is the least-
17538        // invasive check available on the public surface: a fresh
17539        // `with_limits` expander that expands a repeat-call site must
17540        // populate at least one cache entry (bytecode + cache both on),
17541        // and a substitute-only sibling would leave `cache_size()` at
17542        // zero even after every call site expanded. A regression that
17543        // wired the wrong execution strategy into `with_limits`
17544        // (compile_templates: false, cache_enabled: false) fires here.
17545        let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
17546        let forms = read(
17547            "(defmacro id (x) `,x)
17548             (id one)
17549             (id one)",
17550        )
17551        .unwrap();
17552        e.expand_program(forms).unwrap();
17553        assert!(
17554            e.cache_size() >= 1,
17555            "with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
17556            e.cache_size()
17557        );
17558    }
17559
17560    #[test]
17561    fn expander_with_limits_fires_the_arity_gate_from_construction() {
17562        // Behavioral pin — the at-construction bundle assignment MUST
17563        // reach the SAME check sites the individual + bulk POST-
17564        // construction setters do. Construct an expander with
17565        // `max_macro_arity: 1` directly and confirm the arity gate
17566        // fires on a two-arg macro registration. Sibling of
17567        // `resource_limits_bulk_setter_keeps_the_arity_gate_in_effect`
17568        // one CONSTRUCTOR-STAGE axis over — that pin covers the POST-
17569        // construction setter reaching the gate; this pin covers the
17570        // AT-construction assignment reaching the SAME gate through
17571        // the SAME `self.limits.max_macro_arity` load. A regression
17572        // that wired `with_limits` to a shadow field rather than
17573        // `self.limits` fails here.
17574        let mut e = Expander::with_limits(ResourceLimits {
17575            max_macro_arity: 1,
17576            ..DEFAULT_RESOURCE_LIMITS
17577        });
17578        let forms = read("(defmacro two (a b) `,a)").unwrap();
17579        let err = e.expand_program(forms).unwrap_err();
17580        assert!(
17581            matches!(
17582                err,
17583                LispError::MacroArityExceeded {
17584                    arity: 2,
17585                    limit: 1,
17586                    ..
17587                }
17588            ),
17589            "at-construction arity ceiling must reach the register-time gate; got: {err:?}"
17590        );
17591    }
17592
17593    // ── UNBOUNDED_RESOURCE_LIMITS — ceiling-lifted preset posture ──────
17594
17595    #[test]
17596    fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
17597        // Field-level pin — the ceiling-lifted preset carries
17598        // [`usize::MAX`] on every axis of the six-field surface. Pinned
17599        // as six independent field asserts (rather than one struct
17600        // equality) so a drift on ONE field carries a distinct-name
17601        // failure that names the drifting axis. A future extension
17602        // that adds a SEVENTH ceiling requires an additional field
17603        // assert here in lockstep with the const literal — rustc's
17604        // field-exhaustiveness on the const forces the new field to
17605        // appear, and this pin exercises it.
17606        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
17607        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
17608        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
17609        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
17610        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
17611        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
17612    }
17613
17614    #[test]
17615    fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
17616        // Structural-distinctness pin — the two constants
17617        // [`DEFAULT_RESOURCE_LIMITS`] and [`UNBOUNDED_RESOURCE_LIMITS`]
17618        // sit at DISTINCT points on the posture axis of the (posture ×
17619        // aggregation) grid, and the disagreement is EXHAUSTIVE on the
17620        // six-field surface — every shipped `DEFAULT_MAX_*` module
17621        // constant is a concrete positive value strictly less than
17622        // [`usize::MAX`]. Pins that a future re-tuning which cranked one
17623        // shipped default up to [`usize::MAX`] (collapsing the two
17624        // presets on that field) fires here loudly rather than
17625        // silently letting the two presets become indistinguishable
17626        // on that axis. A regression which flipped ONE unbounded-const
17627        // field back to its default value also fires here — either
17628        // half of the disagreement breaks the theorem.
17629        assert_ne!(
17630            UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
17631            DEFAULT_RESOURCE_LIMITS.max_expansion_depth
17632        );
17633        assert_ne!(
17634            UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
17635            DEFAULT_RESOURCE_LIMITS.max_cache_entries
17636        );
17637        assert_ne!(
17638            UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
17639            DEFAULT_RESOURCE_LIMITS.max_expansion_size
17640        );
17641        assert_ne!(
17642            UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
17643            DEFAULT_RESOURCE_LIMITS.max_macro_body_size
17644        );
17645        assert_ne!(
17646            UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
17647            DEFAULT_RESOURCE_LIMITS.max_registered_macros
17648        );
17649        assert_ne!(
17650            UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
17651            DEFAULT_RESOURCE_LIMITS.max_macro_arity
17652        );
17653    }
17654
17655    #[test]
17656    fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
17657        // Round-trip pin at CONSTRUCTION time — an expander built at
17658        // the ceiling-lifted preset MUST project back through
17659        // [`Expander::resource_limits`] to the SAME constant verbatim.
17660        // Peer of `expander_with_limits_seeds_the_provided_resource_posture`
17661        // one PRESET-POSTURE axis over — that pin exercises the
17662        // constructor's carry via a distinct-low-value bundle; this
17663        // pin exercises it via the ceiling-lifted preset constant.
17664        // Cross-pin the six-field snapshot equals the ceiling-lifted
17665        // constant so a regression that broke either the constant OR
17666        // the constructor's carry fires here.
17667        assert_eq!(
17668            Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
17669            UNBOUNDED_RESOURCE_LIMITS
17670        );
17671    }
17672
17673    #[test]
17674    fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
17675        // Behavioral pin — the ceiling-lifted preset MUST reach the
17676        // register-time body-size gate as an actually-lifted ceiling.
17677        // Register a macro whose body node count strictly exceeds
17678        // [`DEFAULT_MAX_MACRO_BODY_SIZE`] (a body which under the
17679        // shipped default would fail [`register_macro_def`] with a
17680        // [`LispError::MacroBodySizeExceeded`]) and confirm the
17681        // registration succeeds under the ceiling-lifted preset. Only
17682        // the body-size ceiling being lifted lets a body of that
17683        // magnitude land; the arity ceiling is left at 1 param via
17684        // struct-update override to prove the preset's other five
17685        // ceilings STILL project cleanly at [`usize::MAX`] alongside
17686        // the ceiling under test. A regression that failed to lift
17687        // ONE ceiling across the preset (a stale `DEFAULT_MAX_*` field
17688        // in the const literal) fires here on the axis it dropped.
17689        let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17690        // Build a huge quasi-quoted list body: `(k k k k k … k) whose
17691        // node_count() eclipses DEFAULT_MAX_MACRO_BODY_SIZE = 16384.
17692        let mut body = String::from("(defmacro huge (k) `(");
17693        for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
17694            body.push_str(",k ");
17695        }
17696        body.push_str("))");
17697        let forms = read(&body).unwrap();
17698        e.expand_program(forms)
17699            .expect("unbounded body-size ceiling must admit a body over the default limit");
17700        assert!(e.has("huge"));
17701    }
17702
17703    #[test]
17704    fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
17705        // Behavioral pin sibling of the body-size pin above one
17706        // RESOURCE-DIMENSION axis over on the REGISTER-time surface —
17707        // the ceiling-lifted preset MUST also reach the arity gate as
17708        // an actually-lifted ceiling. Register a macro whose param
17709        // list strictly exceeds [`DEFAULT_MAX_MACRO_ARITY`] (a
17710        // 129-slot lambda list which under the shipped default fails
17711        // with [`LispError::MacroArityExceeded`]) and confirm the
17712        // registration succeeds under the ceiling-lifted preset.
17713        use std::fmt::Write as _;
17714        let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17715        let mut src = String::from("(defmacro many-arity (");
17716        for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
17717            write!(src, "a-{i} ").unwrap();
17718        }
17719        src.push_str(") `,a-0)");
17720        let forms = read(&src).unwrap();
17721        e.expand_program(forms)
17722            .expect("unbounded arity ceiling must admit a param list over the default limit");
17723        assert!(e.has("many-arity"));
17724    }
17725
17726    #[test]
17727    fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
17728        // Ergonomic pin — the `Copy` posture on [`ResourceLimits`] lets
17729        // a caller build "every ceiling lifted EXCEPT this ONE" as ONE
17730        // struct-update literal on the ceiling-lifted preset AND thread
17731        // that composition through [`Expander::with_limits`] as ONE
17732        // typed call. This is the "only THIS ceiling gates" fixture
17733        // shape a chain of six independent setters cannot express in
17734        // ONE expression, and the shape the last commit named as a
17735        // future benefit of the (getter, setter, at-construction)
17736        // three-corner face closing at ONE bundle.
17737        //
17738        // Set only `max_macro_arity: 1`; confirm the arity gate fires
17739        // on a two-arg registration while the five other lifted
17740        // ceilings project through the getter as [`usize::MAX`]. A
17741        // regression that broke the ceiling-lifted preset would EITHER
17742        // fail to project [`usize::MAX`] on one of the five other
17743        // fields OR fail to gate the isolated arity ceiling at 1.
17744        let mut e = Expander::with_limits(ResourceLimits {
17745            max_macro_arity: 1,
17746            ..UNBOUNDED_RESOURCE_LIMITS
17747        });
17748        assert_eq!(e.max_macro_arity(), 1);
17749        assert_eq!(e.max_expansion_depth(), usize::MAX);
17750        assert_eq!(e.max_cache_entries(), usize::MAX);
17751        assert_eq!(e.max_expansion_size(), usize::MAX);
17752        assert_eq!(e.max_macro_body_size(), usize::MAX);
17753        assert_eq!(e.max_registered_macros(), usize::MAX);
17754        let forms = read("(defmacro two (a b) `,a)").unwrap();
17755        let err = e.expand_program(forms).unwrap_err();
17756        assert!(
17757            matches!(
17758                err,
17759                LispError::MacroArityExceeded {
17760                    arity: 2,
17761                    limit: 1,
17762                    ..
17763                }
17764            ),
17765            "struct-update override on the ceiling-lifted preset must \
17766             leave the isolated arity ceiling gating; got: {err:?}"
17767        );
17768    }
17769
17770    #[test]
17771    fn unbounded_resource_limits_carries_through_the_bulk_setter() {
17772        // Coherence pin between the AT-construction constructor and
17773        // the POST-construction bulk setter — both paths that reach the
17774        // ceiling-lifted preset must land at the SAME six values on
17775        // the expander state. Pre-lift a caller wanting the unbounded
17776        // posture composed six individual setter invocations at the
17777        // call site; post-lift both `Expander::with_limits(UNBOUNDED_
17778        // RESOURCE_LIMITS)` and `e.set_resource_limits(UNBOUNDED_
17779        // RESOURCE_LIMITS)` reach the SAME snapshot through the SAME
17780        // constant. A regression that wired the bulk setter to a
17781        // shadow field, or the ceiling-lifted preset to a stale field
17782        // literal, fires here on the constructor-vs-setter identity.
17783        let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17784        let mut b = Expander::new();
17785        b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
17786        assert_eq!(a.resource_limits(), b.resource_limits());
17787        assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
17788    }
17789
17790    // ── ResourceLimits::strictest / ::most_permissive — meet/join ──────
17791
17792    /// A hand-authored asymmetric posture used across the lattice-law
17793    /// tests. Every field is a distinct positive value strictly less
17794    /// than [`usize::MAX`] and distinct from every corresponding
17795    /// [`DEFAULT_MAX_*`] module constant, so composition witnesses
17796    /// (meet, join, absorption, distributivity) discriminate across
17797    /// all three postures unambiguously.
17798    const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
17799        max_expansion_depth: 7,
17800        max_cache_entries: 11,
17801        max_expansion_size: 13,
17802        max_macro_body_size: 17,
17803        max_registered_macros: 19,
17804        max_macro_arity: 23,
17805    };
17806
17807    /// A second asymmetric posture whose per-axis values are on both
17808    /// sides of `HAND_AUTHORED_MID_POSTURE`'s values — three axes
17809    /// smaller (so the meet picks THIS posture on those axes and the
17810    /// join picks `MID` on those axes) and three axes larger (mirror).
17811    /// Together the two postures exercise every ordering combination
17812    /// on the six-axis pointwise `min`/`max` cascade.
17813    const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
17814        max_expansion_depth: 3,   // smaller than MID's 7
17815        max_cache_entries: 29,    // larger  than MID's 11
17816        max_expansion_size: 5,    // smaller than MID's 13
17817        max_macro_body_size: 31,  // larger  than MID's 17
17818        max_registered_macros: 2, // smaller than MID's 19
17819        max_macro_arity: 41,      // larger  than MID's 23
17820    };
17821
17822    #[test]
17823    fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
17824        // Concrete-preset pin — the meet of the (shipped default,
17825        // ceiling-lifted unbounded) preset pair is the DEFAULT preset
17826        // structurally: every `DEFAULT_MAX_*` module constant is a
17827        // concrete positive value strictly less than [`usize::MAX`],
17828        // so on every axis the pointwise-`min` picks the DEFAULT side.
17829        // Peer of `most_permissive_of_default_and_unbounded_projects_the_unbounded`
17830        // one COMBINATOR axis over on the same shipped-preset-pair
17831        // surface.
17832        assert_eq!(
17833            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17834            DEFAULT_RESOURCE_LIMITS,
17835        );
17836        assert_eq!(
17837            UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17838            DEFAULT_RESOURCE_LIMITS,
17839        );
17840    }
17841
17842    #[test]
17843    fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
17844        // Concrete-preset pin — the join of the (shipped default,
17845        // ceiling-lifted unbounded) preset pair is the UNBOUNDED
17846        // preset structurally: every axis's pointwise-`max` picks the
17847        // [`usize::MAX`] side over any concrete default. Peer of
17848        // `strictest_of_default_and_unbounded_projects_the_default`.
17849        assert_eq!(
17850            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17851            UNBOUNDED_RESOURCE_LIMITS,
17852        );
17853        assert_eq!(
17854            UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17855            UNBOUNDED_RESOURCE_LIMITS,
17856        );
17857    }
17858
17859    #[test]
17860    fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
17861        // Field-level pin — the meet operation projects the pointwise
17862        // `min` across the six-field surface. Pinned as six
17863        // independent field asserts (rather than one struct equality)
17864        // so a drift on ONE axis carries a distinct-name failure that
17865        // names the drifting axis; a future SEVENTH ceiling extension
17866        // requires an additional field assert here in lockstep.
17867        let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
17868        assert_eq!(m.max_expansion_depth, 3);
17869        assert_eq!(m.max_cache_entries, 11);
17870        assert_eq!(m.max_expansion_size, 5);
17871        assert_eq!(m.max_macro_body_size, 17);
17872        assert_eq!(m.max_registered_macros, 2);
17873        assert_eq!(m.max_macro_arity, 23);
17874    }
17875
17876    #[test]
17877    fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
17878        // Field-level pin — the join operation projects the pointwise
17879        // `max` across the six-field surface. Pinned as six
17880        // independent field asserts so a drift on ONE axis carries a
17881        // distinct-name failure that names the drifting axis.
17882        let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
17883        assert_eq!(j.max_expansion_depth, 7);
17884        assert_eq!(j.max_cache_entries, 29);
17885        assert_eq!(j.max_expansion_size, 13);
17886        assert_eq!(j.max_macro_body_size, 31);
17887        assert_eq!(j.max_registered_macros, 19);
17888        assert_eq!(j.max_macro_arity, 41);
17889    }
17890
17891    #[test]
17892    fn resource_limits_strictest_is_idempotent() {
17893        // Lattice law — `a ∧ a = a`. Every posture is a fixed point of
17894        // its own meet with itself; a regression that changed the
17895        // per-axis primitive to something other than pointwise `min`
17896        // (a `min - 1` off-by-one, or an arbitrary tie-breaker on
17897        // equal values) fires here.
17898        assert_eq!(
17899            DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17900            DEFAULT_RESOURCE_LIMITS,
17901        );
17902        assert_eq!(
17903            UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17904            UNBOUNDED_RESOURCE_LIMITS,
17905        );
17906        assert_eq!(
17907            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17908            HAND_AUTHORED_MID_POSTURE,
17909        );
17910    }
17911
17912    #[test]
17913    fn resource_limits_most_permissive_is_idempotent() {
17914        // Lattice law — `a ∨ a = a`. Sibling of the strictest
17915        // idempotence pin one COMBINATOR axis over.
17916        assert_eq!(
17917            DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17918            DEFAULT_RESOURCE_LIMITS,
17919        );
17920        assert_eq!(
17921            UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17922            UNBOUNDED_RESOURCE_LIMITS,
17923        );
17924        assert_eq!(
17925            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17926            HAND_AUTHORED_MID_POSTURE,
17927        );
17928    }
17929
17930    #[test]
17931    fn resource_limits_strictest_is_commutative() {
17932        // Lattice law — `a ∧ b = b ∧ a`. Pointwise `min` is symmetric
17933        // on every axis, so the composition inherits commutativity
17934        // structurally. A regression that broke ordering (e.g. a
17935        // per-axis "prefer left on tie") fires here.
17936        assert_eq!(
17937            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
17938            HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17939        );
17940        assert_eq!(
17941            DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
17942            HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
17943        );
17944    }
17945
17946    #[test]
17947    fn resource_limits_most_permissive_is_commutative() {
17948        // Lattice law — `a ∨ b = b ∨ a`. Sibling of the strictest
17949        // commutativity pin one COMBINATOR axis over.
17950        assert_eq!(
17951            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
17952            HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17953        );
17954        assert_eq!(
17955            DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
17956            HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
17957        );
17958    }
17959
17960    #[test]
17961    fn resource_limits_strictest_is_associative() {
17962        // Lattice law — `(a ∧ b) ∧ c = a ∧ (b ∧ c)`. Pointwise `min`
17963        // is associative on every axis, so the composition inherits
17964        // associativity structurally. A regression that broke
17965        // grouping (e.g. an accidental accumulator that skewed the
17966        // order of `min` invocations) fires here.
17967        let a = DEFAULT_RESOURCE_LIMITS;
17968        let b = HAND_AUTHORED_MID_POSTURE;
17969        let c = HAND_AUTHORED_OTHER_POSTURE;
17970        assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
17971    }
17972
17973    #[test]
17974    fn resource_limits_most_permissive_is_associative() {
17975        // Lattice law — `(a ∨ b) ∨ c = a ∨ (b ∨ c)`. Sibling of the
17976        // strictest associativity pin one COMBINATOR axis over.
17977        let a = DEFAULT_RESOURCE_LIMITS;
17978        let b = HAND_AUTHORED_MID_POSTURE;
17979        let c = HAND_AUTHORED_OTHER_POSTURE;
17980        assert_eq!(
17981            a.most_permissive(b).most_permissive(c),
17982            a.most_permissive(b.most_permissive(c)),
17983        );
17984    }
17985
17986    #[test]
17987    fn resource_limits_meet_and_join_satisfy_absorption() {
17988        // Lattice absorption identities — `a ∧ (a ∨ b) = a` AND
17989        // `a ∨ (a ∧ b) = a`. Together with commutativity, idempotence,
17990        // and associativity they define the lattice axioms; the two
17991        // absorption laws are the operator PAIR's interlock (each
17992        // undoes the other's contribution on the shared operand).
17993        // Any per-axis primitive pair for which `min(a, max(a, b)) = a`
17994        // AND `max(a, min(a, b)) = a` inherits this at the pointwise
17995        // composition; pointwise-`min`/`max` on [`usize`] satisfy both.
17996        let a = HAND_AUTHORED_MID_POSTURE;
17997        let b = HAND_AUTHORED_OTHER_POSTURE;
17998        assert_eq!(a.strictest(a.most_permissive(b)), a);
17999        assert_eq!(a.most_permissive(a.strictest(b)), a);
18000    }
18001
18002    #[test]
18003    fn resource_limits_meet_distributes_over_join() {
18004        // Distributive-lattice pin — `a ∧ (b ∨ c) = (a ∧ b) ∨ (a ∧ c)`.
18005        // Pointwise-`min` distributes over pointwise-`max` on [`usize`]
18006        // on every axis, so the composition inherits distributivity
18007        // structurally. A regression that broke the pointwise
18008        // structure (e.g. a per-axis operation that used SUBTRACTION
18009        // rather than MIN/MAX) fires here — subtraction does NOT
18010        // distribute over addition and would break the law on the
18011        // asymmetric-posture witness pair.
18012        let a = DEFAULT_RESOURCE_LIMITS;
18013        let b = HAND_AUTHORED_MID_POSTURE;
18014        let c = HAND_AUTHORED_OTHER_POSTURE;
18015        assert_eq!(
18016            a.strictest(b.most_permissive(c)),
18017            a.strictest(b).most_permissive(a.strictest(c)),
18018        );
18019    }
18020
18021    #[test]
18022    fn resource_limits_join_distributes_over_meet() {
18023        // Distributive-lattice pin, sibling of the meet-over-join
18024        // distributivity pin one COMBINATOR axis over — `a ∨ (b ∧ c) =
18025        // (a ∨ b) ∧ (a ∨ c)`. In a distributive lattice BOTH
18026        // distributivity identities hold; pin both so a regression
18027        // that broke ONE without the other cannot slip through.
18028        let a = DEFAULT_RESOURCE_LIMITS;
18029        let b = HAND_AUTHORED_MID_POSTURE;
18030        let c = HAND_AUTHORED_OTHER_POSTURE;
18031        assert_eq!(
18032            a.most_permissive(b.strictest(c)),
18033            a.most_permissive(b).strictest(a.most_permissive(c)),
18034        );
18035    }
18036
18037    #[test]
18038    fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
18039        // Meet-lower-bound pin — the meet lies at or below BOTH
18040        // operands on every axis. This is the structural GLB property
18041        // of the meet in the pointwise partial order (tighter-first):
18042        // any admissible input under the meet is admissible under
18043        // both operands, so the meet's ceiling must not exceed either
18044        // operand's ceiling on any axis.
18045        let a = HAND_AUTHORED_MID_POSTURE;
18046        let b = HAND_AUTHORED_OTHER_POSTURE;
18047        let m = a.strictest(b);
18048        assert!(m.max_expansion_depth <= a.max_expansion_depth);
18049        assert!(m.max_expansion_depth <= b.max_expansion_depth);
18050        assert!(m.max_cache_entries <= a.max_cache_entries);
18051        assert!(m.max_cache_entries <= b.max_cache_entries);
18052        assert!(m.max_expansion_size <= a.max_expansion_size);
18053        assert!(m.max_expansion_size <= b.max_expansion_size);
18054        assert!(m.max_macro_body_size <= a.max_macro_body_size);
18055        assert!(m.max_macro_body_size <= b.max_macro_body_size);
18056        assert!(m.max_registered_macros <= a.max_registered_macros);
18057        assert!(m.max_registered_macros <= b.max_registered_macros);
18058        assert!(m.max_macro_arity <= a.max_macro_arity);
18059        assert!(m.max_macro_arity <= b.max_macro_arity);
18060    }
18061
18062    #[test]
18063    fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
18064        // Join-upper-bound pin — the join lies at or above BOTH
18065        // operands on every axis. Sibling of the strictest-lower-
18066        // bound pin one COMBINATOR axis over; the structural LUB
18067        // property of the join in the pointwise partial order.
18068        let a = HAND_AUTHORED_MID_POSTURE;
18069        let b = HAND_AUTHORED_OTHER_POSTURE;
18070        let j = a.most_permissive(b);
18071        assert!(j.max_expansion_depth >= a.max_expansion_depth);
18072        assert!(j.max_expansion_depth >= b.max_expansion_depth);
18073        assert!(j.max_cache_entries >= a.max_cache_entries);
18074        assert!(j.max_cache_entries >= b.max_cache_entries);
18075        assert!(j.max_expansion_size >= a.max_expansion_size);
18076        assert!(j.max_expansion_size >= b.max_expansion_size);
18077        assert!(j.max_macro_body_size >= a.max_macro_body_size);
18078        assert!(j.max_macro_body_size >= b.max_macro_body_size);
18079        assert!(j.max_registered_macros >= a.max_registered_macros);
18080        assert!(j.max_registered_macros >= b.max_registered_macros);
18081        assert!(j.max_macro_arity >= a.max_macro_arity);
18082        assert!(j.max_macro_arity >= b.max_macro_arity);
18083    }
18084
18085    #[test]
18086    fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
18087        // Const-fn pin — the meet is evaluable in const context, so a
18088        // caller can stitch a new `pub const` preset from two shipped
18089        // presets without deferring the composition to runtime. Pinned
18090        // as a `const` binding at the item level so the compiler
18091        // exercises the const-fn path structurally; the runtime
18092        // assertion is redundant for a `const` value that already
18093        // evaluated at compile time, but pins the identity as a typed
18094        // theorem alongside every OTHER `strictest` pin above.
18095        const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
18096            DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
18097        assert_eq!(
18098            DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
18099            min_usize(
18100                DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18101                HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
18102            ),
18103        );
18104        // Extra check: the composition MUST project the MID posture's
18105        // small value on every axis where MID is smaller than DEFAULT
18106        // — MID's per-axis values (7, 11, 13, 17, 19, 23) are all
18107        // strictly less than every `DEFAULT_MAX_*` constant, so the
18108        // meet projects MID verbatim.
18109        assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
18110    }
18111
18112    #[test]
18113    fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
18114        // Const-fn pin sibling of the strictest const-composition pin
18115        // one COMBINATOR axis over — the join is also `const fn`, so
18116        // a caller can stitch a new `pub const` preset via the LUB
18117        // combinator at compile time.
18118        const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
18119            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
18120        assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
18121    }
18122
18123    // ── ResourceLimits::leq — pointwise partial order ─────────────────
18124
18125    #[test]
18126    fn resource_limits_leq_is_pointwise_field_conjunction() {
18127        // Field-level pin — the leq relation projects the pointwise
18128        // `<=` conjunction across the six-field surface. `LOOSER` is
18129        // `MID + 1` on every axis, so `MID.leq(LOOSER)` holds; a
18130        // per-axis tighten on ANY field alone flips the relation to
18131        // false. Pinned as six independent tighten-per-axis asserts so
18132        // a drift on ONE axis carries a distinct-name failure that
18133        // names the drifting axis; a future SEVENTH ceiling extension
18134        // requires an additional per-axis tighten assert here in
18135        // lockstep.
18136        const LOOSER: ResourceLimits = ResourceLimits {
18137            max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18138            max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18139            max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18140            max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18141            max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18142            max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18143        };
18144        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18145        assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
18146
18147        // Per-axis exceed pins: raise MID's field ONE axis above LOOSER
18148        // on that axis; the leq must flip to false. Every other axis
18149        // stays at MID, so LOOSER still dominates on those axes —
18150        // isolating the single-axis violation.
18151        let exceed_depth = ResourceLimits {
18152            max_expansion_depth: LOOSER.max_expansion_depth + 1,
18153            ..HAND_AUTHORED_MID_POSTURE
18154        };
18155        assert!(!exceed_depth.leq(LOOSER));
18156        let exceed_cache = ResourceLimits {
18157            max_cache_entries: LOOSER.max_cache_entries + 1,
18158            ..HAND_AUTHORED_MID_POSTURE
18159        };
18160        assert!(!exceed_cache.leq(LOOSER));
18161        let exceed_expansion = ResourceLimits {
18162            max_expansion_size: LOOSER.max_expansion_size + 1,
18163            ..HAND_AUTHORED_MID_POSTURE
18164        };
18165        assert!(!exceed_expansion.leq(LOOSER));
18166        let exceed_body = ResourceLimits {
18167            max_macro_body_size: LOOSER.max_macro_body_size + 1,
18168            ..HAND_AUTHORED_MID_POSTURE
18169        };
18170        assert!(!exceed_body.leq(LOOSER));
18171        let exceed_registered = ResourceLimits {
18172            max_registered_macros: LOOSER.max_registered_macros + 1,
18173            ..HAND_AUTHORED_MID_POSTURE
18174        };
18175        assert!(!exceed_registered.leq(LOOSER));
18176        let exceed_arity = ResourceLimits {
18177            max_macro_arity: LOOSER.max_macro_arity + 1,
18178            ..HAND_AUTHORED_MID_POSTURE
18179        };
18180        assert!(!exceed_arity.leq(LOOSER));
18181    }
18182
18183    #[test]
18184    fn resource_limits_leq_is_reflexive() {
18185        // Partial-order law — `a ≤ a` for every posture. Reflexivity
18186        // is the identity axiom on any lattice's underlying partial
18187        // order; a regression that changed the per-axis primitive to
18188        // strict `<` (or that added an accidental "prefer other on
18189        // tie" branch) fires here on every shipped posture.
18190        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18191        assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18192        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18193        assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18194    }
18195
18196    #[test]
18197    fn resource_limits_leq_is_antisymmetric() {
18198        // Partial-order law — `a ≤ b ∧ b ≤ a ⇒ a = b`. The mutual-
18199        // dominance witness on a partial order pins the two operands
18200        // to structural equality. On the pointwise `<=` composition
18201        // this holds because `a.max_X <= b.max_X && b.max_X <= a.max_X
18202        // ⇒ a.max_X == b.max_X` on every axis, and struct equality
18203        // is field-conjunction. Sibling to reflexivity one AXIOM axis
18204        // over on the partial-order axiom surface.
18205        //
18206        // Two distinct constructions of the SAME six-field posture
18207        // (one via struct literal, one via `..DEFAULT_RESOURCE_LIMITS`
18208        // spread of the shipped preset) — mutual leq holds; struct
18209        // equality holds; antisymmetry pin closes.
18210        let a = DEFAULT_RESOURCE_LIMITS;
18211        let b = ResourceLimits {
18212            max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18213            max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
18214            max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
18215            max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
18216            max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
18217            max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
18218        };
18219        assert!(a.leq(b));
18220        assert!(b.leq(a));
18221        assert_eq!(a, b);
18222    }
18223
18224    #[test]
18225    fn resource_limits_leq_is_transitive() {
18226        // Partial-order law — `a ≤ b ∧ b ≤ c ⇒ a ≤ c`. Transitivity
18227        // is the composition axiom on any lattice's partial order.
18228        // Pointwise `<=` on [`usize`] is transitive per axis, so the
18229        // six-field conjunction inherits it structurally.
18230        //
18231        // Chain witness: MID's every-axis `+1` shift is LOOSER, and
18232        // LOOSER's every-axis `+1` shift is LOOSEST; the chain MID
18233        // ≤ LOOSER ≤ LOOSEST implies MID ≤ LOOSEST.
18234        const LOOSER: ResourceLimits = ResourceLimits {
18235            max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18236            max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18237            max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18238            max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18239            max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18240            max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18241        };
18242        const LOOSEST: ResourceLimits = ResourceLimits {
18243            max_expansion_depth: LOOSER.max_expansion_depth + 1,
18244            max_cache_entries: LOOSER.max_cache_entries + 1,
18245            max_expansion_size: LOOSER.max_expansion_size + 1,
18246            max_macro_body_size: LOOSER.max_macro_body_size + 1,
18247            max_registered_macros: LOOSER.max_registered_macros + 1,
18248            max_macro_arity: LOOSER.max_macro_arity + 1,
18249        };
18250        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18251        assert!(LOOSER.leq(LOOSEST));
18252        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
18253    }
18254
18255    #[test]
18256    fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
18257        // Concrete-preset pin — the leq relation on the shipped
18258        // preset pair is STRICT: DEFAULT ≤ UNBOUNDED holds (every
18259        // `DEFAULT_MAX_*` module constant is a concrete positive
18260        // value at most [`usize::MAX`]) AND UNBOUNDED ≤ DEFAULT
18261        // fails (every `DEFAULT_MAX_*` is strictly less than
18262        // [`usize::MAX`], so on every axis [`usize::MAX`] exceeds
18263        // the matching default). Peer of
18264        // `strictest_of_default_and_unbounded_projects_the_default`
18265        // one PRIMITIVE-KIND axis over on the same shipped-preset-
18266        // pair surface: where the meet composition projects DEFAULT,
18267        // this relation asserts DEFAULT is the tighter of the two.
18268        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18269        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18270    }
18271
18272    #[test]
18273    fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
18274        // Partial-order NON-total pin — the two hand-authored asymmetric
18275        // postures have MID smaller on three axes (depth, expansion,
18276        // registered) and OTHER smaller on the other three (cache,
18277        // body, arity); neither dominates the other, so BOTH
18278        // directions of the leq relation fail. The pin discriminates
18279        // a regression that promoted `leq` from partial to total
18280        // (e.g. an accidental `||` swap in the conjunction, which
18281        // would make every pair comparable).
18282        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18283        assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18284    }
18285
18286    #[test]
18287    fn resource_limits_leq_agrees_with_meet() {
18288        // Lattice cross-check axiom — `a ≤ b ⇔ a ⊓ b = a`. Every
18289        // classification lattice one crate over
18290        // (`tatara-lattice/src/lib.rs`'s preamble) declares this
18291        // agreement axiom binding the partial order to the meet
18292        // combinator, and the default-impl body on
18293        // `tatara_lattice::Lattice::leq` derives `leq` FROM `meet`
18294        // through it. This pin discharges the same axiom on the
18295        // ResourceLimits algebra as a two-direction structural
18296        // equality across the strict-order preset pair AND the
18297        // reflexivity witness.
18298        //
18299        // Forward (a ≤ b ⇒ a ⊓ b = a):
18300        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18301        assert_eq!(
18302            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18303            DEFAULT_RESOURCE_LIMITS,
18304        );
18305        // Reverse (a ⊓ b = a ⇒ a ≤ b), reflexivity witness:
18306        assert_eq!(
18307            DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18308            DEFAULT_RESOURCE_LIMITS,
18309        );
18310        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18311
18312        // Non-agreement witness — the incomparable asymmetric postures
18313        // have meet distinct from EITHER operand, and leq false in
18314        // both directions; agreement holds on the negative side too.
18315        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18316        assert_ne!(
18317            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18318            HAND_AUTHORED_MID_POSTURE,
18319        );
18320    }
18321
18322    #[test]
18323    fn resource_limits_leq_agrees_with_join() {
18324        // Lattice cross-check axiom, sibling of the meet-agreement
18325        // pin one COMBINATOR axis over — `a ≤ b ⇔ a ⊔ b = b`.
18326        //
18327        // Forward (a ≤ b ⇒ a ⊔ b = b):
18328        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18329        assert_eq!(
18330            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18331            UNBOUNDED_RESOURCE_LIMITS,
18332        );
18333        // Reverse (a ⊔ b = b ⇒ a ≤ b), reflexivity witness:
18334        assert_eq!(
18335            DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18336            DEFAULT_RESOURCE_LIMITS,
18337        );
18338        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18339
18340        // Non-agreement witness — the incomparable asymmetric postures
18341        // have join distinct from EITHER operand.
18342        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18343        assert_ne!(
18344            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18345            HAND_AUTHORED_OTHER_POSTURE,
18346        );
18347    }
18348
18349    #[test]
18350    fn resource_limits_strictest_is_leq_both_operands() {
18351        // Meet-lower-bound pin, typed lattice-relation companion to
18352        // `resource_limits_strictest_is_dominated_by_both_operands_pointwise`
18353        // one PRIMITIVE-KIND axis over — where the per-axis pin
18354        // discharged the six independent `<=` asserts inline, this pin
18355        // routes through the typed `leq` primitive, so the
18356        // greatest-lower-bound property is stated in the LATTICE
18357        // vocabulary (meet-is-leq-both-operands) rather than the
18358        // six-fold field-conjunction vocabulary.
18359        let a = HAND_AUTHORED_MID_POSTURE;
18360        let b = HAND_AUTHORED_OTHER_POSTURE;
18361        let m = a.strictest(b);
18362        assert!(m.leq(a));
18363        assert!(m.leq(b));
18364    }
18365
18366    #[test]
18367    fn resource_limits_most_permissive_is_geq_both_operands() {
18368        // Join-upper-bound pin, typed lattice-relation companion to
18369        // `resource_limits_most_permissive_dominates_both_operands_pointwise`
18370        // one PRIMITIVE-KIND axis over. Stated in the LATTICE
18371        // vocabulary as `a ≤ a ⊔ b ∧ b ≤ a ⊔ b` — sibling of the
18372        // meet-lower-bound pin one COMBINATOR axis over.
18373        let a = HAND_AUTHORED_MID_POSTURE;
18374        let b = HAND_AUTHORED_OTHER_POSTURE;
18375        let j = a.most_permissive(b);
18376        assert!(a.leq(j));
18377        assert!(b.leq(j));
18378    }
18379
18380    #[test]
18381    fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
18382        // Const-fn pin — the leq relation is evaluable in const
18383        // context, so a caller can pin a preset-relation identity at
18384        // compile time. Sibling of the const-fn composition pins on
18385        // `strictest` / `most_permissive` one PRIMITIVE-KIND axis
18386        // over: those two operations compose two presets into a
18387        // const-time third, and this operation decides a const-time
18388        // yes/no on two presets. `const _: ()` at item scope is a
18389        // compile-time proof — the compiler rejects the module if
18390        // either identity does not hold; a runtime `assert!` over the
18391        // same expression would fire at test time on the same value
18392        // clippy's `assertions_on_constants` correctly names as a
18393        // redundant late check on a compile-time constant.
18394        //
18395        // Strict-order preset-pair pin — DEFAULT ≤ UNBOUNDED (every
18396        // `DEFAULT_MAX_*` module constant is at most [`usize::MAX`]):
18397        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18398        // Reverse direction fails — [`usize::MAX`] exceeds every
18399        // concrete `DEFAULT_MAX_*` constant, so the partial order is
18400        // STRICT on the shipped preset pair:
18401        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18402    }
18403
18404    // ── EMPTY_RESOURCE_LIMITS — bounded-lattice bottom preset ─────────
18405
18406    #[test]
18407    fn empty_resource_limits_binds_every_ceiling_to_zero() {
18408        // Field-level pin — the zero preset carries `0` on every axis
18409        // of the six-field surface. Pinned as six independent field
18410        // asserts (rather than one struct equality) so a drift on ONE
18411        // field carries a distinct-name failure that names the drifting
18412        // axis. A future extension that adds a SEVENTH ceiling requires
18413        // an additional field assert here in lockstep with the const
18414        // literal — rustc's field-exhaustiveness on the const forces
18415        // the new field to appear, and this pin exercises the seed
18416        // value. Structural peer of
18417        // `unbounded_resource_limits_binds_every_ceiling_to_usize_max`
18418        // one LATTICE-POLE axis over on the (bottom, top)
18419        // bounded-lattice preset-pair surface.
18420        assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
18421        assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
18422        assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
18423        assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
18424        assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
18425        assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
18426    }
18427
18428    #[test]
18429    fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
18430        // Structural-distinctness pin — the two constants
18431        // [`DEFAULT_RESOURCE_LIMITS`] and [`EMPTY_RESOURCE_LIMITS`] sit
18432        // at DISTINCT points on the preset-posture axis, and the
18433        // disagreement is EXHAUSTIVE on the six-field surface — every
18434        // shipped `DEFAULT_MAX_*` module constant is a concrete
18435        // strictly-positive value, so every axis distinguishes DEFAULT
18436        // from the all-zero bottom. Peer of
18437        // `unbounded_resource_limits_disagrees_with_default_on_every_ceiling`
18438        // one LATTICE-POLE axis over.
18439        assert_ne!(
18440            EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18441            DEFAULT_RESOURCE_LIMITS.max_expansion_depth
18442        );
18443        assert_ne!(
18444            EMPTY_RESOURCE_LIMITS.max_cache_entries,
18445            DEFAULT_RESOURCE_LIMITS.max_cache_entries
18446        );
18447        assert_ne!(
18448            EMPTY_RESOURCE_LIMITS.max_expansion_size,
18449            DEFAULT_RESOURCE_LIMITS.max_expansion_size
18450        );
18451        assert_ne!(
18452            EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18453            DEFAULT_RESOURCE_LIMITS.max_macro_body_size
18454        );
18455        assert_ne!(
18456            EMPTY_RESOURCE_LIMITS.max_registered_macros,
18457            DEFAULT_RESOURCE_LIMITS.max_registered_macros
18458        );
18459        assert_ne!(
18460            EMPTY_RESOURCE_LIMITS.max_macro_arity,
18461            DEFAULT_RESOURCE_LIMITS.max_macro_arity
18462        );
18463    }
18464
18465    #[test]
18466    fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
18467        // Diagonal-distinctness pin — the (BOTTOM, TOP) preset pair
18468        // disagrees on every axis (`0 != usize::MAX` on every field).
18469        // Structural companion to the DEFAULT-vs-EMPTY and
18470        // UNBOUNDED-vs-DEFAULT disagreement pins one PRESET-PAIR axis
18471        // over — together the three pin the six-field surface at the
18472        // full diagonal of the (EMPTY, DEFAULT, UNBOUNDED) triple.
18473        assert_ne!(
18474            EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18475            UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
18476        );
18477        assert_ne!(
18478            EMPTY_RESOURCE_LIMITS.max_cache_entries,
18479            UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
18480        );
18481        assert_ne!(
18482            EMPTY_RESOURCE_LIMITS.max_expansion_size,
18483            UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
18484        );
18485        assert_ne!(
18486            EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18487            UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
18488        );
18489        assert_ne!(
18490            EMPTY_RESOURCE_LIMITS.max_registered_macros,
18491            UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
18492        );
18493        assert_ne!(
18494            EMPTY_RESOURCE_LIMITS.max_macro_arity,
18495            UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
18496        );
18497    }
18498
18499    #[test]
18500    fn empty_resource_limits_is_the_join_identity() {
18501        // Lattice-identity law — `a ⊔ ⊥ = a`. Every posture's
18502        // pointwise `max` against 0 returns the posture verbatim, so
18503        // [`EMPTY_RESOURCE_LIMITS`] acts as the identity element of
18504        // the join monoid. Peer of
18505        // `empty_resource_limits_is_the_meet_annihilator` one
18506        // COMBINATOR axis over on the (meet, join) surface.
18507        //
18508        // Cross-preset exhaustion — verify the identity against every
18509        // named preset (DEFAULT, UNBOUNDED, both hand-authored
18510        // asymmetric postures) so a regression that broke ONE axis of
18511        // the algebra fires here on the operand whose field crosses
18512        // that axis's boundary.
18513        assert_eq!(
18514            DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18515            DEFAULT_RESOURCE_LIMITS,
18516        );
18517        assert_eq!(
18518            EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18519            DEFAULT_RESOURCE_LIMITS,
18520        );
18521        assert_eq!(
18522            UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18523            UNBOUNDED_RESOURCE_LIMITS,
18524        );
18525        assert_eq!(
18526            EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18527            UNBOUNDED_RESOURCE_LIMITS,
18528        );
18529        assert_eq!(
18530            HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18531            HAND_AUTHORED_MID_POSTURE,
18532        );
18533        assert_eq!(
18534            HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18535            HAND_AUTHORED_OTHER_POSTURE,
18536        );
18537        // Idempotence at the identity itself — sibling witness of the
18538        // `most_permissive_is_idempotent` general pin one PRESET axis
18539        // over.
18540        assert_eq!(
18541            EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18542            EMPTY_RESOURCE_LIMITS,
18543        );
18544    }
18545
18546    #[test]
18547    fn empty_resource_limits_is_the_meet_annihilator() {
18548        // Lattice-annihilator law — `a ⊓ ⊥ = ⊥`. Every posture's
18549        // pointwise `min` against 0 returns 0 on every axis, so
18550        // [`EMPTY_RESOURCE_LIMITS`] acts as the annihilator (absorbing
18551        // element) of the meet operation. Peer of
18552        // `empty_resource_limits_is_the_join_identity` one COMBINATOR
18553        // axis over — the identity on one side of the bounded lattice
18554        // is the annihilator on the other side.
18555        assert_eq!(
18556            DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18557            EMPTY_RESOURCE_LIMITS,
18558        );
18559        assert_eq!(
18560            EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18561            EMPTY_RESOURCE_LIMITS,
18562        );
18563        assert_eq!(
18564            UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18565            EMPTY_RESOURCE_LIMITS,
18566        );
18567        assert_eq!(
18568            EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18569            EMPTY_RESOURCE_LIMITS,
18570        );
18571        assert_eq!(
18572            HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18573            EMPTY_RESOURCE_LIMITS,
18574        );
18575        assert_eq!(
18576            HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18577            EMPTY_RESOURCE_LIMITS,
18578        );
18579    }
18580
18581    #[test]
18582    fn empty_resource_limits_is_the_lattice_minimum() {
18583        // Partial-order minimum law — `⊥ ≤ a` for every `a`. Every
18584        // posture's field is a `usize` and `0 <= x` holds for every
18585        // `usize x`, so [`EMPTY_RESOURCE_LIMITS`] sits at or below
18586        // every posture on every axis of the pointwise `<=` conjunction.
18587        // Peer of the UNBOUNDED-is-maximum property on the
18588        // partial-order face one LATTICE-POLE axis over — those two
18589        // pin the (min, max) bounds of the bounded lattice.
18590        assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18591        assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18592        assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18593        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
18594        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
18595        // Reverse direction FAILS on every non-EMPTY preset (each has
18596        // at least one field strictly greater than 0). The partial
18597        // order is STRICT between EMPTY and every other named preset,
18598        // structurally exhausting the diagonal of the
18599        // (EMPTY, DEFAULT, UNBOUNDED, MID, OTHER) 5-preset ordering
18600        // face at the BOTTOM row.
18601        assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18602        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18603        assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18604        assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18605    }
18606
18607    #[test]
18608    fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
18609        // Const-fn pin — the bounded-lattice identity + annihilator +
18610        // minimum laws hold at COMPILE time. Sibling of
18611        // `resource_limits_leq_evaluates_at_compile_time_via_const_fn`
18612        // one LATTICE-STRUCTURE axis over — that pin fixed the strict
18613        // order of the (DEFAULT, UNBOUNDED) preset pair at compile
18614        // time; this pin fixes the bounded-lattice pole role of the
18615        // (EMPTY, UNBOUNDED) preset pair. `const _: ()` at item scope
18616        // is a compile-time proof — a regression that broke either
18617        // identity element or the bottom-is-minimum invariant fires at
18618        // rustc time, not test-run time.
18619        //
18620        // Bottom-is-minimum: EMPTY ≤ DEFAULT ≤ UNBOUNDED (the
18621        // three-preset ordering chain on the shipped preset diagonal).
18622        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18623        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18624        // Reverse strictness — neither DEFAULT nor UNBOUNDED sits
18625        // below EMPTY (each has a strictly-positive field).
18626        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18627        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18628        // Join-identity + meet-annihilator laws cross-pinned through
18629        // the `leq` primitive at const time — the round-trip identity
18630        // (`a ⊔ ⊥ = a ⇔ ⊥ ≤ a`, `a ⊓ ⊥ = ⊥ ⇔ ⊥ ≤ a`) means the two
18631        // leq asserts above discharge both algebraic laws by the
18632        // `leq_agrees_with_meet` + `leq_agrees_with_join` lattice
18633        // cross-check axiom already pinned in this cohort.
18634    }
18635
18636    #[test]
18637    fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
18638        // Fold-identity behavioral pin — a slice of postures folded
18639        // through [`ResourceLimits::most_permissive`] with EMPTY as
18640        // the seed produces the pointwise least upper bound across
18641        // the slice on every axis. The join-identity property
18642        // (`a.most_permissive(EMPTY) == a`) guarantees the seed
18643        // contributes nothing to the fold's result — every field's
18644        // pointwise `max` against the seed's `0` returns the field's
18645        // value verbatim, so the fold reduces to the joined-across-
18646        // -operands upper bound. This is the destination usage the
18647        // constant's docstring names as its fold-identity role.
18648        //
18649        // A regression that seeded from ANY non-empty posture (e.g.
18650        // `DEFAULT_RESOURCE_LIMITS`) would inflate the fold's result
18651        // on every axis where the seed's field exceeds the pointwise
18652        // max of the slice — fires as a distinct-axis inequality here.
18653        let postures: [ResourceLimits; 3] = [
18654            HAND_AUTHORED_MID_POSTURE,
18655            HAND_AUTHORED_OTHER_POSTURE,
18656            DEFAULT_RESOURCE_LIMITS,
18657        ];
18658        let joined = postures
18659            .iter()
18660            .copied()
18661            .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
18662        // Every operand is `leq` the joined result — the least
18663        // upper bound dominates each contributor on every axis.
18664        assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
18665        assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
18666        assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
18667        // Field-level pin — each axis is the pointwise `max` of the
18668        // three sources' values. Six independent field asserts so a
18669        // drift on ONE axis carries a distinct-name failure.
18670        assert_eq!(
18671            joined.max_expansion_depth,
18672            HAND_AUTHORED_MID_POSTURE
18673                .max_expansion_depth
18674                .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18675                .max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18676        );
18677        assert_eq!(
18678            joined.max_cache_entries,
18679            HAND_AUTHORED_MID_POSTURE
18680                .max_cache_entries
18681                .max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18682                .max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18683        );
18684        assert_eq!(
18685            joined.max_expansion_size,
18686            HAND_AUTHORED_MID_POSTURE
18687                .max_expansion_size
18688                .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18689                .max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18690        );
18691        assert_eq!(
18692            joined.max_macro_body_size,
18693            HAND_AUTHORED_MID_POSTURE
18694                .max_macro_body_size
18695                .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18696                .max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18697        );
18698        assert_eq!(
18699            joined.max_registered_macros,
18700            HAND_AUTHORED_MID_POSTURE
18701                .max_registered_macros
18702                .max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18703                .max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18704        );
18705        assert_eq!(
18706            joined.max_macro_arity,
18707            HAND_AUTHORED_MID_POSTURE
18708                .max_macro_arity
18709                .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18710                .max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18711        );
18712        // Empty-slice edge case — folding over an EMPTY slice returns
18713        // the seed verbatim (the identity element as the null aggregate).
18714        // A wrapper-type approach (`Option<ResourceLimits>` for the
18715        // aggregate) would need a distinct absent branch here; the
18716        // typed identity-element approach resolves it structurally.
18717        let empty_slice: [ResourceLimits; 0] = [];
18718        assert_eq!(
18719            empty_slice
18720                .iter()
18721                .copied()
18722                .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18723            EMPTY_RESOURCE_LIMITS,
18724        );
18725    }
18726
18727    #[test]
18728    fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
18729        // Dual fold-identity pin — a slice of postures folded through
18730        // [`ResourceLimits::strictest`] with UNBOUNDED as the seed
18731        // produces the pointwise greatest lower bound across the slice
18732        // on every axis. Peer of the EMPTY-seeds-most_permissive-fold
18733        // pin one COMBINATOR axis over — the identity element for
18734        // meet is the join's ANNIHILATOR (`UNBOUNDED`), and the
18735        // identity element for join is the meet's annihilator
18736        // (`EMPTY`); the two together bind the bounded-lattice's
18737        // aggregation surface at BOTH poles.
18738        let postures: [ResourceLimits; 3] = [
18739            HAND_AUTHORED_MID_POSTURE,
18740            HAND_AUTHORED_OTHER_POSTURE,
18741            DEFAULT_RESOURCE_LIMITS,
18742        ];
18743        let met = postures
18744            .iter()
18745            .copied()
18746            .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
18747        // The greatest lower bound sits `leq` every operand.
18748        assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
18749        assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
18750        assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
18751        // Field-level pin — each axis is the pointwise `min` of the
18752        // three sources' values (the UNBOUNDED seed's `usize::MAX`
18753        // never wins the min against a concrete positive value).
18754        assert_eq!(
18755            met.max_expansion_depth,
18756            HAND_AUTHORED_MID_POSTURE
18757                .max_expansion_depth
18758                .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18759                .min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18760        );
18761        assert_eq!(
18762            met.max_cache_entries,
18763            HAND_AUTHORED_MID_POSTURE
18764                .max_cache_entries
18765                .min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18766                .min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18767        );
18768        assert_eq!(
18769            met.max_expansion_size,
18770            HAND_AUTHORED_MID_POSTURE
18771                .max_expansion_size
18772                .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18773                .min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18774        );
18775        assert_eq!(
18776            met.max_macro_body_size,
18777            HAND_AUTHORED_MID_POSTURE
18778                .max_macro_body_size
18779                .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18780                .min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18781        );
18782        assert_eq!(
18783            met.max_registered_macros,
18784            HAND_AUTHORED_MID_POSTURE
18785                .max_registered_macros
18786                .min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18787                .min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18788        );
18789        assert_eq!(
18790            met.max_macro_arity,
18791            HAND_AUTHORED_MID_POSTURE
18792                .max_macro_arity
18793                .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18794                .min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18795        );
18796        // Empty-slice edge — folding over an empty slice returns the
18797        // UNBOUNDED seed verbatim (the meet-identity as the null
18798        // aggregate). Symmetric to the EMPTY-seed empty-slice pin.
18799        let empty_slice: [ResourceLimits; 0] = [];
18800        assert_eq!(
18801            empty_slice
18802                .iter()
18803                .copied()
18804                .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18805            UNBOUNDED_RESOURCE_LIMITS,
18806        );
18807    }
18808
18809    #[test]
18810    fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
18811        // Null-aggregate pin — the N-ary meet over an empty slice is
18812        // the meet-identity element [`UNBOUNDED_RESOURCE_LIMITS`]. The
18813        // typed identity-element approach resolves the empty-input case
18814        // at the algebra layer; a wrapper-type approach
18815        // (`Option<ResourceLimits>` for the aggregate) would need a
18816        // distinct absent branch here, and a panic-on-empty seed-from-
18817        // -first-element approach would abort the caller.
18818        assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
18819    }
18820
18821    #[test]
18822    fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
18823        // Null-aggregate dual pin — the N-ary join over an empty slice
18824        // is the join-identity element [`EMPTY_RESOURCE_LIMITS`]. Peer
18825        // to `strictest_of_empty_slice` one COMBINATOR axis over on the
18826        // (N-ary meet, N-ary join) surface — the identity element for
18827        // join is the meet's ANNIHILATOR and vice versa; the two
18828        // together bind both bounded-lattice poles as null aggregates.
18829        assert_eq!(
18830            ResourceLimits::most_permissive_of(&[]),
18831            EMPTY_RESOURCE_LIMITS,
18832        );
18833    }
18834
18835    #[test]
18836    fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
18837        // Meet-identity absorption pin at N=1 — the N-ary meet of ONE
18838        // input is the input verbatim, since the meet-identity seed
18839        // (`UNBOUNDED_RESOURCE_LIMITS`) satisfies
18840        // `UNBOUNDED.strictest(a) == a` by the pointwise `min` behavior
18841        // against [`usize::MAX`]. This is the identity-law-at-N=1
18842        // pin: the fold's seed does not distort the 1-input case.
18843        assert_eq!(
18844            ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
18845            HAND_AUTHORED_MID_POSTURE,
18846        );
18847        assert_eq!(
18848            ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18849            HAND_AUTHORED_OTHER_POSTURE,
18850        );
18851        assert_eq!(
18852            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
18853            DEFAULT_RESOURCE_LIMITS,
18854        );
18855    }
18856
18857    #[test]
18858    fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
18859        // Join-identity absorption pin at N=1 — dual of
18860        // `strictest_of_single_element`, using the join-identity seed
18861        // (`EMPTY_RESOURCE_LIMITS`) whose per-axis `0` satisfies
18862        // `EMPTY.most_permissive(a) == a` against any non-zero value.
18863        assert_eq!(
18864            ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
18865            HAND_AUTHORED_MID_POSTURE,
18866        );
18867        assert_eq!(
18868            ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18869            HAND_AUTHORED_OTHER_POSTURE,
18870        );
18871        assert_eq!(
18872            ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
18873            DEFAULT_RESOURCE_LIMITS,
18874        );
18875    }
18876
18877    #[test]
18878    fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
18879        // N=2 reduction pin — the N-ary meet on a 2-element slice
18880        // reduces to the pairwise combinator, since
18881        // `UNBOUNDED.strictest(a).strictest(b) == a.strictest(b)`. The
18882        // strictest_of(&[a, b]) call MUST agree with a.strictest(b) on
18883        // every input pair the algebra distinguishes.
18884        assert_eq!(
18885            ResourceLimits::strictest_of(
18886                &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
18887            ),
18888            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18889        );
18890        assert_eq!(
18891            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
18892            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18893        );
18894    }
18895
18896    #[test]
18897    fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
18898        // N=2 reduction dual pin — the N-ary join on a 2-element slice
18899        // reduces to the pairwise combinator.
18900        assert_eq!(
18901            ResourceLimits::most_permissive_of(&[
18902                HAND_AUTHORED_MID_POSTURE,
18903                HAND_AUTHORED_OTHER_POSTURE,
18904            ]),
18905            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18906        );
18907        assert_eq!(
18908            ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
18909            DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18910        );
18911    }
18912
18913    #[test]
18914    fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
18915        // Method-agrees-with-fold pin — the sole behavioral claim the
18916        // N-ary aggregator makes is that it IS the inline
18917        // `.iter().copied().fold(UNBOUNDED, strictest)` cascade the
18918        // pre-lift caller composed at every consumption site. A drift
18919        // between the method's implementation and the inline fold
18920        // would break this equality on every non-empty slice.
18921        let postures: [ResourceLimits; 3] = [
18922            HAND_AUTHORED_MID_POSTURE,
18923            HAND_AUTHORED_OTHER_POSTURE,
18924            DEFAULT_RESOURCE_LIMITS,
18925        ];
18926        assert_eq!(
18927            ResourceLimits::strictest_of(&postures),
18928            postures
18929                .iter()
18930                .copied()
18931                .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18932        );
18933    }
18934
18935    #[test]
18936    fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
18937        // Method-agrees-with-fold dual pin.
18938        let postures: [ResourceLimits; 3] = [
18939            HAND_AUTHORED_MID_POSTURE,
18940            HAND_AUTHORED_OTHER_POSTURE,
18941            DEFAULT_RESOURCE_LIMITS,
18942        ];
18943        assert_eq!(
18944            ResourceLimits::most_permissive_of(&postures),
18945            postures
18946                .iter()
18947                .copied()
18948                .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18949        );
18950    }
18951
18952    #[test]
18953    fn resource_limits_strictest_of_is_order_independent() {
18954        // Order-independence pin — the fold inherits commutativity AND
18955        // associativity of the pairwise `strictest`, so any permutation
18956        // of the input slice yields the same aggregate. Reversal is
18957        // the maximal permutation for a 3-element input (indices
18958        // 0,1,2 → 2,1,0); an aggregator that leaked fold order would
18959        // discriminate here.
18960        let forward = ResourceLimits::strictest_of(&[
18961            HAND_AUTHORED_MID_POSTURE,
18962            HAND_AUTHORED_OTHER_POSTURE,
18963            DEFAULT_RESOURCE_LIMITS,
18964        ]);
18965        let reversed = ResourceLimits::strictest_of(&[
18966            DEFAULT_RESOURCE_LIMITS,
18967            HAND_AUTHORED_OTHER_POSTURE,
18968            HAND_AUTHORED_MID_POSTURE,
18969        ]);
18970        assert_eq!(forward, reversed);
18971    }
18972
18973    #[test]
18974    fn resource_limits_most_permissive_of_is_order_independent() {
18975        let forward = ResourceLimits::most_permissive_of(&[
18976            HAND_AUTHORED_MID_POSTURE,
18977            HAND_AUTHORED_OTHER_POSTURE,
18978            DEFAULT_RESOURCE_LIMITS,
18979        ]);
18980        let reversed = ResourceLimits::most_permissive_of(&[
18981            DEFAULT_RESOURCE_LIMITS,
18982            HAND_AUTHORED_OTHER_POSTURE,
18983            HAND_AUTHORED_MID_POSTURE,
18984        ]);
18985        assert_eq!(forward, reversed);
18986    }
18987
18988    #[test]
18989    fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
18990        // Compile-time-composition pin — `strictest_of` is `const fn`,
18991        // so a caller binds an N-ary aggregate to a `pub const` and the
18992        // fold evaluates at compile time. Every `DEFAULT_MAX_*` is
18993        // strictly less than `usize::MAX`, so the meet of `[DEFAULT,
18994        // UNBOUNDED]` picks DEFAULT on every axis — a preset-pair
18995        // identity already pinned pairwise, verified here through the
18996        // N-ary method's const-context evaluation. The N-ary const-fn
18997        // peer of the pairwise const-fn pins on
18998        // [`ResourceLimits::strictest`]; a regression that turned
18999        // `strictest_of` into a runtime `fn` would break the `const`
19000        // binding below at compile time.
19001        const AGGREGATED: ResourceLimits =
19002            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
19003        assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19004    }
19005
19006    #[test]
19007    fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
19008        // Compile-time-composition dual pin — every `DEFAULT_MAX_*` is
19009        // strictly greater than `0`, so the join of `[EMPTY, DEFAULT]`
19010        // picks DEFAULT on every axis.
19011        const AGGREGATED: ResourceLimits =
19012            ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
19013        assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19014    }
19015
19016    #[test]
19017    fn resource_limits_strictest_of_result_is_leq_every_operand() {
19018        // Lattice-relation pin — the N-ary meet sits `leq` every
19019        // operand. This is the N-ary extension of the pairwise
19020        // `strictest_is_leq_both_operands` pin; the meet aggregate
19021        // dominates from below (the greatest lower bound of the slice
19022        // sits at-or-below each input on every axis).
19023        let postures: [ResourceLimits; 3] = [
19024            HAND_AUTHORED_MID_POSTURE,
19025            HAND_AUTHORED_OTHER_POSTURE,
19026            DEFAULT_RESOURCE_LIMITS,
19027        ];
19028        let met = ResourceLimits::strictest_of(&postures);
19029        for p in postures {
19030            assert!(met.leq(p));
19031        }
19032    }
19033
19034    #[test]
19035    fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
19036        // Lattice-relation dual pin — every operand sits `leq` the
19037        // N-ary join. The join aggregate is the least upper bound of
19038        // the slice.
19039        let postures: [ResourceLimits; 3] = [
19040            HAND_AUTHORED_MID_POSTURE,
19041            HAND_AUTHORED_OTHER_POSTURE,
19042            DEFAULT_RESOURCE_LIMITS,
19043        ];
19044        let joined = ResourceLimits::most_permissive_of(&postures);
19045        for p in postures {
19046            assert!(p.leq(joined));
19047        }
19048    }
19049
19050    #[test]
19051    fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
19052        // Behavioral pin — an expander constructed from the N-ary meet
19053        // of a slice of postures MUST gate at the tightest ceiling
19054        // across the slice on every axis. Peer of
19055        // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19056        // one AGGREGATION-ARITY axis over: where the pairwise pin
19057        // composes exactly two postures, this pin composes THREE
19058        // through the N-ary aggregator, confirming the arity extension
19059        // preserves the "meet gates at tightest" behavioral contract.
19060        //
19061        // The slice tightens macro_arity to 1 (the tightest across the
19062        // three postures) via the middle operand; every other axis
19063        // stays at usize::MAX (the outer two operands supply
19064        // usize::MAX; the middle operand's struct-update literal
19065        // preserves usize::MAX everywhere except macro_arity).
19066        let tightened = ResourceLimits::strictest_of(&[
19067            UNBOUNDED_RESOURCE_LIMITS,
19068            ResourceLimits {
19069                max_macro_arity: 1,
19070                ..UNBOUNDED_RESOURCE_LIMITS
19071            },
19072            UNBOUNDED_RESOURCE_LIMITS,
19073        ]);
19074        assert_eq!(tightened.max_macro_arity, 1);
19075        assert_eq!(tightened.max_expansion_depth, usize::MAX);
19076        assert_eq!(tightened.max_cache_entries, usize::MAX);
19077        assert_eq!(tightened.max_expansion_size, usize::MAX);
19078        assert_eq!(tightened.max_macro_body_size, usize::MAX);
19079        assert_eq!(tightened.max_registered_macros, usize::MAX);
19080        let mut e = Expander::with_limits(tightened);
19081        let forms = read("(defmacro two (a b) `,a)").unwrap();
19082        let err = e.expand_program(forms).unwrap_err();
19083        assert!(
19084            matches!(
19085                err,
19086                LispError::MacroArityExceeded {
19087                    arity: 2,
19088                    limit: 1,
19089                    ..
19090                }
19091            ),
19092            "expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
19093        );
19094    }
19095
19096    #[test]
19097    fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
19098        // Behavioral pin — an expander constructed from the meet of
19099        // two postures MUST gate at the tighter of the two ceilings
19100        // on every axis. Build the meet of the ceiling-lifted preset
19101        // and a hand-authored posture whose `max_macro_arity` sits at
19102        // 1; the meet inherits the 1-slot arity ceiling (tighter than
19103        // [`usize::MAX`]) AND the [`usize::MAX`] ceiling on every
19104        // other axis (tighter than the hand-authored posture's small
19105        // values — no, wait: the OTHER posture's small values ARE
19106        // tighter. So the meet inherits 1 on arity and the OTHER
19107        // small values elsewhere.) Confirm the constructed expander
19108        // rejects a 2-arg macro registration with the arity gate.
19109        let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
19110            max_macro_arity: 1,
19111            ..UNBOUNDED_RESOURCE_LIMITS
19112        });
19113        assert_eq!(tightened.max_macro_arity, 1);
19114        // Other five axes remain at `usize::MAX` (both operands carry
19115        // `usize::MAX` on those, so the pointwise `min` is `usize::MAX`).
19116        assert_eq!(tightened.max_expansion_depth, usize::MAX);
19117        assert_eq!(tightened.max_cache_entries, usize::MAX);
19118        assert_eq!(tightened.max_expansion_size, usize::MAX);
19119        assert_eq!(tightened.max_macro_body_size, usize::MAX);
19120        assert_eq!(tightened.max_registered_macros, usize::MAX);
19121        let mut e = Expander::with_limits(tightened);
19122        let forms = read("(defmacro two (a b) `,a)").unwrap();
19123        let err = e.expand_program(forms).unwrap_err();
19124        assert!(
19125            matches!(
19126                err,
19127                LispError::MacroArityExceeded {
19128                    arity: 2,
19129                    limit: 1,
19130                    ..
19131                }
19132            ),
19133            "the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
19134        );
19135    }
19136
19137    // ── ResourceLimits::clamp — bounded-lattice bracket combinator ────
19138
19139    /// A tighter-than-MID lower-bracket witness — every axis strictly
19140    /// smaller than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the
19141    /// pointwise `max(mid, floor) == mid` on every axis (`floor.leq(mid)`
19142    /// holds structurally). Peer of `HAND_AUTHORED_CLAMP_CEILING` one
19143    /// BRACKET-BOUND axis over on the (floor, ceiling) bracket-witness
19144    /// pair.
19145    const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
19146        max_expansion_depth: 2,
19147        max_cache_entries: 3,
19148        max_expansion_size: 5,
19149        max_macro_body_size: 7,
19150        max_registered_macros: 11,
19151        max_macro_arity: 13,
19152    };
19153
19154    /// A looser-than-MID upper-bracket witness — every axis strictly larger
19155    /// than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the pointwise
19156    /// `min(mid, ceiling) == mid` on every axis (`mid.leq(ceiling)` holds
19157    /// structurally). Peer of `HAND_AUTHORED_CLAMP_FLOOR` one BRACKET-
19158    /// BOUND axis over. Together with `HAND_AUTHORED_CLAMP_FLOOR` the two
19159    /// bracket `HAND_AUTHORED_MID_POSTURE` strictly on every axis:
19160    /// `FLOOR.leq(MID) && MID.leq(CEILING) && FLOOR != MID && MID !=
19161    /// CEILING`.
19162    const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
19163        max_expansion_depth: 47,
19164        max_cache_entries: 53,
19165        max_expansion_size: 59,
19166        max_macro_body_size: 61,
19167        max_registered_macros: 67,
19168        max_macro_arity: 71,
19169    };
19170
19171    #[test]
19172    fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
19173        // In-range identity — when the input already sits within the
19174        // `[lower, upper]` bracket (`lower.leq(a) && a.leq(upper)` per-
19175        // axis), the clamp is the identity function: the pointwise
19176        // `max(a, lower) == a` (since `lower ≤ a` per-axis picks `a`),
19177        // then the pointwise `min(a, upper) == a` (since `a ≤ upper`
19178        // per-axis picks `a`). The primary invariant on the "input
19179        // already satisfies the bracket" arm of the three-arm bracket-
19180        // membership surface.
19181        //
19182        // Prerequisite pins — the two hand-authored bounds bracket
19183        // `MID` strictly on every axis (structural check that discharges
19184        // the "already in range" premise). A regression that inflated
19185        // `FLOOR` or deflated `CEILING` past `MID` would fail these
19186        // asserts before the clamp identity was tested, making the
19187        // premise's failure name itself rather than showing up as a
19188        // downstream misleading clamp mismatch.
19189        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19190        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19191        let clamped =
19192            HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19193        assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
19194    }
19195
19196    #[test]
19197    fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
19198        // Below-lower floor — when the input is tighter than the
19199        // bracket floor on every axis (`a.leq(lower)` per-axis), the
19200        // clamp lifts the input up to `lower`: the pointwise `max(a,
19201        // lower) == lower` (since `a ≤ lower` per-axis picks `lower`),
19202        // then the pointwise `min(lower, upper) == lower` (since
19203        // `lower ≤ upper` per-axis picks `lower`). Peer of
19204        // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19205        // one BRACKET-POSITION axis over on the (in-range, below-lower,
19206        // above-upper) three-arm bracket-membership surface.
19207        //
19208        // `EMPTY_RESOURCE_LIMITS` sits `leq` every posture (bounded-
19209        // lattice bottom), so `EMPTY.leq(FLOOR)` holds structurally
19210        // for any FLOOR; a clamp of `EMPTY` into `[FLOOR, CEILING]`
19211        // MUST return `FLOOR`.
19212        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
19213        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19214        let clamped =
19215            EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19216        assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
19217    }
19218
19219    #[test]
19220    fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
19221        // Above-upper ceiling — when the input is looser than the
19222        // bracket ceiling on every axis (`upper.leq(a)` per-axis), the
19223        // clamp lowers the input down to `upper`: the pointwise
19224        // `max(a, lower) == a` (since `lower ≤ upper ≤ a` per-axis picks
19225        // `a`), then the pointwise `min(a, upper) == upper` (since
19226        // `upper ≤ a` per-axis picks `upper`). Peer of the
19227        // below-lower arm one BRACKET-POSITION axis over on the three-
19228        // arm bracket-membership surface.
19229        //
19230        // `UNBOUNDED_RESOURCE_LIMITS` sits `geq` every posture (bounded-
19231        // lattice top), so `CEILING.leq(UNBOUNDED)` holds structurally
19232        // for any CEILING; a clamp of `UNBOUNDED` into `[FLOOR, CEILING]`
19233        // MUST return `CEILING`.
19234        assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
19235        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19236        let clamped =
19237            UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19238        assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
19239    }
19240
19241    #[test]
19242    fn resource_limits_clamp_result_sits_within_the_bracket() {
19243        // Bracket-membership contract — the DEFINING invariant on the
19244        // clamp primitive: for every well-formed bracket
19245        // (`lower.leq(upper)`) and every input `a`, the result sits
19246        // within the bracket `lower.leq(a.clamp(lower, upper)) &&
19247        // a.clamp(lower, upper).leq(upper)`. Exercised on ALL FOUR arm
19248        // positions of the bracket-membership surface via the four
19249        // known-preset inputs (below-lower → in-range → above-upper
19250        // → asymmetric-crossing) so a regression that inflated the
19251        // ceiling arm or deflated the floor arm exhausts all four
19252        // input positions and names the failing arm.
19253        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19254        let inputs = [
19255            EMPTY_RESOURCE_LIMITS,       // below-lower
19256            HAND_AUTHORED_MID_POSTURE,   // in-range
19257            UNBOUNDED_RESOURCE_LIMITS,   // above-upper
19258            HAND_AUTHORED_OTHER_POSTURE, // asymmetric (crosses the bracket)
19259        ];
19260        for a in inputs {
19261            let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19262            assert!(
19263                HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
19264                "clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
19265            );
19266            assert!(
19267                clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
19268                "clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
19269            );
19270        }
19271    }
19272
19273    #[test]
19274    fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
19275        // Extrema-bracket identity — clamping against the bounded-
19276        // lattice extrema (`EMPTY_RESOURCE_LIMITS` as the leq-minimum,
19277        // `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum) is the
19278        // identity function on every posture: every posture already
19279        // sits within the widest possible bracket. Cross-checks the
19280        // clamp primitive against the bounded-lattice identity elements
19281        // previously lifted; a regression that swapped the two extrema
19282        // in the impl would collapse every input to
19283        // `UNBOUNDED.strictest(EMPTY) == EMPTY` and fail this pin on
19284        // every non-EMPTY input.
19285        for a in [
19286            DEFAULT_RESOURCE_LIMITS,
19287            HAND_AUTHORED_MID_POSTURE,
19288            HAND_AUTHORED_OTHER_POSTURE,
19289            EMPTY_RESOURCE_LIMITS,
19290            UNBOUNDED_RESOURCE_LIMITS,
19291        ] {
19292            assert_eq!(
19293                a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19294                a,
19295                "clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
19296            );
19297        }
19298    }
19299
19300    #[test]
19301    fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
19302        // Degenerate-bracket collapse — a zero-width bracket
19303        // (`lower == upper`) collapses every input to the single point.
19304        // The pointwise `max(a, x)` lifts every axis to at least `x`,
19305        // then the pointwise `min(., x)` clamps every axis to exactly
19306        // `x` regardless of `a`'s per-axis position (since `x ≤ max(a,
19307        // x) ≤ max(x, x) == x` when `a.leq(x)` and `min(max(a, x), x)
19308        // == x` when `x.leq(a)`).
19309        for x in [
19310            HAND_AUTHORED_MID_POSTURE,
19311            HAND_AUTHORED_OTHER_POSTURE,
19312            DEFAULT_RESOURCE_LIMITS,
19313        ] {
19314            for a in [
19315                EMPTY_RESOURCE_LIMITS,
19316                HAND_AUTHORED_MID_POSTURE,
19317                HAND_AUTHORED_OTHER_POSTURE,
19318                UNBOUNDED_RESOURCE_LIMITS,
19319            ] {
19320                assert_eq!(
19321                    a.clamp(x, x),
19322                    x,
19323                    "zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
19324                );
19325            }
19326        }
19327    }
19328
19329    #[test]
19330    fn resource_limits_clamp_is_idempotent() {
19331        // Idempotence — re-applying the same bracket to an already-
19332        // clamped posture returns the same result. The clamp primitive
19333        // is idempotent because its output already sits within the
19334        // `[lower, upper]` bracket (pinned by
19335        // `resource_limits_clamp_result_sits_within_the_bracket`), so
19336        // the second application hits the in-range identity arm. Peer
19337        // of the idempotence pins on `strictest` and `most_permissive`
19338        // one ARITY axis over (2-input primitive → 3-input bracket) on
19339        // the lattice-algebra idempotence-law surface.
19340        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19341        for a in [
19342            EMPTY_RESOURCE_LIMITS,
19343            HAND_AUTHORED_MID_POSTURE,
19344            HAND_AUTHORED_OTHER_POSTURE,
19345            UNBOUNDED_RESOURCE_LIMITS,
19346        ] {
19347            let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19348            let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19349            assert_eq!(
19350                once, twice,
19351                "clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
19352            );
19353        }
19354    }
19355
19356    #[test]
19357    fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
19358        // Const-fn pin — the bracket combinator is evaluable in const
19359        // context, so a caller can pre-clamp a shipped preset against
19360        // an operator-supplied policy bracket at compile time. Sibling
19361        // of the const-fn composition pins on `strictest` /
19362        // `most_permissive` / `strictest_of` / `most_permissive_of`
19363        // one PRIMITIVE-KIND axis over: those four combinators compose
19364        // presets at const time, and this bracket combinator projects
19365        // a preset into a const-time bracket.
19366        //
19367        // A regression to a runtime `fn` here would fail the `pub
19368        // const` binding below at compile time — a stronger guarantee
19369        // than a runtime `assert!` because the const binding is
19370        // evaluated once at compile time, not per test invocation.
19371        const CLAMPED_DEFAULT: ResourceLimits =
19372            DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
19373        assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
19374
19375        // Compile-time bracket-membership pin — const `assert!` on the
19376        // lattice-relation contract discharges the "clamp result sits
19377        // within the bracket" invariant at compile time on the
19378        // (DEFAULT, EMPTY, UNBOUNDED) preset triple.
19379        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
19380        const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
19381    }
19382
19383    #[test]
19384    fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
19385        // Structural-equivalence pin — the sole behavioral claim the
19386        // clamp primitive makes is that it IS the
19387        // `self.most_permissive(lower).strictest(upper)` two-step
19388        // cascade the pre-lift caller composed. A drift between the
19389        // method and the inline cascade would break this equality on
19390        // every input; the pin exhausts the four bracket-position arms
19391        // of the input surface.
19392        for a in [
19393            EMPTY_RESOURCE_LIMITS,
19394            HAND_AUTHORED_MID_POSTURE,
19395            HAND_AUTHORED_OTHER_POSTURE,
19396            UNBOUNDED_RESOURCE_LIMITS,
19397        ] {
19398            let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19399            let via_cascade = a
19400                .most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
19401                .strictest(HAND_AUTHORED_CLAMP_CEILING);
19402            assert_eq!(via_method, via_cascade);
19403        }
19404    }
19405
19406    #[test]
19407    fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
19408        // Behavioral pin — an expander constructed from a preset
19409        // clamped into `[EMPTY, TIGHT_BRACKET]` MUST gate at the
19410        // bracket ceiling's arity axis. Confirms the clamp primitive
19411        // reaches through to the runtime gates that consume
19412        // `ResourceLimits`, so a future consumer that pre-clamps a
19413        // preset against a policy bracket inherits the bracket's
19414        // ceilings at every guard. Peer of
19415        // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19416        // one COMBINATOR-ARITY axis over on the (pairwise-meet,
19417        // N-ary-meet, bracket) combinator-arity face.
19418        //
19419        // The bracket ceiling tightens `max_macro_arity` to 1; every
19420        // other axis stays at `usize::MAX` (the CEILING preset carries
19421        // `usize::MAX` on those, and the input UNBOUNDED preset also
19422        // carries `usize::MAX` on every axis — the clamp's `strictest`
19423        // arm picks `usize::MAX` from either side).
19424        let bracket_ceiling = ResourceLimits {
19425            max_macro_arity: 1,
19426            ..UNBOUNDED_RESOURCE_LIMITS
19427        };
19428        let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19429        assert_eq!(clamped.max_macro_arity, 1);
19430        assert_eq!(clamped.max_expansion_depth, usize::MAX);
19431        assert_eq!(clamped.max_cache_entries, usize::MAX);
19432        assert_eq!(clamped.max_expansion_size, usize::MAX);
19433        assert_eq!(clamped.max_macro_body_size, usize::MAX);
19434        assert_eq!(clamped.max_registered_macros, usize::MAX);
19435        let mut e = Expander::with_limits(clamped);
19436        let forms = read("(defmacro two (a b) `,a)").unwrap();
19437        let err = e.expand_program(forms).unwrap_err();
19438        assert!(
19439            matches!(
19440                err,
19441                LispError::MacroArityExceeded {
19442                    arity: 2,
19443                    limit: 1,
19444                    ..
19445                }
19446            ),
19447            "the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
19448        );
19449    }
19450
19451    // ── ResourceLimits::within — bracket-membership predicate ─────────
19452
19453    #[test]
19454    fn resource_limits_within_of_posture_in_range_is_true() {
19455        // In-range identity — when the input already sits within the
19456        // `[lower, upper]` bracket on every axis (`lower.leq(a) &&
19457        // a.leq(upper)`), the predicate returns `true`. Peer of
19458        // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19459        // one PRIMITIVE-KIND axis over on the (combinator, predicate)
19460        // face of the bracket-primitive surface.
19461        //
19462        // Prerequisite pins — the two hand-authored bounds bracket
19463        // `MID` strictly on every axis, structurally discharging the
19464        // "already in range" premise.
19465        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19466        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19467        assert!(HAND_AUTHORED_MID_POSTURE
19468            .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19469    }
19470
19471    #[test]
19472    fn resource_limits_within_of_posture_below_lower_is_false() {
19473        // Below-lower rejection — when the input is strictly tighter
19474        // than the bracket floor on some axis (`!lower.leq(a)`), the
19475        // predicate returns `false`. `EMPTY_RESOURCE_LIMITS` has every
19476        // axis at `0`, strictly below `HAND_AUTHORED_CLAMP_FLOOR`'s
19477        // per-axis ceilings; so `FLOOR.leq(EMPTY)` fails on every axis,
19478        // making the conjunction reject regardless of `EMPTY`'s relation
19479        // to `CEILING`. Peer of
19480        // `resource_limits_clamp_of_posture_below_lower_returns_lower`
19481        // one PRIMITIVE-KIND axis over.
19482        assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
19483        assert!(
19484            !EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
19485        );
19486    }
19487
19488    #[test]
19489    fn resource_limits_within_of_posture_above_upper_is_false() {
19490        // Above-upper rejection — when the input is strictly looser
19491        // than the bracket ceiling on some axis (`!a.leq(upper)`), the
19492        // predicate returns `false`. `UNBOUNDED_RESOURCE_LIMITS` has
19493        // every axis at `usize::MAX`, strictly above
19494        // `HAND_AUTHORED_CLAMP_CEILING`'s per-axis ceilings; so
19495        // `UNBOUNDED.leq(CEILING)` fails on every axis. Peer of
19496        // `resource_limits_clamp_of_posture_above_upper_returns_upper`
19497        // one PRIMITIVE-KIND axis over.
19498        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
19499        assert!(!UNBOUNDED_RESOURCE_LIMITS
19500            .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19501    }
19502
19503    #[test]
19504    fn resource_limits_within_with_lattice_extrema_is_true() {
19505        // Extrema-bracket identity — every posture sits within the
19506        // widest possible bracket `[EMPTY_RESOURCE_LIMITS,
19507        // UNBOUNDED_RESOURCE_LIMITS]`. Cross-checks the predicate
19508        // against the bounded-lattice identity elements (`EMPTY` is
19509        // the leq-minimum, `UNBOUNDED` is the leq-maximum). Peer of
19510        // `resource_limits_clamp_with_lattice_extrema_returns_the_input`
19511        // one PRIMITIVE-KIND axis over — the predicate view of the
19512        // "clamp is the identity" property.
19513        for a in [
19514            DEFAULT_RESOURCE_LIMITS,
19515            HAND_AUTHORED_MID_POSTURE,
19516            HAND_AUTHORED_OTHER_POSTURE,
19517            EMPTY_RESOURCE_LIMITS,
19518            UNBOUNDED_RESOURCE_LIMITS,
19519        ] {
19520            assert!(
19521                a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19522                "every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
19523            );
19524        }
19525    }
19526
19527    #[test]
19528    fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
19529        // Degenerate-bracket collapse — a zero-width bracket admits
19530        // only the single point at the bounds. `a.within(x, x)` holds
19531        // iff `x.leq(a) && a.leq(x)` iff `a == x` by antisymmetry of
19532        // `leq`. Peer of `resource_limits_clamp_with_equal_bounds_returns_the_bound`
19533        // one PRIMITIVE-KIND axis over: the clamp collapses to `x`
19534        // unconditionally, and the predicate distinguishes the "was
19535        // already equal" case from the "would have been clamped" case.
19536        let bounds = [
19537            HAND_AUTHORED_MID_POSTURE,
19538            HAND_AUTHORED_OTHER_POSTURE,
19539            DEFAULT_RESOURCE_LIMITS,
19540            EMPTY_RESOURCE_LIMITS,
19541            UNBOUNDED_RESOURCE_LIMITS,
19542        ];
19543        for x in bounds {
19544            for a in bounds {
19545                assert_eq!(
19546                    a.within(x, x),
19547                    a == x,
19548                    "a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
19549                );
19550            }
19551        }
19552    }
19553
19554    #[test]
19555    fn resource_limits_within_of_self_is_reflexive() {
19556        // Reflexive-bracket identity — every posture sits within the
19557        // zero-width bracket at itself, since `a.leq(a)` holds by
19558        // reflexivity of `leq`. The reflexive-bracket peer of the
19559        // reflexive-leq pin (`resource_limits_leq_is_reflexive`) one
19560        // ARITY axis over (pairwise → three-input).
19561        for a in [
19562            EMPTY_RESOURCE_LIMITS,
19563            DEFAULT_RESOURCE_LIMITS,
19564            HAND_AUTHORED_MID_POSTURE,
19565            HAND_AUTHORED_OTHER_POSTURE,
19566            UNBOUNDED_RESOURCE_LIMITS,
19567        ] {
19568            assert!(
19569                a.within(a, a),
19570                "a.within(a, a) must hold by reflexivity of leq; a={a:?}",
19571            );
19572        }
19573    }
19574
19575    #[test]
19576    fn resource_limits_within_agrees_with_clamp_fixed_point() {
19577        // Clamp fixed-point theorem — `a.within(lower, upper) ⇔
19578        // a.clamp(lower, upper) == a`. The CANONICAL cross-check axiom
19579        // binding the `within` predicate to the `clamp` combinator,
19580        // analogous to the meet-agreement (`a.leq(b) ⇔ a.strictest(b)
19581        // == a`, pinned by `resource_limits_leq_agrees_with_meet`) and
19582        // join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`,
19583        // pinned by `resource_limits_leq_agrees_with_join`) axioms
19584        // binding the `leq` relation to its combinator peers.
19585        //
19586        // Exercised on all four bracket-position arms of the input
19587        // surface (below-lower / in-range / above-upper / asymmetric-
19588        // crossing) so a regression that broke either direction of the
19589        // fixed-point equivalence exhausts every position and names the
19590        // failing arm.
19591        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19592        for a in [
19593            EMPTY_RESOURCE_LIMITS,       // below-lower — clamp != a, within false
19594            HAND_AUTHORED_MID_POSTURE,   // in-range   — clamp == a, within true
19595            UNBOUNDED_RESOURCE_LIMITS,   // above-upper — clamp != a, within false
19596            HAND_AUTHORED_OTHER_POSTURE, // asymmetric — clamp != a, within false
19597        ] {
19598            let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19599            let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19600            let clamp_fixed = clamped == a;
19601            assert_eq!(
19602                within_holds, clamp_fixed,
19603                "within(lower, upper) must agree with clamp fixed-point; \
19604                 a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
19605            );
19606        }
19607    }
19608
19609    #[test]
19610    fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
19611        // Structural-equivalence pin — the sole behavioral claim the
19612        // predicate makes is that it IS the `lower.leq(a) && a.leq(upper)`
19613        // two-primitive conjunction the pre-lift caller composed. A drift
19614        // between the method and the inline conjunction would break this
19615        // equality on some input; the pin exhausts the four bracket-
19616        // position arms of the input surface. Peer of
19617        // `resource_limits_clamp_agrees_with_direct_two_step_cascade`
19618        // one PRIMITIVE-KIND axis over (combinator ↔ predicate).
19619        for a in [
19620            EMPTY_RESOURCE_LIMITS,
19621            HAND_AUTHORED_MID_POSTURE,
19622            HAND_AUTHORED_OTHER_POSTURE,
19623            UNBOUNDED_RESOURCE_LIMITS,
19624        ] {
19625            let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19626            let via_conjunction =
19627                HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
19628            assert_eq!(via_method, via_conjunction, "a={a:?}");
19629        }
19630    }
19631
19632    #[test]
19633    fn resource_limits_within_composes_at_compile_time_via_const_fn() {
19634        // Const-fn pin — the bracket predicate is evaluable in const
19635        // context, so a caller can pin a preset-bracket-membership
19636        // identity at compile time. Sibling of the const-fn evaluability
19637        // pins on `leq` and `clamp` one PRIMITIVE-KIND axis over: the
19638        // relation and combinator both evaluate at const time, and the
19639        // predicate closing the (combinator, predicate) row does too.
19640        //
19641        // A regression to a runtime `fn` here would fail the `const _:
19642        // () = assert!(...)` bindings below at compile time.
19643        const _: () = assert!(
19644            DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
19645        );
19646        const _: () = assert!(
19647            DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
19648        );
19649        const _: () = assert!(
19650            !UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
19651        );
19652    }
19653
19654    #[test]
19655    fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
19656        // Behavioral pin — a caller that CHECKS bracket-membership
19657        // before constructing an expander can safely reach through to
19658        // the runtime gates: if `within` reports true, `clamp` is a
19659        // no-op (by the fixed-point theorem), so the expander's
19660        // ceilings are the input posture's own. Peer of
19661        // `expander_built_at_clamped_preset_gates_at_the_bracket_ceiling`
19662        // one PRIMITIVE-KIND axis over — the predicate view of the
19663        // "clamp reaches through to the runtime gates" property.
19664        //
19665        // The candidate sits at the bracket ceiling (arity=1, other
19666        // axes at usize::MAX), so it MUST sit within the [EMPTY,
19667        // bracket_ceiling] bracket — the maximal within-membership arm
19668        // of the fixed-point theorem. Every other axis at usize::MAX
19669        // clears the register-time gates, so the arity gate at 1 is
19670        // the one that trips on a 2-parameter macro registration.
19671        let bracket_ceiling = ResourceLimits {
19672            max_macro_arity: 1,
19673            ..UNBOUNDED_RESOURCE_LIMITS
19674        };
19675        let candidate = ResourceLimits {
19676            max_macro_arity: 1,
19677            ..UNBOUNDED_RESOURCE_LIMITS
19678        };
19679        assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
19680        // Since candidate sits within the bracket, clamp is the
19681        // identity; the expander inherits candidate's ceilings verbatim.
19682        let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19683        assert_eq!(clamped, candidate);
19684        let mut e = Expander::with_limits(clamped);
19685        let forms = read("(defmacro two (a b) `,a)").unwrap();
19686        let err = e.expand_program(forms).unwrap_err();
19687        assert!(
19688            matches!(
19689                err,
19690                LispError::MacroArityExceeded {
19691                    arity: 2,
19692                    limit: 1,
19693                    ..
19694                }
19695            ),
19696            "the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
19697        );
19698    }
19699
19700    // ── ResourceLimits::is_lower_bound_of / is_upper_bound_of ─────────
19701    //   N-ary boolean-conjunction peers of the pairwise `leq` relation,
19702    //   closing the (combinator, predicate) row on the N-ary-aggregation
19703    //   face of the lattice-algebra surface. See the docstring on
19704    //   `ResourceLimits::is_lower_bound_of` for the (pairwise, N-ary,
19705    //   bracket) × (combinator, predicate) placement.
19706
19707    #[test]
19708    fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
19709        // Empty-conjunction identity — every posture is trivially a
19710        // lower bound of the empty set. Peer of `strictest_of(&[]) ==
19711        // UNBOUNDED_RESOURCE_LIMITS`'s empty-slice identity one
19712        // PRIMITIVE-KIND axis over: the empty slice aggregates to the
19713        // identity element of the underlying operation (`true` for
19714        // boolean conjunction, `UNBOUNDED` for pointwise `min`).
19715        assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
19716        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19717        assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19718        assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19719    }
19720
19721    #[test]
19722    fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
19723        // Empty-conjunction dual — every posture is trivially an upper
19724        // bound of the empty set. Peer of
19725        // `is_lower_bound_of_empty_slice_is_vacuously_true` one
19726        // COMBINATOR-DIRECTION axis over, AND peer of
19727        // `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
19728        // slice identity one PRIMITIVE-KIND axis over.
19729        assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
19730        assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19731        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19732        assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19733    }
19734
19735    #[test]
19736    fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
19737        // 1-input identity — the singleton predicate reduces to the
19738        // pairwise partial-order relation. The N-ary predicate on a
19739        // 1-element slice is the pairwise `leq` verbatim, mirroring
19740        // `strictest_of(&[a]) == a`'s reduction on the combinator side.
19741        let cases: [(ResourceLimits, ResourceLimits); 6] = [
19742            (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19743            (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19744            (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19745            (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19746            (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19747            (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19748        ];
19749        for (a, b) in cases {
19750            assert_eq!(
19751                a.is_lower_bound_of(&[b]),
19752                a.leq(b),
19753                "1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
19754            );
19755        }
19756    }
19757
19758    #[test]
19759    fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
19760        // 1-input dual identity — the singleton upper-bound predicate
19761        // is the pairwise `leq` with the direction flipped
19762        // (`b.leq(a)`), since `self` sits ABOVE the operand rather than
19763        // BELOW it.
19764        let cases: [(ResourceLimits, ResourceLimits); 6] = [
19765            (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19766            (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19767            (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19768            (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19769            (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19770            (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19771        ];
19772        for (a, b) in cases {
19773            assert_eq!(
19774                a.is_upper_bound_of(&[b]),
19775                b.leq(a),
19776                "1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
19777            );
19778        }
19779    }
19780
19781    #[test]
19782    fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
19783        // Definitional bridge — the N-ary meet is always a lower bound
19784        // of the slice it aggregates. Pins the link between the N-ary
19785        // COMBINATOR (`strictest_of`) and the N-ary PREDICATE
19786        // (`is_lower_bound_of`): the meet is an ELEMENT of the lower-
19787        // bound set the predicate CHARACTERIZES.
19788        let postures: [ResourceLimits; 3] = [
19789            HAND_AUTHORED_MID_POSTURE,
19790            HAND_AUTHORED_OTHER_POSTURE,
19791            DEFAULT_RESOURCE_LIMITS,
19792        ];
19793        let met = ResourceLimits::strictest_of(&postures);
19794        assert!(met.is_lower_bound_of(&postures));
19795    }
19796
19797    #[test]
19798    fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
19799        // Dual definitional bridge — the N-ary join is always an upper
19800        // bound of the slice it aggregates.
19801        let postures: [ResourceLimits; 3] = [
19802            HAND_AUTHORED_MID_POSTURE,
19803            HAND_AUTHORED_OTHER_POSTURE,
19804            DEFAULT_RESOURCE_LIMITS,
19805        ];
19806        let joined = ResourceLimits::most_permissive_of(&postures);
19807        assert!(joined.is_upper_bound_of(&postures));
19808    }
19809
19810    #[test]
19811    fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
19812        // Universal-bottom witness — the lattice bottom is a common
19813        // lower bound of every slice, since `EMPTY.leq(a) == true` for
19814        // every posture by the lattice-bottom axiom (`0 ≤ x` per-axis).
19815        // The bounded-lattice specialization of the general "the meet-
19816        // identity is a lower bound of every set" theorem.
19817        let postures: [ResourceLimits; 4] = [
19818            HAND_AUTHORED_MID_POSTURE,
19819            HAND_AUTHORED_OTHER_POSTURE,
19820            DEFAULT_RESOURCE_LIMITS,
19821            UNBOUNDED_RESOURCE_LIMITS,
19822        ];
19823        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
19824        // The three preset-carried postures each individually — the
19825        // universal-bottom witness holds on the singleton slice too.
19826        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19827        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19828        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19829    }
19830
19831    #[test]
19832    fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
19833        // Universal-top witness — the lattice top is a common upper
19834        // bound of every slice, since `a.leq(UNBOUNDED) == true` for
19835        // every posture by the lattice-top axiom (`x ≤ usize::MAX`
19836        // per-axis).
19837        let postures: [ResourceLimits; 4] = [
19838            HAND_AUTHORED_MID_POSTURE,
19839            HAND_AUTHORED_OTHER_POSTURE,
19840            DEFAULT_RESOURCE_LIMITS,
19841            EMPTY_RESOURCE_LIMITS,
19842        ];
19843        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
19844        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19845        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19846        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19847    }
19848
19849    #[test]
19850    fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
19851        // Any-operand short-circuit — if `self` fails `leq` against ANY
19852        // operand in the slice, the N-ary conjunction rejects. The dual
19853        // of the "meet is a lower bound" witness on the false side:
19854        // moving `self` STRICTLY ABOVE an operand on any axis costs the
19855        // lower-bound property.
19856        //
19857        // MID sits above the join (element-wise) on some axes and below
19858        // on others, so MID is NEITHER a lower bound of {MID, OTHER}
19859        // (fails against OTHER's smaller-axis values) NOR is OTHER a
19860        // lower bound of {MID, OTHER} (fails against MID's smaller-axis
19861        // values).
19862        assert!(!HAND_AUTHORED_MID_POSTURE
19863            .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19864        assert!(!HAND_AUTHORED_OTHER_POSTURE
19865            .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19866        // A UNIVERSAL rejection — the lattice top is a lower bound
19867        // ONLY of the empty set OR of slices whose every element equals
19868        // UNBOUNDED. Given a strictly-smaller preset in the slice, the
19869        // top's per-axis usize::MAX loses `leq` on every axis whose
19870        // preset value is strictly smaller.
19871        assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19872        assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19873    }
19874
19875    #[test]
19876    fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
19877        // Dual any-operand short-circuit — if ANY operand fails `leq`
19878        // against `self`, the N-ary conjunction rejects.
19879        assert!(!HAND_AUTHORED_MID_POSTURE
19880            .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19881        assert!(!HAND_AUTHORED_OTHER_POSTURE
19882            .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19883        // The bottom cannot bound anything from above except the empty
19884        // set OR a slice of EMPTY-only postures.
19885        assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19886        assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19887    }
19888
19889    #[test]
19890    fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
19891        // Method-vs-scaffold cross-check — the lifted N-ary predicate
19892        // agrees with the pre-lift two-primitive
19893        // `postures.iter().all(|p| self.leq(*p))` scaffolding on every
19894        // preset-carried posture across the shipped bounded-lattice
19895        // extrema AND the two hand-authored asymmetric witnesses. Pins
19896        // the lift is a semantic no-op — post-lift the consumer routes
19897        // through ONE name instead of composing the conjunction inline,
19898        // but the two paths compute the same boolean.
19899        let candidates: [ResourceLimits; 5] = [
19900            EMPTY_RESOURCE_LIMITS,
19901            DEFAULT_RESOURCE_LIMITS,
19902            UNBOUNDED_RESOURCE_LIMITS,
19903            HAND_AUTHORED_MID_POSTURE,
19904            HAND_AUTHORED_OTHER_POSTURE,
19905        ];
19906        let slice: [ResourceLimits; 3] = [
19907            HAND_AUTHORED_MID_POSTURE,
19908            HAND_AUTHORED_OTHER_POSTURE,
19909            DEFAULT_RESOURCE_LIMITS,
19910        ];
19911        for c in candidates {
19912            let direct = slice.iter().all(|p| c.leq(*p));
19913            assert_eq!(
19914                c.is_lower_bound_of(&slice),
19915                direct,
19916                "is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
19917            );
19918        }
19919    }
19920
19921    #[test]
19922    fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
19923        // Dual method-vs-scaffold cross-check.
19924        let candidates: [ResourceLimits; 5] = [
19925            EMPTY_RESOURCE_LIMITS,
19926            DEFAULT_RESOURCE_LIMITS,
19927            UNBOUNDED_RESOURCE_LIMITS,
19928            HAND_AUTHORED_MID_POSTURE,
19929            HAND_AUTHORED_OTHER_POSTURE,
19930        ];
19931        let slice: [ResourceLimits; 3] = [
19932            HAND_AUTHORED_MID_POSTURE,
19933            HAND_AUTHORED_OTHER_POSTURE,
19934            DEFAULT_RESOURCE_LIMITS,
19935        ];
19936        for c in candidates {
19937            let direct = slice.iter().all(|p| p.leq(c));
19938            assert_eq!(
19939                c.is_upper_bound_of(&slice),
19940                direct,
19941                "is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
19942            );
19943        }
19944    }
19945
19946    #[test]
19947    fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
19948        // Const-fn pin — the N-ary lower-bound predicate is evaluable
19949        // in const context, so a caller can pin an N-ary-bound-
19950        // membership identity at compile time. Sibling of the const-fn
19951        // evaluability pins on `leq` and `within` one PRIMITIVE-KIND
19952        // axis over, AND sibling of `strictest_of`'s const-fn pin one
19953        // COMBINATOR-KIND axis over.
19954        //
19955        // A regression to a runtime `fn` here would fail the `const _:
19956        // () = assert!(...)` bindings below at compile time.
19957        const _: () = assert!(EMPTY_RESOURCE_LIMITS
19958            .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
19959        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19960        const _: () =
19961            assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19962    }
19963
19964    #[test]
19965    fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
19966        // Const-fn dual pin.
19967        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
19968            .is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
19969        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19970        const _: () =
19971            assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19972    }
19973
19974    /// The full canonical preset roster the strict-order pins sweep
19975    /// through — the (bottom, middle, top) bounded-lattice preset
19976    /// triple plus the two hand-authored incomparable asymmetric
19977    /// postures. Every strict-order law below quantifies over this
19978    /// slice, so a regression that breaks strict `<` on any preset
19979    /// pair discriminates at ONE named fixture rather than at a
19980    /// per-test ad-hoc literal.
19981    const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
19982        EMPTY_RESOURCE_LIMITS,
19983        DEFAULT_RESOURCE_LIMITS,
19984        UNBOUNDED_RESOURCE_LIMITS,
19985        HAND_AUTHORED_MID_POSTURE,
19986        HAND_AUTHORED_OTHER_POSTURE,
19987    ];
19988
19989    #[test]
19990    fn resource_limits_lt_is_irreflexive() {
19991        // Strict partial-order law — `a < a == false` for every
19992        // posture. Irreflexivity is the strict-relation axiom peer of
19993        // reflexivity on `leq` one STRICTNESS axis over on the
19994        // partial-order axiom surface; a regression that dropped the
19995        // antisymmetric leg (`!other.leq(self)`) — leaving the body at
19996        // `self.leq(other)` — would fire here on every preset since
19997        // `leq` is reflexive.
19998        for &a in STRICT_ORDER_ROSTER {
19999            assert!(!a.lt(a), "irreflexivity failed on {a:?}");
20000        }
20001    }
20002
20003    #[test]
20004    fn resource_limits_lt_is_asymmetric() {
20005        // Strict partial-order law — `a < b ⇒ ¬(b < a)` for every
20006        // pair. The two directions of the strict relation cannot both
20007        // hold, since `a.lt(b)` implies `!b.leq(a)` and `b.lt(a)`
20008        // requires `b.leq(a)`. Sweeps every ordered pair in the
20009        // canonical preset roster; discriminates a regression that
20010        // dropped the antisymmetric leg in either direction.
20011        for &a in STRICT_ORDER_ROSTER {
20012            for &b in STRICT_ORDER_ROSTER {
20013                if a.lt(b) {
20014                    assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
20015                }
20016            }
20017        }
20018    }
20019
20020    #[test]
20021    fn resource_limits_lt_is_transitive() {
20022        // Strict partial-order law — `a < b ∧ b < c ⇒ a < c`. Sweeps
20023        // every ordered triple in the canonical preset roster; each
20024        // consequence is checked whenever both antecedents hold. On
20025        // the (bottom, middle, top) diagonal the canonical witness
20026        // `EMPTY < DEFAULT < UNBOUNDED` composes to
20027        // `EMPTY < UNBOUNDED`, exercising the transitivity chain on
20028        // the bounded-lattice diagonal explicitly; the sweep pins the
20029        // same law on every other transitively-related triple in the
20030        // roster.
20031        for &a in STRICT_ORDER_ROSTER {
20032            for &b in STRICT_ORDER_ROSTER {
20033                for &c in STRICT_ORDER_ROSTER {
20034                    if a.lt(b) && b.lt(c) {
20035                        assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20036                    }
20037                }
20038            }
20039        }
20040    }
20041
20042    #[test]
20043    fn resource_limits_lt_refines_leq() {
20044        // Refinement axiom — `a < b ⇒ a ≤ b`. The strict relation is
20045        // a refinement of the non-strict one; a witness of strict `<`
20046        // is also a witness of `≤`. Sweeps every ordered pair in the
20047        // canonical preset roster.
20048        for &a in STRICT_ORDER_ROSTER {
20049            for &b in STRICT_ORDER_ROSTER {
20050                if a.lt(b) {
20051                    assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
20052                }
20053            }
20054        }
20055    }
20056
20057    #[test]
20058    fn resource_limits_lt_agrees_with_leq_minus_equality() {
20059        // Antisymmetry consequence — on ANY antisymmetric partial
20060        // order (which the pointwise `≤` on `ResourceLimits` is),
20061        // `a < b ⇔ a ≤ b ∧ a ≠ b`. Cross-checks the shipped
20062        // antisymmetric-leq encoding against the leq-and-not-equal
20063        // encoding across the full 5×5 preset matrix; a regression to
20064        // a non-antisymmetric preorder encoding would fire here.
20065        for &a in STRICT_ORDER_ROSTER {
20066            for &b in STRICT_ORDER_ROSTER {
20067                let shipped = a.lt(b);
20068                let alt = a.leq(b) && a != b;
20069                assert_eq!(
20070                    shipped, alt,
20071                    "encoding cross-check failed on ({a:?}, {b:?})",
20072                );
20073            }
20074        }
20075    }
20076
20077    #[test]
20078    fn resource_limits_lt_of_bottom_diagonal_pinned() {
20079        // Concrete-preset pin — the strict chain across the (bottom,
20080        // middle, top) preset triple on the bounded-lattice diagonal.
20081        // Every `DEFAULT_MAX_*` module constant is a concrete positive
20082        // value strictly less than [`usize::MAX`] and strictly greater
20083        // than `0`, so on every axis the three endpoints are strictly
20084        // ordered; the six-axis pointwise conjunction lifts the
20085        // per-axis strict chain to the posture-level strict chain.
20086        //
20087        // Peer of `resource_limits_leq_of_default_and_unbounded_is_a_
20088        // strict_order` one STRICTNESS axis over — where the non-
20089        // strict pin asserted "leq holds one way and not the other"
20090        // via two `leq` calls, this pin asserts the strict-chain
20091        // conclusion via one `lt` call per link and pinpoints the
20092        // three-preset chain on the bounded-lattice diagonal.
20093        assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20094        assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20095        assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20096    }
20097
20098    #[test]
20099    fn resource_limits_lt_rejects_incomparable_postures() {
20100        // Partial-order non-total pin — the two hand-authored
20101        // asymmetric postures have MID smaller on three axes and OTHER
20102        // smaller on the other three, so neither `leq` direction
20103        // holds; strict `<` therefore fails in BOTH directions. The
20104        // pin discriminates a regression that promoted strict `<`
20105        // from partial to total (e.g. treating incomparable pairs as
20106        // strictly `<` in some canonical direction), and pairs with
20107        // `resource_limits_leq_is_not_total_on_asymmetric_postures`
20108        // one STRICTNESS axis over.
20109        assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
20110        assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
20111    }
20112
20113    #[test]
20114    fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
20115        // Direct-encoding cross-check — `a.lt(b) == (a.leq(b) &&
20116        // !b.leq(a))` at every pair by definition. Sibling of the
20117        // `leq_minus_equality` pin one ENCODING axis over: that pin
20118        // routes through the equality-based encoding (an
20119        // antisymmetry consequence), this pin routes through the
20120        // definitional antisymmetric-leq encoding. A regression that
20121        // shipped a different (e.g. axis-wise strict) encoding of
20122        // `lt` diverges from this cross-check on the incomparable
20123        // asymmetric-preset pair.
20124        for &a in STRICT_ORDER_ROSTER {
20125            for &b in STRICT_ORDER_ROSTER {
20126                let shipped = a.lt(b);
20127                let direct = a.leq(b) && !b.leq(a);
20128                assert_eq!(
20129                    shipped, direct,
20130                    "direct-encoding cross-check failed on ({a:?}, {b:?})",
20131                );
20132            }
20133        }
20134    }
20135
20136    #[test]
20137    fn resource_limits_gt_is_dual_of_lt() {
20138        // Direction-flip pin — `a.gt(b) == b.lt(a)` at every pair. The
20139        // shipped `gt` body delegates to `lt` verbatim so this
20140        // agreement holds definitionally, but the pin discriminates a
20141        // regression that re-implemented `gt` independently and
20142        // dropped the direction flip. Sweeps the full 5×5 preset
20143        // matrix so incomparable + strictly-ordered + equal pairs
20144        // are all exercised.
20145        for &a in STRICT_ORDER_ROSTER {
20146            for &b in STRICT_ORDER_ROSTER {
20147                assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
20148            }
20149        }
20150    }
20151
20152    #[test]
20153    fn resource_limits_gt_of_top_diagonal_pinned() {
20154        // Concrete-preset pin — the strict chain across the (top,
20155        // middle, bottom) preset triple on the bounded-lattice
20156        // diagonal, walked in the "above" direction. Peer of
20157        // `resource_limits_lt_of_bottom_diagonal_pinned` one
20158        // DIRECTION axis over.
20159        assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20160        assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20161        assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20162    }
20163
20164    #[test]
20165    fn resource_limits_gt_is_irreflexive() {
20166        // Strict partial-order law dual — `a > a == false` for every
20167        // posture. Inherits from the irreflexivity of `lt` via the
20168        // shipped `gt` body's delegation, but pinned explicitly so a
20169        // future non-delegating re-implementation of `gt` still has
20170        // its irreflexivity gated at the type-level test cohort.
20171        for &a in STRICT_ORDER_ROSTER {
20172            assert!(!a.gt(a), "irreflexivity failed on {a:?}");
20173        }
20174    }
20175
20176    #[test]
20177    fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
20178        // Const-fn pin — the strict `<` relation is evaluable in
20179        // const context, so a caller can pin a preset-strict-order
20180        // identity at compile time. Sibling of the const-fn
20181        // evaluability pin on `leq` one STRICTNESS axis over.
20182        //
20183        // A regression to a runtime `fn` here would fail the `const
20184        // _: () = assert!(...)` bindings below at compile time.
20185        const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20186        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20187        const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20188        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20189        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
20190    }
20191
20192    #[test]
20193    fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
20194        // Const-fn dual pin — sibling of `lt`'s const-fn pin one
20195        // DIRECTION axis over.
20196        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20197        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20198        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20199        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20200        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
20201    }
20202
20203    #[test]
20204    fn resource_limits_geq_is_dual_of_leq() {
20205        // Direction-flip pin — `a.geq(b) == b.leq(a)` at every pair.
20206        // The shipped `geq` body delegates to `leq` verbatim so this
20207        // agreement holds definitionally, but the pin discriminates a
20208        // regression that re-implemented `geq` independently and
20209        // dropped the direction flip. Sweeps the full 5×5 preset
20210        // matrix so incomparable + strictly-ordered + equal pairs are
20211        // all exercised. Peer of `resource_limits_gt_is_dual_of_lt`
20212        // one STRICTNESS axis over on the (below, above) × (non-strict,
20213        // strict) 2×2 pairwise partial-order face.
20214        for &a in STRICT_ORDER_ROSTER {
20215            for &b in STRICT_ORDER_ROSTER {
20216                assert_eq!(
20217                    a.geq(b),
20218                    b.leq(a),
20219                    "geq/leq duality failed on ({a:?}, {b:?})",
20220                );
20221            }
20222        }
20223    }
20224
20225    #[test]
20226    fn resource_limits_geq_is_reflexive() {
20227        // Non-strict partial-order law — `a ≥ a == true` for every
20228        // posture. Reflexivity is the non-strict-relation axiom peer of
20229        // irreflexivity on `gt` one STRICTNESS axis over on the
20230        // partial-order axiom surface; a regression that hardened the
20231        // delegation into a strict `!other.leq(self)` would fire here
20232        // on every preset since the reflexive pair collapses.
20233        for &a in STRICT_ORDER_ROSTER {
20234            assert!(a.geq(a), "reflexivity failed on {a:?}");
20235        }
20236    }
20237
20238    #[test]
20239    fn resource_limits_geq_is_antisymmetric() {
20240        // Non-strict partial-order law — `a ≥ b ∧ b ≥ a ⇒ a == b`.
20241        // Antisymmetry is inherited from `leq` via the shipped `geq`
20242        // body's `other.leq(self)` delegation (which combined with
20243        // `b.geq(a)` — i.e. `a.leq(b)` — gives `leq` antisymmetry on
20244        // the pair). Sweeps the full 5×5 preset matrix; the pin fires
20245        // on any regression that let two DISTINCT postures satisfy
20246        // `geq` in both directions.
20247        for &a in STRICT_ORDER_ROSTER {
20248            for &b in STRICT_ORDER_ROSTER {
20249                if a.geq(b) && b.geq(a) {
20250                    assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
20251                }
20252            }
20253        }
20254    }
20255
20256    #[test]
20257    fn resource_limits_geq_is_transitive() {
20258        // Non-strict partial-order law — `a ≥ b ∧ b ≥ c ⇒ a ≥ c`.
20259        // Inherits from the transitivity of `leq` via the shipped
20260        // delegation; sweeps every ordered triple in the canonical
20261        // preset roster. Peer of `resource_limits_lt_is_transitive`
20262        // one STRICTNESS axis over and `resource_limits_leq_is_
20263        // transitive` one DIRECTION axis over.
20264        for &a in STRICT_ORDER_ROSTER {
20265            for &b in STRICT_ORDER_ROSTER {
20266                for &c in STRICT_ORDER_ROSTER {
20267                    if a.geq(b) && b.geq(c) {
20268                        assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20269                    }
20270                }
20271            }
20272        }
20273    }
20274
20275    #[test]
20276    fn resource_limits_geq_refines_gt() {
20277        // Refinement axiom peer — `a > b ⇒ a ≥ b`. The strict
20278        // relation is a refinement of the non-strict one in the
20279        // "above" direction, exactly as `lt` refines `leq` in the
20280        // "below" direction (pinned by
20281        // `resource_limits_lt_refines_leq` one DIRECTION axis over).
20282        // A witness of strict `>` is also a witness of `≥`.
20283        for &a in STRICT_ORDER_ROSTER {
20284            for &b in STRICT_ORDER_ROSTER {
20285                if a.gt(b) {
20286                    assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
20287                }
20288            }
20289        }
20290    }
20291
20292    #[test]
20293    fn resource_limits_geq_agrees_with_gt_plus_equality() {
20294        // Antisymmetry consequence peer — on ANY antisymmetric partial
20295        // order, `a ≥ b ⇔ a > b ∨ a == b`. Cross-checks the shipped
20296        // delegation against the strict-or-equal encoding across the
20297        // full 5×5 preset matrix; a regression that shipped a non-
20298        // antisymmetric preorder encoding for `geq` fires here. Peer
20299        // of `resource_limits_lt_agrees_with_leq_minus_equality` one
20300        // DIRECTION-AND-STRICTNESS axis over.
20301        for &a in STRICT_ORDER_ROSTER {
20302            for &b in STRICT_ORDER_ROSTER {
20303                let shipped = a.geq(b);
20304                let alt = a.gt(b) || a == b;
20305                assert_eq!(
20306                    shipped, alt,
20307                    "encoding cross-check failed on ({a:?}, {b:?})",
20308                );
20309            }
20310        }
20311    }
20312
20313    #[test]
20314    fn resource_limits_geq_of_top_diagonal_pinned() {
20315        // Concrete-preset pin — the non-strict chain across the (top,
20316        // middle, bottom) preset triple on the bounded-lattice
20317        // diagonal, walked in the "above" direction. Peer of
20318        // `resource_limits_gt_of_top_diagonal_pinned` one STRICTNESS
20319        // axis over: the non-strict pin also holds at the reflexive
20320        // diagonal, so every posture sits above ITSELF via `geq` in
20321        // addition to sitting above the strictly-tighter presets.
20322        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20323        assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20324        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20325        assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20326        assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20327        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20328    }
20329
20330    #[test]
20331    fn resource_limits_geq_rejects_incomparable_postures() {
20332        // Partial-order non-total pin — the two hand-authored
20333        // asymmetric postures have MID smaller on three axes and OTHER
20334        // smaller on the other three, so neither `leq` direction holds
20335        // and therefore neither `geq` direction holds. The pin
20336        // discriminates a regression that promoted `geq` from partial
20337        // to total, and pairs with
20338        // `resource_limits_lt_rejects_incomparable_postures` one
20339        // STRICTNESS-AND-DIRECTION axis over.
20340        assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
20341        assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
20342    }
20343
20344    #[test]
20345    fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
20346        // Arity-reduction pin — `a.geq(b) == a.is_upper_bound_of(&[b])`
20347        // at every pair. The pairwise "above" relation is exactly the
20348        // 1-input specialization of the N-ary upper-bound predicate;
20349        // the pin cross-checks that the two named entries agree on
20350        // the single-element case, which discriminates a regression
20351        // that drifted one from the other. Peer of the single-element
20352        // identity docstring on `is_upper_bound_of` one ARITY axis
20353        // over.
20354        for &a in STRICT_ORDER_ROSTER {
20355            for &b in STRICT_ORDER_ROSTER {
20356                assert_eq!(
20357                    a.geq(b),
20358                    a.is_upper_bound_of(&[b]),
20359                    "arity-reduction failed on ({a:?}, {b:?})",
20360                );
20361            }
20362        }
20363    }
20364
20365    #[test]
20366    fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
20367        // Const-fn pin — the non-strict `≥` relation is evaluable in
20368        // const context, so a caller can pin a preset-non-strict-order
20369        // identity at compile time. Sibling of the const-fn evaluability
20370        // pins on `leq` one DIRECTION axis over AND on `gt` one
20371        // STRICTNESS axis over.
20372        //
20373        // A regression to a runtime `fn` here would fail the `const _:
20374        // () = assert!(...)` bindings below at compile time.
20375        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20376        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20377        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20378        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20379        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20380    }
20381
20382    #[test]
20383    fn resource_limits_is_incomparable_is_irreflexive() {
20384        // Antichain-corner reflexivity pin — `a.is_incomparable(a) ==
20385        // false` on every posture. Every posture is COMPARABLE to
20386        // itself via `leq`'s reflexivity (`a.leq(a) == true`), so the
20387        // conjunction's `!self.leq(other)` leg forces the verdict to
20388        // `false` at every diagonal input. Discriminates a regression
20389        // that dropped the `!` on `self.leq(other)` (leaving the body
20390        // at `self.leq(other) && !self.geq(other)`) — that would fold
20391        // the diagonal to `false` on the reflexive `leq` arm but
20392        // silently flip the antichain corner on non-diagonal cells.
20393        for &a in STRICT_ORDER_ROSTER {
20394            assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
20395        }
20396    }
20397
20398    #[test]
20399    fn resource_limits_is_incomparable_is_symmetric() {
20400        // Antichain-corner symmetry pin — `a.is_incomparable(b) ==
20401        // b.is_incomparable(a)` on every pair. The composition
20402        // `!self.leq(other) && !self.geq(other)` folds through the
20403        // `self.leq(other) == other.geq(self)` dual identity on `geq`
20404        // and the conjunction's commutativity into an order-
20405        // independent projection on its two inputs. Sweeps every
20406        // ordered pair in the canonical preset roster;
20407        // discriminates a regression that broke either leg's dual
20408        // identity — one direction would fold to `false` on cells the
20409        // other still folded to `true`.
20410        for &a in STRICT_ORDER_ROSTER {
20411            for &b in STRICT_ORDER_ROSTER {
20412                assert_eq!(
20413                    a.is_incomparable(b),
20414                    b.is_incomparable(a),
20415                    "symmetry failed on ({a:?}, {b:?})",
20416                );
20417            }
20418        }
20419    }
20420
20421    #[test]
20422    fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
20423        // De Morgan dual pin — `a.is_incomparable(b) == !(a.leq(b) ||
20424        // a.geq(b))` on every pair. The antichain corner is the SET-
20425        // COMPLEMENT of the union of the two pointwise-domination
20426        // directions on the (comparable, incomparable) two-cell
20427        // partition of the ordered pair × verdict surface. Sweeps
20428        // every ordered pair in the canonical preset roster;
20429        // discriminates a regression that drifted the antichain
20430        // characterization off the substrate's `!leq && !geq`
20431        // composition (e.g., a shortcut through `!self.leq(other) ||
20432        // !self.geq(other)` — the OR-form falsely reports
20433        // incomparability on non-equal comparable pairs).
20434        for &a in STRICT_ORDER_ROSTER {
20435            for &b in STRICT_ORDER_ROSTER {
20436                let via_named = a.is_incomparable(b);
20437                let via_composition = !(a.leq(b) || a.geq(b));
20438                assert_eq!(
20439                    via_named, via_composition,
20440                    "De Morgan dual failed on ({a:?}, {b:?})",
20441                );
20442            }
20443        }
20444    }
20445
20446    #[test]
20447    fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
20448        // Two-cell partition pin — on every ordered pair EITHER
20449        // (a.leq(b) || a.geq(b)) OR a.is_incomparable(b), and NEVER
20450        // both. The (comparable, incomparable) partition of the
20451        // ordered pair × verdict surface is exhaustive-and-disjoint —
20452        // a substrate-level EXCLUSIVITY THEOREM the projection pins
20453        // once. Sweeps every ordered pair in the canonical preset
20454        // roster; discriminates a regression that let the two cells
20455        // overlap (e.g., an implementation that reported the antichain
20456        // corner as `true` on the comparable diagonal).
20457        for &a in STRICT_ORDER_ROSTER {
20458            for &b in STRICT_ORDER_ROSTER {
20459                let comparable = a.leq(b) || a.geq(b);
20460                let incomparable = a.is_incomparable(b);
20461                assert!(
20462                    comparable != incomparable,
20463                    "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20464                );
20465            }
20466        }
20467    }
20468
20469    #[test]
20470    fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
20471        // Bottom-pole absorption pin — `a.is_incomparable(
20472        // EMPTY_RESOURCE_LIMITS) == false` on every posture `a`.
20473        // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20474        // `EMPTY.leq(a)` holds on every axis (usize zero is the
20475        // minimum), which gives `a.geq(EMPTY)` through the `geq`
20476        // dual identity, which falsifies the `!self.geq(other)` leg.
20477        // Sweeps the canonical preset roster; pairs with the top-
20478        // pole absorption sibling one POLE axis over.
20479        for &a in STRICT_ORDER_ROSTER {
20480            assert!(
20481                !a.is_incomparable(EMPTY_RESOURCE_LIMITS),
20482                "bottom-pole absorption failed on {a:?}",
20483            );
20484            assert!(
20485                !EMPTY_RESOURCE_LIMITS.is_incomparable(a),
20486                "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20487            );
20488        }
20489    }
20490
20491    #[test]
20492    fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
20493        // Top-pole absorption pin — `a.is_incomparable(
20494        // UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a`.
20495        // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20496        // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20497        // usize::MAX` unconditionally), which falsifies the
20498        // `!self.leq(other)` leg. Sweeps the canonical preset
20499        // roster; pairs with the bottom-pole absorption sibling one
20500        // POLE axis over.
20501        for &a in STRICT_ORDER_ROSTER {
20502            assert!(
20503                !a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
20504                "top-pole absorption failed on {a:?}",
20505            );
20506            assert!(
20507                !UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
20508                "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20509            );
20510        }
20511    }
20512
20513    #[test]
20514    fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
20515        // Antichain load-bearing arm — the two hand-authored
20516        // asymmetric postures sit on distinct branches (each is
20517        // smaller on three axes and larger on the other three), so
20518        // neither pointwise-domination direction closes and the
20519        // antichain verdict HOLDS in both orderings. Pairs with
20520        // `resource_limits_geq_rejects_incomparable_postures` +
20521        // `resource_limits_lt_rejects_incomparable_postures` one COVER
20522        // axis over: those pin the two comparable-direction predicates
20523        // FALSIFY on this pair; THIS pin confirms the antichain
20524        // verdict HOLDS on the SAME pair — the two are the negation-
20525        // paired sides of the (comparable, incomparable) two-cell
20526        // partition.
20527        assert!(
20528            HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
20529            "antichain arm failed on (MID, OTHER)",
20530        );
20531        assert!(
20532            HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
20533            "antichain arm failed on flipped (OTHER, MID)",
20534        );
20535    }
20536
20537    #[test]
20538    fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
20539        // Shipped preset comparability pin — every ordered pair drawn
20540        // from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset triangle
20541        // is COMPARABLE (in both directions the pairwise partial-order
20542        // relation gives a verdict). The three shipped presets form a
20543        // TOTALLY ORDERED chain on the pointwise partial-order (EMPTY
20544        // <= DEFAULT <= UNBOUNDED on every field), so the antichain
20545        // corner MUST fold to `false` on every ordered pair drawn from
20546        // them. Discriminates a regression that promoted a shipped
20547        // preset off the total-order chain by drifting ONE field
20548        // silently.
20549        const TRIANGLE: &[ResourceLimits] = &[
20550            EMPTY_RESOURCE_LIMITS,
20551            DEFAULT_RESOURCE_LIMITS,
20552            UNBOUNDED_RESOURCE_LIMITS,
20553        ];
20554        for &a in TRIANGLE {
20555            for &b in TRIANGLE {
20556                assert!(
20557                    !a.is_incomparable(b),
20558                    "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20559                );
20560            }
20561        }
20562    }
20563
20564    #[test]
20565    fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
20566        // Const-fn pin — the antichain characterization is evaluable
20567        // in const context, so a caller can pin an antichain-
20568        // disagreement identity at compile time. Sibling of the
20569        // const-fn evaluability pins on `leq` / `geq` / `lt` / `gt`
20570        // one COVER axis over on the pairwise-relation surface.
20571        //
20572        // A regression to a runtime `fn` here would fail the `const _:
20573        // () = assert!(...)` bindings below at compile time.
20574        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20575        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20576        const _: () =
20577            assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20578        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20579        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20580        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20581        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20582    }
20583
20584    // ── ResourceLimits::is_comparable ─────────────────────────────────
20585    //
20586    // The COMPARABLE CORNER on the pairwise-relation surface — the
20587    // DIRECT POSITIVE DUAL of `is_incomparable` one COVER-COMPLEMENT
20588    // axis over on the (comparable, incomparable) two-cell partition
20589    // of the ordered pair × verdict surface. See `ResourceLimits::is_comparable`
20590    // for the algebra; the pins below fix each documented property
20591    // exactly once so a regression on any leg fires on a NAMED test
20592    // rather than an ad-hoc downstream break.
20593
20594    #[test]
20595    fn resource_limits_is_comparable_is_reflexive() {
20596        // Comparable-corner reflexivity pin — `a.is_comparable(a) ==
20597        // true` on every posture. Every posture is COMPARABLE to
20598        // itself via `leq`'s reflexivity (`a.leq(a) == true`), so
20599        // the disjunction's `self.leq(other)` leg holds at every
20600        // diagonal input. Sibling of `is_incomparable`'s
20601        // irreflexivity one CELL axis over on the (comparable,
20602        // incomparable) partition — the diagonal MUST fold to `true`
20603        // here iff it folds to `false` there.
20604        for &a in STRICT_ORDER_ROSTER {
20605            assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
20606        }
20607    }
20608
20609    #[test]
20610    fn resource_limits_is_comparable_is_symmetric() {
20611        // Comparable-corner symmetry pin — `a.is_comparable(b) ==
20612        // b.is_comparable(a)` on every pair. The composition
20613        // `self.leq(other) || self.geq(other)` folds through the
20614        // `self.leq(other) == other.geq(self)` dual identity on
20615        // `geq` and the disjunction's commutativity into an order-
20616        // independent projection on its two inputs. Both cells of an
20617        // EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface
20618        // must themselves be symmetric — the two verdicts flip in
20619        // lockstep on argument swap.
20620        for &a in STRICT_ORDER_ROSTER {
20621            for &b in STRICT_ORDER_ROSTER {
20622                assert_eq!(
20623                    a.is_comparable(b),
20624                    b.is_comparable(a),
20625                    "symmetry failed on ({a:?}, {b:?})",
20626                );
20627            }
20628        }
20629    }
20630
20631    #[test]
20632    fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
20633        // De Morgan dual pin — `a.is_comparable(b) ==
20634        // !a.is_incomparable(b)` on every pair. The comparable corner
20635        // is the SET-COMPLEMENT of the antichain corner on the
20636        // (comparable, incomparable) two-cell partition of the
20637        // ordered pair × verdict surface. The DIRECT dual of
20638        // `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
20639        // one CELL axis over: that test pins the antichain corner as
20640        // the negation of the `leq || geq` disjunction; THIS test
20641        // pins the comparability corner as the negation of the
20642        // antichain projection. Together the two pin BOTH SIDES of
20643        // the partition, closing it as a substrate-level
20644        // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM.
20645        for &a in STRICT_ORDER_ROSTER {
20646            for &b in STRICT_ORDER_ROSTER {
20647                let via_named = a.is_comparable(b);
20648                let via_negated_dual = !a.is_incomparable(b);
20649                assert_eq!(
20650                    via_named, via_negated_dual,
20651                    "De Morgan dual failed on ({a:?}, {b:?})",
20652                );
20653            }
20654        }
20655    }
20656
20657    #[test]
20658    fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
20659        // Direct composition pin — `a.is_comparable(b) == a.leq(b) ||
20660        // a.geq(b)` on every pair. Discriminates a regression that
20661        // drifted the comparability body off the substrate's `leq ||
20662        // geq` composition (e.g., a shortcut through `a.leq(b) &&
20663        // a.geq(b)` — the AND-form falsely reports comparability
20664        // only on the equality diagonal, dropping the strict-order
20665        // cells). Anchors the two-primitive delegation as a
20666        // substrate-level identity.
20667        for &a in STRICT_ORDER_ROSTER {
20668            for &b in STRICT_ORDER_ROSTER {
20669                let via_named = a.is_comparable(b);
20670                let via_composition = a.leq(b) || a.geq(b);
20671                assert_eq!(
20672                    via_named, via_composition,
20673                    "leq||geq composition failed on ({a:?}, {b:?})",
20674                );
20675            }
20676        }
20677    }
20678
20679    #[test]
20680    fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
20681        // Bottom-pole absorption pin — `a.is_comparable(
20682        // EMPTY_RESOURCE_LIMITS) == true` on every posture `a`.
20683        // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20684        // `EMPTY.leq(a)` holds on every axis (`usize` zero is the
20685        // minimum), which gives `a.geq(EMPTY)` through the `geq`
20686        // dual identity, which satisfies the `self.geq(other)` leg.
20687        // The DIRECT COMPLEMENT of
20688        // `resource_limits_is_incomparable_folds_false_at_the_bottom_pole`
20689        // one CELL axis over on the SAME preset roster — where the
20690        // antichain verdict FOLDS FALSE at the bottom pole, the
20691        // comparability verdict FOLDS TRUE.
20692        for &a in STRICT_ORDER_ROSTER {
20693            assert!(
20694                a.is_comparable(EMPTY_RESOURCE_LIMITS),
20695                "bottom-pole absorption failed on {a:?}",
20696            );
20697            assert!(
20698                EMPTY_RESOURCE_LIMITS.is_comparable(a),
20699                "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20700            );
20701        }
20702    }
20703
20704    #[test]
20705    fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
20706        // Top-pole absorption pin — `a.is_comparable(
20707        // UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a`.
20708        // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20709        // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20710        // usize::MAX` unconditionally), which satisfies the
20711        // `self.leq(other)` leg. The DIRECT COMPLEMENT of
20712        // `resource_limits_is_incomparable_folds_false_at_the_top_pole`
20713        // one CELL axis over on the SAME preset roster.
20714        for &a in STRICT_ORDER_ROSTER {
20715            assert!(
20716                a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
20717                "top-pole absorption failed on {a:?}",
20718            );
20719            assert!(
20720                UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
20721                "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20722            );
20723        }
20724    }
20725
20726    #[test]
20727    fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
20728        // Antichain load-bearing arm — the two hand-authored
20729        // asymmetric postures sit on distinct branches (each is
20730        // smaller on three axes and larger on the other three), so
20731        // NEITHER pointwise-domination direction closes and the
20732        // comparability verdict FALSIFIES in both orderings. The
20733        // DIRECT NEGATION of
20734        // `resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair`
20735        // one CELL axis over on the SAME hand-authored pair — the
20736        // two are the negation-paired sides of the (comparable,
20737        // incomparable) two-cell partition, and pinning BOTH cells'
20738        // verdicts on the SAME load-bearing antichain pair closes
20739        // the partition as a substrate-level EXCLUSIVITY THEOREM.
20740        assert!(
20741            !HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
20742            "antichain arm failed on (MID, OTHER)",
20743        );
20744        assert!(
20745            !HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
20746            "antichain arm failed on flipped (OTHER, MID)",
20747        );
20748    }
20749
20750    #[test]
20751    fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
20752        // Shipped preset comparability pin — every ordered pair
20753        // drawn from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset
20754        // triangle is COMPARABLE (in at least one direction the
20755        // pairwise partial-order relation gives a verdict). The
20756        // three shipped presets form a TOTALLY ORDERED chain on the
20757        // pointwise partial-order (EMPTY <= DEFAULT <= UNBOUNDED on
20758        // every field), so the comparability corner MUST fold to
20759        // `true` on every ordered pair drawn from them. The DIRECT
20760        // COMPLEMENT of
20761        // `resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle`
20762        // one CELL axis over on the SAME triangle. Discriminates a
20763        // regression that promoted a shipped preset off the total-
20764        // order chain by drifting ONE field silently.
20765        const TRIANGLE: &[ResourceLimits] = &[
20766            EMPTY_RESOURCE_LIMITS,
20767            DEFAULT_RESOURCE_LIMITS,
20768            UNBOUNDED_RESOURCE_LIMITS,
20769        ];
20770        for &a in TRIANGLE {
20771            for &b in TRIANGLE {
20772                assert!(
20773                    a.is_comparable(b),
20774                    "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20775                );
20776            }
20777        }
20778    }
20779
20780    #[test]
20781    fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
20782        // Exhaustive-and-disjoint partition pin — on every ordered
20783        // pair EXACTLY ONE of `a.is_comparable(b)` OR
20784        // `a.is_incomparable(b)` holds. The (comparable,
20785        // incomparable) partition of the ordered pair × verdict
20786        // surface is exhaustive-and-disjoint — a substrate-level
20787        // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM the two projections
20788        // together pin. Complements
20789        // `resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable`
20790        // one CELL axis over: that test pins the partition against
20791        // the raw `a.leq(b) || a.geq(b)` composition; THIS test pins
20792        // the partition against the NAMED comparability projection —
20793        // a regression that drifted `is_comparable` off the
20794        // composition without breaking the composition-based test
20795        // would still fire here.
20796        for &a in STRICT_ORDER_ROSTER {
20797            for &b in STRICT_ORDER_ROSTER {
20798                let comparable = a.is_comparable(b);
20799                let incomparable = a.is_incomparable(b);
20800                assert!(
20801                    comparable != incomparable,
20802                    "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20803                );
20804            }
20805        }
20806    }
20807
20808    #[test]
20809    fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
20810        // Const-fn pin — the comparability characterization is
20811        // evaluable in const context, so a caller can pin a
20812        // comparability-agreement identity at compile time. Sibling
20813        // of the const-fn evaluability pins on `leq` / `geq` / `lt`
20814        // / `gt` / `is_incomparable` one COVER axis over on the
20815        // pairwise-relation surface.
20816        //
20817        // A regression to a runtime `fn` here would fail the `const _:
20818        // () = assert!(...)` bindings below at compile time.
20819        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20820        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20821        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20822        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20823        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20824        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20825        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20826    }
20827
20828    // ── ResourceLimits::partial_cmp ───────────────────────────────────
20829    //
20830    // The `Option<Ordering>` FULL-VERDICT corner on the pairwise-
20831    // relation surface — the JOINT LIFT of the (comparable, incomparable)
20832    // two-cell partition together with the ordering-direction refinement
20833    // on the comparable arm. See `ResourceLimits::partial_cmp` for the
20834    // algebra; the pins below fix each documented property exactly once
20835    // so a regression on any leg fires on a NAMED test rather than an
20836    // ad-hoc downstream break.
20837
20838    #[test]
20839    fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
20840        // Diagonal-equality pin — `a.partial_cmp(a) ==
20841        // Some(Ordering::Equal)` on every posture. The `leq`
20842        // reflexivity closes both arms of the equality-guarding
20843        // conjunction, so the diagonal cell of the (Less, Equal,
20844        // Greater, None) 4-tile partition pins to the Equal tile.
20845        // Sibling of `is_comparable_is_reflexive` one PROJECTION
20846        // axis over — where the projection folds TRUE at the
20847        // diagonal, THIS method refines the verdict to `Equal`.
20848        for &a in STRICT_ORDER_ROSTER {
20849            assert_eq!(
20850                a.partial_cmp(a),
20851                Some(Ordering::Equal),
20852                "diagonal-equality failed on {a:?}",
20853            );
20854        }
20855    }
20856
20857    #[test]
20858    fn resource_limits_partial_cmp_reverses_under_argument_swap() {
20859        // Antisymmetric-swap pin — `a.partial_cmp(b) ==
20860        // b.partial_cmp(a).map(Ordering::reverse)` on every pair.
20861        // Swapping the argument pair FLIPS the ordering-direction
20862        // verdict (`Less <-> Greater`), fixes the diagonal-equality
20863        // arm, and fixes the antichain arm. Discriminates
20864        // `is_incomparable` / `is_comparable` (both SYMMETRIC on
20865        // argument swap) one POSTURE axis over — the FULL verdict
20866        // carries directional content that inverts under swap where
20867        // the projections do not.
20868        for &a in STRICT_ORDER_ROSTER {
20869            for &b in STRICT_ORDER_ROSTER {
20870                let forward = a.partial_cmp(b);
20871                let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
20872                assert_eq!(
20873                    forward, swapped_reversed,
20874                    "swap-reverse failed on ({a:?}, {b:?})",
20875                );
20876            }
20877        }
20878    }
20879
20880    #[test]
20881    fn resource_limits_partial_cmp_none_iff_incomparable() {
20882        // Antichain-arm identity pin — `a.partial_cmp(b).is_none()
20883        // == a.is_incomparable(b)` on every pair. The `None` arm of
20884        // the return coincides EXACTLY with the antichain-corner
20885        // projection. Anchors the joint-lift claim: THIS method
20886        // subsumes `is_incomparable` at its `is_none()` projection
20887        // without drifting the antichain characterization.
20888        for &a in STRICT_ORDER_ROSTER {
20889            for &b in STRICT_ORDER_ROSTER {
20890                let via_partial_cmp = a.partial_cmp(b).is_none();
20891                let via_is_incomparable = a.is_incomparable(b);
20892                assert_eq!(
20893                    via_partial_cmp, via_is_incomparable,
20894                    "none-iff-incomparable failed on ({a:?}, {b:?})",
20895                );
20896            }
20897        }
20898    }
20899
20900    #[test]
20901    fn resource_limits_partial_cmp_some_iff_comparable() {
20902        // Comparability-arm identity pin — `a.partial_cmp(b).is_some()
20903        // == a.is_comparable(b)` on every pair. The `Some(_)` arm of
20904        // the return coincides EXACTLY with the comparability-corner
20905        // projection. The DUAL of the antichain-arm pin one CELL axis
20906        // over on the (comparable, incomparable) partition — together
20907        // the two pins close the JOINT LIFT of both cells at THIS
20908        // method.
20909        for &a in STRICT_ORDER_ROSTER {
20910            for &b in STRICT_ORDER_ROSTER {
20911                let via_partial_cmp = a.partial_cmp(b).is_some();
20912                let via_is_comparable = a.is_comparable(b);
20913                assert_eq!(
20914                    via_partial_cmp, via_is_comparable,
20915                    "some-iff-comparable failed on ({a:?}, {b:?})",
20916                );
20917            }
20918        }
20919    }
20920
20921    #[test]
20922    fn resource_limits_partial_cmp_less_iff_lt() {
20923        // Strict-less agreement pin — `a.partial_cmp(b) ==
20924        // Some(Ordering::Less)` iff `a.lt(b)`. The strict-less arm of
20925        // the (Less, Equal, Greater, None) 4-tile partition binds to
20926        // the named `lt` companion primitive at exactly one dispatch
20927        // site. A regression that drifted the arm off `lt` (e.g., to
20928        // `leq` — including the equality diagonal) would fire here.
20929        for &a in STRICT_ORDER_ROSTER {
20930            for &b in STRICT_ORDER_ROSTER {
20931                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
20932                let via_lt = a.lt(b);
20933                assert_eq!(
20934                    via_partial_cmp, via_lt,
20935                    "less-iff-lt failed on ({a:?}, {b:?})",
20936                );
20937            }
20938        }
20939    }
20940
20941    #[test]
20942    fn resource_limits_partial_cmp_greater_iff_gt() {
20943        // Strict-greater agreement pin — `a.partial_cmp(b) ==
20944        // Some(Ordering::Greater)` iff `a.gt(b)`. Dual of the strict-
20945        // less agreement pin one DIRECTION axis over on the (Less,
20946        // Greater) strict-order pair — a regression that dropped the
20947        // asymmetry between the two arms (e.g., collapsed both to
20948        // `Less`) would fire here.
20949        for &a in STRICT_ORDER_ROSTER {
20950            for &b in STRICT_ORDER_ROSTER {
20951                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
20952                let via_gt = a.gt(b);
20953                assert_eq!(
20954                    via_partial_cmp, via_gt,
20955                    "greater-iff-gt failed on ({a:?}, {b:?})",
20956                );
20957            }
20958        }
20959    }
20960
20961    #[test]
20962    fn resource_limits_partial_cmp_equal_iff_eq() {
20963        // Equality agreement pin — `a.partial_cmp(b) ==
20964        // Some(Ordering::Equal)` iff `a == b`. The equality arm of the
20965        // 4-tile partition binds through the pointwise antisymmetric-
20966        // equality characterization (`a.leq(b) && b.leq(a) → a == b`,
20967        // pinned by `resource_limits_leq_is_antisymmetric`) to the
20968        // `PartialEq::eq` companion. Discriminates a regression that
20969        // drifted the Equal arm off the antisymmetric conjunction.
20970        for &a in STRICT_ORDER_ROSTER {
20971            for &b in STRICT_ORDER_ROSTER {
20972                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
20973                let via_eq = a == b;
20974                assert_eq!(
20975                    via_partial_cmp, via_eq,
20976                    "equal-iff-eq failed on ({a:?}, {b:?})",
20977                );
20978            }
20979        }
20980    }
20981
20982    #[test]
20983    fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
20984        // Bottom-to-top-pole ordering pin — every strictly-ascending
20985        // pair on the shipped preset chain (EMPTY <= DEFAULT <=
20986        // UNBOUNDED) folds to `Some(Ordering::Less)`. The three
20987        // shipped presets form a TOTALLY ORDERED chain on the
20988        // pointwise partial-order (every DEFAULT_MAX_* is a concrete
20989        // positive value strictly less than usize::MAX), so the
20990        // strict-less arm fires on every ordered pair drawn in
20991        // ascending direction.
20992        assert_eq!(
20993            EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20994            Some(Ordering::Less),
20995        );
20996        assert_eq!(
20997            DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20998            Some(Ordering::Less),
20999        );
21000        assert_eq!(
21001            EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21002            Some(Ordering::Less),
21003        );
21004    }
21005
21006    #[test]
21007    fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
21008        // Top-to-bottom-pole ordering pin — every strictly-descending
21009        // pair on the shipped preset chain folds to
21010        // `Some(Ordering::Greater)`. The DUAL of the ascending pin
21011        // one DIRECTION axis over — together the two pins close the
21012        // (ascending, descending) direction pair on the shipped
21013        // preset chain at the ordering-direction refinement.
21014        assert_eq!(
21015            DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21016            Some(Ordering::Greater),
21017        );
21018        assert_eq!(
21019            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21020            Some(Ordering::Greater),
21021        );
21022        assert_eq!(
21023            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21024            Some(Ordering::Greater),
21025        );
21026    }
21027
21028    #[test]
21029    fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
21030        // Antichain load-bearing arm — the two hand-authored
21031        // asymmetric postures sit on distinct branches (each is
21032        // smaller on three axes and larger on the other three), so
21033        // NEITHER pointwise-domination direction closes and the
21034        // fall-through `None` arm fires in both orderings. The DIRECT
21035        // LIFT of `is_incomparable_holds_on_the_hand_authored_antichain_pair`
21036        // one PROJECTION axis over on the SAME hand-authored pair —
21037        // where the projection folds TRUE, THIS method folds to
21038        // `None`. The two are the negation-paired sides of the
21039        // (Some(_), None) two-cell partition of `Option<Ordering>`.
21040        assert_eq!(
21041            HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
21042            None,
21043        );
21044        assert_eq!(
21045            HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
21046            None,
21047        );
21048    }
21049
21050    #[test]
21051    fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
21052        // Const-fn pin — the full-verdict projection is evaluable in
21053        // const context, so a caller can pin an ordering-direction
21054        // identity at compile time. Sibling of the const-fn
21055        // evaluability pins on `leq` / `geq` / `lt` / `gt` /
21056        // `is_incomparable` / `is_comparable` one COVER axis over on
21057        // the pairwise-relation surface.
21058        //
21059        // `matches!` in const context requires the pattern's variants
21060        // to be const-constructible — `Ordering::Less` / `Equal` /
21061        // `Greater` are plain enum constructors, so the assertions
21062        // below stay inside stable const-fn scope. A regression to a
21063        // runtime `fn` here would fail the `const _: () = assert!(
21064        // matches!(...))` bindings below at compile time.
21065        const _: () = assert!(matches!(
21066            EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21067            Some(Ordering::Equal),
21068        ));
21069        const _: () = assert!(matches!(
21070            EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21071            Some(Ordering::Less),
21072        ));
21073        const _: () = assert!(matches!(
21074            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21075            Some(Ordering::Greater),
21076        ));
21077        const _: () = assert!(matches!(
21078            DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21079            Some(Ordering::Less),
21080        ));
21081        const _: () = assert!(matches!(
21082            EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21083            Some(Ordering::Less),
21084        ));
21085    }
21086
21087    #[test]
21088    fn resource_limits_is_chain_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_lower_bound_of(&[]) == true`'s empty-slice identity
21092        // one PRIMITIVE-KIND axis over on the (leq, comparable) ×
21093        // (pairwise, N-ary) primitive surface.
21094        assert!(ResourceLimits::is_chain(&[]));
21095    }
21096
21097    #[test]
21098    fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
21099        // Empty-slice vacuous truth — the empty conjunction is vacuously
21100        // true; the empty slice contains no distinct pairs to reject.
21101        // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21102        // COVER-COMPLEMENT axis over: both cells of the (chain, antichain)
21103        // set-level pair AGREE at the empty slice on the vacuous-truth
21104        // verdict — the two cells coincide at the empty face of the
21105        // set-level verdict surface.
21106        assert!(ResourceLimits::is_antichain(&[]));
21107    }
21108
21109    #[test]
21110    fn resource_limits_is_chain_singleton_is_vacuously_true() {
21111        // Singleton vacuous truth — a one-element slice contains no
21112        // distinct pairs, so the outer-and-inner-index conjunction never
21113        // enters the inner loop and returns `true` vacuously. Pinned on
21114        // every canonical preset AND on both hand-authored asymmetric
21115        // postures so the vacuous-truth verdict holds regardless of the
21116        // singleton element's lattice position.
21117        assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21118        assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
21119        assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
21120        assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
21121        assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
21122    }
21123
21124    #[test]
21125    fn resource_limits_is_antichain_singleton_is_vacuously_true() {
21126        // Singleton vacuous truth — a one-element slice contains no
21127        // distinct pairs, so the outer-and-inner-index conjunction never
21128        // enters the inner loop and returns `true` vacuously. Peer of
21129        // `is_chain`'s singleton-vacuous-truth identity: both cells AGREE
21130        // at singleton slices — the (chain, antichain) verdict surface
21131        // coincides at every slice with strictly fewer than two distinct
21132        // pairs.
21133        assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21134        assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
21135        assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
21136        assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
21137        assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
21138    }
21139
21140    #[test]
21141    fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
21142        // Diagonal-duplicate identity — `is_comparable` is REFLEXIVE
21143        // (`a.is_comparable(a) == true`), so the single distinct index
21144        // pair `(0, 1)` binds `a.is_comparable(a) == true` and the
21145        // conjunction holds at every diagonal-duplicate slice.
21146        // Distinguishes it from `is_antichain` one COVER-COMPLEMENT axis
21147        // over, whose diagonal-duplicate verdict is `false` via
21148        // `is_incomparable`'s IRREFLEXIVITY.
21149        assert!(ResourceLimits::is_chain(&[
21150            DEFAULT_RESOURCE_LIMITS,
21151            DEFAULT_RESOURCE_LIMITS,
21152        ]));
21153        assert!(ResourceLimits::is_chain(&[
21154            HAND_AUTHORED_MID_POSTURE,
21155            HAND_AUTHORED_MID_POSTURE,
21156        ]));
21157    }
21158
21159    #[test]
21160    fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
21161        // Diagonal-duplicate rejection — `is_incomparable` is
21162        // IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
21163        // distinct index pair `(0, 1)` binds `a.is_incomparable(a) ==
21164        // false` and the conjunction rejects at every diagonal-duplicate
21165        // slice. The reflexivity/irreflexivity divergence between the
21166        // two pair-level projections PROPAGATES to the set-level
21167        // projections at any slice with a duplicated element — the two
21168        // set-level cells DIVERGE at every diagonal-duplicate slice, the
21169        // mirror of their AGREEMENT at every empty-or-singleton slice
21170        // one CARDINALITY axis over.
21171        assert!(!ResourceLimits::is_antichain(&[
21172            DEFAULT_RESOURCE_LIMITS,
21173            DEFAULT_RESOURCE_LIMITS,
21174        ]));
21175        assert!(!ResourceLimits::is_antichain(&[
21176            HAND_AUTHORED_MID_POSTURE,
21177            HAND_AUTHORED_MID_POSTURE,
21178        ]));
21179    }
21180
21181    #[test]
21182    fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
21183        // Ordered-chain closure — the shipped-preset triple on the
21184        // bounded-lattice diagonal is an ascending chain (`EMPTY <=
21185        // DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among
21186        // the three is comparable. Pinned in every permutation of the
21187        // triple so the doubly-indexed all-pairs walk closes on every
21188        // enumeration of the same chain.
21189        assert!(ResourceLimits::is_chain(&[
21190            EMPTY_RESOURCE_LIMITS,
21191            DEFAULT_RESOURCE_LIMITS,
21192            UNBOUNDED_RESOURCE_LIMITS,
21193        ]));
21194        assert!(ResourceLimits::is_chain(&[
21195            UNBOUNDED_RESOURCE_LIMITS,
21196            DEFAULT_RESOURCE_LIMITS,
21197            EMPTY_RESOURCE_LIMITS,
21198        ]));
21199        assert!(ResourceLimits::is_chain(&[
21200            DEFAULT_RESOURCE_LIMITS,
21201            EMPTY_RESOURCE_LIMITS,
21202            UNBOUNDED_RESOURCE_LIMITS,
21203        ]));
21204    }
21205
21206    #[test]
21207    fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
21208        // Chain rejection — the shipped-preset pair on the bounded-
21209        // lattice diagonal is a strict chain (`EMPTY.lt(DEFAULT)`), so
21210        // the single distinct pair is COMPARABLE and the antichain
21211        // conjunction rejects. The DIRECT FALSIFICATION of
21212        // `is_chain_holds_on_the_shipped_preset_triple` one CELL axis
21213        // over on the SAME lattice diagonal — the two set-level cells
21214        // carry the negation-paired verdict at every strictly-chained
21215        // slice, the mirror of their AGREEMENT at every empty-or-
21216        // singleton slice one CARDINALITY axis over.
21217        assert!(!ResourceLimits::is_antichain(&[
21218            EMPTY_RESOURCE_LIMITS,
21219            DEFAULT_RESOURCE_LIMITS,
21220        ]));
21221        assert!(!ResourceLimits::is_antichain(&[
21222            DEFAULT_RESOURCE_LIMITS,
21223            UNBOUNDED_RESOURCE_LIMITS,
21224        ]));
21225        assert!(!ResourceLimits::is_antichain(&[
21226            EMPTY_RESOURCE_LIMITS,
21227            DEFAULT_RESOURCE_LIMITS,
21228            UNBOUNDED_RESOURCE_LIMITS,
21229        ]));
21230    }
21231
21232    #[test]
21233    fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
21234        // Antichain rejection — the two hand-authored asymmetric
21235        // postures sit on distinct branches (each is smaller on three
21236        // axes and larger on the other three), so neither pointwise-
21237        // domination direction closes and the pair is INCOMPARABLE. The
21238        // chain conjunction rejects at the first incomparable pair.
21239        // Pinned in both orderings so the doubly-indexed pair walk
21240        // rejects regardless of index order (the diagonal-adjacent
21241        // pair-level rejection is symmetric on argument swap).
21242        assert!(!ResourceLimits::is_chain(&[
21243            HAND_AUTHORED_MID_POSTURE,
21244            HAND_AUTHORED_OTHER_POSTURE,
21245        ]));
21246        assert!(!ResourceLimits::is_chain(&[
21247            HAND_AUTHORED_OTHER_POSTURE,
21248            HAND_AUTHORED_MID_POSTURE,
21249        ]));
21250    }
21251
21252    #[test]
21253    fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
21254        // Hand-authored antichain closure — the two hand-authored
21255        // asymmetric postures sit on distinct branches (each is smaller
21256        // on three axes and larger on the other three), so neither
21257        // pointwise-domination direction closes and the pair is
21258        // INCOMPARABLE. The antichain conjunction accepts every distinct
21259        // pair of the slice. DIRECT LIFT of `is_chain`'s antichain
21260        // rejection one CELL axis over on the SAME hand-authored pair —
21261        // pinning BOTH cells' verdicts on the SAME load-bearing
21262        // antichain pair closes the (chain, antichain) two-cell face on
21263        // arity-2 slices as a substrate-level EXCLUSIVITY THEOREM.
21264        assert!(ResourceLimits::is_antichain(&[
21265            HAND_AUTHORED_MID_POSTURE,
21266            HAND_AUTHORED_OTHER_POSTURE,
21267        ]));
21268        assert!(ResourceLimits::is_antichain(&[
21269            HAND_AUTHORED_OTHER_POSTURE,
21270            HAND_AUTHORED_MID_POSTURE,
21271        ]));
21272    }
21273
21274    #[test]
21275    fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
21276        // Mixed-set rejection — both `(DEFAULT, MID)` and `(DEFAULT,
21277        // OTHER)` are comparable pairs (DEFAULT's shipped `DEFAULT_MAX_*`
21278        // per-axis values are all much larger than either hand-authored
21279        // posture's, so DEFAULT dominates both pointwise), but the
21280        // `(MID, OTHER)` pair is the hand-authored INCOMPARABLE pair.
21281        // The set-level chain verdict is stricter than the union of its
21282        // pair-level verdicts — one antichain pair anywhere in the
21283        // slice falsifies the whole projection.
21284        assert!(!ResourceLimits::is_chain(&[
21285            DEFAULT_RESOURCE_LIMITS,
21286            HAND_AUTHORED_MID_POSTURE,
21287            HAND_AUTHORED_OTHER_POSTURE,
21288        ]));
21289    }
21290
21291    #[test]
21292    fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
21293        // Mixed-set rejection — even though the second-and-third pair
21294        // is incomparable (the hand-authored antichain pair), the first
21295        // pair (`DEFAULT`-and-`MID`) is comparable, so the whole-set
21296        // conjunction rejects. Pins the three-cell (chain, antichain,
21297        // mixed) set-level face at the mixed-set cell: the same slice
21298        // both `is_chain` AND `is_antichain` reject at, and the third
21299        // cell of the three-cell face the two set-level projections
21300        // carry the ends of.
21301        assert!(!ResourceLimits::is_antichain(&[
21302            DEFAULT_RESOURCE_LIMITS,
21303            HAND_AUTHORED_MID_POSTURE,
21304            HAND_AUTHORED_OTHER_POSTURE,
21305        ]));
21306    }
21307
21308    #[test]
21309    fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
21310        // De Morgan mirror at arity 2 — on any two-element slice `[a,
21311        // b]` with `a != b`, the set-level (chain, antichain) two-cell
21312        // face reduces to the pair-level (comparable, incomparable)
21313        // two-cell partition and the two cells become MUTUALLY
21314        // EXCLUSIVE (the mixed cell requires ≥3 elements to open). The
21315        // set-level De Morgan mirror of the pair-level identity
21316        // `a.is_comparable(b) == !a.is_incomparable(b)` `is_comparable`
21317        // pins one CARDINALITY axis over.
21318        let presets: [ResourceLimits; 5] = [
21319            EMPTY_RESOURCE_LIMITS,
21320            DEFAULT_RESOURCE_LIMITS,
21321            UNBOUNDED_RESOURCE_LIMITS,
21322            HAND_AUTHORED_MID_POSTURE,
21323            HAND_AUTHORED_OTHER_POSTURE,
21324        ];
21325        for a in presets {
21326            for b in presets {
21327                if a == b {
21328                    continue;
21329                }
21330                let pair = [a, b];
21331                let chain = ResourceLimits::is_chain(&pair);
21332                let antichain = ResourceLimits::is_antichain(&pair);
21333                assert_ne!(
21334                    chain, antichain,
21335                    "distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
21336                );
21337            }
21338        }
21339    }
21340
21341    #[test]
21342    fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
21343        // Empty-and-singleton agreement — the two set-level projections
21344        // carry the SAME vacuous-truth verdict at every slice with
21345        // strictly fewer than two distinct pairs (empty AND singleton).
21346        // Pinned as a substrate-level identity so a future rewrite of
21347        // either projection cannot silently drift from the shared
21348        // vacuous-truth verdict.
21349        assert_eq!(
21350            ResourceLimits::is_chain(&[]),
21351            ResourceLimits::is_antichain(&[]),
21352        );
21353        let presets: [ResourceLimits; 5] = [
21354            EMPTY_RESOURCE_LIMITS,
21355            DEFAULT_RESOURCE_LIMITS,
21356            UNBOUNDED_RESOURCE_LIMITS,
21357            HAND_AUTHORED_MID_POSTURE,
21358            HAND_AUTHORED_OTHER_POSTURE,
21359        ];
21360        for a in presets {
21361            let single = [a];
21362            assert_eq!(
21363                ResourceLimits::is_chain(&single),
21364                ResourceLimits::is_antichain(&single),
21365                "singleton agreement failed on {a:?}",
21366            );
21367        }
21368    }
21369
21370    #[test]
21371    fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
21372        // Const-fn pin — the set-level chain projection is evaluable in
21373        // const context, so a caller can pin a chain-membership
21374        // identity at compile time. Sibling of the const-fn
21375        // evaluability pins on `is_comparable` one CARDINALITY axis
21376        // over and on `is_lower_bound_of` / `is_upper_bound_of` one
21377        // PRIMITIVE-KIND axis over on the N-ary aggregation surface.
21378        const _: () = assert!(ResourceLimits::is_chain(&[]));
21379        const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21380        const _: () = assert!(ResourceLimits::is_chain(&[
21381            EMPTY_RESOURCE_LIMITS,
21382            DEFAULT_RESOURCE_LIMITS,
21383            UNBOUNDED_RESOURCE_LIMITS,
21384        ]));
21385    }
21386
21387    #[test]
21388    fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
21389        // Const-fn pin — the set-level antichain projection is
21390        // evaluable in const context, so a caller can pin an antichain-
21391        // membership identity at compile time. Sibling of the const-fn
21392        // evaluability pins on `is_incomparable` one CARDINALITY axis
21393        // over and on `is_chain` one COVER-COMPLEMENT axis over.
21394        const _: () = assert!(ResourceLimits::is_antichain(&[]));
21395        const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21396        const _: () = assert!(ResourceLimits::is_antichain(&[
21397            HAND_AUTHORED_MID_POSTURE,
21398            HAND_AUTHORED_OTHER_POSTURE,
21399        ]));
21400    }
21401
21402    #[test]
21403    fn resource_limits_is_mixed_empty_slice_is_false() {
21404        // Empty-slice contract — is_chain and is_antichain BOTH return
21405        // `true` vacuously, so `!true && !true == false`. The empty
21406        // slice is NOT mixed. Sibling posture to the vacuous-truth
21407        // verdicts of is_chain + is_antichain one CELL axis over on the
21408        // (chain, antichain, mixed) three-cell face.
21409        assert!(!ResourceLimits::is_mixed(&[]));
21410    }
21411
21412    #[test]
21413    fn resource_limits_is_mixed_singleton_is_false() {
21414        // Singleton contract — a one-element slice contains no distinct
21415        // pairs; is_chain + is_antichain BOTH return `true` vacuously.
21416        // Pinned on every canonical preset AND on both hand-authored
21417        // asymmetric postures so the false-at-singleton verdict holds
21418        // regardless of the singleton element's lattice position.
21419        assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21420        assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
21421        assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
21422        assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
21423        assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
21424    }
21425
21426    #[test]
21427    fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
21428        // Diagonal-duplicate contract — is_chain returns `true` via
21429        // is_comparable's REFLEXIVITY; is_antichain returns `false` via
21430        // is_incomparable's IRREFLEXIVITY; the conjunction
21431        // `!true && !false == false`. The diagonal-duplicate slice is a
21432        // CHAIN (per the reflexivity arm), not mixed.
21433        assert!(!ResourceLimits::is_mixed(&[
21434            DEFAULT_RESOURCE_LIMITS,
21435            DEFAULT_RESOURCE_LIMITS,
21436        ]));
21437        assert!(!ResourceLimits::is_mixed(&[
21438            HAND_AUTHORED_MID_POSTURE,
21439            HAND_AUTHORED_MID_POSTURE,
21440        ]));
21441    }
21442
21443    #[test]
21444    fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
21445        // Chain rejection — the shipped-preset triple on the bounded-
21446        // lattice diagonal is an ascending chain, is_chain returns
21447        // `true`, and `!true && …` short-circuits to `false`. A PURE
21448        // chain is not mixed. Pinned in every permutation of the triple
21449        // so the projection factors through the ordering-agnostic
21450        // sibling is_chain verdict on the same chain.
21451        assert!(!ResourceLimits::is_mixed(&[
21452            EMPTY_RESOURCE_LIMITS,
21453            DEFAULT_RESOURCE_LIMITS,
21454            UNBOUNDED_RESOURCE_LIMITS,
21455        ]));
21456        assert!(!ResourceLimits::is_mixed(&[
21457            UNBOUNDED_RESOURCE_LIMITS,
21458            DEFAULT_RESOURCE_LIMITS,
21459            EMPTY_RESOURCE_LIMITS,
21460        ]));
21461    }
21462
21463    #[test]
21464    fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
21465        // Antichain rejection — the hand-authored antichain pair binds
21466        // is_antichain to `true`, and `!… && !true == false`. A PURE
21467        // antichain is not mixed. Pinned in both orderings so the
21468        // projection factors through the argument-swap-symmetric
21469        // sibling is_antichain verdict on the same antichain.
21470        assert!(!ResourceLimits::is_mixed(&[
21471            HAND_AUTHORED_MID_POSTURE,
21472            HAND_AUTHORED_OTHER_POSTURE,
21473        ]));
21474        assert!(!ResourceLimits::is_mixed(&[
21475            HAND_AUTHORED_OTHER_POSTURE,
21476            HAND_AUTHORED_MID_POSTURE,
21477        ]));
21478    }
21479
21480    #[test]
21481    fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
21482    ) {
21483        // Mixed-set closure — LOAD-BEARING `true`-arm catch. The
21484        // (DEFAULT, MID) pair is comparable (falsifying is_antichain)
21485        // while the (MID, OTHER) pair is incomparable (falsifying
21486        // is_chain); the conjunction `!false && !false == true`. This
21487        // is the SAME fixture the sibling is_chain + is_antichain BOTH
21488        // reject at — the DISCRIMINATING positive-arm catch for the
21489        // (mixed) cell of the three-cell face the two set-level
21490        // projections carry the ends of.
21491        assert!(ResourceLimits::is_mixed(&[
21492            DEFAULT_RESOURCE_LIMITS,
21493            HAND_AUTHORED_MID_POSTURE,
21494            HAND_AUTHORED_OTHER_POSTURE,
21495        ]));
21496    }
21497
21498    #[test]
21499    fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
21500    ) {
21501        // Trichotomy exhaustiveness at arity ≥2 with pairwise-distinct
21502        // elements — EXACTLY ONE of is_chain, is_antichain, is_mixed
21503        // returns `true` on every pair OR triple drawn from the shipped
21504        // presets whose elements are pairwise distinct. The three cells
21505        // partition the set-level verdict surface at every non-
21506        // degenerate slice. Substrate-level identity: pinning it here
21507        // catches a future rewrite of any of the three projections that
21508        // silently drifts from the partition contract.
21509        let presets: [ResourceLimits; 5] = [
21510            EMPTY_RESOURCE_LIMITS,
21511            DEFAULT_RESOURCE_LIMITS,
21512            UNBOUNDED_RESOURCE_LIMITS,
21513            HAND_AUTHORED_MID_POSTURE,
21514            HAND_AUTHORED_OTHER_POSTURE,
21515        ];
21516        // Arity-2 sweep — the mixed cell CANNOT open at arity 2 (only
21517        // one distinct pair), so is_mixed is always false and the
21518        // partition reduces to the (chain, antichain) two-cell face
21519        // that is_chain + is_antichain already pinned MUTUALLY
21520        // EXCLUSIVE. Cross-check that the three-cell projection
21521        // AGREES with the two-cell projection at arity 2.
21522        for a in presets {
21523            for b in presets {
21524                if a == b {
21525                    continue;
21526                }
21527                let pair = [a, b];
21528                let chain = ResourceLimits::is_chain(&pair);
21529                let antichain = ResourceLimits::is_antichain(&pair);
21530                let mixed = ResourceLimits::is_mixed(&pair);
21531                let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21532                assert_eq!(
21533                    true_count, 1,
21534                    "trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21535                );
21536                assert!(
21537                    !mixed,
21538                    "is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
21539                );
21540            }
21541        }
21542        // Arity-3 sweep on distinct triples — the mixed cell opens
21543        // whenever the triple carries both a comparable and an
21544        // incomparable pair (e.g. DEFAULT + MID + OTHER above).
21545        for a in presets {
21546            for b in presets {
21547                for c in presets {
21548                    if a == b || b == c || a == c {
21549                        continue;
21550                    }
21551                    let triple = [a, b, c];
21552                    let chain = ResourceLimits::is_chain(&triple);
21553                    let antichain = ResourceLimits::is_antichain(&triple);
21554                    let mixed = ResourceLimits::is_mixed(&triple);
21555                    let true_count =
21556                        usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21557                    assert_eq!(
21558                        true_count, 1,
21559                        "trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21560                    );
21561                }
21562            }
21563        }
21564    }
21565
21566    #[test]
21567    fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
21568        // Const-fn pin — the set-level mixed projection is evaluable
21569        // in const context, so a caller can pin a mixed-set identity
21570        // at compile time. Sibling of the const-fn evaluability pins
21571        // on is_chain + is_antichain one CELL axis over.
21572        const _: () = assert!(!ResourceLimits::is_mixed(&[]));
21573        const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21574        const _: () = assert!(ResourceLimits::is_mixed(&[
21575            DEFAULT_RESOURCE_LIMITS,
21576            HAND_AUTHORED_MID_POSTURE,
21577            HAND_AUTHORED_OTHER_POSTURE,
21578        ]));
21579    }
21580
21581    // ── ResourceLimits::is_ascending / ::is_descending — sequence-level ──
21582
21583    #[test]
21584    fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
21585        // Empty-slice vacuous truth — the empty conjunction is vacuously
21586        // true; the empty slice contains no consecutive pairs to reject.
21587        // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21588        // CONSECUTIVE-VS-ALL-PAIRS axis over: both cells of the (set-
21589        // level, sequence-level) projection pair AGREE at the empty slice
21590        // on the vacuous-truth verdict.
21591        assert!(ResourceLimits::is_ascending(&[]));
21592    }
21593
21594    #[test]
21595    fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
21596        // Peer of is_ascending's empty-slice identity one PAIR-LEVEL-
21597        // PRIMITIVE axis over: both cells of the (ascending, descending)
21598        // sequence-level pair AGREE at the empty slice.
21599        assert!(ResourceLimits::is_descending(&[]));
21600    }
21601
21602    #[test]
21603    fn resource_limits_is_ascending_singleton_is_vacuously_true() {
21604        // Singleton vacuous truth — a one-element slice contains no
21605        // consecutive pairs, so the walk never enters the loop and
21606        // returns `true` vacuously. Pinned on every canonical preset AND
21607        // on both hand-authored asymmetric postures.
21608        assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21609        assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
21610        assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
21611        assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
21612        assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
21613    }
21614
21615    #[test]
21616    fn resource_limits_is_descending_singleton_is_vacuously_true() {
21617        assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21618        assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
21619        assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
21620        assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
21621        assert!(ResourceLimits::is_descending(&[
21622            HAND_AUTHORED_OTHER_POSTURE
21623        ]));
21624    }
21625
21626    #[test]
21627    fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
21628        // Diagonal-duplicate identity — `leq` is REFLEXIVE
21629        // (`a.leq(a) == true` via each axis's `<=` reflexivity), so the
21630        // single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
21631        // the conjunction holds at every diagonal-duplicate slice.
21632        // AGREES with is_chain's diagonal-duplicate verdict — the two
21633        // projections diverge only where the ORDER matters, and
21634        // duplicated elements carry no ordering information.
21635        assert!(ResourceLimits::is_ascending(&[
21636            DEFAULT_RESOURCE_LIMITS,
21637            DEFAULT_RESOURCE_LIMITS,
21638        ]));
21639        assert!(ResourceLimits::is_ascending(&[
21640            HAND_AUTHORED_MID_POSTURE,
21641            HAND_AUTHORED_MID_POSTURE,
21642        ]));
21643    }
21644
21645    #[test]
21646    fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
21647        // Diagonal-duplicate identity — `geq` is REFLEXIVE, so the
21648        // single consecutive pair `(0, 1)` binds `a.geq(a) == true`.
21649        // AGREES with is_ascending's diagonal-duplicate verdict — both
21650        // leq and geq carry reflexivity, so the sequence-level (T, T)
21651        // corner opens at every diagonal-duplicate slice, distinct from
21652        // the set-level (T, F) verdict is_chain / is_antichain carry on
21653        // the same slice.
21654        assert!(ResourceLimits::is_descending(&[
21655            DEFAULT_RESOURCE_LIMITS,
21656            DEFAULT_RESOURCE_LIMITS,
21657        ]));
21658        assert!(ResourceLimits::is_descending(&[
21659            HAND_AUTHORED_MID_POSTURE,
21660            HAND_AUTHORED_MID_POSTURE,
21661        ]));
21662    }
21663
21664    #[test]
21665    fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
21666        // Ordered ascending closure — the shipped-preset triple on the
21667        // bounded-lattice diagonal enumerated leq-ascending has
21668        // `EMPTY.leq(DEFAULT) == true` and `DEFAULT.leq(UNBOUNDED) ==
21669        // true`, so every consecutive pair passes the conjunction.
21670        // Pinned on the arity-2 prefix AND on the full arity-3 chain so
21671        // the walk closes at every non-degenerate ascending enumeration
21672        // length.
21673        assert!(ResourceLimits::is_ascending(&[
21674            EMPTY_RESOURCE_LIMITS,
21675            DEFAULT_RESOURCE_LIMITS,
21676        ]));
21677        assert!(ResourceLimits::is_ascending(&[
21678            DEFAULT_RESOURCE_LIMITS,
21679            UNBOUNDED_RESOURCE_LIMITS,
21680        ]));
21681        assert!(ResourceLimits::is_ascending(&[
21682            EMPTY_RESOURCE_LIMITS,
21683            DEFAULT_RESOURCE_LIMITS,
21684            UNBOUNDED_RESOURCE_LIMITS,
21685        ]));
21686    }
21687
21688    #[test]
21689    fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
21690        // Ordered descending closure — the reversed enumeration is a
21691        // geq-monotone descending sequence. The DIRECT MIRROR of
21692        // is_ascending's ascending closure one PERMUTATION axis over.
21693        assert!(ResourceLimits::is_descending(&[
21694            DEFAULT_RESOURCE_LIMITS,
21695            EMPTY_RESOURCE_LIMITS,
21696        ]));
21697        assert!(ResourceLimits::is_descending(&[
21698            UNBOUNDED_RESOURCE_LIMITS,
21699            DEFAULT_RESOURCE_LIMITS,
21700        ]));
21701        assert!(ResourceLimits::is_descending(&[
21702            UNBOUNDED_RESOURCE_LIMITS,
21703            DEFAULT_RESOURCE_LIMITS,
21704            EMPTY_RESOURCE_LIMITS,
21705        ]));
21706    }
21707
21708    #[test]
21709    fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
21710        // Order-sensitivity — the reversed enumeration of the same
21711        // shipped-preset chain has `UNBOUNDED.leq(DEFAULT) == false` at
21712        // the first consecutive pair. is_chain accepts BOTH orderings
21713        // (order-INSENSITIVE); this projection accepts only the leq-
21714        // sorted one — the DISCRIMINATING arm between the sequence-level
21715        // and set-level projections.
21716        assert!(!ResourceLimits::is_ascending(&[
21717            UNBOUNDED_RESOURCE_LIMITS,
21718            DEFAULT_RESOURCE_LIMITS,
21719            EMPTY_RESOURCE_LIMITS,
21720        ]));
21721    }
21722
21723    #[test]
21724    fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
21725        // The direct falsification of is_ascending's ascending closure
21726        // one CELL axis over on the SAME slice.
21727        assert!(!ResourceLimits::is_descending(&[
21728            EMPTY_RESOURCE_LIMITS,
21729            DEFAULT_RESOURCE_LIMITS,
21730            UNBOUNDED_RESOURCE_LIMITS,
21731        ]));
21732    }
21733
21734    #[test]
21735    fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
21736        // Non-monotone chain permutation — [DEFAULT, EMPTY, UNBOUNDED]
21737        // is a permutation of the shipped-preset chain, so it IS a chain
21738        // (is_chain accepts it), but the first consecutive pair binds
21739        // `DEFAULT.leq(EMPTY) == false`, so the sequence-level ascending
21740        // walk rejects. Also is_descending rejects — the third
21741        // consecutive pair binds `EMPTY.geq(UNBOUNDED) == false`. Pins
21742        // the DISCRIMINATING arm between the sequence-level and set-
21743        // level projections at a slice that is a chain but neither
21744        // ascending nor descending.
21745        let slice = [
21746            DEFAULT_RESOURCE_LIMITS,
21747            EMPTY_RESOURCE_LIMITS,
21748            UNBOUNDED_RESOURCE_LIMITS,
21749        ];
21750        assert!(ResourceLimits::is_chain(&slice));
21751        assert!(!ResourceLimits::is_ascending(&slice));
21752        assert!(!ResourceLimits::is_descending(&slice));
21753    }
21754
21755    #[test]
21756    fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
21757        // The two hand-authored asymmetric postures are incomparable
21758        // (neither MID.leq(OTHER) nor OTHER.leq(MID)), so the single
21759        // consecutive pair rejects. Pinned in both orderings — the pair-
21760        // level leq verdict rejects on the antichain pair regardless of
21761        // which side is `self`.
21762        assert!(!ResourceLimits::is_ascending(&[
21763            HAND_AUTHORED_MID_POSTURE,
21764            HAND_AUTHORED_OTHER_POSTURE,
21765        ]));
21766        assert!(!ResourceLimits::is_ascending(&[
21767            HAND_AUTHORED_OTHER_POSTURE,
21768            HAND_AUTHORED_MID_POSTURE,
21769        ]));
21770    }
21771
21772    #[test]
21773    fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
21774        assert!(!ResourceLimits::is_descending(&[
21775            HAND_AUTHORED_MID_POSTURE,
21776            HAND_AUTHORED_OTHER_POSTURE,
21777        ]));
21778        assert!(!ResourceLimits::is_descending(&[
21779            HAND_AUTHORED_OTHER_POSTURE,
21780            HAND_AUTHORED_MID_POSTURE,
21781        ]));
21782    }
21783
21784    #[test]
21785    fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
21786        // Transitivity theorem — for every slice `postures`,
21787        // `is_ascending(postures) == true` implies `is_chain(postures)
21788        // == true`. The pointwise partial order is TRANSITIVE, so a
21789        // consecutive-pair leq-chain closes under transitive composition
21790        // into the all-pairs leq-chain. Substrate-level THEOREM: the
21791        // sequence-level ascending verdict is strictly STRONGER than the
21792        // set-level chain verdict. Same identity for is_descending —
21793        // both sequence-level projections imply the set-level chain
21794        // projection.
21795        let presets: [ResourceLimits; 5] = [
21796            EMPTY_RESOURCE_LIMITS,
21797            DEFAULT_RESOURCE_LIMITS,
21798            UNBOUNDED_RESOURCE_LIMITS,
21799            HAND_AUTHORED_MID_POSTURE,
21800            HAND_AUTHORED_OTHER_POSTURE,
21801        ];
21802        // Every arity-2 slice from the 5×5 preset matrix witnesses the
21803        // implication in both directions of the (ascending, descending)
21804        // pair.
21805        for a in presets {
21806            for b in presets {
21807                let slice = [a, b];
21808                if ResourceLimits::is_ascending(&slice) {
21809                    assert!(
21810                        ResourceLimits::is_chain(&slice),
21811                        "is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
21812                    );
21813                }
21814                if ResourceLimits::is_descending(&slice) {
21815                    assert!(
21816                        ResourceLimits::is_chain(&slice),
21817                        "is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
21818                    );
21819                }
21820            }
21821        }
21822        // Load-bearing arity-3 witness — the shipped ascending chain
21823        // triple satisfies is_ascending AND is_chain; the descending
21824        // reversal satisfies is_descending AND is_chain.
21825        let asc = [
21826            EMPTY_RESOURCE_LIMITS,
21827            DEFAULT_RESOURCE_LIMITS,
21828            UNBOUNDED_RESOURCE_LIMITS,
21829        ];
21830        assert!(ResourceLimits::is_ascending(&asc));
21831        assert!(ResourceLimits::is_chain(&asc));
21832        let desc = [
21833            UNBOUNDED_RESOURCE_LIMITS,
21834            DEFAULT_RESOURCE_LIMITS,
21835            EMPTY_RESOURCE_LIMITS,
21836        ];
21837        assert!(ResourceLimits::is_descending(&desc));
21838        assert!(ResourceLimits::is_chain(&desc));
21839    }
21840
21841    #[test]
21842    fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
21843        // Empty-and-singleton agreement — the two sequence-level
21844        // projections carry the SAME vacuous-truth verdict at every
21845        // slice with fewer than two consecutive pairs. Pinned as a
21846        // substrate-level identity so a future rewrite of either
21847        // projection cannot silently drift from the shared vacuous-
21848        // truth verdict.
21849        assert_eq!(
21850            ResourceLimits::is_ascending(&[]),
21851            ResourceLimits::is_descending(&[]),
21852        );
21853        let presets: [ResourceLimits; 5] = [
21854            EMPTY_RESOURCE_LIMITS,
21855            DEFAULT_RESOURCE_LIMITS,
21856            UNBOUNDED_RESOURCE_LIMITS,
21857            HAND_AUTHORED_MID_POSTURE,
21858            HAND_AUTHORED_OTHER_POSTURE,
21859        ];
21860        for a in presets {
21861            let single = [a];
21862            assert_eq!(
21863                ResourceLimits::is_ascending(&single),
21864                ResourceLimits::is_descending(&single),
21865                "empty-and-singleton agreement failed on singleton {a:?}",
21866            );
21867            let dup = [a, a];
21868            assert_eq!(
21869                ResourceLimits::is_ascending(&dup),
21870                ResourceLimits::is_descending(&dup),
21871                "diagonal-duplicate agreement failed on {a:?}",
21872            );
21873        }
21874    }
21875
21876    #[test]
21877    fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
21878        // Const-fn pin — the sequence-level ascending projection is
21879        // evaluable in const context, so a caller can pin an ascending-
21880        // sequence identity at compile time.
21881        const _: () = assert!(ResourceLimits::is_ascending(&[]));
21882        const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21883        const _: () = assert!(ResourceLimits::is_ascending(&[
21884            EMPTY_RESOURCE_LIMITS,
21885            DEFAULT_RESOURCE_LIMITS,
21886            UNBOUNDED_RESOURCE_LIMITS,
21887        ]));
21888        const _: () = assert!(!ResourceLimits::is_ascending(&[
21889            UNBOUNDED_RESOURCE_LIMITS,
21890            EMPTY_RESOURCE_LIMITS,
21891        ]));
21892    }
21893
21894    #[test]
21895    fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
21896        // Const-fn pin — sibling of is_ascending one PAIR-LEVEL-
21897        // PRIMITIVE axis over.
21898        const _: () = assert!(ResourceLimits::is_descending(&[]));
21899        const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21900        const _: () = assert!(ResourceLimits::is_descending(&[
21901            UNBOUNDED_RESOURCE_LIMITS,
21902            DEFAULT_RESOURCE_LIMITS,
21903            EMPTY_RESOURCE_LIMITS,
21904        ]));
21905        const _: () = assert!(!ResourceLimits::is_descending(&[
21906            EMPTY_RESOURCE_LIMITS,
21907            UNBOUNDED_RESOURCE_LIMITS,
21908        ]));
21909    }
21910}