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    /// Three-way benchmark: substitute-only vs bytecode-no-cache vs bytecode-cache.
7284    /// Each path must produce identical output; the cache should show a real,
7285    /// visible speedup because the workload (10 000 calls across 10 unique
7286    /// (macro, args) pairs = 99.9% cache hit rate) is cache-friendly.
7287    #[test]
7288    fn expansion_layers_agree_on_output_and_cache_wins() {
7289        use std::time::Instant;
7290
7291        let macros = "
7292            (defmacro m1 (a b) `(list ,a ,b))
7293            (defmacro m2 (x) `(if ,x true false))
7294            (defmacro m3 (a b c) `(list ,a ,b ,c ,a ,b ,c))
7295            (defmacro m4 (f &rest args) `(,f ,@args))
7296            (defmacro m5 (x) `(and ,x (not (not ,x))))
7297            (defmacro m6 (a b) `(or ,a ,b (and ,a ,b)))
7298            (defmacro m7 (x) `(debug (at timestamp) (in region) (value ,x)))
7299            (defmacro m8 (x y) `(cond ((= ,x ,y) equal) (#t not-equal)))
7300            (defmacro m9 (x) `(loop (times 10) (eval ,x)))
7301            (defmacro m10 (f g &rest args) `(,f (,g ,@args)))
7302        ";
7303        let mut call_src = String::with_capacity(80_000);
7304        for i in 0..10_000 {
7305            match i % 10 {
7306                0 => call_src.push_str("(m1 a b)\n"),
7307                1 => call_src.push_str("(m2 true)\n"),
7308                2 => call_src.push_str("(m3 x y z)\n"),
7309                3 => call_src.push_str("(m4 f a b c d e)\n"),
7310                4 => call_src.push_str("(m5 y)\n"),
7311                5 => call_src.push_str("(m6 a b)\n"),
7312                6 => call_src.push_str("(m7 answer)\n"),
7313                7 => call_src.push_str("(m8 p q)\n"),
7314                8 => call_src.push_str("(m9 body)\n"),
7315                _ => call_src.push_str("(m10 f g a b c)\n"),
7316            }
7317        }
7318        let all_src = format!("{macros}\n{call_src}");
7319        let forms = read(&all_src).unwrap();
7320
7321        let mut subst = Expander::new_substitute_only();
7322        let t0 = Instant::now();
7323        let out_subst = subst.expand_program(forms.clone()).unwrap();
7324        let t_subst = t0.elapsed();
7325
7326        let mut byte_no_cache = Expander::new_bytecode_no_cache();
7327        let t0 = Instant::now();
7328        let out_byte = byte_no_cache.expand_program(forms.clone()).unwrap();
7329        let t_byte = t0.elapsed();
7330
7331        let mut byte_cache = Expander::new();
7332        let t0 = Instant::now();
7333        let out_cached = byte_cache.expand_program(forms).unwrap();
7334        let t_cached = t0.elapsed();
7335
7336        // Rigorous: all three paths agree.
7337        assert_eq!(out_subst, out_byte);
7338        assert_eq!(out_subst, out_cached);
7339
7340        // Cache captured the 10 unique (macro, args) pairs (plus some inner
7341        // expansions — macros that expand into calls to other macros).
7342        let cache_size = byte_cache.cache_size();
7343        assert!(
7344            (10..=50).contains(&cache_size),
7345            "expected ~10 unique cache entries, got {cache_size}"
7346        );
7347
7348        eprintln!(
7349            "\n=== macroexpand: 10k calls × 10 unique (macro, args) pairs ===\n\
7350             substitute only     : {t_subst:?}\n\
7351             bytecode no cache   : {t_byte:?}\n\
7352             bytecode + cache    : {t_cached:?}   (cache_size={cache_size})\n\
7353             cache speedup vs subst : {:.2}×\n\
7354             cache speedup vs byte  : {:.2}×\n",
7355            t_subst.as_secs_f64() / t_cached.as_secs_f64(),
7356            t_byte.as_secs_f64() / t_cached.as_secs_f64(),
7357        );
7358
7359        // The cache MUST win against both baselines for this cache-friendly
7360        // workload. Using a 1.5× threshold so the test is stable across hosts.
7361        assert!(
7362            t_cached < t_subst,
7363            "cache should beat substitute ({t_cached:?} vs {t_subst:?})"
7364        );
7365        assert!(
7366            t_cached < t_byte,
7367            "cache should beat bytecode-no-cache ({t_cached:?} vs {t_byte:?})"
7368        );
7369    }
7370
7371    #[test]
7372    fn cache_respects_arg_changes() {
7373        // Cache must not return stale results when args differ.
7374        let src = "
7375            (defmacro wrap (x) `(list ,x ,x))
7376            (wrap a)
7377            (wrap b)
7378            (wrap a)   ;; same as first — cached hit
7379        ";
7380        let mut e = Expander::new();
7381        let out = e.expand_program(read(src).unwrap()).unwrap();
7382        assert_eq!(out.len(), 3);
7383        assert_eq!(out[0], parse("(list a a)"));
7384        assert_eq!(out[1], parse("(list b b)"));
7385        assert_eq!(out[2], parse("(list a a)"));
7386        // Two distinct args → 2 cache entries.
7387        assert_eq!(e.cache_size(), 2);
7388    }
7389
7390    #[test]
7391    fn clear_cache_empties_memo() {
7392        let mut e = Expander::new();
7393        let out = e
7394            .expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
7395            .unwrap();
7396        assert_eq!(out.len(), 2);
7397        assert_eq!(e.cache_size(), 2);
7398        e.clear_cache();
7399        assert_eq!(e.cache_size(), 0);
7400    }
7401
7402    // ── Unbound template-var: structural variant + did-you-mean hint ──
7403
7404    /// Helper for the unbound-template-var tests — pins the variant shape
7405    /// and carries any error context up to the assert site for legibility.
7406    fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
7407        match err {
7408            LispError::UnboundTemplateVar { prefix, name, hint } => {
7409                (*prefix, name.as_str(), hint.as_deref())
7410            }
7411            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7412        }
7413    }
7414
7415    #[test]
7416    fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
7417        // `,xs` against macro params `[x]` — distance 1, bound 1 — hints `,x`.
7418        // Path: compile_node Unquote (the bytecode-template compile, default
7419        // expander).
7420        let mut e = Expander::new();
7421        let err = e
7422            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7423            .expect_err("unbound template var must error");
7424        let (prefix, name, hint) = unbound_var(&err);
7425        assert_eq!(prefix, UnquoteForm::Unquote);
7426        assert_eq!(name, "xs");
7427        assert_eq!(hint, Some("x"));
7428    }
7429
7430    #[test]
7431    fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
7432        // `,@argz` against macro params `[args]` — distance 1, bound 2 —
7433        // hints `,@args`. Path: compile_node UnquoteSplice.
7434        let mut e = Expander::new();
7435        let err = e
7436            .expand_program(
7437                read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7438            )
7439            .expect_err("unbound splice must error");
7440        let (prefix, name, hint) = unbound_var(&err);
7441        assert_eq!(prefix, UnquoteForm::Splice);
7442        assert_eq!(name, "argz");
7443        assert_eq!(hint, Some("args"));
7444    }
7445
7446    #[test]
7447    fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
7448        // Same shape but routed through the substitute-only expander — proves
7449        // the substitute path emits the same variant as the bytecode path.
7450        let mut e = Expander::new_substitute_only();
7451        let err = e
7452            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7453            .expect_err("substitute unbound must error");
7454        let (prefix, name, hint) = unbound_var(&err);
7455        assert_eq!(prefix, UnquoteForm::Unquote);
7456        assert_eq!(name, "xs");
7457        assert_eq!(hint, Some("x"));
7458    }
7459
7460    #[test]
7461    fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
7462        // The substitute path's UnquoteSplice branch fires for splices that
7463        // appear inside a list during the recursive walk. `,@argz` against
7464        // `[args]` hints `,@args`.
7465        let mut e = Expander::new_substitute_only();
7466        let err = e
7467            .expand_program(
7468                read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7469            )
7470            .expect_err("substitute splice unbound must error");
7471        let (prefix, name, hint) = unbound_var(&err);
7472        assert_eq!(prefix, UnquoteForm::Splice);
7473        assert_eq!(name, "argz");
7474        assert_eq!(hint, Some("args"));
7475    }
7476
7477    #[test]
7478    fn unbound_template_var_omits_hint_when_no_close_match() {
7479        // `,wholly-unrelated` against `[x]` — far past the bound, so no
7480        // hint. Negative control: a wrong hint is worse than no hint, so
7481        // the slot must stay empty when the substrate isn't confident.
7482        let mut e = Expander::new();
7483        let err = e
7484            .expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
7485            .expect_err("unrelated unbound must error");
7486        let (prefix, name, hint) = unbound_var(&err);
7487        assert_eq!(prefix, UnquoteForm::Unquote);
7488        assert_eq!(name, "wholly-unrelated");
7489        assert_eq!(hint, None);
7490    }
7491
7492    #[test]
7493    fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
7494        // End-to-end through the Display impl — pins the rendered diagnostic
7495        // a downstream tool sees today (REPL, tatara-check). Hint stays
7496        // additive: the legacy `"unbound"` substring still appears, so any
7497        // assertion that pattern-matches on it keeps passing.
7498        let mut e = Expander::new();
7499        let err = e
7500            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7501            .expect_err("unbound must error");
7502        let msg = format!("{err}");
7503        assert!(
7504            msg.contains("did you mean ,x?"),
7505            "expected hint suffix in message, got: {msg}"
7506        );
7507        assert!(
7508            msg.contains("unbound"),
7509            "expected legacy `unbound` substring in message, got: {msg}"
7510        );
7511        assert!(
7512            msg.contains(",xs"),
7513            "expected the offending form in message, got: {msg}"
7514        );
7515    }
7516
7517    #[test]
7518    fn unbound_template_var_position_is_none_today() {
7519        // Negative control for the future-spans move: until `Sexp` carries
7520        // source positions, `position()` returns `None` for this variant.
7521        let mut e = Expander::new();
7522        let err = e
7523            .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7524            .expect_err("unbound must error");
7525        assert_eq!(err.position(), None);
7526    }
7527
7528    // ── Non-symbol unquote target: structural variant ─────────────────
7529
7530    /// Helper for the non-symbol-unquote-target tests — pins the variant
7531    /// shape and carries any error context up to the assert site for
7532    /// legibility. Sibling of `unbound_var` and `splice_outside_list_got`;
7533    /// returns the `display` projection of the typed `SexpWitness` so the
7534    /// existing call sites stay byte-for-byte comparable to the legacy
7535    /// `got: String` shape.
7536    fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
7537        match err {
7538            LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
7539            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7540        }
7541    }
7542
7543    #[test]
7544    fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
7545        // `,(list 1 2)` — the inner is a list, not a symbol. Path:
7546        // compile_node Unquote (the bytecode-template compile, default
7547        // expander). Pins variant identity AND prefix AND the offending
7548        // literal so a regression that re-inlines the legacy
7549        // `LispError::Compile` shape fails-loudly here.
7550        let mut e = Expander::new();
7551        let err = e
7552            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7553            .expect_err("non-symbol unquote target must error");
7554        let (prefix, got) = non_symbol_target(&err);
7555        assert_eq!(prefix, UnquoteForm::Unquote);
7556        assert_eq!(got, "(list 1 2)");
7557    }
7558
7559    #[test]
7560    fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
7561        // `,@5` — the inner is an int atom, not a symbol. Path:
7562        // compile_node UnquoteSplice. The integer literal round-trips
7563        // through the variant's `got` slot via `Sexp::Display`.
7564        let mut e = Expander::new();
7565        let err = e
7566            .expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
7567            .expect_err("non-symbol splice target must error");
7568        let (prefix, got) = non_symbol_target(&err);
7569        assert_eq!(prefix, UnquoteForm::Splice);
7570        assert_eq!(got, "5");
7571    }
7572
7573    #[test]
7574    fn non_symbol_unquote_in_substitute_emits_structural_variant() {
7575        // Same shape as the bytecode path but routed through the
7576        // substitute-only expander — proves the substitute path emits the
7577        // same variant as the compile_node path. Pins that the lift is
7578        // path-uniform.
7579        let mut e = Expander::new_substitute_only();
7580        let err = e
7581            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7582            .expect_err("substitute non-symbol target must error");
7583        let (prefix, got) = non_symbol_target(&err);
7584        assert_eq!(prefix, UnquoteForm::Unquote);
7585        assert_eq!(got, "(list 1 2)");
7586    }
7587
7588    #[test]
7589    fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
7590        // The substitute path's UnquoteSplice-inside-list branch fires for
7591        // splices that appear inside a list during the recursive walk.
7592        // `,@(list 1 2)` inside the body — the inner is a literal list, not
7593        // a symbol — emits the same variant as the compile_node path.
7594        let mut e = Expander::new_substitute_only();
7595        let err = e
7596            .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7597            .expect_err("substitute non-symbol splice must error");
7598        let (prefix, got) = non_symbol_target(&err);
7599        assert_eq!(prefix, UnquoteForm::Splice);
7600        assert_eq!(got, "(list 1 2)");
7601    }
7602
7603    #[test]
7604    fn non_symbol_unquote_target_position_is_none_today() {
7605        // Negative control for the future-spans move: until `Sexp` carries
7606        // source positions, `position()` returns `None` for this variant.
7607        // A future run that gives `Sexp` source spans adds `pos:
7608        // Option<usize>` to ONE place; this test gives that change a
7609        // deliberate fail-before/pass-after delta.
7610        let mut e = Expander::new();
7611        let err = e
7612            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7613            .expect_err("non-symbol target must error");
7614        assert_eq!(err.position(), None);
7615    }
7616
7617    // ── unquote_target_symbol: typed gate-1 primitive for ,X / ,@X ──────
7618    //
7619    // The `unquote_target_symbol(inner, form)?` primitive lifts the
7620    // inline `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(
7621    // form, inner))?` pattern that previously appeared at four call
7622    // sites (`compile_node` Unquote/UnquoteSplice + `substitute` Unquote
7623    // + `substitute` list-inner UnquoteSplice) behind ONE named
7624    // primitive. The tests below pin: (a) the Ok-arm borrows the
7625    // symbol name from `inner` for both UnquoteForm variants; (b) the
7626    // Err-arm routes through `non_symbol_unquote_target` and emits the
7627    // structural `LispError::NonSymbolUnquoteTarget` variant carrying
7628    // the typed `SexpWitness` (joint shape + display identity) for the
7629    // closed set of reachable non-symbol shapes (int / keyword / list /
7630    // nil); (c) the helper is path-uniform — the same Ok / Err
7631    // contracts hold regardless of which call site invokes it. A
7632    // regression that re-inlines the gate-1 projection at any of the
7633    // four call sites can no longer drift independent of the others —
7634    // the helper IS the gate.
7635
7636    #[test]
7637    fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
7638        // Positive control for the Ok-arm: `inner = Sexp::Symbol("xs")`
7639        // under `UnquoteForm::Unquote` projects through `as_symbol()`
7640        // to the borrowed `&str`. The returned slice's lifetime is
7641        // tied to `inner` so the caller can feed it directly into
7642        // `params.iter().position(...)` (`compile_node`) or
7643        // `bindings.get(...)` (`substitute`) without an intermediate
7644        // allocation. Fail-before/pass-after: this assert is meaningless
7645        // pre-lift because the helper does not exist; post-lift it
7646        // pins the typed gate-1 contract at the named primitive.
7647        let inner = Sexp::symbol("xs");
7648        let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7649            .expect("symbol inner must project to Ok");
7650        assert_eq!(name, "xs");
7651    }
7652
7653    #[test]
7654    fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
7655        // Sibling positive control: `UnquoteForm::Splice` shares the
7656        // gate-1 contract with `Unquote`. The helper is path-uniform
7657        // across both syntactic markers — a regression that bifurcates
7658        // the two arms (e.g., accepting non-symbols for `,@X` but not
7659        // `,X`) fails-loudly here. Pins that the closed-set
7660        // `UnquoteForm` enum's two variants share ONE projection
7661        // posture across the gate-1 boundary.
7662        let inner = Sexp::symbol("rest");
7663        let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
7664            .expect("symbol inner must project to Ok under Splice");
7665        assert_eq!(name, "rest");
7666    }
7667
7668    #[test]
7669    fn unquote_target_symbol_rejects_int_inner_under_unquote() {
7670        // Negative control for the Err-arm: `inner = Sexp::Int(5)` is
7671        // NOT a symbol — the gate-1 projection fires and routes through
7672        // `non_symbol_unquote_target` to the structural
7673        // `LispError::NonSymbolUnquoteTarget` variant. Pin the variant
7674        // identity AND the typed `SexpWitness` joint identity (shape +
7675        // display literal): a regression that drops the witness shape
7676        // or display fails-loudly here.
7677        let inner = Sexp::int(5);
7678        let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7679            .expect_err("int inner must error at gate-1");
7680        match err {
7681            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7682                assert_eq!(prefix, UnquoteForm::Unquote);
7683                assert_eq!(got.shape, crate::error::SexpShape::Int);
7684                assert_eq!(got.display, "5");
7685            }
7686            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7687        }
7688    }
7689
7690    #[test]
7691    fn unquote_target_symbol_rejects_list_inner_under_splice() {
7692        // Sibling negative control: `inner = (list 1 2)` is a list, not
7693        // a symbol — the gate-1 projection fires AND routes through
7694        // `non_symbol_unquote_target(UnquoteForm::Splice, inner)`. Pins
7695        // both the variant identity AND the typed witness's joint
7696        // shape (`SexpShape::List`) + display (`"(list 1 2)"`) so a
7697        // future shape drift fails-loudly. Sibling of the Int / Unquote
7698        // pin: closes the gate-1 contract across the closed-set
7699        // product of {Int, List, Keyword, …} × {Unquote, Splice}.
7700        let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
7701        let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
7702            .expect_err("list inner must error at gate-1");
7703        match err {
7704            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7705                assert_eq!(prefix, UnquoteForm::Splice);
7706                assert_eq!(got.shape, crate::error::SexpShape::List);
7707                assert_eq!(got.display, "(list 1 2)");
7708            }
7709            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7710        }
7711    }
7712
7713    #[test]
7714    fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
7715        // Pin a third reachable non-symbol shape: `Sexp::Keyword(":foo")`.
7716        // The gate-1 projection rejects keywords AS WELL as ints and
7717        // lists — closes the closed-set of "non-symbol shapes the gate
7718        // rejects" across one more reachable variant. The typed witness
7719        // carries `SexpShape::Keyword` + display `:foo` jointly so
7720        // authoring tools (REPL, LSP) bind on the structural shape
7721        // directly.
7722        let inner = Sexp::keyword("foo");
7723        let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7724            .expect_err("keyword inner must error at gate-1");
7725        match err {
7726            LispError::NonSymbolUnquoteTarget { prefix, got } => {
7727                assert_eq!(prefix, UnquoteForm::Unquote);
7728                assert_eq!(got.shape, crate::error::SexpShape::Keyword);
7729                assert_eq!(got.display, ":foo");
7730            }
7731            other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7732        }
7733    }
7734
7735    #[test]
7736    fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
7737        // Path-uniformity pin: end-to-end through ALL FOUR call sites
7738        // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7739        // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7740        // every non-symbol unquote target now routes through the SAME
7741        // `unquote_target_symbol(inner, form)?` helper. The four
7742        // end-to-end expansions below all reject with the SAME variant
7743        // (`NonSymbolUnquoteTarget`) — pins that the lift preserves the
7744        // path-uniform rejection contract `non_symbol_unquote_target`'s
7745        // prior lift established (and that drove the bytecode-vs-
7746        // substitute reunification in 0e9c… and successors). A
7747        // regression that re-inlines the gate-1 projection at one of
7748        // the four sites can drift the four call sites independent of
7749        // each other — this test would catch that drift.
7750        let cases: &[(&str, UnquoteForm)] = &[
7751            // compile_node Unquote (bytecode-path)
7752            ("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
7753            // compile_node UnquoteSplice (bytecode-path)
7754            ("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
7755        ];
7756        for (src, expected_form) in cases {
7757            let mut e = Expander::new();
7758            let err = e
7759                .expand_program(read(src).unwrap())
7760                .expect_err("non-symbol unquote target must error end-to-end");
7761            match err {
7762                LispError::NonSymbolUnquoteTarget { prefix, .. } => {
7763                    assert_eq!(prefix, *expected_form, "for src: {src}");
7764                }
7765                other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
7766            }
7767        }
7768        // substitute Unquote (substitute-only path) — sibling pin to
7769        // `non_symbol_unquote_in_substitute_emits_structural_variant`.
7770        let mut e_subst = Expander::new_substitute_only();
7771        let err = e_subst
7772            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7773            .expect_err("substitute Unquote must error end-to-end");
7774        assert!(
7775            matches!(
7776                err,
7777                LispError::NonSymbolUnquoteTarget {
7778                    prefix: UnquoteForm::Unquote,
7779                    ..
7780                }
7781            ),
7782            "expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
7783        );
7784        // substitute list-inner UnquoteSplice (substitute-only path) —
7785        // sibling pin to
7786        // `non_symbol_unquote_splice_inside_list_in_substitute_emits_…`.
7787        let mut e_subst2 = Expander::new_substitute_only();
7788        let err = e_subst2
7789            .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7790            .expect_err("substitute UnquoteSplice-in-list must error end-to-end");
7791        assert!(
7792            matches!(
7793                err,
7794                LispError::NonSymbolUnquoteTarget {
7795                    prefix: UnquoteForm::Splice,
7796                    ..
7797                }
7798            ),
7799            "expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
7800        );
7801    }
7802
7803    // ── Gate-2 (must-be-bound-in-scope) typed primitives ──────────────
7804    // Pins the contract of the two gate-2 helpers — `resolve_param_index`
7805    // (bytecode-template compile path) and `resolve_binding`
7806    // (substitute path) — that the four inline `<lookup>.ok_or_else(||
7807    // unbound_template_var(FORM, name, candidates))` projections at
7808    // `compile_node` Unquote/UnquoteSplice AND `substitute` Unquote/
7809    // UnquoteSplice-inside-list collapse behind. Tests pin: (a) Ok-arm
7810    // projection under both `UnquoteForm` variants — the helper returns
7811    // the resolved `usize` (compile path) or `&Sexp` (substitute path)
7812    // for in-scope names; (b) Err-arm projection routes through
7813    // `unbound_template_var` to the typed `LispError::UnboundTemplateVar`
7814    // variant with the correct `prefix` AND the suggest-driven `hint`;
7815    // (c) the helpers are path-uniform — both compile-path arms share
7816    // ONE `resolve_param_index`; both substitute-path arms share ONE
7817    // `resolve_binding`. A regression that re-inlines the gate-2
7818    // projection at any of the four call sites can no longer drift
7819    // independent of the others — the two helpers ARE the gate.
7820
7821    #[test]
7822    fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
7823        // Positive control for the Ok-arm: `name = "x"` against
7824        // `params = ["a", "x", "rest"]` projects through
7825        // `params.iter().position(|p| *p == name)` to `Some(1)`, which
7826        // the helper unwraps to `Ok(1)`. The returned index feeds
7827        // directly into `TemplateOp::Subst(idx)` at the compile site.
7828        let params = ["a", "x", "rest"];
7829        let idx = resolve_param_index("x", &params, UnquoteForm::Unquote)
7830            .expect("bound name must project to Ok at gate-2");
7831        assert_eq!(idx, 1);
7832    }
7833
7834    #[test]
7835    fn resolve_param_index_returns_position_for_bound_name_under_splice() {
7836        // Sibling positive control: `UnquoteForm::Splice` shares the
7837        // gate-2 contract with `Unquote`. The helper is path-uniform
7838        // across both syntactic markers on the compile path — a
7839        // regression that bifurcates the two arms fails-loudly here.
7840        let params = ["a", "x", "rest"];
7841        let idx = resolve_param_index("rest", &params, UnquoteForm::Splice)
7842            .expect("bound name must project to Ok at gate-2 under Splice");
7843        assert_eq!(idx, 2);
7844    }
7845
7846    #[test]
7847    fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
7848        // Negative control for the Err-arm: `name = "xs"` against
7849        // `params = ["x"]` — distance 1, bound 1 — routes through
7850        // `unbound_template_var` to the structural
7851        // `LispError::UnboundTemplateVar` variant with `hint = Some("x")`.
7852        // Pin the variant identity AND the prefix AND the suggest-driven
7853        // hint: a regression that drops the suggestion fails-loudly here.
7854        let params = ["x"];
7855        let err = resolve_param_index("xs", &params, UnquoteForm::Unquote)
7856            .expect_err("unbound name must error at gate-2");
7857        match err {
7858            LispError::UnboundTemplateVar { prefix, name, hint } => {
7859                assert_eq!(prefix, UnquoteForm::Unquote);
7860                assert_eq!(name, "xs");
7861                assert_eq!(hint.as_deref(), Some("x"));
7862            }
7863            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7864        }
7865    }
7866
7867    #[test]
7868    fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
7869        // Sibling negative control: `name = "wholly-unrelated"` against
7870        // `params = ["x"]` — past the bounded edit distance, so no hint.
7871        // Pin that the suggest-driven hint stays empty under Splice when
7872        // the substrate isn't confident — a wrong hint is worse than no
7873        // hint. Closes the closed-set product of {hint, no-hint} ×
7874        // {Unquote, Splice} on the compile-path gate-2.
7875        let params = ["x"];
7876        let err = resolve_param_index("wholly-unrelated", &params, UnquoteForm::Splice)
7877            .expect_err("unrelated unbound must error at gate-2");
7878        match err {
7879            LispError::UnboundTemplateVar { prefix, name, hint } => {
7880                assert_eq!(prefix, UnquoteForm::Splice);
7881                assert_eq!(name, "wholly-unrelated");
7882                assert_eq!(hint, None);
7883            }
7884            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7885        }
7886    }
7887
7888    #[test]
7889    fn resolve_binding_returns_value_for_bound_name_under_unquote() {
7890        // Positive control for the substitute-path Ok-arm: `name = "x"`
7891        // against a bindings map `{x: 42, y: "hi"}` projects through
7892        // `bindings.get(name)` to `Some(&Sexp::Int(42))`, which the
7893        // helper unwraps to `Ok(&Sexp::Int(42))`. The returned
7894        // `&Sexp` borrows from the bindings map — the top-level
7895        // `Sexp::Unquote(_)` substitute caller adds a single
7896        // `.cloned()` to satisfy its owned-`Sexp` return obligation.
7897        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7898        bindings.insert("x".to_string(), Sexp::int(42));
7899        bindings.insert("y".to_string(), Sexp::string("hi"));
7900        let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
7901            .expect("bound name must project to Ok at gate-2 (substitute)");
7902        assert_eq!(val, &Sexp::int(42));
7903    }
7904
7905    #[test]
7906    fn resolve_binding_returns_value_for_bound_name_under_splice() {
7907        // Sibling positive control: `UnquoteForm::Splice` shares the
7908        // gate-2 contract with `Unquote` on the substitute path too.
7909        // The bound value is a `Sexp::List` because the splice arm's
7910        // caller match expression expects `Sexp::List(items)` — but
7911        // the helper itself doesn't inspect the value's shape; it
7912        // just hands back the borrow. A regression that gate-checks
7913        // the value's shape inside `resolve_binding` (instead of at
7914        // the caller match arm) fails-loudly here.
7915        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7916        bindings.insert(
7917            "args".to_string(),
7918            Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
7919        );
7920        let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
7921            .expect("bound name must project to Ok at gate-2 under Splice");
7922        assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
7923    }
7924
7925    #[test]
7926    fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
7927        // Negative control for the substitute-path Err-arm: `name =
7928        // "xs"` against bindings `{x: 1}` — distance 1, bound 1 —
7929        // routes through `unbound_template_var` to the structural
7930        // `LispError::UnboundTemplateVar` variant with `hint =
7931        // Some("x")`. The candidate set is drawn from
7932        // `bound_names(bindings)` — the live bindings' keys, never a
7933        // stale snapshot.
7934        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7935        bindings.insert("x".to_string(), Sexp::int(1));
7936        let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
7937            .expect_err("unbound name must error at gate-2 (substitute)");
7938        match err {
7939            LispError::UnboundTemplateVar { prefix, name, hint } => {
7940                assert_eq!(prefix, UnquoteForm::Unquote);
7941                assert_eq!(name, "xs");
7942                assert_eq!(hint.as_deref(), Some("x"));
7943            }
7944            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7945        }
7946    }
7947
7948    #[test]
7949    fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
7950        // Sibling negative control on the substitute path: past-bound
7951        // distance → no hint. Closes the closed-set product of
7952        // {hint, no-hint} × {Unquote, Splice} on the substitute-path
7953        // gate-2.
7954        let mut bindings: HashMap<String, Sexp> = HashMap::new();
7955        bindings.insert("args".to_string(), Sexp::Nil);
7956        let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
7957            .expect_err("unrelated unbound must error at gate-2");
7958        match err {
7959            LispError::UnboundTemplateVar { prefix, name, hint } => {
7960                assert_eq!(prefix, UnquoteForm::Splice);
7961                assert_eq!(name, "wholly-unrelated");
7962                assert_eq!(hint, None);
7963            }
7964            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7965        }
7966    }
7967
7968    #[test]
7969    fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
7970        // Path-uniformity pin: end-to-end through ALL FOUR call sites
7971        // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7972        // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7973        // every unbound-template-var rejection now routes through one
7974        // of the TWO `resolve_param_index` / `resolve_binding` helpers
7975        // — `Expander::new()` runs the compile path, so its two arms
7976        // share `resolve_param_index`; `Expander::new_substitute_only()`
7977        // runs the substitute path, so its two arms share
7978        // `resolve_binding`. The four end-to-end expansions below all
7979        // reject with the SAME variant (`UnboundTemplateVar`) with the
7980        // expected `prefix` — pins that the lift preserves the
7981        // path-uniform rejection contract `unbound_template_var`'s
7982        // prior naming established. A regression that re-inlines the
7983        // gate-2 projection at one of the four sites can drift the
7984        // four call sites independent of each other — this test would
7985        // catch that drift.
7986        struct Case {
7987            src: &'static str,
7988            expander: fn() -> Expander,
7989            expected_form: UnquoteForm,
7990        }
7991        let cases: &[Case] = &[
7992            // compile_node Unquote (bytecode path) — uses resolve_param_index
7993            Case {
7994                src: "(defmacro w (x) `(list ,xs)) (w 1)",
7995                expander: Expander::new,
7996                expected_form: UnquoteForm::Unquote,
7997            },
7998            // compile_node UnquoteSplice (bytecode path) — uses resolve_param_index
7999            Case {
8000                src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8001                expander: Expander::new,
8002                expected_form: UnquoteForm::Splice,
8003            },
8004            // substitute Unquote (substitute-only path) — uses resolve_binding
8005            Case {
8006                src: "(defmacro w (x) `(list ,xs)) (w 1)",
8007                expander: Expander::new_substitute_only,
8008                expected_form: UnquoteForm::Unquote,
8009            },
8010            // substitute UnquoteSplice-in-list (substitute-only path) — uses resolve_binding
8011            Case {
8012                src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8013                expander: Expander::new_substitute_only,
8014                expected_form: UnquoteForm::Splice,
8015            },
8016        ];
8017        for case in cases {
8018            let mut e = (case.expander)();
8019            let err = e
8020                .expand_program(read(case.src).unwrap())
8021                .expect_err("unbound template var must error end-to-end");
8022            match err {
8023                LispError::UnboundTemplateVar { prefix, .. } => {
8024                    assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8025                }
8026                other => panic!(
8027                    "expected UnboundTemplateVar for {}, got: {other:?}",
8028                    case.src
8029                ),
8030            }
8031        }
8032    }
8033
8034    // ── resolve_unquote_in_params / _in_bindings: gate-1+gate-2 composition ─
8035
8036    #[test]
8037    fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
8038        // Ok-arm composition under `UnquoteForm::Unquote`: gate-1 projects
8039        // the symbol-inner to "x"; gate-2 looks "x" up in `params` and
8040        // returns its index. The combined helper returns the gate-2
8041        // result directly — pins that gate-1's Ok-arm threads into
8042        // gate-2's input without intermediate state.
8043        let inner = Sexp::symbol("x");
8044        let params = ["x", "y"];
8045        let idx = resolve_unquote_in_params(&inner, &params, UnquoteForm::Unquote)
8046            .expect("symbol-inner bound at index 0 must resolve");
8047        assert_eq!(idx, 0);
8048    }
8049
8050    #[test]
8051    fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
8052        // Sibling Ok-arm under `UnquoteForm::Splice`: pins that the
8053        // marker doesn't change the projection — only the rejection
8054        // path's `prefix` slot.
8055        let inner = Sexp::symbol("args");
8056        let params = ["f", "args"];
8057        let idx = resolve_unquote_in_params(&inner, &params, UnquoteForm::Splice)
8058            .expect("symbol-inner bound at index 1 must resolve");
8059        assert_eq!(idx, 1);
8060    }
8061
8062    #[test]
8063    fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
8064        // Err-arm at gate-1 (must-be-a-symbol): the inner is a list, not
8065        // a symbol, so gate-1 rejects via `non_symbol_unquote_target`
8066        // BEFORE gate-2's param lookup runs. Pins that the composition's
8067        // sequencing is gate-1-then-gate-2: a regression that runs
8068        // gate-2 first would attempt to look up "(list 1 2)" as a param
8069        // name and emit `LispError::UnboundTemplateVar { name: "(list 1
8070        // 2)", ... }` — a confusing diagnostic that would substring-grep
8071        // "unbound" instead of "expected symbol". This test pins the
8072        // structural floor: a non-symbol inner is rejected as a non-
8073        // symbol, never re-treated as a bound-name lookup key.
8074        let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
8075        let params = ["x"];
8076        let err = resolve_unquote_in_params(&inner, &params, UnquoteForm::Unquote)
8077            .expect_err("non-symbol inner must reject at gate-1");
8078        match err {
8079            LispError::NonSymbolUnquoteTarget { prefix, got } => {
8080                assert_eq!(prefix, UnquoteForm::Unquote);
8081                assert_eq!(got.display, "(list 1 2)");
8082            }
8083            other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8084        }
8085    }
8086
8087    #[test]
8088    fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
8089        // Err-arm at gate-2 (must-be-bound-in-scope): the inner IS a
8090        // symbol (gate-1 passes) but the name isn't in `params`, so
8091        // gate-2 rejects via `unbound_template_var`. Pins that gate-1
8092        // forwards its Ok-arm `&str` borrow into gate-2's lookup, and
8093        // that the marker `prefix` is threaded into gate-2's rejection
8094        // unchanged (a regression that hard-codes `UnquoteForm::Unquote`
8095        // at the composition boundary would fail this Splice-marker
8096        // assertion).
8097        let inner = Sexp::symbol("missing");
8098        let params = ["x", "y"];
8099        let err = resolve_unquote_in_params(&inner, &params, UnquoteForm::Splice)
8100            .expect_err("unbound symbol must reject at gate-2");
8101        match err {
8102            LispError::UnboundTemplateVar { prefix, name, .. } => {
8103                assert_eq!(prefix, UnquoteForm::Splice);
8104                assert_eq!(name, "missing");
8105            }
8106            other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8107        }
8108    }
8109
8110    #[test]
8111    fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
8112        // Substitute-path sibling of `resolve_unquote_in_params_returns_
8113        // index_for_symbol_inner_under_unquote`. The combined helper
8114        // composes gate-1 (project inner to symbol) THEN gate-2 (look
8115        // up name in bindings). The returned `&Sexp` borrows from
8116        // `bindings` so the list-inner caller threads it straight into
8117        // the splice-expansion match without an intermediate allocation.
8118        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8119        bindings.insert("v".to_string(), Sexp::int(42));
8120        let inner = Sexp::symbol("v");
8121        let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8122            .expect("symbol-inner bound to 42 must resolve");
8123        assert_eq!(val, &Sexp::int(42));
8124    }
8125
8126    #[test]
8127    fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
8128        // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8129        // non_symbol_inner_at_gate_1`. Pins the gate-1-then-gate-2
8130        // sequencing on the substitute path: a non-symbol inner is
8131        // rejected as a non-symbol BEFORE the bindings map is consulted.
8132        let bindings: HashMap<String, Sexp> = HashMap::new();
8133        let inner = Sexp::int(5);
8134        let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
8135            .expect_err("non-symbol inner must reject at gate-1");
8136        match err {
8137            LispError::NonSymbolUnquoteTarget { prefix, got } => {
8138                assert_eq!(prefix, UnquoteForm::Splice);
8139                assert_eq!(got.display, "5");
8140            }
8141            other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8142        }
8143    }
8144
8145    #[test]
8146    fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
8147        // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8148        // unbound_symbol_at_gate_2`. Pins the gate-2 rejection on the
8149        // substitute path with the marker threaded into the rejection's
8150        // `prefix` slot.
8151        let mut bindings: HashMap<String, Sexp> = HashMap::new();
8152        bindings.insert("known".to_string(), Sexp::Nil);
8153        let inner = Sexp::symbol("missing");
8154        let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8155            .expect_err("unbound symbol must reject at gate-2");
8156        match err {
8157            LispError::UnboundTemplateVar { prefix, name, .. } => {
8158                assert_eq!(prefix, UnquoteForm::Unquote);
8159                assert_eq!(name, "missing");
8160            }
8161            other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8162        }
8163    }
8164
8165    #[test]
8166    fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
8167        // End-to-end pin: all FOUR call sites of the gate-1+gate-2
8168        // composition (compile_node Unquote, compile_node UnquoteSplice,
8169        // substitute Unquote, substitute list-inner UnquoteSplice) now
8170        // share TWO composed primitives — `resolve_unquote_in_params`
8171        // on the bytecode path, `resolve_unquote_in_bindings` on the
8172        // substitute path — and ALL four reject gate-1 failures (non-
8173        // symbol inner) with the SAME `LispError::NonSymbolUnquoteTarget`
8174        // variant carrying the expected `prefix` slot. Before the lift,
8175        // each site threaded `form` twice through two helper calls; this
8176        // test pins that the lift preserves the gate's rejection-shape
8177        // identity across all four sites for a non-symbol inner — i.e.
8178        // gate-1 fires identically across both expansion strategies.
8179        struct Case {
8180            src: &'static str,
8181            expander: fn() -> Expander,
8182            expected_form: UnquoteForm,
8183        }
8184        let cases: &[Case] = &[
8185            Case {
8186                src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8187                expander: Expander::new,
8188                expected_form: UnquoteForm::Unquote,
8189            },
8190            Case {
8191                src: "(defmacro w (x) `(outer ,@5)) (w 1)",
8192                expander: Expander::new,
8193                expected_form: UnquoteForm::Splice,
8194            },
8195            Case {
8196                src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8197                expander: Expander::new_substitute_only,
8198                expected_form: UnquoteForm::Unquote,
8199            },
8200            Case {
8201                src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
8202                expander: Expander::new_substitute_only,
8203                expected_form: UnquoteForm::Splice,
8204            },
8205        ];
8206        for case in cases {
8207            let mut e = (case.expander)();
8208            let err = e
8209                .expand_program(read(case.src).unwrap())
8210                .expect_err("non-symbol inner must error end-to-end");
8211            match err {
8212                LispError::NonSymbolUnquoteTarget { prefix, .. } => {
8213                    assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8214                }
8215                other => panic!(
8216                    "expected NonSymbolUnquoteTarget for {}, got: {other:?}",
8217                    case.src
8218                ),
8219            }
8220        }
8221    }
8222
8223    // ── Splice outside list: structural variant + path-uniform rejection ─
8224
8225    /// Helper for the splice-outside-list tests — pins the variant shape
8226    /// and carries the offending `got` field up to the assert site for
8227    /// legibility. Sibling of `unbound_var` and `non_symbol_target`.
8228    fn splice_outside_list_got(err: &LispError) -> &str {
8229        match err {
8230            LispError::SpliceOutsideList { got } => got.display.as_str(),
8231            other => panic!("expected SpliceOutsideList, got: {other:?}"),
8232        }
8233    }
8234
8235    #[test]
8236    fn splice_outside_list_in_substitute_emits_structural_variant() {
8237        // `,@xs` at the body's top level — there is no containing list to
8238        // splice into. Path: substitute (the `Expander::new_substitute_only`
8239        // path's top-level `Sexp::UnquoteSplice(_)` arm). Pins variant
8240        // identity AND the offending inner so a regression that re-inlines
8241        // the legacy `LispError::Compile` shape fails-loudly here.
8242        let mut e = Expander::new_substitute_only();
8243        let err = e
8244            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8245            .expect_err("splice outside list must error");
8246        assert_eq!(splice_outside_list_got(&err), "xs");
8247    }
8248
8249    #[test]
8250    fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
8251        // `,@(list 1 2)` at the body's top level — the inner is a literal
8252        // list, not a symbol. The structural variant carries the inner's
8253        // Sexp::Display projection so the operator sees the literal value
8254        // they wrote in the parenthetical.
8255        let mut e = Expander::new_substitute_only();
8256        let err = e
8257            .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8258            .expect_err("splice outside list must error");
8259        assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8260    }
8261
8262    #[test]
8263    fn splice_outside_list_in_compile_template_emits_structural_variant() {
8264        // The bytecode path's `compile_template` gate now rejects top-level
8265        // `,@X` bodies BEFORE walking — closing the prior silent-divergence
8266        // where the bytecode interpreter's outermost stack frame absorbed
8267        // the splice. Pins that the bytecode path emits the SAME structural
8268        // variant the substitute path emits — `,@-outside-list` is rejected
8269        // path-uniformly. Path: `Expander::new()` (compile_templates = true)
8270        // → `compile_template` gate.
8271        let mut e = Expander::new();
8272        let err = e
8273            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8274            .expect_err("compile-template splice outside list must error");
8275        assert_eq!(splice_outside_list_got(&err), "xs");
8276    }
8277
8278    #[test]
8279    fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
8280        // Same shape as the substitute test but routed through the bytecode
8281        // path's `compile_template` gate. Proves the gate fires on a
8282        // non-symbol inner too — the slot's contents are irrelevant; only
8283        // the syntactic position matters.
8284        let mut e = Expander::new();
8285        let err = e
8286            .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8287            .expect_err("compile-template splice outside list must error");
8288        assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8289    }
8290
8291    #[test]
8292    fn splice_outside_list_substitute_and_bytecode_paths_agree() {
8293        // Path-uniform rejection: the SAME source emits the SAME structural
8294        // variant (`SpliceOutsideList { got: "xs" }`) under both expansion
8295        // strategies. Before the `compile_template` gate, the bytecode path
8296        // silently produced a list while the substitute path errored —
8297        // expansion strategy was observable. After the gate, the gate is
8298        // strategy-uniform, so a macro that registers under one strategy
8299        // registers under the other.
8300        let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
8301        let mut subst = Expander::new_substitute_only();
8302        let mut bytecode = Expander::new();
8303        let err_subst = subst
8304            .expand_program(read(src).unwrap())
8305            .expect_err("substitute must error");
8306        let err_byte = bytecode
8307            .expand_program(read(src).unwrap())
8308            .expect_err("bytecode must error");
8309        assert_eq!(splice_outside_list_got(&err_subst), "xs");
8310        assert_eq!(splice_outside_list_got(&err_byte), "xs");
8311    }
8312
8313    #[test]
8314    fn splice_outside_list_position_is_none_today() {
8315        // Negative control for the future-spans move: until `Sexp` carries
8316        // source positions, `position()` returns `None` for this variant.
8317        // A future run that gives `Sexp` source spans adds `pos:
8318        // Option<usize>` to ONE place; this test gives that change a
8319        // deliberate fail-before/pass-after delta.
8320        let mut e = Expander::new_substitute_only();
8321        let err = e
8322            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8323            .expect_err("splice outside list must error");
8324        assert_eq!(err.position(), None);
8325    }
8326
8327    #[test]
8328    fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
8329        // End-to-end through the Display impl — pins the rendered diagnostic
8330        // a downstream tool sees today (REPL, tatara-check). The legacy
8331        // substring `"\`,@\` may only appear inside a list"` is preserved
8332        // verbatim AND the parenthetical `(got ,@xs)` names the offending
8333        // form; tools that pattern-match on the variant gain structural
8334        // binding to `got`.
8335        let mut e = Expander::new_substitute_only();
8336        let err = e
8337            .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8338            .expect_err("splice outside list must error");
8339        let msg = format!("{err}");
8340        assert_eq!(
8341            msg,
8342            "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
8343        );
8344    }
8345
8346    #[test]
8347    fn splice_inside_list_still_succeeds_under_both_paths() {
8348        // Negative control: a well-positioned splice (`,@xs` INSIDE a list)
8349        // continues to succeed under both paths — the new gate only fires
8350        // when the splice is the entire body. Pins that the gate is scoped
8351        // to top-level only, not all `,@` occurrences. Uses a `&rest`-bound
8352        // list so `xs` is unambiguously a Sexp::List `(1 2)` rather than a
8353        // bare list-literal whose first symbol would also splice through.
8354        let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
8355        let mut subst = Expander::new_substitute_only();
8356        let mut bytecode = Expander::new();
8357        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8358        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8359        assert_eq!(out_subst, out_byte);
8360        assert_eq!(out_subst[0], parse("(outer 1 2)"));
8361    }
8362
8363    // ── splice_value_into: the shared splice-result coercion ──
8364
8365    #[test]
8366    fn splice_value_into_list_flattens_elements_into_builder() {
8367        // The canonical splice arm: a bound LIST contributes its elements
8368        // in order, preserving anything already in the builder.
8369        let mut builder = vec![Sexp::symbol("outer")];
8370        splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8371        assert_eq!(
8372            builder,
8373            vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
8374        );
8375    }
8376
8377    #[test]
8378    fn splice_value_into_nil_is_a_noop() {
8379        // Splicing the empty list (`Sexp::Nil`) contributes nothing —
8380        // the builder is unchanged.
8381        let mut builder = vec![Sexp::symbol("outer")];
8382        splice_value_into(&mut builder, &Sexp::Nil);
8383        assert_eq!(builder, vec![Sexp::symbol("outer")]);
8384    }
8385
8386    #[test]
8387    fn splice_value_into_scalar_pushes_single_element() {
8388        // A non-list, non-nil bound value degrades `,@x` to `,x`: it
8389        // splices as exactly one element. Pins the "free middle" coercion
8390        // every scalar shape (int, keyword, …) shares.
8391        let mut builder = vec![Sexp::symbol("outer")];
8392        splice_value_into(&mut builder, &Sexp::int(5));
8393        assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
8394        let mut other: Vec<Sexp> = vec![];
8395        splice_value_into(&mut other, &Sexp::keyword("k"));
8396        assert_eq!(other, vec![Sexp::keyword("k")]);
8397    }
8398
8399    #[test]
8400    fn splice_of_non_list_value_coerces_identically_under_both_paths() {
8401        // The point of the lift: the NON-list splice arms (scalar → single
8402        // element, nil → nothing) coerce identically under the substitute
8403        // AND bytecode strategies. Before the coercion was lifted to ONE
8404        // primitive these two arms lived inline at two sites; this test
8405        // pins that the two strategies cannot drift on the non-list arms.
8406        let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
8407        let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
8408        for src in [scalar, empty] {
8409            let mut subst = Expander::new_substitute_only();
8410            let mut bytecode = Expander::new();
8411            let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8412            let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8413            assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
8414        }
8415        let mut e = Expander::new();
8416        assert_eq!(
8417            e.expand_program(read(scalar).unwrap()).unwrap()[0],
8418            parse("(outer 5)")
8419        );
8420        let mut e2 = Expander::new();
8421        assert_eq!(
8422            e2.expand_program(read(empty).unwrap()).unwrap()[0],
8423            parse("(outer)")
8424        );
8425    }
8426
8427    // ── Missing macro arg: structural variant + path-uniform rejection ──
8428
8429    /// Helper for the missing-macro-arg tests — pins the variant shape
8430    /// and carries the failing macro's name + un-bound param up to the
8431    /// assert site for legibility. Sibling of `unbound_var`,
8432    /// `non_symbol_target`, and `splice_outside_list_got`.
8433    fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
8434        match err {
8435            LispError::MissingMacroArg { macro_name, param } => {
8436                (macro_name.as_str(), param.as_str())
8437            }
8438            other => panic!("expected MissingMacroArg, got: {other:?}"),
8439        }
8440    }
8441
8442    #[test]
8443    fn missing_macro_arg_in_compile_template_emits_structural_variant() {
8444        // `(need-two 1)` against `(need-two a b)` — `b` has no arg. Path:
8445        // `apply_compiled` (the bytecode-template path, default expander).
8446        // Pins variant identity AND macro_name AND the un-bound param so a
8447        // regression that re-inlines the legacy `LispError::Compile` shape
8448        // fails-loudly here.
8449        let mut e = Expander::new();
8450        let err = e
8451            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8452            .expect_err("missing required macro arg must error");
8453        let (macro_name, param) = missing_macro_arg_fields(&err);
8454        assert_eq!(macro_name, "need-two");
8455        assert_eq!(param, "b");
8456    }
8457
8458    #[test]
8459    fn missing_macro_arg_in_substitute_emits_structural_variant() {
8460        // Same shape as the bytecode test but routed through the
8461        // substitute-only expander → `bind_args` is the failing site.
8462        // Proves the substitute path emits the SAME structural variant the
8463        // bytecode path emits — `missing required arg` rejection is
8464        // path-uniform across both expansion strategies.
8465        let mut e = Expander::new_substitute_only();
8466        let err = e
8467            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8468            .expect_err("missing required macro arg must error");
8469        let (macro_name, param) = missing_macro_arg_fields(&err);
8470        assert_eq!(macro_name, "need-two");
8471        assert_eq!(param, "b");
8472    }
8473
8474    #[test]
8475    fn missing_macro_arg_first_position_is_named() {
8476        // `(f)` against `(f a b)` — `a` (the FIRST required param) has no
8477        // arg. The variant names `a`, not `b` — naming the LEFTMOST
8478        // un-bound param is the shape `bind_args` / `apply_compiled` both
8479        // emit (each iterates positionally and bails on the first missing
8480        // slot). Pins the leftmost-bail contract so a regression that
8481        // names the rightmost (or a surplus) param fails-loudly.
8482        let mut e = Expander::new();
8483        let err = e
8484            .expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
8485            .expect_err("missing first required arg must error");
8486        let (macro_name, param) = missing_macro_arg_fields(&err);
8487        assert_eq!(macro_name, "f");
8488        assert_eq!(param, "a");
8489    }
8490
8491    #[test]
8492    fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
8493        // Path-uniform rejection: the SAME source emits the SAME structural
8494        // variant under both expansion strategies. Negative control for
8495        // the divergence-closing posture: a future refactor that drifts
8496        // either path's rejection shape (or drops one path's rejection
8497        // entirely) fails-loudly here. Sibling of
8498        // `splice_outside_list_substitute_and_bytecode_paths_agree` —
8499        // both close `THEORY.md §II.1 invariant 2 — free middle` for one
8500        // failure mode each.
8501        let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
8502        let mut subst = Expander::new_substitute_only();
8503        let mut bytecode = Expander::new();
8504        let err_subst = subst
8505            .expand_program(read(src).unwrap())
8506            .expect_err("substitute must error");
8507        let err_byte = bytecode
8508            .expand_program(read(src).unwrap())
8509            .expect_err("bytecode must error");
8510        assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
8511        assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
8512    }
8513
8514    #[test]
8515    fn missing_macro_arg_position_is_none_today() {
8516        // Negative control for the future-spans move: until `Sexp` carries
8517        // source positions, `position()` returns `None` for this variant.
8518        // A future run that gives `Sexp` source spans adds `pos:
8519        // Option<usize>` to ONE place; this test gives that change a
8520        // deliberate fail-before/pass-after delta. Parallel to
8521        // `splice_outside_list_position_is_none_today`.
8522        let mut e = Expander::new();
8523        let err = e
8524            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8525            .expect_err("missing required macro arg must error");
8526        assert_eq!(err.position(), None);
8527    }
8528
8529    #[test]
8530    fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
8531        // End-to-end through the Display impl — pins the rendered diagnostic
8532        // a downstream tool sees today (REPL, tatara-check). The legacy
8533        // substring `"missing required arg: {param}"` is preserved verbatim
8534        // AND the head clause names the failing macro via `"call to
8535        // {macro_name}"`; tools that pattern-match on the variant gain
8536        // structural binding to `macro_name` / `param`.
8537        let mut e = Expander::new();
8538        let err = e
8539            .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8540            .expect_err("missing required macro arg must error");
8541        assert_eq!(
8542            format!("{err}"),
8543            "compile error in call to need-two: missing required arg: b"
8544        );
8545    }
8546
8547    #[test]
8548    fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
8549        // Both `macro_name` (`wrap-twice`) and `param` (`notify-ref`)
8550        // round-trip through the variant unchanged. Pinning this contract
8551        // means a regression that camelCases or lowercases either side
8552        // fails-loudly here. Parallel to the
8553        // `unknown_kwarg_display_carries_kebab_case_keys_unchanged`
8554        // assertion for the kwarg-gate's symmetric surface.
8555        let mut e = Expander::new();
8556        let err = e
8557            .expand_program(
8558                read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
8559                    .unwrap(),
8560            )
8561            .expect_err("missing required macro arg must error");
8562        let (macro_name, param) = missing_macro_arg_fields(&err);
8563        assert_eq!(macro_name, "wrap-twice");
8564        assert_eq!(param, "body");
8565    }
8566
8567    #[test]
8568    fn rest_param_only_macro_with_no_args_still_succeeds() {
8569        // Negative control: a macro whose only param is `&rest` must NOT
8570        // error when called with zero args — the rest-param binds to the
8571        // empty list. The new structural variant fires only on REQUIRED
8572        // params; the `Param::Rest` arm in both `bind_args` and
8573        // `apply_compiled` continues to bind the empty tail. Pins that the
8574        // helper is scoped to required-param failure, not all
8575        // arity-mismatch shapes.
8576        let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
8577        let mut subst = Expander::new_substitute_only();
8578        let mut bytecode = Expander::new();
8579        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8580        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8581        assert_eq!(out_subst, out_byte);
8582        assert_eq!(out_subst[0], parse("(list)"));
8583    }
8584
8585    // ── TooManyMacroArgs: call-site mirror of RestParamTrailingTokens ──
8586    //
8587    // A rest-less param list has a FIXED maximum arity equal to
8588    // `required.len() + optional.len()`. Surplus call args have nowhere to
8589    // bind. Before this gate the surplus was silently truncated to the
8590    // slice the binder could consume — the typed-entry macro-call-gate
8591    // rejected too-few-args loudly (`MissingMacroArg`) but accepted
8592    // too-many silently, an asymmetry the definition-side `&rest <name>
8593    // extra` rejection (`RestParamTrailingTokens`) had no call-side dual.
8594    // After this gate the call-site arity surface is structurally
8595    // complete in both directions; the substitute + bytecode paths share
8596    // `MacroParams::bind`, so both inherit the rejection without drift.
8597
8598    /// Helper for the too-many-args tests — projects to (macro_name,
8599    /// expected, got) for legibility. Sibling of `missing_macro_arg_fields`.
8600    fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
8601        match err {
8602            LispError::TooManyMacroArgs {
8603                macro_name,
8604                expected,
8605                got,
8606            } => (macro_name.as_str(), *expected, *got),
8607            other => panic!("expected TooManyMacroArgs, got: {other:?}"),
8608        }
8609    }
8610
8611    #[test]
8612    fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
8613        // `(defmacro f (a b) ...)` called as `(f 1 2 3)` — `3` has
8614        // nowhere to bind. The rest-less binder rejects via
8615        // `TooManyMacroArgs { macro_name: "f", expected: 2, got: 3 }`,
8616        // NOT silently drops `3`. Pins both the variant identity AND the
8617        // structural fields the typed gate exposes for authoring-tool
8618        // quick-fixes ("you supplied 3 args; the macro takes at most
8619        // 2").
8620        let mut e = Expander::new();
8621        let err = e
8622            .expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8623            .expect_err("surplus arg on rest-less call must error");
8624        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8625        assert_eq!(macro_name, "f");
8626        assert_eq!(expected, 2);
8627        assert_eq!(got, 3);
8628    }
8629
8630    #[test]
8631    fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
8632        // The rest-less binder's fixed maximum arity is `required.len() +
8633        // optional.len()` — the optional section CONTRIBUTES to capacity.
8634        // `(defmacro f (a &optional b) ...)` accepts 1 OR 2 args; 3
8635        // args rejects with `expected: 2` (required + optional, NOT just
8636        // required). Pins the optional-counts-in-capacity contract so a
8637        // regression that omits optionals from the expected calculation
8638        // (and erroneously rejects 2-arg calls) fails-loudly here.
8639        let mut e = Expander::new();
8640        let err = e
8641            .expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8642            .expect_err("surplus arg beyond required+optional must error");
8643        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8644        assert_eq!(macro_name, "f");
8645        assert_eq!(expected, 2);
8646        assert_eq!(got, 3);
8647    }
8648
8649    #[test]
8650    fn too_many_macro_args_required_plus_two_optionals_arity_three() {
8651        // Larger optional section (capacity 1 + 2 = 3). 4 args rejects
8652        // with `expected: 3`. Pins that the capacity calculation scales
8653        // with optional.len(), not just at-most-one. Mixes a bare
8654        // optional with an optional carrying a default form — both shapes
8655        // contribute identically to capacity (the typed `OptionalParam`
8656        // entry's `default: Option<Sexp>` is irrelevant to the arity gate).
8657        let mut e = Expander::new();
8658        let err = e
8659            .expand_program(
8660                read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
8661            )
8662            .expect_err("surplus arg beyond required+two-optional must error");
8663        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8664        assert_eq!(macro_name, "f");
8665        assert_eq!(expected, 3);
8666        assert_eq!(got, 4);
8667    }
8668
8669    #[test]
8670    fn too_many_macro_args_does_not_fire_when_rest_is_present() {
8671        // Negative control: a rest-PRESENT param list has no maximum
8672        // arity — the `&rest` slot collects every trailing arg into a
8673        // `Sexp::List`. `(defmacro f (a &rest xs) ...)` called as
8674        // `(f 1 2 3 4)` MUST succeed; the new gate fires ONLY when
8675        // `MacroParams.rest` is `None`. Pins the rest-present-path
8676        // remains permissive — a regression that wrongly fires the
8677        // too-many gate for any surplus (including the rest-collecting
8678        // path) would break every `&rest`-using macro.
8679        let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
8680        let mut subst = Expander::new_substitute_only();
8681        let mut bytecode = Expander::new();
8682        let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8683        let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8684        assert_eq!(out_subst, out_byte);
8685        assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
8686    }
8687
8688    #[test]
8689    fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
8690        // Negative control: the rest-less gate fires STRICTLY when
8691        // `args.len() > expected` — at exact arity the binder accepts.
8692        // `(defmacro f (a &optional b) ...)` called as `(f 1 2)` binds
8693        // a=1, b=2 successfully (the optional takes its supplied arg,
8694        // not the default). Pins the boundary condition so a regression
8695        // that flips the comparison to `>=` (rejecting exact-arity
8696        // calls) fails-loudly here.
8697        let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
8698        let mut e = Expander::new();
8699        let out = e.expand_program(read(src).unwrap()).unwrap();
8700        assert_eq!(out[0], parse("(list 1 2)"));
8701    }
8702
8703    #[test]
8704    fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
8705        // Path-uniform rejection: the SAME source emits the SAME
8706        // structural variant under both expansion strategies. The
8707        // shared `MacroParams::bind` makes the rejection lands once and
8708        // both paths inherit it. Mirror of
8709        // `missing_macro_arg_substitute_and_bytecode_paths_agree` —
8710        // both close `THEORY.md §II.1 invariant 2 — free middle` for
8711        // one failure mode each.
8712        let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
8713        let mut subst = Expander::new_substitute_only();
8714        let mut bytecode = Expander::new();
8715        let err_subst = subst
8716            .expand_program(read(src).unwrap())
8717            .expect_err("substitute must error");
8718        let err_byte = bytecode
8719            .expand_program(read(src).unwrap())
8720            .expect_err("bytecode must error");
8721        assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
8722        assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
8723    }
8724
8725    #[test]
8726    fn too_many_macro_args_fires_after_missing_required_priority_held() {
8727        // Priority discipline: the required walk fires
8728        // `MissingMacroArg` BEFORE the rest-less surplus gate is
8729        // reached. `(defmacro f (a b c) …) (f 1)` is `MissingMacroArg
8730        // { param: "b" }`, NOT `TooManyMacroArgs` (and certainly not a
8731        // collision). The two failure modes are structurally disjoint:
8732        // too-few-required vs. too-many-with-no-rest. Pins the bail-on-
8733        // first-missing-required contract so a regression that swaps
8734        // the two gates' order would emit the wrong variant.
8735        let mut e = Expander::new();
8736        let err = e
8737            .expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
8738            .expect_err("missing required must error");
8739        assert!(
8740            matches!(err, LispError::MissingMacroArg { .. }),
8741            "expected MissingMacroArg (priority), got: {err:?}"
8742        );
8743    }
8744
8745    #[test]
8746    fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
8747        // Degenerate case: a nullary macro `(defmacro f () ...)` has
8748        // capacity 0; ANY supplied arg rejects with `expected: 0`. Pins
8749        // the gate fires even when the rest-less max-arity is zero —
8750        // i.e. the rejection is structural, not conditional on a
8751        // non-empty required+optional.
8752        let mut e = Expander::new();
8753        let err = e
8754            .expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
8755            .expect_err("nullary macro called with arg must error");
8756        let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8757        assert_eq!(macro_name, "f");
8758        assert_eq!(expected, 0);
8759        assert_eq!(got, 1);
8760    }
8761
8762    #[test]
8763    fn too_many_macro_args_display_renders_legacy_compile_substring() {
8764        // The rendered Display matches the legacy `Compile`-shaped
8765        // diagnostic style — `"compile error in call to {macro_name}:
8766        // too many args: expected at most {expected}, got {got}"` — so
8767        // the existing `"compile error in call to"` substring authoring
8768        // tools' assertions key on stays unchanged. Pins the byte-level
8769        // rendered shape so a regression that drifts the prefix /
8770        // separator / labels fails-loudly here.
8771        let err = LispError::TooManyMacroArgs {
8772            macro_name: "pair".into(),
8773            expected: 2,
8774            got: 5,
8775        };
8776        assert_eq!(
8777            err.to_string(),
8778            "compile error in call to pair: too many args: expected at most 2, got 5"
8779        );
8780    }
8781
8782    #[test]
8783    fn too_many_macro_args_position_is_none_today() {
8784        // Negative control for the future-spans move: until `Sexp`
8785        // carries source positions, `position()` returns `None` for
8786        // this variant. A future run that gives `Sexp` source spans
8787        // adds `pos: Option<usize>` to ONE place; this test gives that
8788        // change a deliberate fail-before/pass-after delta. Parallel to
8789        // `missing_macro_arg_position_is_none_today`.
8790        let err = LispError::TooManyMacroArgs {
8791            macro_name: "pair".into(),
8792            expected: 2,
8793            got: 3,
8794        };
8795        assert_eq!(err.position(), None);
8796    }
8797
8798    /// Helper for the non-symbol-param tests — pins the variant shape and
8799    /// carries the failing position + offending element up to the assert
8800    /// site for legibility. Sibling of `missing_macro_arg_fields`.
8801    fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
8802        match err {
8803            LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
8804            other => panic!("expected NonSymbolParam, got: {other:?}"),
8805        }
8806    }
8807
8808    #[test]
8809    fn non_symbol_param_at_first_position_emits_structural_variant() {
8810        // `(defmacro f (5) ...)` — the first element of the param list is
8811        // an integer literal, not a symbol. Pins variant identity AND
8812        // that `position` is the loop index inside `parse_params` (0 for
8813        // the first slot) AND that `got` is the offending element via
8814        // `Sexp::Display` (`5`). A regression that re-inlines the legacy
8815        // `LispError::Compile` shape (which named neither the position
8816        // nor the offending element) fails-loudly here.
8817        let mut e = Expander::new();
8818        let err = e
8819            .expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
8820            .expect_err("non-symbol param must error");
8821        let (position, got) = non_symbol_param_fields(&err);
8822        assert_eq!(position, 0);
8823        assert_eq!(got, "5");
8824    }
8825
8826    #[test]
8827    fn non_symbol_param_at_second_position_emits_structural_variant() {
8828        // `(defmacro f (a 5) ...)` — `a` parses fine, `5` at position 1
8829        // misfires. Pins that `position` advances with the loop index, so
8830        // an LSP quick-fix that wants to point at "the second element of
8831        // your param list" gains the index as data, no source re-parse
8832        // required.
8833        let mut e = Expander::new();
8834        let err = e
8835            .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8836            .expect_err("non-symbol param must error");
8837        let (position, got) = non_symbol_param_fields(&err);
8838        assert_eq!(position, 1);
8839        assert_eq!(got, "5");
8840    }
8841
8842    #[test]
8843    fn non_symbol_param_carries_keyword_value_unchanged() {
8844        // `:k` at a param-list position. `Sexp::Display` for
8845        // `Atom::Keyword(s)` writes `:s`; pins that the variant's `got`
8846        // field round-trips the keyword form unchanged so an LSP that
8847        // surfaces "you wrote `:k` where a symbol was expected" gains
8848        // the literal keyword value as data, no re-parsing required.
8849        let mut e = Expander::new();
8850        let err = e
8851            .expand_program(read("(defmacro f (:k) `(list))").unwrap())
8852            .expect_err("non-symbol param must error");
8853        let (position, got) = non_symbol_param_fields(&err);
8854        assert_eq!(position, 0);
8855        assert_eq!(got, ":k");
8856    }
8857
8858    #[test]
8859    fn non_symbol_param_carries_nested_list_value_unchanged() {
8860        // A nested list at a param-list position. `Sexp::Display` for
8861        // `List(xs)` writes `(<x1> <x2> ...)`; pins that the variant's
8862        // `got` field carries the nested form's full Display projection
8863        // unchanged so the operator sees what they wrote.
8864        let mut e = Expander::new();
8865        let err = e
8866            .expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
8867            .expect_err("non-symbol param must error");
8868        let (position, got) = non_symbol_param_fields(&err);
8869        assert_eq!(position, 0);
8870        assert_eq!(got, "(nested)");
8871    }
8872
8873    #[test]
8874    fn non_symbol_param_in_defpoint_template_emits_same_variant() {
8875        // `defpoint-template` shares `parse_params` with `defmacro` (all
8876        // three head keywords route through `macro_def_from`). Pins that
8877        // the lift fires path-uniformly across the three head keywords
8878        // — `defmacro`, `defpoint-template`, `defcheck` — so the
8879        // structural-completeness floor holds for every defmacro-shaped
8880        // form, not just the one with the `defmacro` head literal.
8881        let mut e = Expander::new();
8882        let err = e
8883            .expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
8884            .expect_err("non-symbol param must error");
8885        let (position, got) = non_symbol_param_fields(&err);
8886        assert_eq!(position, 0);
8887        assert_eq!(got, "5");
8888    }
8889
8890    #[test]
8891    fn non_symbol_param_in_defcheck_emits_same_variant() {
8892        // Sibling of the defpoint-template test — `defcheck` is the
8893        // third head keyword `macro_def_from` recognizes. All three
8894        // route through the same `parse_params` and now reject
8895        // non-symbol params with the same structural variant.
8896        let mut e = Expander::new();
8897        let err = e
8898            .expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
8899            .expect_err("non-symbol param must error");
8900        let (position, got) = non_symbol_param_fields(&err);
8901        assert_eq!(position, 1);
8902        assert_eq!(got, "5");
8903    }
8904
8905    #[test]
8906    fn non_symbol_param_position_is_none_today() {
8907        // Negative control for the future-spans move: until `Sexp`
8908        // carries source positions, `position()` on `LispError` returns
8909        // `None` for this variant. A future run that gives `Sexp`
8910        // source spans adds `pos: Option<usize>` to ONE place; this
8911        // test gives that change a deliberate fail-before/pass-after
8912        // delta. Parallel to `missing_macro_arg_position_is_none_today`.
8913        let mut e = Expander::new();
8914        let err = e
8915            .expand_program(read("(defmacro f (5) `(list))").unwrap())
8916            .expect_err("non-symbol param must error");
8917        assert_eq!(err.position(), None);
8918    }
8919
8920    #[test]
8921    fn non_symbol_param_message_renders_legacy_substring_with_position() {
8922        // End-to-end through Display — pins the rendered diagnostic that
8923        // downstream tools (REPL, `tatara-check`) see today. Legacy
8924        // substrings `"defmacro params"` AND `"expected symbol"` are
8925        // preserved verbatim; the appended `at position {position}, got
8926        // {got}` clause is the new structural detail. Tools that
8927        // pattern-match on the variant gain structural binding to
8928        // `position` / `got`.
8929        let mut e = Expander::new();
8930        let err = e
8931            .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8932            .expect_err("non-symbol param must error");
8933        assert_eq!(
8934            format!("{err}"),
8935            "compile error in defmacro params: \
8936             expected symbol at position 1, got 5"
8937        );
8938    }
8939
8940    #[test]
8941    fn non_symbol_param_substitute_and_bytecode_paths_agree() {
8942        // Path-uniform rejection: the SAME source emits the SAME
8943        // structural variant under both expansion strategies. The
8944        // defmacro-syntax-gate fires inside `macro_def_from` →
8945        // `parse_params`, BEFORE either strategy's expansion path runs;
8946        // so both `Expander::new()` (bytecode) and
8947        // `Expander::new_substitute_only()` (substitute) reject the
8948        // SAME malformed defmacro at the SAME gate. Sibling of
8949        // `missing_macro_arg_substitute_and_bytecode_paths_agree`.
8950        let src = "(defmacro f (a 5) `(,a))";
8951        let mut subst = Expander::new_substitute_only();
8952        let mut bytecode = Expander::new();
8953        let err_subst = subst
8954            .expand_program(read(src).unwrap())
8955            .expect_err("substitute must error");
8956        let err_byte = bytecode
8957            .expand_program(read(src).unwrap())
8958            .expect_err("bytecode must error");
8959        assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
8960        assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
8961    }
8962
8963    /// Helper for the rest-param-missing-name tests — pins the variant
8964    /// shape and carries the marker position + offending follower (or
8965    /// its absence) up to the assert site for legibility. Sibling of
8966    /// `non_symbol_param_fields`.
8967    fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
8968        match err {
8969            LispError::RestParamMissingName { rest_position, got } => {
8970                (*rest_position, got.as_ref().map(|w| w.display.as_str()))
8971            }
8972            other => panic!("expected RestParamMissingName, got: {other:?}"),
8973        }
8974    }
8975
8976    #[test]
8977    fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
8978        // `(defmacro f (&rest))` — the marker is the only param-list
8979        // element; nothing follows. Pins variant identity AND that
8980        // `rest_position == 0` (the first slot) AND that `got == None`
8981        // (no follower exists). A regression that re-inlines the legacy
8982        // `LispError::Compile` shape (which named neither field) fails-
8983        // loudly here.
8984        let mut e = Expander::new();
8985        let err = e
8986            .expand_program(read("(defmacro f (&rest) `(list))").unwrap())
8987            .expect_err("&rest with no follower must error");
8988        let (rest_position, got) = rest_param_missing_name_fields(&err);
8989        assert_eq!(rest_position, 0);
8990        assert_eq!(got, None);
8991    }
8992
8993    #[test]
8994    fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
8995        // `(defmacro f (a &rest))` — `a` parses fine, `&rest` at param-list
8996        // position 1 has no follower at all. Pins that `rest_position`
8997        // advances with the loop index, so an LSP quick-fix that wants to
8998        // point at "your `&rest` at position 1 has no name" gains the
8999        // marker position as data, no source re-parse required.
9000        let mut e = Expander::new();
9001        let err = e
9002            .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9003            .expect_err("&rest with no follower must error");
9004        let (rest_position, got) = rest_param_missing_name_fields(&err);
9005        assert_eq!(rest_position, 1);
9006        assert_eq!(got, None);
9007    }
9008
9009    #[test]
9010    fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
9011        // `(defmacro f (&rest 5))` — `&rest` at position 0 followed by
9012        // `5` (an integer literal, not a symbol). Pins that the variant's
9013        // `got` field is `Some` and carries the offending follower's
9014        // `Sexp::Display` projection; the bifurcation between "missing
9015        // entirely" and "present but non-symbol" is in the renderable
9016        // detail, not in what the gate rejects.
9017        let mut e = Expander::new();
9018        let err = e
9019            .expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
9020            .expect_err("&rest followed by non-symbol must error");
9021        let (rest_position, got) = rest_param_missing_name_fields(&err);
9022        assert_eq!(rest_position, 0);
9023        assert_eq!(got, Some("5"));
9024    }
9025
9026    #[test]
9027    fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
9028        // `(defmacro f (a &rest :foo))` — keyword follower at the rest-name
9029        // slot. `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pins
9030        // that the variant's `got` field round-trips the keyword form
9031        // unchanged so an LSP that surfaces "you wrote `:foo` where a
9032        // rest-name was expected" gains the literal keyword value as
9033        // data, no re-parsing required.
9034        let mut e = Expander::new();
9035        let err = e
9036            .expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
9037            .expect_err("&rest followed by keyword must error");
9038        let (rest_position, got) = rest_param_missing_name_fields(&err);
9039        assert_eq!(rest_position, 1);
9040        assert_eq!(got, Some(":foo"));
9041    }
9042
9043    #[test]
9044    fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
9045        // `(defmacro f (&rest (nested)))` — nested-list follower at the
9046        // rest-name slot. `Sexp::Display` for `List(xs)` writes
9047        // `(<x1> <x2> ...)`; pins that the variant's `got` field carries
9048        // the nested form's full Display projection unchanged so the
9049        // operator sees what they wrote.
9050        let mut e = Expander::new();
9051        let err = e
9052            .expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
9053            .expect_err("&rest followed by list must error");
9054        let (rest_position, got) = rest_param_missing_name_fields(&err);
9055        assert_eq!(rest_position, 0);
9056        assert_eq!(got, Some("(nested)"));
9057    }
9058
9059    #[test]
9060    fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
9061        // `defpoint-template` shares `parse_params` with `defmacro` (all
9062        // three head keywords route through `macro_def_from`). Pins that
9063        // the lift fires path-uniformly across the three head keywords —
9064        // a regression that handles `defpoint-template`'s param list
9065        // differently from `defmacro`'s would fail-loudly here.
9066        let mut e = Expander::new();
9067        let err = e
9068            .expand_program(read("(defpoint-template t (a &rest) `(,a))").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, 1);
9072        assert_eq!(got, None);
9073    }
9074
9075    #[test]
9076    fn rest_param_missing_name_in_defcheck_emits_same_variant() {
9077        // Sibling for the `defcheck` head; rounds out the three-head-
9078        // keyword coverage so the lift is path-uniform across
9079        // `defmacro` / `defpoint-template` / `defcheck`. After this
9080        // test the defmacro-syntax-gate rejects `&rest`-without-name
9081        // identically across all three head keywords — the
9082        // typed-entry surface is single-shape across the cluster.
9083        let mut e = Expander::new();
9084        let err = e
9085            .expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
9086            .expect_err("&rest followed by non-symbol must error");
9087        let (rest_position, got) = rest_param_missing_name_fields(&err);
9088        assert_eq!(rest_position, 0);
9089        assert_eq!(got, Some("5"));
9090    }
9091
9092    #[test]
9093    fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
9094        // Path-uniform rejection: the SAME source emits the SAME
9095        // structural variant under both expansion strategies. The
9096        // defmacro-syntax-gate fires inside `macro_def_from` →
9097        // `parse_params`, BEFORE either strategy's expansion path
9098        // runs; so both `Expander::new()` (bytecode) and
9099        // `Expander::new_substitute_only()` (substitute) reject the
9100        // SAME malformed defmacro at the SAME gate. Sibling of
9101        // `non_symbol_param_substitute_and_bytecode_paths_agree`.
9102        let src = "(defmacro f (a &rest 5) `(,a))";
9103        let mut subst = Expander::new_substitute_only();
9104        let mut bytecode = Expander::new();
9105        let err_subst = subst
9106            .expand_program(read(src).unwrap())
9107            .expect_err("substitute must error");
9108        let err_byte = bytecode
9109            .expand_program(read(src).unwrap())
9110            .expect_err("bytecode must error");
9111        assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
9112        assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
9113    }
9114
9115    #[test]
9116    fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
9117        // End-to-end through Display — pins the rendered diagnostic
9118        // consumers see today (REPL, tatara-check) AND the new `(rest
9119        // marker at position {rest_position}, got {got})` clause. The
9120        // legacy `"&rest needs a name"` substring rides through
9121        // verbatim.
9122        let mut e = Expander::new();
9123        let err = e
9124            .expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
9125            .expect_err("&rest followed by non-symbol must error");
9126        assert_eq!(
9127            format!("{err}"),
9128            "compile error in defmacro params: &rest needs a name \
9129             (rest marker at position 1, got 5)"
9130        );
9131    }
9132
9133    #[test]
9134    fn rest_param_missing_name_message_renders_none_provided_when_follower_absent() {
9135        // Same as the prior test but for the "missing entirely" branch.
9136        // The renderable detail is `(rest marker at position
9137        // {rest_position}, none provided)` — naming the absence
9138        // structurally instead of an empty / partial parenthetical.
9139        let mut e = Expander::new();
9140        let err = e
9141            .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9142            .expect_err("&rest with no follower must error");
9143        assert_eq!(
9144            format!("{err}"),
9145            "compile error in defmacro params: &rest needs a name \
9146             (rest marker at position 1, none provided)"
9147        );
9148    }
9149
9150    #[test]
9151    fn rest_param_missing_name_position_is_none_today() {
9152        // Pins that `position()` returns `None` so the future `pos:
9153        // Option<usize>` add (once `Sexp` carries source spans) lands
9154        // as a deliberate fail-before/pass-after delta rather than a
9155        // silent default. Parallel to
9156        // `non_symbol_param_position_is_none_today` and
9157        // `missing_macro_arg_position_is_none_today`.
9158        let err_missing = LispError::RestParamMissingName {
9159            rest_position: 1,
9160            got: None,
9161        };
9162        assert_eq!(err_missing.position(), None);
9163        let err_got = LispError::RestParamMissingName {
9164            rest_position: 0,
9165            got: Some(crate::error::SexpWitness::new(
9166                crate::error::SexpShape::Int,
9167                "5",
9168            )),
9169        };
9170        assert_eq!(err_got.position(), None);
9171    }
9172
9173    // --- RestParamTrailingTokens: the parse_params gate's third (and
9174    // final) definition-site failure mode ---
9175    //
9176    // A `&rest <name>` absorbs every remaining call arg, so it is the LAST
9177    // thing a param list can name. Before this variant `parse_params`
9178    // returned the moment it bound the rest name, SILENTLY DROPPING any
9179    // trailing tokens — `(a &rest xs extra)` parsed as if `extra` weren't
9180    // there. These tests pin the loud rejection that replaces the silent
9181    // drop; the symbol `RestParamTrailingTokens` exists only after this
9182    // change, so the whole block is fail-before/pass-after by construction
9183    // (compile-time edge) and the end-to-end regression guard below pins
9184    // that the malformed defmacro no longer expands cleanly.
9185
9186    /// Helper mirroring `rest_param_missing_name_fields` — pins the variant
9187    /// shape and lifts the marker position, trailing count, and first
9188    /// offender's display up to the assert site.
9189    fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
9190        match err {
9191            LispError::RestParamTrailingTokens {
9192                rest_position,
9193                extra,
9194                first,
9195            } => (*rest_position, *extra, first.display.as_str()),
9196            other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
9197        }
9198    }
9199
9200    #[test]
9201    fn parse_params_rejects_single_trailing_token_after_rest_name() {
9202        // `(a &rest c extra)` — `&rest c` is well-formed, but `extra`
9203        // follows. The rest name is bound at position 2, the marker at 1;
9204        // the lone trailing token `extra` is reported (extra == 1, first ==
9205        // "extra"). Before this variant `parse_params` returned at the rest
9206        // name and `extra` vanished.
9207        let err = parse_params(&read("a &rest c extra").unwrap())
9208            .expect_err("a trailing token after the rest name must error");
9209        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9210    }
9211
9212    #[test]
9213    fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
9214        // `(&rest c x y z)` — three tokens follow the rest name. `extra`
9215        // counts ALL of them (3), `first` is the first (`x`), and the
9216        // marker is at position 0. A regression that reports only the
9217        // first trailing token's presence (extra hard-coded to 1) fails
9218        // loudly here.
9219        let err = parse_params(&read("&rest c x y z").unwrap())
9220            .expect_err("multiple trailing tokens must error");
9221        assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
9222    }
9223
9224    #[test]
9225    fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
9226        // `(a &rest c 5)` — the rest NAME `c` is a valid symbol, so this is
9227        // NOT a `RestParamMissingName`; the integer `5` is a trailing token
9228        // AFTER a well-formed `&rest c`. Pins that the two sibling failure
9229        // modes don't collide: a malformed rest-name is `RestParamMissingName`,
9230        // a well-formed rest-name followed by junk is
9231        // `RestParamTrailingTokens`. `first` round-trips `5` via the typed
9232        // witness's `Sexp::Display` projection.
9233        let err = parse_params(&read("a &rest c 5").unwrap())
9234            .expect_err("a trailing non-symbol after the rest name must error");
9235        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
9236    }
9237
9238    #[test]
9239    fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
9240        // The fidelity fix, end-to-end through `expand_program`: a defmacro
9241        // whose param list carries a stray token after `&rest <name>` now
9242        // ERRORS at the typed-entry gate instead of expanding as though the
9243        // stray token weren't there. This is the regression guard for the
9244        // silent-drop bug — before this change the same source expanded
9245        // cleanly and `extra` was discarded with no signal.
9246        let mut e = Expander::new();
9247        let err = e
9248            .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9249            .expect_err("trailing token after &rest name must error");
9250        assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9251    }
9252
9253    #[test]
9254    fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
9255        // Path-uniform rejection: the gate fires inside `macro_def_from` →
9256        // `parse_params`, BEFORE either expansion strategy runs, so both
9257        // `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
9258        // (substitute) reject the SAME malformed defmacro at the SAME gate.
9259        // Sibling of `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9260        let src = "(defmacro f (a &rest xs extra) `(,a))";
9261        let mut subst = Expander::new_substitute_only();
9262        let mut bytecode = Expander::new();
9263        let err_subst = subst
9264            .expand_program(read(src).unwrap())
9265            .expect_err("substitute must error");
9266        let err_byte = bytecode
9267            .expand_program(read(src).unwrap())
9268            .expect_err("bytecode must error");
9269        assert_eq!(
9270            rest_param_trailing_tokens_fields(&err_subst),
9271            (1, 1, "extra")
9272        );
9273        assert_eq!(
9274            rest_param_trailing_tokens_fields(&err_byte),
9275            (1, 1, "extra")
9276        );
9277    }
9278
9279    #[test]
9280    fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
9281        // End-to-end through Display — pins the rendered diagnostic AND the
9282        // new `(rest marker at position {n}, {extra} trailing after name,
9283        // first: {first})` clause. The `compile error in defmacro params:`
9284        // prefix matches the sibling `&rest needs a name` rendering's shape.
9285        let mut e = Expander::new();
9286        let err = e
9287            .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9288            .expect_err("trailing token after &rest name must error");
9289        assert_eq!(
9290            format!("{err}"),
9291            "compile error in defmacro params: &rest name must be last \
9292             (rest marker at position 1, 1 trailing after name, first: extra)"
9293        );
9294    }
9295
9296    #[test]
9297    fn rest_param_trailing_tokens_position_is_none_today() {
9298        // Pins `position() == None` so the future `pos: Option<usize>` add
9299        // (once `Sexp` carries source spans) lands as a deliberate
9300        // fail-before/pass-after delta. Parallel to
9301        // `rest_param_missing_name_position_is_none_today`.
9302        let err = LispError::RestParamTrailingTokens {
9303            rest_position: 1,
9304            extra: 1,
9305            first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
9306        };
9307        assert_eq!(err.position(), None);
9308    }
9309
9310    // --- MacroDefHead enum (the closed-set lift) ---
9311    //
9312    // The next nine tests pin the typed-enum lift that closes the
9313    // three-times rule on the `head: &str → &'static str` projection
9314    // idiom previously inlined at FOUR sites (the `matches!` gate at
9315    // the top of `macro_def_from` plus the projection match inside
9316    // each of `defmacro_arity`, `defmacro_non_symbol_name`,
9317    // `defmacro_non_list_params`). Every test in this block names
9318    // `MacroDefHead` directly — the symbol exists only after the
9319    // lift, so the entire block is fail-before/pass-after by
9320    // construction (compile-time edge). Theory anchor: THEORY.md
9321    // §VI.1 — three-times rule; THEORY.md §V.1 — the closed set is
9322    // a TYPE rather than a `matches!` literal.
9323
9324    #[test]
9325    fn macro_def_head_from_keyword_recognizes_defmacro() {
9326        // Pins that `MacroDefHead::from_keyword("defmacro")` returns
9327        // `Some(MacroDefHead::Defmacro)` — the first of the three
9328        // canonical macro-definition head keywords. A regression that
9329        // re-inlines a `matches!`-only gate (without the typed-enum
9330        // projection) deletes `from_keyword` and fails-loudly here.
9331        assert_eq!(
9332            MacroDefHead::from_keyword("defmacro"),
9333            Some(MacroDefHead::Defmacro)
9334        );
9335    }
9336
9337    #[test]
9338    fn macro_def_head_from_keyword_recognizes_defpoint_template() {
9339        // Pins that `MacroDefHead::from_keyword("defpoint-template")`
9340        // returns `Some(MacroDefHead::DefpointTemplate)` — the second
9341        // of the three canonical head keywords. The `defpoint-template`
9342        // form is the K8s-as-processes authoring surface (see
9343        // tatara-process); `macro_def_from` must recognize it
9344        // identically to `defmacro` so the `(defpoint-template …)`
9345        // form's macro-style binding works the same way.
9346        assert_eq!(
9347            MacroDefHead::from_keyword("defpoint-template"),
9348            Some(MacroDefHead::DefpointTemplate)
9349        );
9350    }
9351
9352    #[test]
9353    fn macro_def_head_from_keyword_recognizes_defcheck() {
9354        // Pins that `MacroDefHead::from_keyword("defcheck")` returns
9355        // `Some(MacroDefHead::Defcheck)` — the third and final
9356        // canonical head keyword. The `defcheck` form is the
9357        // workspace-coherence authoring surface (see
9358        // tatara-reconciler/checks.lisp); `macro_def_from` must
9359        // recognize it identically to `defmacro` so user-defined
9360        // checks inherit the macro-style binding semantics.
9361        assert_eq!(
9362            MacroDefHead::from_keyword("defcheck"),
9363            Some(MacroDefHead::Defcheck)
9364        );
9365    }
9366
9367    #[test]
9368    fn macro_def_head_from_keyword_rejects_unknown() {
9369        // Pins that `MacroDefHead::from_keyword` returns `None` for
9370        // anything outside the closed set — a non-symbol keyword
9371        // (`"if"`), a near-miss spelling (`"defmacroo"`,
9372        // `"defcheckk"`), and the empty string. `macro_def_from`
9373        // depends on this `None` projection to mean "this form is
9374        // not a defmacro form" and walk past — a regression that
9375        // accidentally accepts a near-miss head (e.g. via a
9376        // lower-cased `EqualFold` match) would route `(defmacroo …)`
9377        // through the arity gate, which is wrong. Pins all four
9378        // canonical near-miss / non-canonical inputs.
9379        assert_eq!(MacroDefHead::from_keyword("if"), None);
9380        assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
9381        assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
9382        assert_eq!(MacroDefHead::from_keyword(""), None);
9383    }
9384
9385    #[test]
9386    fn macro_def_head_keyword_round_trips_each_variant() {
9387        // Pins that `MacroDefHead::keyword` returns the canonical
9388        // `&'static str` literal for each variant. Together with
9389        // `from_keyword` this closes the bidirectional projection:
9390        // for every canonical head keyword `s`, `MacroDefHead::
9391        // from_keyword(s).unwrap().keyword() == s`. The `&'static
9392        // str` lifetime on the return type is load-bearing — it's
9393        // what lets the `LispError::Defmacro*` variants carry
9394        // `head: &'static str` slots without an arbitrary owned
9395        // `String`. Pinning the `: &'static str` binding here
9396        // makes the lifetime requirement load-bearing in the test.
9397        let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
9398        let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
9399        let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
9400        assert_eq!(s_defmacro, "defmacro");
9401        assert_eq!(s_defpoint, "defpoint-template");
9402        assert_eq!(s_defcheck, "defcheck");
9403    }
9404
9405    #[test]
9406    fn macro_def_head_keyword_round_trips_through_from_keyword() {
9407        // Pins that the two halves of the projection compose to the
9408        // identity on the closed set: for every canonical head
9409        // keyword, projecting `&str → MacroDefHead → &'static str`
9410        // returns the original literal. Sibling of
9411        // `macro_def_head_keyword_round_trips_each_variant` —
9412        // together they pin both directions of the bidirection.
9413        for kw in ["defmacro", "defpoint-template", "defcheck"] {
9414            let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
9415            assert_eq!(head.keyword(), kw);
9416        }
9417    }
9418
9419    #[test]
9420    fn macro_def_head_threads_through_defmacro_arity_helper() {
9421        // Pins that `defmacro_arity` accepts a typed `MacroDefHead`
9422        // and threads it through to the variant's typed `head` slot
9423        // unchanged — no `&str` projection at the helper boundary
9424        // (the projection through `MacroDefHead::keyword()` happens at
9425        // Display rendering time inside the `#[error(...)]`
9426        // annotation). A regression that drops the `MacroDefHead`
9427        // parameter type (e.g. by reverting to `head: &str`) breaks
9428        // compilation here. Pinning each of the three variants gives
9429        // the typed-head threading the same path-uniformity edge the
9430        // existing `defmacro_arity_in_*_emits_same_variant` tests pin
9431        // for the call-site path through `macro_def_from`.
9432        for head in [
9433            MacroDefHead::Defmacro,
9434            MacroDefHead::DefpointTemplate,
9435            MacroDefHead::Defcheck,
9436        ] {
9437            let err = defmacro_arity(head, 2);
9438            match err {
9439                LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
9440                other => panic!("expected DefmacroArity, got: {other:?}"),
9441            }
9442        }
9443    }
9444
9445    #[test]
9446    fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
9447        // Sibling of the `defmacro_arity` threading test — pins that
9448        // `defmacro_non_symbol_name` accepts a typed `MacroDefHead`
9449        // and threads it through to the variant's typed `head` slot
9450        // unchanged. The `got: &Sexp` parameter rides through
9451        // `crate::domain::sexp_witness` into the variant's typed
9452        // `got: SexpWitness` slot so BOTH the structural shape AND
9453        // the rendered literal are preserved across the helper
9454        // boundary, parallel to how `non_symbol_param` and
9455        // `non_symbol_unquote_target` project their `&Sexp` arguments
9456        // through the same typed joint primitive.
9457        let got = parse("5");
9458        for head in [
9459            MacroDefHead::Defmacro,
9460            MacroDefHead::DefpointTemplate,
9461            MacroDefHead::Defcheck,
9462        ] {
9463            let err = defmacro_non_symbol_name(head, &got);
9464            match err {
9465                LispError::DefmacroNonSymbolName { head: h, got: g } => {
9466                    assert_eq!(h, head);
9467                    assert_eq!(g.shape, crate::error::SexpShape::Int);
9468                    assert_eq!(g.display, "5");
9469                }
9470                other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9471            }
9472        }
9473    }
9474
9475    #[test]
9476    fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
9477        // Sibling of the `defmacro_arity` and
9478        // `defmacro_non_symbol_name` threading tests — pins that
9479        // `defmacro_non_list_params` accepts a typed `MacroDefHead`
9480        // and threads it through to the variant's typed `head` slot
9481        // unchanged. Together the three threading tests close the
9482        // typed-enum lift across all three error helpers — every
9483        // call site that constructs a `LispError::Defmacro*` variant
9484        // takes its `head` from a `MacroDefHead`, never from a `&str`
9485        // match. The `got: &Sexp` parameter rides through
9486        // `crate::domain::sexp_witness` into the variant's typed
9487        // `got: SexpWitness` slot so BOTH the structural shape AND
9488        // the rendered literal are preserved across the helper
9489        // boundary, parallel to how `defmacro_non_symbol_name`,
9490        // `non_symbol_param`, and `non_symbol_unquote_target` project
9491        // their `&Sexp` arguments through the same typed joint
9492        // primitive.
9493        let got = parse("x");
9494        for head in [
9495            MacroDefHead::Defmacro,
9496            MacroDefHead::DefpointTemplate,
9497            MacroDefHead::Defcheck,
9498        ] {
9499            let err = defmacro_non_list_params(head, &got);
9500            match err {
9501                LispError::DefmacroNonListParams { head: h, got: g } => {
9502                    assert_eq!(h, head);
9503                    assert_eq!(g.shape, crate::error::SexpShape::Symbol);
9504                    assert_eq!(g.display, "x");
9505                }
9506                other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9507            }
9508        }
9509    }
9510
9511    /// Helper for the defmacro-arity tests — pins the variant shape and
9512    /// carries the head / arity up to the assert site for legibility.
9513    /// Sibling of `non_symbol_param_fields` and
9514    /// `rest_param_missing_name_fields`.
9515    fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
9516        match err {
9517            LispError::DefmacroArity { head, arity } => (*head, *arity),
9518            other => panic!("expected DefmacroArity, got: {other:?}"),
9519        }
9520    }
9521
9522    #[test]
9523    fn defmacro_arity_with_head_only_emits_structural_variant() {
9524        // `(defmacro)` — only the head, no name / params / body. Pins
9525        // variant identity AND that `arity == 1` (just the head
9526        // element) AND that `head == "defmacro"`. A regression that
9527        // re-inlines the legacy `LispError::Compile` shape (which
9528        // named neither field) fails-loudly here.
9529        let mut e = Expander::new();
9530        let err = e
9531            .expand_program(read("(defmacro)").unwrap())
9532            .expect_err("defmacro arity gate must error");
9533        let (head, arity) = defmacro_arity_fields(&err);
9534        assert_eq!(head, MacroDefHead::Defmacro);
9535        assert_eq!(arity, 1);
9536    }
9537
9538    #[test]
9539    fn defmacro_arity_with_head_and_name_emits_structural_variant() {
9540        // `(defmacro f)` — head + name, missing params + body. Pins
9541        // that `arity` advances with the actual form length (2 for
9542        // this case) so an LSP quick-fix that wants to surface "you
9543        // wrote 2 elements; need 4" gains the count as data, no
9544        // source re-parse required.
9545        let mut e = Expander::new();
9546        let err = e
9547            .expand_program(read("(defmacro f)").unwrap())
9548            .expect_err("defmacro arity gate must error");
9549        let (head, arity) = defmacro_arity_fields(&err);
9550        assert_eq!(head, MacroDefHead::Defmacro);
9551        assert_eq!(arity, 2);
9552    }
9553
9554    #[test]
9555    fn defmacro_arity_with_head_name_params_emits_structural_variant() {
9556        // `(defmacro f ())` — head + name + params, missing body
9557        // (the most-complete partial defmacro that still trips the
9558        // arity gate). Pins that `arity == 3` exactly so an LSP
9559        // quick-fix that wants to surface "your defmacro is one
9560        // element short — body is missing" gains the count as data.
9561        let mut e = Expander::new();
9562        let err = e
9563            .expand_program(read("(defmacro f ())").unwrap())
9564            .expect_err("defmacro arity gate must error");
9565        let (head, arity) = defmacro_arity_fields(&err);
9566        assert_eq!(head, MacroDefHead::Defmacro);
9567        assert_eq!(arity, 3);
9568    }
9569
9570    #[test]
9571    fn defmacro_arity_in_defpoint_template_emits_same_variant() {
9572        // `defpoint-template` shares `macro_def_from` with `defmacro`
9573        // (all three head keywords route through the same gate). Pins
9574        // that the lift fires path-uniformly across the three head
9575        // keywords AND that the variant's `head` slot carries the
9576        // actual head literal — `defpoint-template`, not `defmacro`
9577        // — so an LSP that wants to point at "your defpoint-template
9578        // form is missing elements" gains the head as data.
9579        let mut e = Expander::new();
9580        let err = e
9581            .expand_program(read("(defpoint-template t)").unwrap())
9582            .expect_err("defpoint-template arity gate must error");
9583        let (head, arity) = defmacro_arity_fields(&err);
9584        assert_eq!(head, MacroDefHead::DefpointTemplate);
9585        assert_eq!(arity, 2);
9586    }
9587
9588    #[test]
9589    fn defmacro_arity_in_defcheck_emits_same_variant() {
9590        // Sibling of the defpoint-template test — `defcheck` is the
9591        // third head keyword `macro_def_from` recognizes. All three
9592        // route through the same arity gate and now reject too-short
9593        // forms with the same structural variant.
9594        let mut e = Expander::new();
9595        let err = e
9596            .expand_program(read("(defcheck)").unwrap())
9597            .expect_err("defcheck arity gate must error");
9598        let (head, arity) = defmacro_arity_fields(&err);
9599        assert_eq!(head, MacroDefHead::Defcheck);
9600        assert_eq!(arity, 1);
9601    }
9602
9603    #[test]
9604    fn defmacro_arity_substitute_and_bytecode_paths_agree() {
9605        // Path-uniform rejection: the SAME source emits the SAME
9606        // structural variant under both expansion strategies. The
9607        // arity gate fires inside `macro_def_from` BEFORE either
9608        // strategy's expansion path runs; so both `Expander::new()`
9609        // (bytecode) and `Expander::new_substitute_only()`
9610        // (substitute) reject the SAME malformed defmacro at the
9611        // SAME gate. Sibling of
9612        // `non_symbol_param_substitute_and_bytecode_paths_agree` and
9613        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9614        let src = "(defmacro f)";
9615        let mut subst = Expander::new_substitute_only();
9616        let mut bytecode = Expander::new();
9617        let err_subst = subst
9618            .expand_program(read(src).unwrap())
9619            .expect_err("substitute must error");
9620        let err_byte = bytecode
9621            .expand_program(read(src).unwrap())
9622            .expect_err("bytecode must error");
9623        assert_eq!(
9624            defmacro_arity_fields(&err_subst),
9625            (MacroDefHead::Defmacro, 2)
9626        );
9627        assert_eq!(
9628            defmacro_arity_fields(&err_byte),
9629            (MacroDefHead::Defmacro, 2)
9630        );
9631    }
9632
9633    #[test]
9634    fn defmacro_arity_message_renders_legacy_substring_with_arity() {
9635        // End-to-end through Display — pins the rendered diagnostic
9636        // consumers see today (REPL, `tatara-check`) AND the new
9637        // `(got {arity} elements, need 4)` clause. The legacy
9638        // `"(defmacro name (params) body) required"` substring
9639        // rides through verbatim. Tools that pattern-match on the
9640        // variant gain structural binding to `head` / `arity`.
9641        let mut e = Expander::new();
9642        let err = e
9643            .expand_program(read("(defmacro f)").unwrap())
9644            .expect_err("defmacro arity gate must error");
9645        assert_eq!(
9646            format!("{err}"),
9647            "compile error in defmacro: (defmacro name (params) body) required \
9648             (got 2 elements, need 4)"
9649        );
9650    }
9651
9652    #[test]
9653    fn defmacro_arity_position_is_none_today() {
9654        // Negative control for the future-spans move: until `Sexp`
9655        // carries source positions, `position()` on `LispError`
9656        // returns `None` for this variant. A future run that gives
9657        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9658        // this test gives that change a deliberate fail-before/pass-
9659        // after delta. Parallel to
9660        // `non_symbol_param_position_is_none_today` and
9661        // `rest_param_missing_name_position_is_none_today`.
9662        let mut e = Expander::new();
9663        let err = e
9664            .expand_program(read("(defmacro)").unwrap())
9665            .expect_err("defmacro arity gate must error");
9666        assert_eq!(err.position(), None);
9667    }
9668
9669    #[test]
9670    fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
9671        // Negative control: a defmacro with exactly 4 elements (head
9672        // + name + params + body) passes the arity gate. Pins that
9673        // the lift is scoped to the arity-deficient case, not to
9674        // every defmacro form. After this test, a regression that
9675        // tightens the arity gate to >= 5 (e.g. spuriously requiring
9676        // a docstring slot) fails-loudly here.
9677        let mut e = Expander::new();
9678        let out = e
9679            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9680            .expect("well-formed defmacro must succeed");
9681        assert_eq!(out[0], Sexp::int(42));
9682    }
9683
9684    /// Helper for the defmacro-non-symbol-name tests — pins variant
9685    /// shape and carries the head / got up to the assert site for
9686    /// legibility. Sibling of `defmacro_arity_fields`,
9687    /// `non_symbol_param_fields`, and `rest_param_missing_name_fields`.
9688    fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
9689        match err {
9690            LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
9691            other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9692        }
9693    }
9694
9695    #[test]
9696    fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
9697        // `(defmacro 5 () body)` — the form passes the arity gate
9698        // (4 elements) but list[1] is `5`, not a symbol. Pins variant
9699        // identity AND that `head == "defmacro"` AND that `got ==
9700        // "5"`. A regression that re-inlines the legacy
9701        // `LispError::Compile { form: "defmacro", message: "expected
9702        // name symbol" }` shape (which named the failure mode but
9703        // not the offending element) fails-loudly here.
9704        let mut e = Expander::new();
9705        let err = e
9706            .expand_program(read("(defmacro 5 () body)").unwrap())
9707            .expect_err("defmacro non-symbol name gate must error");
9708        let (head, got) = defmacro_non_symbol_name_fields(&err);
9709        assert_eq!(head, MacroDefHead::Defmacro);
9710        assert_eq!(got, "5");
9711    }
9712
9713    #[test]
9714    fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
9715        // `(defmacro :foo () body)` — list[1] is the keyword `:foo`,
9716        // not a symbol. Pins that `Sexp::Display` for
9717        // `Atom::Keyword(s)` writes `:s` and the variant's `got` slot
9718        // carries the keyword form unchanged. An LSP that wants to
9719        // surface "you wrote `:foo` where a name symbol was expected"
9720        // gains the literal keyword value as data, no source re-parse
9721        // required.
9722        let mut e = Expander::new();
9723        let err = e
9724            .expand_program(read("(defmacro :foo () body)").unwrap())
9725            .expect_err("defmacro non-symbol name gate must error");
9726        let (head, got) = defmacro_non_symbol_name_fields(&err);
9727        assert_eq!(head, MacroDefHead::Defmacro);
9728        assert_eq!(got, ":foo");
9729    }
9730
9731    #[test]
9732    fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
9733        // `(defmacro "name" () body)` — list[1] is the string
9734        // literal `"name"`, not a symbol. Pins that `Sexp::Display`
9735        // for `Atom::String(s)` writes `"s"` (with quotes) and the
9736        // variant's `got` slot carries the quoted form unchanged.
9737        let mut e = Expander::new();
9738        let err = e
9739            .expand_program(read("(defmacro \"name\" () body)").unwrap())
9740            .expect_err("defmacro non-symbol name gate must error");
9741        let (head, got) = defmacro_non_symbol_name_fields(&err);
9742        assert_eq!(head, MacroDefHead::Defmacro);
9743        assert_eq!(got, "\"name\"");
9744    }
9745
9746    #[test]
9747    fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
9748        // `(defmacro (nested) () body)` — list[1] is a nested list,
9749        // not a symbol. Pins that `Sexp::Display` for a list writes
9750        // `(elements)` and the variant's `got` slot carries the
9751        // parenthesized form unchanged.
9752        let mut e = Expander::new();
9753        let err = e
9754            .expand_program(read("(defmacro (nested) () body)").unwrap())
9755            .expect_err("defmacro non-symbol name gate must error");
9756        let (head, got) = defmacro_non_symbol_name_fields(&err);
9757        assert_eq!(head, MacroDefHead::Defmacro);
9758        assert_eq!(got, "(nested)");
9759    }
9760
9761    #[test]
9762    fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
9763        // `defpoint-template` shares `macro_def_from` with `defmacro`
9764        // (all three head keywords route through the same gate).
9765        // Pins that the lift fires path-uniformly across the three
9766        // head keywords AND that the variant's `head` slot carries
9767        // the actual head literal — `defpoint-template`, not
9768        // `defmacro` — so an LSP that wants to point at "your
9769        // defpoint-template form's name slot isn't a symbol" gains
9770        // the head as data.
9771        let mut e = Expander::new();
9772        let err = e
9773            .expand_program(read("(defpoint-template 7 () body)").unwrap())
9774            .expect_err("defpoint-template non-symbol name gate must error");
9775        let (head, got) = defmacro_non_symbol_name_fields(&err);
9776        assert_eq!(head, MacroDefHead::DefpointTemplate);
9777        assert_eq!(got, "7");
9778    }
9779
9780    #[test]
9781    fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
9782        // Sibling for the `defcheck` head — third head keyword
9783        // `macro_def_from` recognizes. Rounds out the three-head-
9784        // keyword coverage so the lift is path-uniform across
9785        // `defmacro` / `defpoint-template` / `defcheck`.
9786        let mut e = Expander::new();
9787        let err = e
9788            .expand_program(read("(defcheck :k () body)").unwrap())
9789            .expect_err("defcheck non-symbol name gate must error");
9790        let (head, got) = defmacro_non_symbol_name_fields(&err);
9791        assert_eq!(head, MacroDefHead::Defcheck);
9792        assert_eq!(got, ":k");
9793    }
9794
9795    #[test]
9796    fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
9797        // Path-uniform rejection: the SAME source emits the SAME
9798        // structural variant under both expansion strategies. The
9799        // name-symbol gate fires inside `macro_def_from` BEFORE
9800        // either expansion strategy runs, so the gate is naturally
9801        // path-uniform; pinning it gives a regression that drifts
9802        // either strategy's handling of non-symbol-name defmacros (or
9803        // makes one strategy accept what the other rejects) a fail-
9804        // before/pass-after edge. Sibling of
9805        // `defmacro_arity_substitute_and_bytecode_paths_agree`,
9806        // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
9807        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9808        let src = "(defmacro 5 () body)";
9809        let mut subst = Expander::new_substitute_only();
9810        let mut bytecode = Expander::new();
9811        let err_subst = subst
9812            .expand_program(read(src).unwrap())
9813            .expect_err("substitute must error");
9814        let err_byte = bytecode
9815            .expand_program(read(src).unwrap())
9816            .expect_err("bytecode must error");
9817        assert_eq!(
9818            defmacro_non_symbol_name_fields(&err_subst),
9819            (MacroDefHead::Defmacro, "5")
9820        );
9821        assert_eq!(
9822            defmacro_non_symbol_name_fields(&err_byte),
9823            (MacroDefHead::Defmacro, "5")
9824        );
9825    }
9826
9827    #[test]
9828    fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
9829        // End-to-end through Display — pins the rendered diagnostic
9830        // consumers see today (REPL, `tatara-check`) AND the new
9831        // `, got {got}` clause. The legacy `"expected name symbol"`
9832        // substring rides through verbatim; the prefix matches the
9833        // legacy `Compile { form: "defmacro", message: "expected name
9834        // symbol" }` byte-for-byte. Tools that pattern-match on the
9835        // variant gain structural binding to `head` / `got`.
9836        let mut e = Expander::new();
9837        let err = e
9838            .expand_program(read("(defmacro 5 () body)").unwrap())
9839            .expect_err("defmacro non-symbol name gate must error");
9840        assert_eq!(
9841            format!("{err}"),
9842            "compile error in defmacro: expected name symbol, got 5"
9843        );
9844    }
9845
9846    #[test]
9847    fn defmacro_non_symbol_name_position_is_none_today() {
9848        // Negative control for the future-spans move: until `Sexp`
9849        // carries source positions, `position()` on `LispError`
9850        // returns `None` for this variant. A future run that gives
9851        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9852        // this test gives that change a deliberate fail-before/pass-
9853        // after delta. Parallel to
9854        // `defmacro_arity_position_is_none_today`,
9855        // `non_symbol_param_position_is_none_today`, and
9856        // `rest_param_missing_name_position_is_none_today`.
9857        let mut e = Expander::new();
9858        let err = e
9859            .expand_program(read("(defmacro 5 () body)").unwrap())
9860            .expect_err("defmacro non-symbol name gate must error");
9861        assert_eq!(err.position(), None);
9862    }
9863
9864    #[test]
9865    fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
9866        // Negative control: a defmacro whose name slot IS a symbol
9867        // passes the name-symbol gate. Pins that the lift is scoped
9868        // to the non-symbol-name case, not to every defmacro form.
9869        // After this test, a regression that tightens the gate to
9870        // reject e.g. kebab-cased names fails-loudly here.
9871        let mut e = Expander::new();
9872        let out = e
9873            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9874            .expect("well-formed defmacro must succeed");
9875        assert_eq!(out[0], Sexp::int(42));
9876    }
9877
9878    #[test]
9879    fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
9880        // Pins the gate ordering: a 4-element defmacro whose name
9881        // slot is non-symbol fires `DefmacroNonSymbolName`, NOT
9882        // `DefmacroArity`. The arity gate (>= 4 elements) admits
9883        // this form; the name-symbol gate is the next checkpoint.
9884        // A regression that swaps the gate ordering (e.g. checks
9885        // name-symbol before arity, so `(defmacro 5)` would emit
9886        // `DefmacroNonSymbolName` instead of `DefmacroArity`) fails-
9887        // loudly here.
9888        let mut e = Expander::new();
9889        let err = e
9890            .expand_program(read("(defmacro 5 () body)").unwrap())
9891            .expect_err("name-symbol gate must error");
9892        assert!(
9893            matches!(err, LispError::DefmacroNonSymbolName { .. }),
9894            "expected DefmacroNonSymbolName, got: {err:?}"
9895        );
9896
9897        let err_arity = e
9898            .expand_program(read("(defmacro 5)").unwrap())
9899            .expect_err("arity gate must error");
9900        assert!(
9901            matches!(err_arity, LispError::DefmacroArity { .. }),
9902            "expected DefmacroArity (arity < 4 short-circuits before name check), \
9903             got: {err_arity:?}"
9904        );
9905    }
9906
9907    /// Helper for the defmacro-non-list-params tests — pins variant
9908    /// shape and carries the head / got up to the assert site for
9909    /// legibility. Sibling of `defmacro_arity_fields`,
9910    /// `defmacro_non_symbol_name_fields`, `non_symbol_param_fields`,
9911    /// and `rest_param_missing_name_fields`.
9912    fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
9913        match err {
9914            LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
9915            other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9916        }
9917    }
9918
9919    #[test]
9920    fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
9921        // `(defmacro f x body)` — the form passes both the arity gate
9922        // (4 elements) AND the name-symbol gate (`f` is a symbol) but
9923        // list[2] is the symbol `x`, not a list. Pins variant identity
9924        // AND that `head == "defmacro"` AND that `got == "x"`. A
9925        // regression that re-inlines the legacy `LispError::Compile {
9926        // form: "defmacro", message: "expected param list" }` shape
9927        // (which named the failure mode but not the offending element)
9928        // fails-loudly here.
9929        let mut e = Expander::new();
9930        let err = e
9931            .expand_program(read("(defmacro f x body)").unwrap())
9932            .expect_err("defmacro non-list params gate must error");
9933        let (head, got) = defmacro_non_list_params_fields(&err);
9934        assert_eq!(head, MacroDefHead::Defmacro);
9935        assert_eq!(got, "x");
9936    }
9937
9938    #[test]
9939    fn defmacro_non_list_params_with_int_emits_structural_variant() {
9940        // `(defmacro f 5 body)` — list[2] is `5`, not a list. Pins
9941        // that `Sexp::Display` for `Atom::Int(n)` writes `n` and the
9942        // variant's `got` slot carries the integer form unchanged. An
9943        // LSP that surfaces "you wrote `5` where a param list was
9944        // expected" gains the literal value as data, no source
9945        // re-parse required.
9946        let mut e = Expander::new();
9947        let err = e
9948            .expand_program(read("(defmacro f 5 body)").unwrap())
9949            .expect_err("defmacro non-list params gate must error");
9950        let (head, got) = defmacro_non_list_params_fields(&err);
9951        assert_eq!(head, MacroDefHead::Defmacro);
9952        assert_eq!(got, "5");
9953    }
9954
9955    #[test]
9956    fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
9957        // `(defmacro f :foo body)` — list[2] is the keyword `:foo`,
9958        // not a list. Pins that `Sexp::Display` for `Atom::Keyword(s)`
9959        // writes `:s` and the variant's `got` slot carries the
9960        // keyword form unchanged.
9961        let mut e = Expander::new();
9962        let err = e
9963            .expand_program(read("(defmacro f :foo body)").unwrap())
9964            .expect_err("defmacro non-list params gate must error");
9965        let (head, got) = defmacro_non_list_params_fields(&err);
9966        assert_eq!(head, MacroDefHead::Defmacro);
9967        assert_eq!(got, ":foo");
9968    }
9969
9970    #[test]
9971    fn defmacro_non_list_params_with_string_emits_structural_variant() {
9972        // `(defmacro f "params" body)` — list[2] is the string literal
9973        // `"params"`, not a list. Pins that `Sexp::Display` for
9974        // `Atom::String(s)` writes `"s"` (with quotes) and the
9975        // variant's `got` slot carries the quoted form unchanged.
9976        let mut e = Expander::new();
9977        let err = e
9978            .expand_program(read("(defmacro f \"params\" body)").unwrap())
9979            .expect_err("defmacro non-list params gate must error");
9980        let (head, got) = defmacro_non_list_params_fields(&err);
9981        assert_eq!(head, MacroDefHead::Defmacro);
9982        assert_eq!(got, "\"params\"");
9983    }
9984
9985    #[test]
9986    fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
9987        // `defpoint-template` shares `macro_def_from` with `defmacro`
9988        // (all three head keywords route through the same gate).
9989        // Pins that the lift fires path-uniformly across the three
9990        // head keywords AND that the variant's `head` slot carries
9991        // the actual head literal — `defpoint-template`, not
9992        // `defmacro` — so an LSP that wants to point at "your
9993        // defpoint-template form's param-list slot isn't a list"
9994        // gains the head as data.
9995        let mut e = Expander::new();
9996        let err = e
9997            .expand_program(read("(defpoint-template t x body)").unwrap())
9998            .expect_err("defpoint-template non-list params gate must error");
9999        let (head, got) = defmacro_non_list_params_fields(&err);
10000        assert_eq!(head, MacroDefHead::DefpointTemplate);
10001        assert_eq!(got, "x");
10002    }
10003
10004    #[test]
10005    fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
10006        // Sibling for the `defcheck` head — third head keyword
10007        // `macro_def_from` recognizes. Rounds out the three-head-
10008        // keyword coverage so the lift is path-uniform across
10009        // `defmacro` / `defpoint-template` / `defcheck`.
10010        let mut e = Expander::new();
10011        let err = e
10012            .expand_program(read("(defcheck c 7 body)").unwrap())
10013            .expect_err("defcheck non-list params gate must error");
10014        let (head, got) = defmacro_non_list_params_fields(&err);
10015        assert_eq!(head, MacroDefHead::Defcheck);
10016        assert_eq!(got, "7");
10017    }
10018
10019    #[test]
10020    fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
10021        // Path-uniform rejection: the SAME source emits the SAME
10022        // structural variant under both expansion strategies. The
10023        // param-list gate fires inside `macro_def_from` BEFORE either
10024        // expansion strategy runs, so the gate is naturally path-
10025        // uniform; pinning it gives a regression that drifts either
10026        // strategy's handling of non-list-params defmacros (or makes
10027        // one strategy accept what the other rejects) a fail-before/
10028        // pass-after edge. Sibling of
10029        // `defmacro_arity_substitute_and_bytecode_paths_agree`,
10030        // `defmacro_non_symbol_name_substitute_and_bytecode_paths_agree`,
10031        // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
10032        // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
10033        let src = "(defmacro f x body)";
10034        let mut subst = Expander::new_substitute_only();
10035        let mut bytecode = Expander::new();
10036        let err_subst = subst
10037            .expand_program(read(src).unwrap())
10038            .expect_err("substitute must error");
10039        let err_byte = bytecode
10040            .expand_program(read(src).unwrap())
10041            .expect_err("bytecode must error");
10042        assert_eq!(
10043            defmacro_non_list_params_fields(&err_subst),
10044            (MacroDefHead::Defmacro, "x")
10045        );
10046        assert_eq!(
10047            defmacro_non_list_params_fields(&err_byte),
10048            (MacroDefHead::Defmacro, "x")
10049        );
10050    }
10051
10052    #[test]
10053    fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
10054        // End-to-end through Display — pins the rendered diagnostic
10055        // consumers see today (REPL, `tatara-check`) AND the new
10056        // `, got {got}` clause. The legacy `"expected param list"`
10057        // substring rides through verbatim; the prefix matches the
10058        // legacy `Compile { form: "defmacro", message: "expected
10059        // param list" }` byte-for-byte. Tools that pattern-match on
10060        // the variant gain structural binding to `head` / `got`.
10061        let mut e = Expander::new();
10062        let err = e
10063            .expand_program(read("(defmacro f x body)").unwrap())
10064            .expect_err("defmacro non-list params gate must error");
10065        assert_eq!(
10066            format!("{err}"),
10067            "compile error in defmacro: expected param list, got x"
10068        );
10069    }
10070
10071    #[test]
10072    fn defmacro_non_list_params_position_is_none_today() {
10073        // Negative control for the future-spans move: until `Sexp`
10074        // carries source positions, `position()` on `LispError`
10075        // returns `None` for this variant. A future run that gives
10076        // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
10077        // this test gives that change a deliberate fail-before/pass-
10078        // after delta. Parallel to
10079        // `defmacro_arity_position_is_none_today`,
10080        // `defmacro_non_symbol_name_position_is_none_today`,
10081        // `non_symbol_param_position_is_none_today`, and
10082        // `rest_param_missing_name_position_is_none_today`.
10083        let mut e = Expander::new();
10084        let err = e
10085            .expand_program(read("(defmacro f x body)").unwrap())
10086            .expect_err("defmacro non-list params gate must error");
10087        assert_eq!(err.position(), None);
10088    }
10089
10090    #[test]
10091    fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
10092        // Negative control: a defmacro whose param-list slot IS a
10093        // list passes the param-list gate. Pins that the lift is
10094        // scoped to the non-list-params case, not to every defmacro
10095        // form. After this test, a regression that tightens the gate
10096        // to reject e.g. empty param lists fails-loudly here.
10097        let mut e = Expander::new();
10098        let out = e
10099            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10100            .expect("well-formed defmacro must succeed");
10101        assert_eq!(out[0], Sexp::int(42));
10102    }
10103
10104    #[test]
10105    fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
10106        // Pins the gate ordering: a 4-element defmacro whose name
10107        // slot IS a symbol but whose param-list slot is non-list
10108        // fires `DefmacroNonListParams`, NOT `DefmacroNonSymbolName`.
10109        // The name-symbol gate admits this form; the param-list gate
10110        // is the next checkpoint. A regression that swaps the gate
10111        // ordering (e.g. checks param-list before name-symbol, so
10112        // `(defmacro 5 x body)` would emit `DefmacroNonListParams`
10113        // instead of `DefmacroNonSymbolName`) fails-loudly here.
10114        let mut e = Expander::new();
10115        let err = e
10116            .expand_program(read("(defmacro f x body)").unwrap())
10117            .expect_err("param-list gate must error");
10118        assert!(
10119            matches!(err, LispError::DefmacroNonListParams { .. }),
10120            "expected DefmacroNonListParams, got: {err:?}"
10121        );
10122
10123        let err_name = e
10124            .expand_program(read("(defmacro 5 x body)").unwrap())
10125            .expect_err("name-symbol gate must error");
10126        assert!(
10127            matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
10128            "expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
10129             got: {err_name:?}"
10130        );
10131    }
10132
10133    #[test]
10134    fn defmacro_non_list_params_fires_after_arity_gate_passes() {
10135        // Pins the full gate ordering: a 4-element defmacro whose
10136        // first three slots are head/symbol/non-list fires
10137        // `DefmacroNonListParams`, NOT `DefmacroArity`. The arity
10138        // gate (>= 4 elements) admits this form; the name-symbol
10139        // gate admits the symbol; the param-list gate is the third
10140        // checkpoint. A regression that drifts the gate sequence
10141        // (e.g. fires `DefmacroArity` for a 4-element form) fails-
10142        // loudly here. Parallel to
10143        // `defmacro_non_symbol_name_fires_after_arity_gate_passes`
10144        // — together they pin the full
10145        // arity → name-symbol → param-list ordering inside
10146        // `macro_def_from`.
10147        let mut e = Expander::new();
10148        let err = e
10149            .expand_program(read("(defmacro f x body)").unwrap())
10150            .expect_err("param-list gate must error");
10151        assert!(
10152            matches!(err, LispError::DefmacroNonListParams { .. }),
10153            "expected DefmacroNonListParams, got: {err:?}"
10154        );
10155
10156        let err_arity = e
10157            .expand_program(read("(defmacro f x)").unwrap())
10158            .expect_err("arity gate must error");
10159        assert!(
10160            matches!(err_arity, LispError::DefmacroArity { .. }),
10161            "expected DefmacroArity (arity < 4 short-circuits before param-list check), \
10162             got: {err_arity:?}"
10163        );
10164    }
10165
10166    #[test]
10167    fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
10168        // Negative control: `&rest` is a symbol (`Atom::Symbol("&rest")`)
10169        // at the parser level, so `as_symbol()` succeeds for it. The
10170        // `NonSymbolParam` variant does NOT fire on the `&rest` marker
10171        // itself; the dedicated `&rest needs a name` rejection (a
10172        // separate failure mode in this cluster) handles malformed
10173        // rest-param shapes. Pins that the lift is scoped to
10174        // non-symbol elements at param-list positions, not to
10175        // every malformed-param shape.
10176        let mut e = Expander::new();
10177        let out = e
10178            .expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
10179            .expect("&rest with name must succeed");
10180        assert_eq!(out[0], parse("(list 1 2 3)"));
10181    }
10182
10183    #[test]
10184    fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
10185        // End-to-end through the Display impl — pins the rendered diagnostic
10186        // a downstream tool sees today (REPL, tatara-check). The shape is
10187        // parallel to the existing `TypeMismatch` variant: form, expected
10188        // shape, offending literal — all three slots present.
10189        let mut e = Expander::new();
10190        let err = e
10191            .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
10192            .expect_err("non-symbol target must error");
10193        assert_eq!(
10194            format!("{err}"),
10195            "compile error in ,: expected symbol, got (list 1 2)"
10196        );
10197    }
10198
10199    // ── template_invariant_violation: structural lift ───────────────
10200    //
10201    // The four byte-identical inline `LispError::Compile { form:
10202    // macro_name.into(), message: <invariant> }` triples in `apply_compiled`
10203    // (Subst-bad-index, Splice-bad-index, EndList-empty-stack,
10204    // final-no-value gates) were lifted to `template_invariant_violation`,
10205    // and the helper's emission was promoted from `LispError::Compile`-
10206    // shape to the structural `LispError::TemplateInvariant { macro_name,
10207    // kind: TemplateInvariantKind }` variant. The index payload of the
10208    // Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
10209    // / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
10210    // kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
10211    // is structurally unrepresentable. Display matches the legacy
10212    // `Compile`-shaped diagnostic byte-for-byte via the closed-set
10213    // `TemplateInvariantKind::message()` projection so authoring-tool
10214    // substring greps see no drift across the lift.
10215    //
10216    // The tests below pin: (a) the helper produces the structural
10217    // `LispError::TemplateInvariant` variant with `macro_name` and `kind`
10218    // first-class; (b) the Subst / Splice gates thread the bad index
10219    // through the typed variants `SubstBadIndex(usize)` / `SpliceBadIndex(usize)`
10220    // unchanged; (c) the two REACHABLE invariant-violation paths through
10221    // `apply_compiled` — Subst with out-of-bounds idx, Splice with
10222    // out-of-bounds idx — route through the helper end-to-end (the
10223    // EndList / no-value paths are guarded by `last_mut().unwrap()`
10224    // ahead of `pop().ok_or_else()` and are not reachable through any
10225    // single CompiledTemplate; they remain defensive against future
10226    // changes to the stack discipline); (d) the legacy Display
10227    // rendering matches byte-for-byte across the lift; (e) positive
10228    // controls: a well-formed CompiledTemplate routes PAST the helper
10229    // cleanly, and unrelated macro errors (missing-required-arg) do
10230    // NOT route through the helper.
10231
10232    #[test]
10233    fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
10234        // Direct unit test of the helper: a fixed macro_name and a
10235        // `TemplateInvariantKind` produce a `LispError::TemplateInvariant`
10236        // variant with the macro_name in the `macro_name` slot and the
10237        // kind passed through verbatim in the `kind` slot. A regression
10238        // that drifts the variant (e.g., back to `LispError::Compile`)
10239        // or swaps the slot positions fails-loudly here.
10240        let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
10241        match err {
10242            LispError::TemplateInvariant { macro_name, kind } => {
10243                assert_eq!(macro_name, "test-macro");
10244                assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10245            }
10246            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10247        }
10248    }
10249
10250    #[test]
10251    fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
10252        // The Subst gate's `usize` idx lives INSIDE the
10253        // `TemplateInvariantKind::SubstBadIndex(usize)` variant rather
10254        // than being substring-rendered into a free-form `message`
10255        // slot. Pin that the helper threads the bad index through the
10256        // typed variant unchanged; a regression that drops the index
10257        // payload (e.g., via a `usize -> ()` projection) fails here.
10258        let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
10259        match err {
10260            LispError::TemplateInvariant { macro_name, kind } => {
10261                assert_eq!(macro_name, "wrap");
10262                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10263            }
10264            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10265        }
10266    }
10267
10268    #[test]
10269    fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
10270        // Hand-crafted CompiledTemplate with a Subst(99) op against
10271        // an empty params list: `args_by_index` has length 0, so
10272        // `.get(99)` returns None and the `ok_or_else` triggers
10273        // through the helper. Fail-before-pass-after: this same input
10274        // pre-lift went through `LispError::Compile { form: macro_name,
10275        // message: format!("compiled template referenced bad param
10276        // index {idx}") }`; post-lift it routes through
10277        // `template_invariant_violation` and emits the structural
10278        // `TemplateInvariant { macro_name, kind: SubstBadIndex(99) }`
10279        // variant with the bad index threaded through as typed data.
10280        let tmpl = CompiledTemplate {
10281            ops: vec![TemplateOp::Subst(99)],
10282        };
10283        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10284            .expect_err("bad idx must error");
10285        match err {
10286            LispError::TemplateInvariant { macro_name, kind } => {
10287                assert_eq!(macro_name, "test-macro");
10288                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10289            }
10290            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10291        }
10292    }
10293
10294    #[test]
10295    fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
10296        // Hand-crafted CompiledTemplate with a Splice(42) op against
10297        // an empty params list. Sibling of the Subst-bad-idx test;
10298        // pins the Splice gate routes through the helper with the
10299        // typed `SpliceBadIndex(42)` kind carrying the bad index.
10300        let tmpl = CompiledTemplate {
10301            ops: vec![TemplateOp::Splice(42)],
10302        };
10303        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10304            .expect_err("bad splice idx must error");
10305        match err {
10306            LispError::TemplateInvariant { macro_name, kind } => {
10307                assert_eq!(macro_name, "call-macro");
10308                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10309            }
10310            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10311        }
10312    }
10313
10314    #[test]
10315    fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
10316        // End-to-end through the `LispError` Display impl — pins the
10317        // rendered diagnostic byte-for-byte: `"compile error in
10318        // test-macro: compiled template referenced bad param index 99"`.
10319        // Authoring tools that substring-grep the rendered diagnostic
10320        // (`tatara-check`'s diagnostic capture, REPL substring-greps)
10321        // see no drift across the lift. Parallel to how
10322        // `compile_named_non_symbol_name_renders_legacy_compile_shape`
10323        // pins the sibling-file (compile.rs) lift's Display contract.
10324        let tmpl = CompiledTemplate {
10325            ops: vec![TemplateOp::Subst(99)],
10326        };
10327        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10328            .expect_err("bad idx must error");
10329        assert_eq!(
10330            format!("{err}"),
10331            "compile error in test-macro: compiled template referenced bad param index 99"
10332        );
10333    }
10334
10335    #[test]
10336    fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
10337        // Sibling Display test for the Splice gate. Pins the message
10338        // byte-for-byte through the `LispError` Display impl: `"compile
10339        // error in call-macro: compiled template referenced bad splice
10340        // index 42"`.
10341        let tmpl = CompiledTemplate {
10342            ops: vec![TemplateOp::Splice(42)],
10343        };
10344        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10345            .expect_err("bad splice idx must error");
10346        assert_eq!(
10347            format!("{err}"),
10348            "compile error in call-macro: compiled template referenced bad splice index 42"
10349        );
10350    }
10351
10352    #[test]
10353    fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
10354        // Positive control: a CompiledTemplate produced by the
10355        // bytecode compiler (`compile_template`) for a well-formed
10356        // macro never references an out-of-bounds index nor
10357        // unbalances the stack, so `apply_compiled` routes PAST the
10358        // helper cleanly. A regression that fires the helper on
10359        // well-formed bytecode (e.g., off-by-one in the index
10360        // resolution) would fail here. End-to-end through the public
10361        // `Expander` surface so the test exercises the same code
10362        // path users see.
10363        let mut e = Expander::new();
10364        let out = e
10365            .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10366            .expect("well-formed macro expansion must not fire template-invariant-violation");
10367        assert_eq!(out.len(), 1);
10368        assert_eq!(out[0], Sexp::int(42));
10369    }
10370
10371    #[test]
10372    fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
10373        // Negative control: the `missing_macro_arg` gate in the shared
10374        // positional binder (`MacroParams::bind`) fires BEFORE the bytecode
10375        // loop runs,
10376        // so a missing required arg routes through
10377        // `LispError::MissingMacroArg`, NOT through
10378        // `template_invariant_violation`. Pins the helper is
10379        // precisely scoped to bytecode-runtime invariant violations
10380        // (Subst / Splice / stack gates), not to macro-call arity
10381        // errors (the latter has its own structural variant). A
10382        // regression that conflates the two gate clusters would
10383        // route this case through `Compile { ... }` instead of
10384        // `MissingMacroArg` and fail-loudly here.
10385        let mut e = Expander::new();
10386        let err = e
10387            .expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
10388            .expect_err("missing required arg must error");
10389        assert!(
10390            matches!(err, LispError::MissingMacroArg { .. }),
10391            "expected MissingMacroArg, got: {err:?}"
10392        );
10393    }
10394
10395    // ── resolve_bound_arg: bytecode-runtime bound-arg-by-index lookup ──
10396    //
10397    // `resolve_bound_arg(args_by_index, idx, macro_name, kind)` lifts the
10398    // `args_by_index.get(*idx).ok_or_else(|| template_invariant_violation(
10399    // macro_name, KIND(*idx)))?` projection that recurred at BOTH the
10400    // `TemplateOp::Subst` and `TemplateOp::Splice` arms inside
10401    // `apply_compiled`. The arms differ in the kind constructor
10402    // (`SubstBadIndex` vs. `SpliceBadIndex`) and in their post-lookup
10403    // verb (clone+push vs. splice-coerce), but the lookup-and-reject
10404    // prelude is byte-identical modulo the constructor. These tests
10405    // pin the lifted helper's contract directly; the existing
10406    // `apply_compiled_*_bad_idx_*` tests are the path-uniformity
10407    // guards proving both production arms route through it without
10408    // behavior drift.
10409
10410    #[test]
10411    fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
10412        // For an in-range index, the helper returns `Ok(&args[idx])`
10413        // borrowed VERBATIM — same pointer as `args_by_index.get(idx)`.
10414        // Pins the borrow-not-clone contract: a regression that drifts
10415        // the helper to clone+return (`Result<Sexp>` instead of
10416        // `Result<&Sexp>`) would allocate per lookup at the production
10417        // `Subst`/`Splice` hot path. The kind constructor must NOT
10418        // fire on the success path (`FnOnce`'s lazy semantics) — pin
10419        // that the test passes a constructor that would panic if
10420        // called, asserting the helper short-circuits before invoking
10421        // it on the in-range arm.
10422        let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10423        let got = resolve_bound_arg(&args, 1, "m", |_| {
10424            panic!("kind constructor must not fire on the in-range path")
10425        })
10426        .expect("in-range lookup must succeed");
10427        assert!(
10428            std::ptr::eq(got, &args[1]),
10429            "resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
10430        );
10431        assert_eq!(*got, Sexp::int(2));
10432    }
10433
10434    #[test]
10435    fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
10436        // For an out-of-range index, the helper raises the structural
10437        // `LispError::TemplateInvariant` variant with the caller-
10438        // supplied `SubstBadIndex` kind constructor applied to the bad
10439        // index. Pins the post-lift emission shape (variant identity
10440        // + the kind constructor threaded with the actual idx); a
10441        // regression that drops the idx payload (e.g., via a `usize ->
10442        // ()` projection) or hard-codes a different kind at the helper
10443        // boundary fails-loudly here. Fail-before-pass-after: this
10444        // assert is contradicted by the pre-lift code path (which
10445        // never called `resolve_bound_arg` because it didn't exist),
10446        // ratifies the post-lift one.
10447        let args: Vec<Sexp> = Vec::new();
10448        let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
10449            .expect_err("out-of-range lookup must error");
10450        match err {
10451            LispError::TemplateInvariant { macro_name, kind } => {
10452                assert_eq!(macro_name, "test-macro");
10453                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10454            }
10455            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10456        }
10457    }
10458
10459    #[test]
10460    fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
10461        // Path-uniformity for the per-call-site kind constructor: the
10462        // SAME out-of-range idx via the `SpliceBadIndex` constructor
10463        // emits `kind: SpliceBadIndex(7)` — distinct from the sibling
10464        // `SubstBadIndex(7)` variant. Pins that the constructor is
10465        // chosen per call site (not hard-coded at the helper boundary),
10466        // closing the structural matrix `{Subst, Splice} × {in-range,
10467        // out-of-range}` the two production arms span across the
10468        // bytecode-runtime's bound-arg-by-index reads. A regression
10469        // that hard-codes a single kind at the helper boundary would
10470        // emit the same variant identity for both call sites and
10471        // fail-loudly here.
10472        let args: Vec<Sexp> = Vec::new();
10473        let err = resolve_bound_arg(
10474            &args,
10475            7,
10476            "test-macro",
10477            TemplateInvariantKind::SpliceBadIndex,
10478        )
10479        .expect_err("out-of-range lookup must error");
10480        match err {
10481            LispError::TemplateInvariant { macro_name, kind } => {
10482                assert_eq!(macro_name, "test-macro");
10483                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
10484            }
10485            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10486        }
10487    }
10488
10489    #[test]
10490    fn resolve_bound_arg_threads_macro_name_verbatim() {
10491        // Path-uniformity for the `macro_name` slot: the helper threads
10492        // the caller's borrow into the variant's owned `String` slot
10493        // verbatim. Pin two distinct macro names route through with no
10494        // mutual interference — a regression that hard-codes a single
10495        // macro_name at the helper boundary or swaps the parameter
10496        // ordering fails-loudly here. Same posture as
10497        // `compiler_spec_io_err_threads_each_stage_through_unchanged`
10498        // pins the typed `stage` slot in the disk-persistence sibling
10499        // lift.
10500        let args: Vec<Sexp> = Vec::new();
10501        for name in ["wrap", "call-macro", "obs"] {
10502            let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
10503                .expect_err("out-of-range lookup must error");
10504            match err {
10505                LispError::TemplateInvariant { macro_name, kind } => {
10506                    assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
10507                    assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10508                }
10509                other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10510            }
10511        }
10512    }
10513
10514    #[test]
10515    fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
10516        // Edge case: idx 0 with a single-element args_by_index returns
10517        // `Ok(&args[0])`. Pins the lower-bound of the in-range surface
10518        // — a regression that off-by-ones the lookup (e.g., `get(idx +
10519        // 1)` or `get(idx).filter(|_| idx > 0)`) would fail here.
10520        // Sibling to the upper-bound `resolve_bound_arg_out_of_range_
10521        // with_subst_kind_emits_typed_invariant` test.
10522        let args = vec![Sexp::int(42)];
10523        let got = resolve_bound_arg(&args, 0, "m", |_| {
10524            panic!("kind constructor must not fire on the in-range path")
10525        })
10526        .expect("idx-0 lookup must succeed");
10527        assert!(std::ptr::eq(got, &args[0]));
10528        assert_eq!(*got, Sexp::int(42));
10529    }
10530
10531    #[test]
10532    fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
10533        // Edge case: idx `len - 1` is the highest valid index. Pin
10534        // that it routes through the success arm (NOT the error arm),
10535        // closing the in-range surface end-to-end with the lower-
10536        // bound sibling. A regression that off-by-ones the upper
10537        // bound (e.g., `get(idx).filter(|_| idx < args.len() - 1)`)
10538        // would fail here.
10539        let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10540        let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
10541            panic!("kind constructor must not fire on the in-range path")
10542        })
10543        .expect("last-element lookup must succeed");
10544        assert!(std::ptr::eq(got, args.last().unwrap()));
10545        assert_eq!(*got, Sexp::int(3));
10546    }
10547
10548    #[test]
10549    fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
10550        // Boundary case: idx EQUAL to `args.len()` is out-of-range
10551        // (since `get` is 0-indexed). Pin that this routes through
10552        // the error arm with the kind constructor applied to the
10553        // EXACT idx that was tried. A regression that off-by-ones
10554        // the boundary (e.g., admits `idx == len`) would fail here.
10555        // This is the canonical off-by-one trap; the helper's
10556        // contract pins it at the variant-construction boundary.
10557        let args = vec![Sexp::int(1)];
10558        let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
10559            .expect_err("idx == len must error");
10560        match err {
10561            LispError::TemplateInvariant { kind, .. } => {
10562                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
10563            }
10564            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10565        }
10566    }
10567
10568    #[test]
10569    fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
10570        // Boundary case: an empty `args_by_index` slice rejects every
10571        // idx (including 0). Pin that the helper's emission shape is
10572        // uniform regardless of which out-of-range idx fires the
10573        // rejection — `SubstBadIndex(0)` for an empty slice is the
10574        // bytecode-runtime mirror of a zero-arity macro template
10575        // referencing the 0-th param.
10576        let args: Vec<Sexp> = Vec::new();
10577        let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
10578            .expect_err("empty slice rejects every idx");
10579        match err {
10580            LispError::TemplateInvariant { macro_name, kind } => {
10581                assert_eq!(macro_name, "zero-arity");
10582                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10583            }
10584            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10585        }
10586    }
10587
10588    #[test]
10589    fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
10590        // End-to-end path-uniformity: a `Subst(99)` op against a
10591        // zero-arity macro routes the bytecode-runtime's bound-arg
10592        // lookup through `resolve_bound_arg` with the
10593        // `SubstBadIndex` constructor, emitting the structural
10594        // variant with `kind: SubstBadIndex(99)`. The pre-lift
10595        // sibling test `apply_compiled_subst_bad_idx_routes_through_
10596        // template_invariant_violation` pins that the same input
10597        // routes through `template_invariant_violation`; this test
10598        // pins that BOTH still hold under the post-lift composition
10599        // — `resolve_bound_arg` calls `template_invariant_violation`
10600        // internally on the rejection arm. A regression that drifts
10601        // ONE arm's projection from the other (e.g., swaps the
10602        // constructor at one call site, or short-circuits the
10603        // composition) would fail here.
10604        let tmpl = CompiledTemplate {
10605            ops: vec![TemplateOp::Subst(99)],
10606        };
10607        let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10608            .expect_err("bad idx must error");
10609        match err {
10610            LispError::TemplateInvariant { macro_name, kind } => {
10611                assert_eq!(macro_name, "test-macro");
10612                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10613            }
10614            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10615        }
10616    }
10617
10618    #[test]
10619    fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
10620        // Sibling end-to-end path-uniformity for the `Splice` arm:
10621        // the post-lift composition routes a `Splice(42)` op through
10622        // `resolve_bound_arg` with the `SpliceBadIndex` constructor,
10623        // emitting `kind: SpliceBadIndex(42)`. Together with the
10624        // `Subst` sibling test above, this pins the structural matrix
10625        // `{Subst, Splice} × resolve_bound_arg` end-to-end through
10626        // the public `apply_compiled` surface, so a regression that
10627        // drifts ONE arm's kind constructor (e.g., the `Splice` arm
10628        // accidentally emits `SubstBadIndex` after a copy-paste
10629        // refactor) fails-loudly here.
10630        let tmpl = CompiledTemplate {
10631            ops: vec![TemplateOp::Splice(42)],
10632        };
10633        let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10634            .expect_err("bad splice idx must error");
10635        match err {
10636            LispError::TemplateInvariant { macro_name, kind } => {
10637                assert_eq!(macro_name, "call-macro");
10638                assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10639            }
10640            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10641        }
10642    }
10643
10644    #[test]
10645    fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
10646        // Positive control: a `Subst(0)` op against a one-arg macro
10647        // routes through `resolve_bound_arg`'s success arm and the
10648        // `Subst` post-lookup verb (clone + push) emits the bound
10649        // value verbatim. Pin the post-lift composition's success
10650        // path: the clone-and-push semantics live at the call site
10651        // (NOT in `resolve_bound_arg`, which only borrows), and a
10652        // regression that drifts the borrow contract (e.g., the
10653        // helper clones internally + the call site clones again)
10654        // would still pass observationally but would regress the
10655        // hot-path allocation count.
10656        let params = MacroParams {
10657            required: vec!["x".into()],
10658            optional: Vec::new(),
10659            rest: None,
10660        };
10661        let tmpl = CompiledTemplate {
10662            ops: vec![TemplateOp::Subst(0)],
10663        };
10664        let out = apply_compiled("id", &params, &tmpl, &[Sexp::int(42)])
10665            .expect("in-range Subst must succeed");
10666        assert_eq!(out, Sexp::int(42));
10667    }
10668
10669    // ── current_builder_mut: the bytecode-runtime top-of-stack projection ──
10670    //
10671    // `current_builder_mut(stack)` lifts the `stack.last_mut().unwrap()`
10672    // projection that appeared at FOUR sites inside `apply_compiled`'s
10673    // op-loop (Literal, Subst, Splice, post-EndList parent-fold) into ONE
10674    // named primitive. The expect message names the bytecode-runtime
10675    // invariant ("at least one stack frame during op-loop") so a
10676    // regression that drifts the loop's frame management (a new op that
10677    // pops without pushing, an early-return that bypasses EndList's
10678    // stack-check) surfaces a NAMED panic rather than a silent unwrap.
10679    // These tests pin the projection's contract directly; the existing
10680    // `apply_compiled_*` tests + the cross-strategy `expansion_layers_
10681    // agree_on_output_and_cache_wins` benchmark are the path-uniformity
10682    // guards proving the four sites still emit the canonical bytecode-
10683    // runtime output across the lift.
10684
10685    #[test]
10686    fn current_builder_mut_returns_the_top_frame_reference() {
10687        // The simplest projection: on a single-frame stack, the helper
10688        // returns a `&mut Vec<Sexp>` pointing at THAT frame. Pin the
10689        // projection's identity end-to-end via a push that mutates
10690        // through the borrow and observe the original frame carries the
10691        // pushed value back.
10692        let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
10693        current_builder_mut(&mut stack).push(Sexp::int(42));
10694        assert_eq!(stack.len(), 1);
10695        assert_eq!(stack[0], vec![Sexp::int(42)]);
10696    }
10697
10698    #[test]
10699    fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
10700        // The projection MUST target the topmost frame, not the bottom
10701        // one — every `TemplateOp::BeginList` pushes a fresh frame the
10702        // subsequent ops emit into, and a regression that flipped the
10703        // projection to `first_mut` (or to a fixed bottom-frame
10704        // reference) would silently smear all op output into the
10705        // outermost result. Pin path-uniformity with the bytecode-
10706        // runtime's mid-list emission posture: with three frames on
10707        // the stack (one outer + two pending lists), the helper
10708        // returns a borrow into the third frame, leaving frames 0 and
10709        // 1 untouched.
10710        let mut stack: Vec<Vec<Sexp>> = vec![
10711            vec![Sexp::symbol("outer")],
10712            vec![Sexp::symbol("inner-a")],
10713            vec![Sexp::symbol("inner-b")],
10714        ];
10715        current_builder_mut(&mut stack).push(Sexp::int(99));
10716        assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10717        assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
10718        assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
10719    }
10720
10721    #[test]
10722    fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
10723        // Structural identity binding the lift to its pre-lift inline
10724        // shape: `current_builder_mut(&mut stack)` IS
10725        // `stack.last_mut().unwrap()` — the same `&mut Vec<Sexp>`,
10726        // pointing at the same allocation. Pin pointer equality via
10727        // `std::ptr::eq` on the projected slice's `as_ptr()` to rule
10728        // out any allocation-shape drift across the lift.
10729        let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
10730        let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
10731        let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
10732        assert!(
10733            std::ptr::eq(via_lift_ptr, via_inline_ptr),
10734            "current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
10735        );
10736    }
10737
10738    #[test]
10739    #[should_panic(
10740        expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
10741    )]
10742    fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
10743        // The bytecode-runtime invariant is encoded in the expect
10744        // message: an empty stack at the projection boundary is
10745        // structurally unreachable inside `apply_compiled`'s op-loop
10746        // (the outermost frame is seeded at entry and every BeginList
10747        // / EndList pair preserves the count >= 1). Pin that the
10748        // NAMED invariant fires on the failure path so a regression
10749        // that drifts the loop's frame management surfaces a
10750        // diagnostic-grade panic rather than a silent unwrap over
10751        // `None`. Authoring tools / future debug-mode hooks can
10752        // pattern-match on the named invariant string instead of
10753        // tracking down an unnamed unwrap site.
10754        let mut empty: Vec<Vec<Sexp>> = Vec::new();
10755        let _ = current_builder_mut(&mut empty);
10756    }
10757
10758    #[test]
10759    fn current_builder_mut_routes_apply_compiled_literal_emit() {
10760        // End-to-end path-uniformity guard: a single-op program
10761        // `TemplateOp::Literal(s)` routes its push through
10762        // `current_builder_mut(&mut stack)` and the literal lands in
10763        // the outermost frame. After the op-loop completes the outer
10764        // `stack.pop().FinalNoValue` gate sees a non-empty top frame
10765        // containing exactly one element, which `apply_compiled`'s
10766        // tail (`top.len() == 1 { top.remove(0) }`) projects back as
10767        // the bound value. Pre-lift the same emission ran through
10768        // `stack.last_mut().unwrap().push(s.clone())`; post-lift it
10769        // runs through `current_builder_mut(&mut stack).push(s.clone())`
10770        // — the byte-identical outcome pins that the Literal arm's
10771        // routing through the new projection preserves the bytecode-
10772        // runtime's emission shape.
10773        let tmpl = CompiledTemplate {
10774            ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
10775        };
10776        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10777            .expect("literal-only template must succeed");
10778        assert_eq!(out, Sexp::symbol("hello"));
10779    }
10780
10781    #[test]
10782    fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
10783        // End-to-end path-uniformity guard for the post-EndList
10784        // parent-fold push: `(BeginList, Literal(a), Literal(b),
10785        // EndList)` builds an inner frame `[a, b]`, pops it on
10786        // EndList, then pushes `Sexp::List([a, b])` into the parent
10787        // (outer) frame via `current_builder_mut`. The outermost
10788        // `stack.pop()` then surfaces that list as the bound result.
10789        // Pre-lift the parent-fold push ran through
10790        // `stack.last_mut().unwrap().push(Sexp::List(items))`; post-
10791        // lift it runs through `current_builder_mut(&mut stack).
10792        // push(Sexp::List(items))` — pin the byte-identical outcome
10793        // so a regression that drifts the parent-fold target (e.g.,
10794        // pushes onto the just-popped frame's pointer instead of the
10795        // new top) fails loudly here.
10796        let tmpl = CompiledTemplate {
10797            ops: vec![
10798                TemplateOp::BeginList,
10799                TemplateOp::Literal(Sexp::symbol("a")),
10800                TemplateOp::Literal(Sexp::symbol("b")),
10801                TemplateOp::EndList,
10802            ],
10803        };
10804        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10805            .expect("BeginList/EndList template must succeed");
10806        assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
10807    }
10808
10809    #[test]
10810    fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
10811        // End-to-end path-uniformity guard for BOTH index-reading
10812        // arms routing through the lifted projection: a one-required +
10813        // one-rest macro `(call f &rest args)` with template
10814        // `(BeginList, Subst(0), Splice(1), EndList)` exercises both
10815        // Subst's clone-and-push AND Splice's splice-value-into
10816        // emit-paths against the current builder via
10817        // `current_builder_mut(&mut stack)`. The composed result is
10818        // `(foo 1 2 3)` — Subst lands the bound `f = foo` and
10819        // Splice flattens `args = (1 2 3)` — and the byte-identical
10820        // outcome pins that BOTH Subst and Splice arms' emits route
10821        // through the SHARED projection. Sibling to
10822        // `apply_compiled_splice_in_range_routes_past_resolve_bound
10823        // _arg_into_splice_value_into` which already exercises this
10824        // shape end-to-end; the addition here is the path-uniformity
10825        // anchor for the `current_builder_mut` lift specifically.
10826        let params = MacroParams {
10827            required: vec!["f".into()],
10828            optional: Vec::new(),
10829            rest: Some("args".into()),
10830        };
10831        let tmpl = CompiledTemplate {
10832            ops: vec![
10833                TemplateOp::BeginList,
10834                TemplateOp::Subst(0),
10835                TemplateOp::Splice(1),
10836                TemplateOp::EndList,
10837            ],
10838        };
10839        let out = apply_compiled(
10840            "call",
10841            &params,
10842            &tmpl,
10843            &[
10844                Sexp::symbol("foo"),
10845                Sexp::int(1),
10846                Sexp::int(2),
10847                Sexp::int(3),
10848            ],
10849        )
10850        .expect("Subst + Splice template must succeed");
10851        assert_eq!(
10852            out,
10853            Sexp::List(vec![
10854                Sexp::symbol("foo"),
10855                Sexp::int(1),
10856                Sexp::int(2),
10857                Sexp::int(3),
10858            ])
10859        );
10860    }
10861
10862    #[test]
10863    fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
10864        // Positive control for the `Splice` arm: a `&rest` macro that
10865        // splices a bound list routes through `resolve_bound_arg`'s
10866        // success arm and the `Splice` post-lookup verb
10867        // (`splice_value_into`) flattens the bound list into the
10868        // builder. Pin the composition's success path end-to-end:
10869        // the bound `Sexp::List([1, 2, 3])` at idx 1 flattens into
10870        // the outer builder's `(call 1 2 3)` shape — the same output
10871        // `rest_param_splices_with_at` pins through the public
10872        // surface, here pinned with the bytecode-runtime composition
10873        // exposed directly.
10874        let params = MacroParams {
10875            required: vec!["f".into()],
10876            optional: Vec::new(),
10877            rest: Some("args".into()),
10878        };
10879        let tmpl = CompiledTemplate {
10880            ops: vec![
10881                TemplateOp::BeginList,
10882                TemplateOp::Subst(0),
10883                TemplateOp::Splice(1),
10884                TemplateOp::EndList,
10885            ],
10886        };
10887        let out = apply_compiled(
10888            "call",
10889            &params,
10890            &tmpl,
10891            &[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
10892        )
10893        .expect("in-range Splice must succeed");
10894        assert_eq!(
10895            out,
10896            Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
10897        );
10898    }
10899
10900    // ── pop_builder_frame: the bytecode-runtime stack-frame consume ─────
10901    //
10902    // `pop_builder_frame(stack, macro_name, kind)` lifts the
10903    // `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
10904    // kind))?` chain that recurred at two sites inside `apply_compiled`
10905    // (the `EndList` arm + the post-loop final pop) into ONE named
10906    // primitive on the bytecode-runtime's stack-frame algebra. Sibling of
10907    // `current_builder_mut` (the top-frame-borrow projection — the same
10908    // `&mut Vec<Vec<Sexp>>` consumed at the borrow face) and
10909    // `resolve_bound_arg` (the bound-arg-by-index lookup primitive that
10910    // also routes through `TemplateInvariantKind` as the per-call-site
10911    // rejection identity). These tests pin the primitive's contract
10912    // directly; the existing `apply_compiled_*` tests are the path-
10913    // uniformity guards proving the two sites route through it without
10914    // behavior drift.
10915
10916    #[test]
10917    fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
10918        // Happy path: a two-frame stack pops the topmost frame off and
10919        // returns it AS-IS while shrinking the stack by exactly one
10920        // element. Pin both the return value (the popped frame's
10921        // contents are byte-identical to what was pushed) AND the
10922        // mutation (`stack.len()` drops from 2 to 1).
10923        let mut stack: Vec<Vec<Sexp>> = vec![
10924            vec![Sexp::symbol("outer")],
10925            vec![Sexp::int(1), Sexp::int(2)],
10926        ];
10927        let popped =
10928            pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
10929                .expect("non-empty stack must pop cleanly");
10930        assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
10931        assert_eq!(stack.len(), 1);
10932        assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10933    }
10934
10935    #[test]
10936    fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
10937        // Empty-stack rejection: an empty stack flows through the
10938        // `EndListEmptyStack` kind constructor into a structural
10939        // `LispError::TemplateInvariant { macro_name, kind:
10940        // EndListEmptyStack }` variant. Fail-before-pass-after: pre-lift
10941        // the same input would route through the inline
10942        // `stack.pop().ok_or_else(|| template_invariant_violation(_,
10943        // EndListEmptyStack))?` chain at the `EndList` arm; post-lift
10944        // it routes through ONE named primitive both pop-emitting
10945        // sites share, and a regression that drops the kind threading
10946        // (e.g. unifies both kinds into one constant) fails here.
10947        let mut empty: Vec<Vec<Sexp>> = Vec::new();
10948        let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
10949            .expect_err("empty stack must reject");
10950        match err {
10951            LispError::TemplateInvariant { macro_name, kind } => {
10952                assert_eq!(macro_name, "wrap");
10953                assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
10954            }
10955            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10956        }
10957    }
10958
10959    #[test]
10960    fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
10961        // Path-uniformity across the closed-set of pop-emitting kinds:
10962        // the same primitive threads `FinalNoValue` verbatim through
10963        // its `TemplateInvariantKind` slot, with the macro_name
10964        // identity preserved across the call boundary. Sibling of the
10965        // `EndListEmptyStack` test above — together they pin the
10966        // primitive's closed-set posture (BOTH reachable
10967        // pop-emitting kinds route through the same primitive, neither
10968        // is hard-coded), so a future `TemplateInvariantKind` variant
10969        // added for a new pop-emitting op (e.g. a hypothetical
10970        // `EndManyEmptyStack`) extends the primitive's reachability
10971        // mechanically by passing the new kind through the same slot.
10972        let mut empty: Vec<Vec<Sexp>> = Vec::new();
10973        let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
10974            .expect_err("empty stack must reject");
10975        match err {
10976            LispError::TemplateInvariant { macro_name, kind } => {
10977                assert_eq!(macro_name, "id");
10978                assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10979            }
10980            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10981        }
10982    }
10983
10984    #[test]
10985    fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
10986        // Closed-set posture check: even kinds the production
10987        // `apply_compiled` op-loop ROUTES through `resolve_bound_arg`
10988        // rather than `pop_builder_frame` (the indexed `SubstBadIndex(_)`
10989        // / `SpliceBadIndex(_)` siblings) MUST compose correctly with
10990        // this primitive's typed slot — they are NOT reachable here
10991        // from the production loop, but `TemplateInvariantKind` does
10992        // not distinguish "indexed" kinds from "stack-gate" kinds at
10993        // the helper's signature, so a regression that special-cases
10994        // one kind family would be a silent type-narrowing the closed-
10995        // set typed enum was lifted to prevent. Pin the universal
10996        // routing: ANY kind variant feeds through the primitive's
10997        // `kind` slot identically. Sibling assertion to
10998        // `template_invariant_violation_threads_subst_idx_through_typed_variant`
10999        // — same compose-the-kind-into-the-variant contract, one
11000        // composition step further down the substrate stack.
11001        let mut empty: Vec<Vec<Sexp>> = Vec::new();
11002        let err = pop_builder_frame(
11003            &mut empty,
11004            "compose",
11005            TemplateInvariantKind::SubstBadIndex(42),
11006        )
11007        .expect_err("empty stack must reject regardless of kind family");
11008        match err {
11009            LispError::TemplateInvariant { macro_name, kind } => {
11010                assert_eq!(macro_name, "compose");
11011                assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
11012            }
11013            other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11014        }
11015    }
11016
11017    #[test]
11018    fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
11019        // Structural-identity binding the lift to its pre-lift inline
11020        // shape: `pop_builder_frame(stack, macro_name, kind)` IS
11021        // `stack.pop().ok_or_else(|| template_invariant_violation(
11022        // macro_name, kind))?` — both reachable arms (success +
11023        // failure) must produce byte-identical outcomes. The success
11024        // arm checks the popped Vec contents AND the post-pop stack
11025        // length match the inline path; the failure arm checks the
11026        // emitted variant's identity AND its `macro_name` / `kind`
11027        // slots match the inline path. A regression that drifts the
11028        // primitive's projection (e.g. a `stack.swap_remove(_)` typo,
11029        // or a kind-rewrite at the helper boundary) fails on at least
11030        // one of the two arms.
11031        // Success arm:
11032        let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11033        let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11034        let via_lift = pop_builder_frame(
11035            &mut stack_lift,
11036            "macro",
11037            TemplateInvariantKind::EndListEmptyStack,
11038        )
11039        .expect("non-empty stack pops cleanly through lift");
11040        let via_inline = stack_inline.pop().ok_or_else(|| {
11041            template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
11042        });
11043        assert_eq!(
11044            via_lift,
11045            via_inline.unwrap(),
11046            "popped frame must be byte-identical across lift vs inline"
11047        );
11048        assert_eq!(
11049            stack_lift.len(),
11050            stack_inline.len(),
11051            "post-pop stack length must be byte-identical across lift vs inline"
11052        );
11053        // Failure arm:
11054        let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
11055        let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
11056        let err_lift = pop_builder_frame(
11057            &mut empty_lift,
11058            "macro",
11059            TemplateInvariantKind::FinalNoValue,
11060        )
11061        .expect_err("empty stack rejects through lift");
11062        let err_inline = empty_inline
11063            .pop()
11064            .ok_or_else(|| {
11065                template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
11066            })
11067            .expect_err("empty stack rejects through inline");
11068        match (err_lift, err_inline) {
11069            (
11070                LispError::TemplateInvariant {
11071                    macro_name: m_lift,
11072                    kind: k_lift,
11073                },
11074                LispError::TemplateInvariant {
11075                    macro_name: m_inline,
11076                    kind: k_inline,
11077                },
11078            ) => {
11079                assert_eq!(m_lift, m_inline);
11080                assert_eq!(k_lift, k_inline);
11081            }
11082            (l, i) => panic!(
11083                "expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
11084            ),
11085        }
11086    }
11087
11088    #[test]
11089    fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
11090        // End-to-end path-uniformity guard for the `EndList` arm: a
11091        // `(BeginList, Literal(x), EndList)` program pushes a child
11092        // frame, populates it with one literal, then routes the child
11093        // frame OUT of the stack via `pop_builder_frame` (kind
11094        // `EndListEmptyStack` — unreachable on this valid input, but
11095        // the kind threads through the primitive identically). The
11096        // post-pop verb (`Sexp::List(items)` push into the parent via
11097        // `current_builder_mut`) yields the same `Sexp::List([x])`
11098        // shape the consumer projected pre-lift. Pin the byte-
11099        // identical outcome so a regression that drifts the EndList
11100        // arm's routing (e.g. swaps the kind constructor, or routes
11101        // through a different stack-mutating primitive) fails loudly
11102        // here. Sibling of
11103        // `current_builder_mut_routes_apply_compiled_end_list_parent_fold`
11104        // — that test pins the parent-fold PUSH; this test pins the
11105        // child-frame POP that immediately precedes it. Together the
11106        // two close the EndList arm's path-uniformity across the
11107        // pop-then-push composition.
11108        let tmpl = CompiledTemplate {
11109            ops: vec![
11110                TemplateOp::BeginList,
11111                TemplateOp::Literal(Sexp::symbol("only")),
11112                TemplateOp::EndList,
11113            ],
11114        };
11115        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11116            .expect("BeginList/EndList one-literal template must succeed");
11117        assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
11118    }
11119
11120    #[test]
11121    fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
11122        // End-to-end path-uniformity guard for the post-loop final
11123        // pop: a single-op `Literal(s)` program emits `s` into the
11124        // outermost (seed) frame, then the post-loop tail routes the
11125        // seed frame OUT via `pop_builder_frame` (kind `FinalNoValue`
11126        // — unreachable on this valid input). The post-pop arity gate
11127        // (`top.len() == 1 { top.remove(0) }`) projects the literal
11128        // back as the bound value. Pre-lift the same emission ran
11129        // through the inline `stack.pop().ok_or_else(|| template_
11130        // invariant_violation(_, FinalNoValue))?` chain at the
11131        // post-loop tail; post-lift it routes through ONE named
11132        // primitive the EndList arm ALSO routes through, and the
11133        // single-literal outcome is byte-identical across both code
11134        // paths. Sibling of
11135        // `current_builder_mut_routes_apply_compiled_literal_emit` —
11136        // that test pins the EMIT into the seed frame; this test pins
11137        // the POP of the same seed frame at the post-loop tail.
11138        // Together the two close the Literal-only program's path-
11139        // uniformity across the emit-then-consume composition.
11140        let tmpl = CompiledTemplate {
11141            ops: vec![TemplateOp::Literal(Sexp::int(123))],
11142        };
11143        let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11144            .expect("literal-only template must succeed");
11145        assert_eq!(out, Sexp::int(123));
11146    }
11147
11148    // ── MacroParams: the typed param-list primitive ─────────────────────
11149    //
11150    // `parse_params` now yields a `MacroParams { required, optional, rest }`
11151    // whose shape makes the canonical lambda-list ordering (required →
11152    // optional → rest, "&rest is last + at-most-one", "&optional at most
11153    // once") structural rather than a construction discipline a `Vec<Param>`
11154    // only happened to uphold. These tests pin the parser's mapping into the
11155    // typed shape, the flat-index contract `names()` exposes to the template
11156    // bytecode, and the single positional binder `bind()` both expansion
11157    // strategies now route through. The end-to-end `rest_param_splices_with_at`
11158    // and `compiled_template_matches_substitute_path` tests above are the
11159    // path-uniformity guards proving both strategies still agree.
11160
11161    #[test]
11162    fn parse_params_maps_required_then_rest_into_typed_shape() {
11163        // `(a b &rest c)` — two required, one rest. The rest name lands in
11164        // the `Option`, never in `required`.
11165        let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
11166        assert_eq!(
11167            params,
11168            MacroParams {
11169                required: vec!["a".into(), "b".into()],
11170                optional: Vec::new(),
11171                rest: Some("c".into()),
11172            }
11173        );
11174    }
11175
11176    #[test]
11177    fn parse_params_rest_absent_leaves_none() {
11178        // `(x y)` — no `&rest`, so `rest` is structurally `None`. There is
11179        // no representation in which a rest-less list carries a stray rest.
11180        let params = parse_params(&read("x y").unwrap()).unwrap();
11181        assert_eq!(
11182            params,
11183            MacroParams {
11184                required: vec!["x".into(), "y".into()],
11185                optional: Vec::new(),
11186                rest: None,
11187            }
11188        );
11189    }
11190
11191    #[test]
11192    fn parse_params_maps_optional_section_between_required_and_rest() {
11193        // `(a &optional b c &rest d)` — the canonical lambda-list order. `a`
11194        // is required, `b`/`c` are optional, `d` is rest. The `&optional`
11195        // marker switches collection from `required` to `optional`; `&rest`
11196        // remains terminal.
11197        let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
11198        assert_eq!(
11199            params,
11200            MacroParams {
11201                required: vec!["a".into()],
11202                optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11203                rest: Some("d".into()),
11204            }
11205        );
11206    }
11207
11208    #[test]
11209    fn parse_params_optional_with_no_rest_leaves_rest_none() {
11210        // `(&optional x)` — a leading `&optional` (zero required) with no
11211        // rest. `required` is empty, `x` is the sole optional, `rest` None.
11212        let params = parse_params(&read("&optional x").unwrap()).unwrap();
11213        assert_eq!(
11214            params,
11215            MacroParams {
11216                required: Vec::new(),
11217                optional: vec![OptionalParam::bare("x")],
11218                rest: None,
11219            }
11220        );
11221    }
11222
11223    #[test]
11224    fn parse_params_rejects_repeated_optional_marker() {
11225        // `(a &optional b &optional c)` — a second `&optional` is
11226        // unrepresentable (one flat optional section), so the gate REJECTS
11227        // rather than binding args to a marker symbol named `&optional`. The
11228        // two marker positions (1 and 3) are named.
11229        let err = parse_params(&read("a &optional b &optional c").unwrap())
11230            .expect_err("repeated &optional must error");
11231        assert!(
11232            matches!(
11233                err,
11234                LispError::OptionalMarkerRepeated {
11235                    first_position: 1,
11236                    second_position: 3,
11237                }
11238            ),
11239            "expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
11240        );
11241    }
11242
11243    #[test]
11244    fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
11245        // `(&rest xs &optional y)` — `&rest <name>` is terminal, so the
11246        // `&optional y` tail is REJECTED as trailing tokens (not silently
11247        // dropped, and not a repeated-optional error: the rest gate fires
11248        // first). Pins the interaction the prior run (3627426) signposted.
11249        let err = parse_params(&read("&rest xs &optional y").unwrap())
11250            .expect_err("tokens after &rest <name> must error");
11251        assert!(
11252            matches!(err, LispError::RestParamTrailingTokens { .. }),
11253            "expected RestParamTrailingTokens, got: {err:?}"
11254        );
11255    }
11256
11257    #[test]
11258    fn names_are_required_then_optional_then_rest_in_flat_index_order() {
11259        // The flat-index contract the bytecode `Subst(idx)`/`Splice(idx)`
11260        // depends on: required names at 0.., then optional names, then the
11261        // rest name last.
11262        let params = MacroParams {
11263            required: vec!["a".into(), "b".into()],
11264            optional: vec![OptionalParam::bare("c")],
11265            rest: Some("d".into()),
11266        };
11267        assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
11268        // Optional names occupy the indices immediately after the required run.
11269        assert_eq!(params.names()[params.required.len()], "c");
11270        // The rest name is last, after required + optional — i.e. at the
11271        // structural `fixed_arity()` boundary the typed primitive names.
11272        assert_eq!(params.names()[params.fixed_arity()], "d");
11273    }
11274
11275    // ── `MacroParams::{REST_MARKER, OPTIONAL_MARKER,
11276    // LAMBDA_LIST_KEYWORD_LEAD}` — the typed CL lambda-list-keyword
11277    // marker algebra ────────────────────────────────────────────────
11278    //
11279    // The three `pub const`s close the Common-Lisp lambda-list keyword
11280    // family at the typed [`MacroParams`] algebra: `REST_MARKER` and
11281    // `OPTIONAL_MARKER` are the two `&'static str` markers the parser's
11282    // typed dispatch specialises on; `LAMBDA_LIST_KEYWORD_LEAD` is the
11283    // canonical `'&'` char shared as the LEAD byte of both markers.
11284    // Pre-lift the same two `&'static str` markers lived as two inline
11285    // `s == "..."` comparisons at `parse_params` — post-lift both
11286    // comparisons route through the typed constants so a delimiter swap
11287    // (e.g. a Racket-compat `#!rest` port, a Clojure-compat `&` port)
11288    // lands at ONE constant on the typed algebra.
11289    //
11290    // These pins cover: (a) the exact `&'static str` / `char` values,
11291    // (b) the structural round-trip law binding each `&'static str`
11292    // marker to its shared `char` LEAD byte, (c) the pairwise
11293    // disjointness of the two `&'static str` markers, (d) the cross-
11294    // axis disjointness of the `char` LEAD byte against every sibling
11295    // outer-marker `char` the substrate's other closed-set algebras
11296    // specialise on, and (e) the path-uniformity of `parse_params`
11297    // through the typed constants.
11298
11299    #[test]
11300    fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
11301        // Pins the constant's exact `&'static str` bytes so a typo
11302        // (`"&res"`, `"&Rest"`, `"&rst"`) or an accidental redefinition
11303        // surfaces immediately. Sibling-shape pin to
11304        // `macro_params_optional_marker_projects_canonical_ampersand_optional_str`
11305        // on the peer `&optional` axis.
11306        assert_eq!(
11307            MacroParams::REST_MARKER,
11308            "&rest",
11309            "MacroParams::REST_MARKER drifted from the substrate- \
11310             canonical CL lambda-list `&rest` marker — the parser's \
11311             `parse_params` rest-slot dispatch AND every downstream \
11312             authoring / rendering surface binds to this ONE typed \
11313             constant.",
11314        );
11315    }
11316
11317    #[test]
11318    fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
11319        // Pins the constant's exact `&'static str` bytes so a typo
11320        // (`"&opt"`, `"&Optional"`, `"&option"`) or an accidental
11321        // redefinition surfaces immediately. Sibling-shape pin to
11322        // `macro_params_rest_marker_projects_canonical_ampersand_rest_str`
11323        // on the peer `&rest` axis.
11324        assert_eq!(
11325            MacroParams::OPTIONAL_MARKER,
11326            "&optional",
11327            "MacroParams::OPTIONAL_MARKER drifted from the substrate- \
11328             canonical CL lambda-list `&optional` marker — the parser's \
11329             `parse_params` optional-section dispatch AND every \
11330             downstream authoring / rendering surface binds to this \
11331             ONE typed constant.",
11332        );
11333    }
11334
11335    #[test]
11336    fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
11337        // Pins the constant's exact `char` value so a typo (`'#'`,
11338        // `'@'`, `'!'`) or an accidental redefinition surfaces
11339        // immediately. Sibling-shape pin to
11340        // `atom_keyword_marker_lead_projects_canonical_colon_char`,
11341        // `atom_bool_literal_lead_projects_canonical_hash_char`,
11342        // `atom_str_delimiter_projects_canonical_double_quote_char`
11343        // on the peer per-role LEAD-byte axes across the substrate's
11344        // closed-set outer algebras.
11345        assert_eq!(
11346            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11347            '&',
11348            "LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
11349             canonical `&` LEAD byte — the CL lambda-list-keyword \
11350             family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
11351             typed constant as their common LEAD byte.",
11352        );
11353    }
11354
11355    #[test]
11356    fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
11357        // STRUCTURAL ROUND-TRIP CONTRACT: the `&'static str` marker
11358        // `MacroParams::REST_MARKER` starts with the `char` LEAD byte
11359        // `MacroParams::LAMBDA_LIST_KEYWORD_LEAD` — the projection law
11360        // binding the two typed constants on the [`MacroParams`]
11361        // algebra. A regression that renames the `&'static str` (e.g.
11362        // to Racket's `"#!rest"` keyword-args) OR the `char` (e.g. to
11363        // `'#'` for the `#!` shebang lead) without updating the other
11364        // fails HERE. Sibling-shape pin to
11365        // `atom_keyword_marker_lead_prefixes_keyword_marker` on the
11366        // peer `Atom`-algebra Keyword-prefix LEAD-byte axis.
11367        assert!(
11368            MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11369            "MacroParams::REST_MARKER `{}` does NOT start with \
11370             MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11371             typed constants have drifted apart on the [`MacroParams`] \
11372             algebra; the CL lambda-list-keyword family disjointness \
11373             contract can no longer bind to ONE shared LEAD byte.",
11374            MacroParams::REST_MARKER,
11375            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11376        );
11377    }
11378
11379    #[test]
11380    fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
11381        // STRUCTURAL ROUND-TRIP CONTRACT: peer to
11382        // `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
11383        // on the `&optional` axis. Both `&'static str` markers of the
11384        // CL lambda-list-keyword family MUST share the canonical `char`
11385        // LEAD byte so the typed-marker disjointness contract can bind
11386        // to ONE shared LEAD byte on the [`MacroParams`] algebra.
11387        assert!(
11388            MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11389            "MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
11390             MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11391             typed constants have drifted apart on the [`MacroParams`] \
11392             algebra; the CL lambda-list-keyword family disjointness \
11393             contract can no longer bind to ONE shared LEAD byte.",
11394            MacroParams::OPTIONAL_MARKER,
11395            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11396        );
11397    }
11398
11399    #[test]
11400    fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
11401        // PAIRWISE DISJOINTNESS PIN: the two `&'static str` markers on
11402        // the CL lambda-list-keyword algebra MUST differ so the
11403        // parser's typed dispatch cascade at `parse_params` (which
11404        // tests `REST_MARKER` FIRST, then `OPTIONAL_MARKER`) cannot
11405        // silently route both dispatches through the same arm. A
11406        // regression that aliases the two markers (e.g. both to
11407        // `"&rest"` after a typo) would silently drop the optional
11408        // section's structural distinction — every `&optional` name
11409        // would misclassify as a rest-slot marker.
11410        assert_ne!(
11411            MacroParams::REST_MARKER,
11412            MacroParams::OPTIONAL_MARKER,
11413            "REST_MARKER and OPTIONAL_MARKER collide — the parser's \
11414             typed dispatch cascade at `parse_params` can no longer \
11415             distinguish the rest-slot boundary from the optional- \
11416             section boundary.",
11417        );
11418    }
11419
11420    #[test]
11421    fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
11422        // CROSS-AXIS DISJOINTNESS PIN: `MacroParams::LAMBDA_LIST_KEYWORD_LEAD`
11423        // MUST NOT alias any sibling outer-marker `char` on the
11424        // substrate's other closed-set algebras — the Atom-payload
11425        // markers (`STR_DELIMITER`, `STR_ESCAPE_LEAD`,
11426        // `KEYWORD_MARKER_LEAD`, `BOOL_LITERAL_LEAD`), the paired list
11427        // delimiters (`Sexp::LIST_OPEN` / `Sexp::LIST_CLOSE`), the
11428        // paired line-comment delimiters (`Sexp::COMMENT_LEAD` /
11429        // `Sexp::COMMENT_TERM`), every `QuoteForm::lead_char`
11430        // projection, AND `QuoteForm::SPLICE_DISCRIMINATOR`. A
11431        // collision would silently break the reader's outer dispatch:
11432        // an `&`-prefixed bare atom `&rest` / `&optional` would collide
11433        // with whichever marker it aliased. Sibling-shape pin to
11434        // `atom_keyword_marker_lead_distinct_from_every_other_algebra_marker`
11435        // on the peer `Atom`-algebra Keyword-prefix LEAD-byte axis —
11436        // pins the SAME shape on the CL lambda-list-keyword LEAD-byte
11437        // axis. A future outer-marker extension that collided with
11438        // `'&'` fails HERE at the cross-axis enumeration.
11439        use crate::ast::{Atom, QuoteForm, Sexp};
11440
11441        assert_ne!(
11442            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11443            Atom::STR_DELIMITER,
11444            "LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
11445             bare `&rest` at a param-list position would ambiguously \
11446             begin a lambda-list keyword AND open a string.",
11447        );
11448        assert_ne!(
11449            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11450            Atom::STR_ESCAPE_LEAD,
11451            "LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
11452             the reader's Str-escape lead byte would alias the CL \
11453             lambda-list-keyword LEAD byte.",
11454        );
11455        assert_ne!(
11456            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11457            Atom::KEYWORD_MARKER_LEAD,
11458            "LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
11459             — a bare `&rest` at a param-list position would \
11460             ambiguously begin a lambda-list keyword AND begin an \
11461             `:foo` keyword.",
11462        );
11463        assert_ne!(
11464            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11465            Atom::BOOL_LITERAL_LEAD,
11466            "LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
11467             a bare `&rest` at a param-list position would ambiguously \
11468             begin a lambda-list keyword AND classify as a Bool prefix.",
11469        );
11470        assert_ne!(
11471            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11472            Sexp::LIST_OPEN,
11473            "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
11474             `&rest` at a param-list position would ambiguously begin \
11475             a lambda-list keyword AND open a list.",
11476        );
11477        assert_ne!(
11478            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11479            Sexp::LIST_CLOSE,
11480            "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
11481             `&rest` at a param-list position would ambiguously begin \
11482             a lambda-list keyword AND close a list.",
11483        );
11484        assert_ne!(
11485            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11486            Sexp::COMMENT_LEAD,
11487            "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
11488             bare `&rest` at a param-list position would ambiguously \
11489             begin a lambda-list keyword AND begin a comment.",
11490        );
11491        assert_ne!(
11492            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11493            Sexp::COMMENT_TERM,
11494            "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
11495             reader's line-comment discard loop would terminate on the \
11496             SAME byte the parser's lambda-list-keyword LEAD dispatch \
11497             binds to.",
11498        );
11499        for qf in QuoteForm::ALL {
11500            assert_ne!(
11501                MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11502                qf.lead_char(),
11503                "LAMBDA_LIST_KEYWORD_LEAD collides with \
11504                 QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
11505                 param-list position would ambiguously begin a lambda- \
11506                 list keyword AND begin a quote-family prefix.",
11507            );
11508        }
11509        assert_ne!(
11510            MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11511            QuoteForm::SPLICE_DISCRIMINATOR,
11512            "LAMBDA_LIST_KEYWORD_LEAD collides with \
11513             SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
11514             peek byte would alias the CL lambda-list-keyword LEAD \
11515             byte.",
11516        );
11517    }
11518
11519    #[test]
11520    fn parse_params_recognizes_rest_marker_via_typed_constant() {
11521        // PATH-UNIFORMITY PIN: the parser's rest-slot dispatch at
11522        // `parse_params` MUST classify the typed constant
11523        // `MacroParams::REST_MARKER` as the rest-slot boundary —
11524        // authoring surfaces that assemble a param list through the
11525        // typed constant (rather than through an inline string literal
11526        // read from user source) must reach the SAME rest-slot binding
11527        // the reader-driven path does.
11528        //
11529        // The `read(REST_MARKER)` composition here is the load-bearing
11530        // structural link: we build the param-list source through the
11531        // typed constant, then parse it through the reader, then the
11532        // parser MUST bind `xs` at the `rest` slot. A regression that
11533        // drifts the parser's dispatch away from the typed constant
11534        // (e.g. re-inlines a string literal that goes stale against
11535        // the constant) fails HERE.
11536        let src = format!("a {} xs", MacroParams::REST_MARKER);
11537        let params = parse_params(&read(&src).unwrap()).unwrap();
11538        assert_eq!(
11539            params,
11540            MacroParams {
11541                required: vec!["a".into()],
11542                optional: Vec::new(),
11543                rest: Some("xs".into()),
11544            },
11545            "parse_params dispatch drifted away from REST_MARKER — the \
11546             typed constant no longer routes to the rest-slot arm.",
11547        );
11548    }
11549
11550    #[test]
11551    fn parse_params_recognizes_optional_marker_via_typed_constant() {
11552        // PATH-UNIFORMITY PIN: peer to
11553        // `parse_params_recognizes_rest_marker_via_typed_constant` on
11554        // the `&optional` axis. The parser's optional-section dispatch
11555        // at `parse_params` MUST classify the typed constant
11556        // `MacroParams::OPTIONAL_MARKER` as the section-switch
11557        // boundary that reroutes subsequent bare-symbol names from the
11558        // `required` bin to the `optional` bin.
11559        let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
11560        let params = parse_params(&read(&src).unwrap()).unwrap();
11561        assert_eq!(
11562            params,
11563            MacroParams {
11564                required: vec!["a".into()],
11565                optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11566                rest: None,
11567            },
11568            "parse_params dispatch drifted away from OPTIONAL_MARKER — \
11569             the typed constant no longer routes to the optional- \
11570             section arm.",
11571        );
11572    }
11573
11574    // ── `MacroParams::LAMBDA_LIST_KEYWORDS` + `is_lambda_list_keyword` —
11575    // the closed-set forced-arity ALL array + membership gate that closes
11576    // the CL lambda-list-keyword family at the typed [`MacroParams`]
11577    // algebra. Sibling posture to the closed set of `pub const ALL: [Self;
11578    // N]` forced-arity arrays across the substrate's other closed-set
11579    // outer algebras (`AtomKind::ALL`, `QuoteForm::ALL`, `SexpShape::ALL`,
11580    // `UnquoteForm::ALL`, `MacroDefHead::ALL`); a future third marker
11581    // (`&key`, `&aux`, `&body`) extends the array's arity + adds ONE `pub
11582    // const` for the marker and every downstream family-wide contract
11583    // sweep (LEAD-byte round-trip, pairwise disjointness, membership
11584    // gate) picks up the extension at ONE structural site.
11585    //
11586    // These pins cover: (a) the cardinality contract (the ALL array's
11587    // length matches the pre-lift per-role marker count), (b) the
11588    // ordering contract (each ALL entry matches its corresponding per-
11589    // role `pub const` by-index), (c) the family-wide structural round-
11590    // trip (every ALL element starts_with LEAD), (d) the family-wide
11591    // pairwise disjointness (every distinct ALL index pair yields
11592    // distinct markers), (e) the membership-gate acceptance side (every
11593    // ALL element classifies as `true`), and (f) the membership-gate
11594    // rejection side (the bare LEAD byte + unrecognised `&`-prefixed
11595    // names classify as `false`).
11596    //
11597    // Fail-before/pass-after: every test below references
11598    // `MacroParams::LAMBDA_LIST_KEYWORDS` or
11599    // `MacroParams::is_lambda_list_keyword`, which simply did not exist
11600    // on `MacroParams` before this lift — every assertion was a
11601    // compile-time error against the prior surface.
11602
11603    #[test]
11604    fn macro_params_lambda_list_keywords_has_expected_cardinality() {
11605        // CARDINALITY PIN: the ALL array closes the family at exactly TWO
11606        // entries — the two `&'static str` markers the parser's typed
11607        // dispatch specialises on today (`&rest` + `&optional`). A future
11608        // third marker (`&key`, `&aux`, `&body`) extends this arity to
11609        // 3 AND every downstream family-wide contract sweep picks up the
11610        // extension at ONE structural site. A regression that drops a
11611        // marker from the ALL array (silently narrowing the family) OR
11612        // aliases two markers to the same index (silently narrowing the
11613        // typed dispatch) fails HERE at the cardinality contract before
11614        // the structural round-trip / pairwise-disjointness contracts
11615        // even fire.
11616        assert_eq!(
11617            MacroParams::LAMBDA_LIST_KEYWORDS.len(),
11618            2,
11619            "LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
11620             lambda-list-keyword family closure now names a different \
11621             number of markers than the two the parser's typed dispatch \
11622             specialises on.",
11623        );
11624    }
11625
11626    #[test]
11627    fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
11628        // ORDERING PIN: each ALL entry matches its corresponding per-role
11629        // `pub const` by-index. A regression that swaps the ordering
11630        // (`[OPTIONAL_MARKER, REST_MARKER]` after a rebase) would keep
11631        // the cardinality + membership contracts intact but silently
11632        // reorder downstream consumers that iterate `LAMBDA_LIST_KEYWORDS`
11633        // in canonical order (a future authoring surface that renders
11634        // "supported CL lambda-list keywords: &rest, &optional" would
11635        // silently render "supported CL lambda-list keywords: &optional,
11636        // &rest" instead). Pins the array's declaration-order binding
11637        // structurally so a reorder fails HERE at each element rather
11638        // than only at consumer sites downstream.
11639        assert_eq!(
11640            MacroParams::LAMBDA_LIST_KEYWORDS[0],
11641            MacroParams::REST_MARKER,
11642            "LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
11643             array's declaration-order binding to the per-role `pub \
11644             const` broke at the rest-slot marker slot.",
11645        );
11646        assert_eq!(
11647            MacroParams::LAMBDA_LIST_KEYWORDS[1],
11648            MacroParams::OPTIONAL_MARKER,
11649            "LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
11650             ALL array's declaration-order binding to the per-role \
11651             `pub const` broke at the optional-section marker slot.",
11652        );
11653    }
11654
11655    #[test]
11656    fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
11657        // FAMILY-WIDE STRUCTURAL ROUND-TRIP PIN: every element of the ALL
11658        // array MUST start with the canonical LEAD byte. Where the two
11659        // per-marker `_prefixed_by_lambda_list_keyword_lead` pins each
11660        // named ONE marker inline as a duplicate 3-line
11661        // `assert!(A.starts_with(LEAD), ...)` shape, this family-wide
11662        // sweep routes the SAME contract through the closed-set ALL
11663        // array — a future third marker (`&key`, `&aux`, `&body`)
11664        // extends the array AND is automatically covered by this sweep
11665        // without adding a third `_prefixed_by_...` per-marker pin. The
11666        // per-marker pins stay as fine-grained fail-loud sites; this
11667        // pin is the algebra-wide closure the family-extending refactor
11668        // routes through.
11669        for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11670            assert!(
11671                m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11672                "LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
11673                 LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
11674                 list-keyword family's structural round-trip contract \
11675                 no longer binds every marker to its shared LEAD byte.",
11676                lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11677            );
11678        }
11679    }
11680
11681    #[test]
11682    fn macro_params_lambda_list_keywords_pairwise_distinct() {
11683        // FAMILY-WIDE PAIRWISE DISJOINTNESS PIN: every distinct index pair
11684        // `(i, j)` in the ALL array yields distinct markers. Where the
11685        // pre-lift `_rest_and_optional_markers_pairwise_disjoint` pin
11686        // named the TWO markers as a hand-rolled `assert_ne!` pair, this
11687        // family-wide sweep routes the SAME contract through the closed-
11688        // set ALL array so a future third marker automatically extends
11689        // the sweep to `(3 * 2) / 2 == 3` distinct pairs without re-
11690        // deriving per-marker `assert_ne!` calls. The pre-lift pin
11691        // stays as the fine-grained fail-loud site; this pin is the
11692        // family-wide closure the algebra-extending refactor routes
11693        // through.
11694        for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11695            for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11696                if i == j {
11697                    continue;
11698                }
11699                assert_ne!(
11700                    a, b,
11701                    "LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
11702                     LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
11703                     list-keyword family's pairwise disjointness \
11704                     contract no longer binds distinct index pairs \
11705                     to distinct markers.",
11706                );
11707            }
11708        }
11709    }
11710
11711    #[test]
11712    fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
11713        // ACCEPTANCE-SIDE MEMBERSHIP PIN: every element of the ALL array
11714        // MUST classify as `true` through the typed membership gate. The
11715        // gate is defined as `LAMBDA_LIST_KEYWORDS.contains(&s)`, so this
11716        // pin is the structural closure binding the gate's return value
11717        // to the ALL array's element set. A regression that specialises
11718        // the gate to a subset of the array (e.g. a `matches!(s,
11719        // "&rest")` inline that silently drops the `&optional` branch)
11720        // fails HERE at the ALL sweep. Sibling-shape acceptance sweep to
11721        // the closed-set `ClosedSet::parse_label` roundtrip pin — every
11722        // element of `Self::ALL` decodes through the projection back to
11723        // itself.
11724        for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11725            assert!(
11726                MacroParams::is_lambda_list_keyword(m),
11727                "is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
11728                 element `{m}` — the closed-set membership gate's \
11729                 acceptance side drifted from the ALL array.",
11730            );
11731        }
11732    }
11733
11734    #[test]
11735    fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
11736        // REJECTION-SIDE MEMBERSHIP PIN (LEAD byte alone): the bare LEAD
11737        // byte `"&"` MUST classify as `false`. A future Clojure-compat
11738        // port might land a bare `&` marker — but until such an
11739        // extension explicitly lands on the ALL array, the substrate
11740        // MUST NOT silently classify the LEAD byte alone as a
11741        // recognised CL lambda-list keyword. Pins the gate's
11742        // rejection-side contract against a plausible future
11743        // near-miss extension.
11744        let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
11745        assert!(
11746            !MacroParams::is_lambda_list_keyword(&bare_lead),
11747            "is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
11748             the closed-set membership gate silently classifies the bare `&` \
11749             LEAD byte as a recognised CL lambda-list keyword despite the ALL \
11750             array containing only the two suffixed markers.",
11751        );
11752    }
11753
11754    #[test]
11755    fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
11756        // REJECTION-SIDE MEMBERSHIP PIN (unrecognised near-misses): the
11757        // three plausible future CL lambda-list keywords (`&key` for
11758        // keyword-args, `&aux` for auxiliaries, `&body` for the
11759        // docstring-carrying tail of `defmacro` bodies) MUST classify as
11760        // `false` UNTIL they explicitly land on the ALL array. Pins the
11761        // gate's conservative rejection contract — the closed-set
11762        // membership gate is not a "starts with `&`" heuristic; it is a
11763        // typed enumeration over the currently-recognised markers. A
11764        // regression that silently loosens the gate to a `starts_with`
11765        // check (dropping the closed-set discipline) fails HERE.
11766        for candidate in ["&key", "&aux", "&body"] {
11767            assert!(
11768                !MacroParams::is_lambda_list_keyword(candidate),
11769                "is_lambda_list_keyword accepted the unrecognised \
11770                 `&`-prefixed name `{candidate}` — the closed-set \
11771                 membership gate silently loosened its acceptance beyond \
11772                 the two markers the ALL array names today.",
11773            );
11774        }
11775    }
11776
11777    #[test]
11778    fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
11779        // REJECTION-SIDE MEMBERSHIP PIN (bare identifiers + empty): a
11780        // bare identifier (`"a"`, `"xs"`, `"foo"`) and the empty string
11781        // MUST classify as `false`. These are the shapes the parser's
11782        // bare-symbol dispatch cascade sees at `parse_params` when
11783        // walking a param list, so pinning them here binds the
11784        // membership gate's rejection contract to the parser's most
11785        // common non-marker input shapes.
11786        for candidate in ["a", "xs", "foo", ""] {
11787            assert!(
11788                !MacroParams::is_lambda_list_keyword(candidate),
11789                "is_lambda_list_keyword accepted the bare non-marker \
11790                 input `{candidate}` — the closed-set membership gate's \
11791                 rejection side no longer excludes bare identifiers \
11792                 the parser routes through the fall-through cascade.",
11793            );
11794        }
11795    }
11796
11797    // ── fixed_arity: the rest-start / rest-less max-arity primitive ─────
11798    //
11799    // `fixed_arity()` lifts the `self.required.len() + self.optional.len()`
11800    // arithmetic that recurred three times inside `MacroParams::bind` — at
11801    // the `Vec::with_capacity` site (where it adds `usize::from(rest.is_some())`
11802    // to get the bound-values count), at the `rest_start` site (inside the
11803    // `if let Some(rest)` branch), and at the `expected` site (inside the
11804    // rest-less `else`). The latter two sites live in mutually-exclusive
11805    // branches yet name ONE structural concept; lifting them collapses the
11806    // arithmetic to one named primitive. These tests pin the primitive's
11807    // contract directly; the existing `bind_*` tests are the path-uniformity
11808    // guards proving `bind`'s sites route through the same value without
11809    // behavior drift.
11810    //
11811    // Fail-before/pass-after: every test below references
11812    // `params.fixed_arity()`, which simply did not exist on `MacroParams`
11813    // before this lift — every assertion's `expect: ___ == params.fixed_arity()`
11814    // line was a compile-time error against the prior surface.
11815
11816    #[test]
11817    fn fixed_arity_is_zero_for_the_empty_param_list() {
11818        // `()` — a nullary macro has fixed arity 0, the rest-less binder
11819        // boundary at which the FIRST surplus arg already rejects.
11820        let params = MacroParams::default();
11821        assert_eq!(params.fixed_arity(), 0);
11822    }
11823
11824    #[test]
11825    fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
11826        // `(a b c)` — three required, no optional, no rest. fixed_arity is
11827        // exactly the required length.
11828        let params = MacroParams {
11829            required: vec!["a".into(), "b".into(), "c".into()],
11830            optional: Vec::new(),
11831            rest: None,
11832        };
11833        assert_eq!(params.fixed_arity(), 3);
11834    }
11835
11836    #[test]
11837    fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
11838        // `(&optional x y)` — two optional, no required. fixed_arity is the
11839        // optional length; the optional section participates in the fixed
11840        // run because supplied positional args bind to it.
11841        let params = MacroParams {
11842            required: Vec::new(),
11843            optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
11844            rest: None,
11845        };
11846        assert_eq!(params.fixed_arity(), 2);
11847    }
11848
11849    #[test]
11850    fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
11851        // `(a b &optional c d e)` — two required + three optional, no rest.
11852        // fixed_arity is 5: the maximum arity a rest-less call can supply.
11853        let params = MacroParams {
11854            required: vec!["a".into(), "b".into()],
11855            optional: vec![
11856                OptionalParam::bare("c"),
11857                OptionalParam::bare("d"),
11858                OptionalParam::bare("e"),
11859            ],
11860            rest: None,
11861        };
11862        assert_eq!(params.fixed_arity(), 5);
11863    }
11864
11865    #[test]
11866    fn fixed_arity_ignores_rest_slot_by_construction() {
11867        // `(a &optional b &rest r)` and `(a &optional b)` — identical fixed
11868        // arity (2). The `&rest` slot has NO maximum and is structurally
11869        // excluded from `fixed_arity`. Naming this invariant pins that a
11870        // regression that drifts the primitive to "required + optional +
11871        // rest.is_some() as usize" fails loudly here — that drift would
11872        // collapse `fixed_arity` into `names().len()`, losing the rest-start
11873        // vs total-bound-values distinction the typed shape relies on.
11874        let with_rest = MacroParams {
11875            required: vec!["a".into()],
11876            optional: vec![OptionalParam::bare("b")],
11877            rest: Some("r".into()),
11878        };
11879        let without_rest = MacroParams {
11880            required: vec!["a".into()],
11881            optional: vec![OptionalParam::bare("b")],
11882            rest: None,
11883        };
11884        assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
11885        assert_eq!(with_rest.fixed_arity(), 2);
11886    }
11887
11888    #[test]
11889    fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
11890        // When `rest` is `Some`, `names()[fixed_arity()]` IS the rest name
11891        // — the rest-start reading of the primitive. Same arithmetic the
11892        // bytecode index would hit (`Subst(fixed_arity())` resolves to the
11893        // rest-bound `Sexp::List`).
11894        let params = MacroParams {
11895            required: vec!["a".into(), "b".into()],
11896            optional: vec![OptionalParam::bare("c")],
11897            rest: Some("r".into()),
11898        };
11899        assert_eq!(params.fixed_arity(), 3);
11900        assert_eq!(params.names()[params.fixed_arity()], "r");
11901    }
11902
11903    #[test]
11904    fn fixed_arity_equals_names_length_when_rest_is_absent() {
11905        // When `rest` is `None`, `names().len() == fixed_arity()` — there
11906        // is no rest-name slot to extend the flat run past the fixed
11907        // boundary. Pins the structural identity
11908        // `names().len() == fixed_arity() + usize::from(rest.is_some())`
11909        // for the rest-less case; the rest-present case is pinned by the
11910        // sibling test above (where the boundary is the rest-name index,
11911        // i.e. one short of `names().len()`).
11912        let params = MacroParams {
11913            required: vec!["a".into(), "b".into()],
11914            optional: vec![OptionalParam::bare("c")],
11915            rest: None,
11916        };
11917        assert_eq!(params.names().len(), params.fixed_arity());
11918        assert_eq!(params.names().len(), 3);
11919    }
11920
11921    #[test]
11922    fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
11923        // The `expected` field of `TooManyMacroArgs` IS `fixed_arity()` —
11924        // the rest-less binder rejects iff `args.len() > fixed_arity()`.
11925        // This pin is the path-uniformity guard binding the typed primitive
11926        // to the binder's rejection contract: a regression that drifts
11927        // `bind`'s `expected` arithmetic from `fixed_arity()` would silently
11928        // surface a different boundary in the diagnostic without touching
11929        // the primitive — and this assertion fails loudly. Mirror of the
11930        // sibling rest-less surplus pin (`bind_rest_less_params_reject_
11931        // surplus_args`); this test pins WHAT the `expected` slot's value
11932        // structurally IS, that pin checks the variant SHAPE.
11933        let params = MacroParams {
11934            required: vec!["a".into(), "b".into()],
11935            optional: vec![OptionalParam::bare("c")],
11936            rest: None,
11937        };
11938        assert_eq!(params.fixed_arity(), 3);
11939        let err = params
11940            .bind(
11941                "m",
11942                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
11943            )
11944            .expect_err("4 args against fixed_arity 3 must reject");
11945        match err {
11946            LispError::TooManyMacroArgs {
11947                expected,
11948                got,
11949                macro_name,
11950            } => {
11951                assert_eq!(expected, params.fixed_arity());
11952                assert_eq!(got, 4);
11953                assert_eq!(macro_name, "m");
11954            }
11955            other => panic!("expected TooManyMacroArgs, got {other:?}"),
11956        }
11957    }
11958
11959    #[test]
11960    fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
11961        // When `rest` is `Some`, `bind` collects `args[fixed_arity()..]`
11962        // into the rest's `Sexp::List`. Pin that the rest list contents
11963        // are exactly the suffix beginning at `fixed_arity()` — the
11964        // primitive's rest-start reading IS the slice index the binder
11965        // consumes. A regression that drifts `bind`'s rest-collection
11966        // slice from `fixed_arity()` would surface as a misaligned rest
11967        // list (off-by-one in either direction) and this assertion fails
11968        // loudly. Sibling of `fixed_arity_is_the_rest_less_surplus_
11969        // rejection_boundary` on the rest-PRESENT branch.
11970        let params = MacroParams {
11971            required: vec!["a".into()],
11972            optional: vec![OptionalParam::bare("b")],
11973            rest: Some("r".into()),
11974        };
11975        assert_eq!(params.fixed_arity(), 2);
11976        let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
11977        let vals = params.bind("m", &args).unwrap();
11978        // Bound vec: [a=1, b=2, r=(3 4)] — the rest list IS args[fixed_arity()..].
11979        let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
11980        assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
11981    }
11982
11983    #[test]
11984    fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
11985        // Exactly-saturated rest-present call: `args.len() == fixed_arity()`.
11986        // The rest slot collects the empty slice; bind succeeds, never
11987        // misaligning to an off-by-one underflow. Pin the boundary on the
11988        // rest-PRESENT path — the rest-less mirror is
11989        // `too_many_macro_args_does_not_fire_at_exact_max_arity` above.
11990        let params = MacroParams {
11991            required: vec!["a".into()],
11992            optional: vec![OptionalParam::bare("b")],
11993            rest: Some("r".into()),
11994        };
11995        assert_eq!(params.fixed_arity(), 2);
11996        let vals = params
11997            .bind("m", &[Sexp::int(1), Sexp::int(2)])
11998            .expect("rest-present at exact fixed_arity must bind cleanly");
11999        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
12000    }
12001
12002    #[test]
12003    fn bind_threads_required_positionally_and_collects_rest_as_list() {
12004        // `(a b &rest c)` bound to `1 2 3 4`: a=1, b=2, c=(3 4). The bound
12005        // vec is parallel to `names()`, so the rest list sits at the rest's
12006        // flat index.
12007        let params = MacroParams {
12008            required: vec!["a".into(), "b".into()],
12009            optional: Vec::new(),
12010            rest: Some("c".into()),
12011        };
12012        let vals = params
12013            .bind(
12014                "m",
12015                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12016            )
12017            .unwrap();
12018        assert_eq!(
12019            vals,
12020            vec![
12021                Sexp::int(1),
12022                Sexp::int(2),
12023                Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12024            ]
12025        );
12026    }
12027
12028    #[test]
12029    fn bind_supplied_optional_takes_its_positional_arg() {
12030        // `(a &optional b)` bound to `1 2`: a=1, b=2. A supplied optional
12031        // behaves exactly like a positional — only its ABSENCE differs.
12032        let params = MacroParams {
12033            required: vec!["a".into()],
12034            optional: vec![OptionalParam::bare("b")],
12035            rest: None,
12036        };
12037        let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
12038        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
12039    }
12040
12041    #[test]
12042    fn bind_unsupplied_optional_defaults_to_nil() {
12043        // `(a &optional b c)` bound to just `1`: a=1, then b and c run out of
12044        // args and bind to `Sexp::Nil` — CL's default for an `&optional` with
12045        // no supplied default-form. The bound vec is still parallel to
12046        // `names()`, so the template's `,b` / `,c` resolve to nil, not a
12047        // missing-arg error.
12048        let params = MacroParams {
12049            required: vec!["a".into()],
12050            optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
12051            rest: None,
12052        };
12053        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12054        assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
12055    }
12056
12057    #[test]
12058    fn bind_rest_collects_args_beyond_required_and_optional() {
12059        // `(a &optional b &rest c)` bound to `1 2 3 4`: a=1, b=2 (supplied),
12060        // c=(3 4). The rest starts AFTER the required+optional run, so the
12061        // optional's supplied arg is not swept into the rest.
12062        let params = MacroParams {
12063            required: vec!["a".into()],
12064            optional: vec![OptionalParam::bare("b")],
12065            rest: Some("c".into()),
12066        };
12067        let vals = params
12068            .bind(
12069                "m",
12070                &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12071            )
12072            .unwrap();
12073        assert_eq!(
12074            vals,
12075            vec![
12076                Sexp::int(1),
12077                Sexp::int(2),
12078                Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12079            ]
12080        );
12081    }
12082
12083    #[test]
12084    fn bind_unsupplied_optional_then_empty_rest() {
12085        // `(a &optional b &rest c)` bound to just `1`: a=1, b=nil (absent),
12086        // c=() (nothing left). Both the optional default AND the empty-rest
12087        // contract hold in the same bind.
12088        let params = MacroParams {
12089            required: vec!["a".into()],
12090            optional: vec![OptionalParam::bare("b")],
12091            rest: Some("c".into()),
12092        };
12093        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12094        assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
12095    }
12096
12097    #[test]
12098    fn bind_rest_with_no_remaining_args_is_the_empty_list() {
12099        // Exactly-saturated required args + a rest that captures nothing →
12100        // the rest binds to the empty list, never errors. Mirrors the
12101        // splice contract `,@()` contributes nothing.
12102        let params = MacroParams {
12103            required: vec!["a".into()],
12104            optional: Vec::new(),
12105            rest: Some("c".into()),
12106        };
12107        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12108        assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
12109    }
12110
12111    #[test]
12112    fn bind_missing_required_errors_before_any_rest_collection() {
12113        // A required name with no arg at its position is a
12114        // `MissingMacroArg` — the gate fires during the required walk,
12115        // before the rest is ever collected.
12116        let params = MacroParams {
12117            required: vec!["a".into(), "b".into()],
12118            optional: Vec::new(),
12119            rest: Some("c".into()),
12120        };
12121        let err = params
12122            .bind("m", &[Sexp::int(1)])
12123            .expect_err("missing required `b` must error");
12124        assert!(
12125            matches!(err, LispError::MissingMacroArg { .. }),
12126            "expected MissingMacroArg, got: {err:?}"
12127        );
12128    }
12129
12130    #[test]
12131    fn bind_missing_required_errors_even_with_optional_present() {
12132        // An absent REQUIRED arg errors even when the param list has an
12133        // optional section: the required walk fires `MissingMacroArg` before
12134        // the optional arm (which would otherwise default to nil) is reached.
12135        // Required absence is an error; optional absence is a nil default.
12136        let params = MacroParams {
12137            required: vec!["a".into(), "b".into()],
12138            optional: vec![OptionalParam::bare("c")],
12139            rest: None,
12140        };
12141        let err = params
12142            .bind("m", &[Sexp::int(1)])
12143            .expect_err("missing required `b` must error before optional defaulting");
12144        assert!(
12145            matches!(err, LispError::MissingMacroArg { .. }),
12146            "expected MissingMacroArg, got: {err:?}"
12147        );
12148    }
12149
12150    #[test]
12151    fn bind_rest_less_params_reject_surplus_args() {
12152        // The rest-less binder REJECTS surplus call args via the
12153        // structural `TooManyMacroArgs { macro_name, expected, got }`
12154        // rejection — the call-site mirror of `RestParamTrailingTokens`
12155        // (the definition-site rejection lifted at the parse_params
12156        // boundary). Closes the asymmetry where the typed-entry
12157        // macro-call-gate rejected too-few-args loudly
12158        // (`MissingMacroArg`) but silently truncated too-many. `expected`
12159        // is the rest-less binder's fixed maximum arity
12160        // (`required.len() + optional.len()`); `got` is the actual
12161        // call-site arg count.
12162        let params = MacroParams {
12163            required: vec!["a".into()],
12164            optional: Vec::new(),
12165            rest: None,
12166        };
12167        let err = params
12168            .bind("m", &[Sexp::int(1), Sexp::int(2)])
12169            .expect_err("rest-less surplus must error");
12170        match err {
12171            LispError::TooManyMacroArgs {
12172                macro_name,
12173                expected,
12174                got,
12175            } => {
12176                assert_eq!(macro_name, "m");
12177                assert_eq!(expected, 1);
12178                assert_eq!(got, 2);
12179            }
12180            other => panic!("expected TooManyMacroArgs, got: {other:?}"),
12181        }
12182    }
12183
12184    // ── OptionalParam: per-param default forms — `&optional (x DEFAULT)` ──
12185    //
12186    // The `&optional` section now admits both bare-symbol entries (`x`) AND
12187    // list-form entries (`(x DEFAULT)`). The typed `OptionalParam.default:
12188    // Option<Sexp>` slot makes the per-param default a FIELD on each
12189    // optional entry, not a discipline a sibling `Vec<Sexp>` would have had
12190    // to maintain in lock-step with `Vec<String>`. These tests pin: the
12191    // parser admits both shapes side-by-side; the four malformed list-spec
12192    // shapes (empty / missing-default / extra-elements / non-symbol-name)
12193    // are rejected via `OptionalParamMalformed` with the typed
12194    // `OptionalParamMalformedReason`; the binder consults the default form
12195    // when the arg is absent and ignores it when supplied; and the end-to-
12196    // end expansion agrees between the bytecode and substitute strategies
12197    // (invariant 2 — free middle).
12198
12199    #[test]
12200    fn parse_params_admits_optional_list_spec_with_default() {
12201        // `(a &optional (b 5))` — one bare optional becomes
12202        // `OptionalParam { name: "b", default: Some(Int(5)) }`. The
12203        // surrounding `MacroParams` shape is otherwise identical.
12204        let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
12205        assert_eq!(
12206            params,
12207            MacroParams {
12208                required: vec!["a".into()],
12209                optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12210                rest: None,
12211            }
12212        );
12213    }
12214
12215    #[test]
12216    fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
12217        // `(a &optional b (c "x") d (e 9) &rest r)` — the optional section
12218        // interleaves bare and list-form specs. Each lands in its own
12219        // `OptionalParam` entry; `names()` still yields the flat
12220        // required-then-optional-then-rest order.
12221        let params =
12222            parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
12223        assert_eq!(
12224            params,
12225            MacroParams {
12226                required: vec!["a".into()],
12227                optional: vec![
12228                    OptionalParam::bare("b"),
12229                    OptionalParam::with_default("c", Sexp::string("x")),
12230                    OptionalParam::bare("d"),
12231                    OptionalParam::with_default("e", Sexp::int(9)),
12232                ],
12233                rest: Some("r".into()),
12234            }
12235        );
12236        assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
12237    }
12238
12239    #[test]
12240    fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
12241        // `(&optional (x (list 1 2)))` — the default form is itself a list.
12242        // Without an evaluator, the literal Sexp is parked verbatim into
12243        // `default`; the binder produces it for any absent call.
12244        let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
12245        let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12246        assert_eq!(
12247            params,
12248            MacroParams {
12249                required: Vec::new(),
12250                optional: vec![OptionalParam::with_default("x", want_default)],
12251                rest: None,
12252            }
12253        );
12254    }
12255
12256    #[test]
12257    fn parse_params_rejects_empty_list_optional_spec() {
12258        // `(&optional ())` — a zero-element list is the empty-list rejection.
12259        // Without the gate the loop would `as_symbol()` on a `Sexp::List` and
12260        // fall through to `NonSymbolParam`, which mis-classifies the failure
12261        // (this is a malformed DEFAULT-FORM spec, not a "param must be a
12262        // symbol" rejection).
12263        let err = parse_params(&read("&optional ()").unwrap())
12264            .expect_err("empty list optional spec must error");
12265        assert!(
12266            matches!(
12267                err,
12268                LispError::OptionalParamMalformed {
12269                    position: 1,
12270                    reason: crate::error::OptionalParamMalformedReason::EmptyList,
12271                    ..
12272                }
12273            ),
12274            "expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
12275        );
12276    }
12277
12278    #[test]
12279    fn parse_params_rejects_one_element_optional_list_as_missing_default() {
12280        // `(&optional (x))` — a one-element list. REJECTED with reason
12281        // `MissingDefault` rather than reinterpreted as `&optional x`, because
12282        // a parenthesized single-element spec is structurally ambiguous and
12283        // the bare-symbol form `x` IS the canonical "no default" shape.
12284        let err = parse_params(&read("&optional (x)").unwrap())
12285            .expect_err("one-element list optional spec must error");
12286        assert!(
12287            matches!(
12288                err,
12289                LispError::OptionalParamMalformed {
12290                    position: 1,
12291                    reason: crate::error::OptionalParamMalformedReason::MissingDefault,
12292                    ..
12293                }
12294            ),
12295            "expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
12296        );
12297    }
12298
12299    #[test]
12300    fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
12301        // `(&optional (x 5 6))` — a three-element list. CL's `(name default
12302        // supplied-p)` shape is not yet supported (no evaluator → no
12303        // supplied-p variable binding), so the third element is structurally
12304        // surplus. REJECTED with reason `ExtraElements{length: 3}`.
12305        let err = parse_params(&read("&optional (x 5 6)").unwrap())
12306            .expect_err("three-element list optional spec must error");
12307        assert!(
12308            matches!(
12309                err,
12310                LispError::OptionalParamMalformed {
12311                    position: 1,
12312                    reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
12313                    ..
12314                }
12315            ),
12316            "expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
12317        );
12318    }
12319
12320    #[test]
12321    fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
12322        // `(&optional (5 default))` — the name slot must be a symbol; a
12323        // numeric literal is REJECTED with reason `NonSymbolName`. Without
12324        // this branch the gate would silently populate
12325        // `OptionalParam.name` from a stringified non-symbol value (`"5"`),
12326        // breaking the invariant that param names are symbols.
12327        let err = parse_params(&read("&optional (5 default)").unwrap())
12328            .expect_err("non-symbol-name optional spec must error");
12329        assert!(
12330            matches!(
12331                err,
12332                LispError::OptionalParamMalformed {
12333                    position: 1,
12334                    reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
12335                    ..
12336                }
12337            ),
12338            "expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
12339        );
12340    }
12341
12342    #[test]
12343    fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
12344        // `((a 5))` — a list in the REQUIRED section is NOT a default-form
12345        // spec; default forms are an optional-section affordance. The gate
12346        // must fall through to `NonSymbolParam` (parity with the prior
12347        // behavior on lists in the required section), not silently admit
12348        // the list as a default-form spec.
12349        let err =
12350            parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
12351        assert!(
12352            matches!(err, LispError::NonSymbolParam { position: 0, .. }),
12353            "expected NonSymbolParam{{position: 0}}, got: {err:?}"
12354        );
12355    }
12356
12357    #[test]
12358    fn bind_unsupplied_optional_with_default_takes_the_default() {
12359        // `(a &optional (b 5))` bound to just `1`: a=1, b=5 (the declared
12360        // default), not nil. The default form is consulted ONLY when the
12361        // call ran out of args.
12362        let params = MacroParams {
12363            required: vec!["a".into()],
12364            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12365            rest: None,
12366        };
12367        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12368        assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
12369    }
12370
12371    #[test]
12372    fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
12373        // `(&optional (b 5))` bound to `42`: b=42, NOT the default. A
12374        // supplied optional ALWAYS takes its arg; the default is the
12375        // absence-only fallback. Pins that the default form does not
12376        // shadow a supplied call arg.
12377        let params = MacroParams {
12378            required: Vec::new(),
12379            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12380            rest: None,
12381        };
12382        let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12383        assert_eq!(vals, vec![Sexp::int(42)]);
12384    }
12385
12386    #[test]
12387    fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
12388        // `(a &optional (b 5) c (d "z"))` bound to just `1`: a=1, b=5
12389        // (default), c=nil (bare floor), d="z" (default). The three
12390        // absence cases coexist in one bind: per-default fill, nil floor,
12391        // and a tail with a literal-string default.
12392        let params = MacroParams {
12393            required: vec!["a".into()],
12394            optional: vec![
12395                OptionalParam::with_default("b", Sexp::int(5)),
12396                OptionalParam::bare("c"),
12397                OptionalParam::with_default("d", Sexp::string("z")),
12398            ],
12399            rest: None,
12400        };
12401        let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12402        assert_eq!(
12403            vals,
12404            vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
12405        );
12406    }
12407
12408    // ── OptionalParam::resolved_default: the absent-call binder accessor ──
12409    //
12410    // `resolved_default` lifts the `param.default.clone().unwrap_or(Sexp::Nil)`
12411    // two-arm fallback that previously inlined at `MacroParams::bind`'s
12412    // optional arm into ONE named accessor on the typed `OptionalParam`.
12413    // The constructor pair `bare` / `with_default` defines the typed
12414    // shapes of the `default` slot; this accessor names the symmetric
12415    // bound-value projection both shapes yield at the absence boundary.
12416    // Tests pin: (a) `bare(name).resolved_default()` is the `Sexp::Nil`
12417    // no-default floor; (b) `with_default(name, d).resolved_default()` is
12418    // `d.clone()`; (c) the projection is `Clone`-stable across repeated
12419    // calls (the typed `default` field is not consumed); (d) path-
12420    // uniformity at the binder — `bind`'s optional arm routes through
12421    // `resolved_default` for both shapes; (e) end-to-end through both
12422    // expansion strategies, the absent-call binding agrees.
12423
12424    #[test]
12425    fn resolved_default_is_nil_for_bare_optional() {
12426        // `OptionalParam::bare(name).default` is `None`, so
12427        // `resolved_default()` projects to `Sexp::Nil` — the CL
12428        // `&optional` no-default-form floor. Fail-before/pass-after: this
12429        // assert is meaningless pre-lift because the helper does not
12430        // exist; post-lift it pins the typed accessor's `Sexp::Nil` arm
12431        // at the named primitive. Sibling of `bare` itself: the
12432        // constructor defines the shape (`default: None`); the accessor
12433        // names the bound-value projection of that shape.
12434        let p = OptionalParam::bare("x");
12435        assert_eq!(p.resolved_default(), Sexp::Nil);
12436    }
12437
12438    #[test]
12439    fn resolved_default_clones_declared_default_for_with_default_optional() {
12440        // `OptionalParam::with_default(name, d).default` is `Some(d)`, so
12441        // `resolved_default()` projects to `d.clone()` — the declared
12442        // default form. Sibling of the bare-floor pin: the closed-set
12443        // `default: Option<Sexp>` slot's two shapes correspond 1:1 with
12444        // the two arms of `resolved_default`. Pins the closed-set
12445        // exhaustive coverage of `Option<Sexp>` × `{Some, None}`.
12446        let p = OptionalParam::with_default("x", Sexp::int(5));
12447        assert_eq!(p.resolved_default(), Sexp::int(5));
12448    }
12449
12450    #[test]
12451    fn resolved_default_clones_arbitrary_sexp_default_form() {
12452        // The declared default can be any `Sexp` — a literal list, a
12453        // keyword, a string, a quasi-quoted form — because v0 has no
12454        // evaluator and the typed slot parks the literal verbatim. Pin
12455        // that `resolved_default()` is faithful to the parked literal
12456        // regardless of shape: a regression that special-cases an arm
12457        // (e.g., projecting `Sexp::List(_)` to `Sexp::Nil`, or "normalizing"
12458        // a `Sexp::Quote`) fails here. The accessor is exactly
12459        // `default.clone()` for the `Some` arm — no shape rewriting.
12460        let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12461        let p = OptionalParam::with_default("x", arbitrary.clone());
12462        assert_eq!(p.resolved_default(), arbitrary);
12463    }
12464
12465    #[test]
12466    fn resolved_default_is_clone_stable_across_repeated_calls() {
12467        // The accessor takes `&self` and projects through `Clone`, so
12468        // repeated calls yield IDENTICAL values — the typed `default`
12469        // field is not consumed. Pins that the accessor is idempotent
12470        // for the same `OptionalParam`, which is the contract the binder
12471        // relies on across multiple `bind` invocations of the same
12472        // macro: every call that leaves the optional unfilled yields
12473        // the SAME bound value, never a partially-consumed shape. A
12474        // regression that converted the accessor to `self.default.take()`
12475        // (consuming the field) would still type-check at the call site
12476        // but would silently desync repeated absent-call bindings; this
12477        // test catches that drift.
12478        let p = OptionalParam::with_default("x", Sexp::string("hi"));
12479        let first = p.resolved_default();
12480        let second = p.resolved_default();
12481        assert_eq!(first, second);
12482        assert_eq!(first, Sexp::string("hi"));
12483    }
12484
12485    #[test]
12486    fn resolved_default_is_the_binders_absent_optional_projection() {
12487        // Path-uniformity pin at the binder boundary: `MacroParams::bind`'s
12488        // optional arm consults `param.resolved_default()` for any
12489        // absent slot. Two-arm coverage: a bare optional (`b`) binds to
12490        // `Sexp::Nil` via the `None` arm; a with-default optional
12491        // (`(c 5)`) binds to `Sexp::int(5)` via the `Some` arm. The
12492        // single bind call exercises both arms in one walk, and the
12493        // bound values vec is parallel to `names()` so position
12494        // checking pins the arm-to-slot mapping. A regression that
12495        // re-inlines the two-arm fallback at the binder, drifting it
12496        // independently from the accessor, would still type-check but
12497        // a future shape change to `resolved_default` (e.g., adding a
12498        // typed `&supplied-p` companion slot) would silently desync
12499        // the binder from the accessor — this test catches that drift.
12500        let params = MacroParams {
12501            required: Vec::new(),
12502            optional: vec![
12503                OptionalParam::bare("b"),
12504                OptionalParam::with_default("c", Sexp::int(5)),
12505            ],
12506            rest: None,
12507        };
12508        // Empty args → both optionals are absent → both arms fire.
12509        let vals = params.bind("m", &[]).unwrap();
12510        assert_eq!(vals.len(), 2);
12511        assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
12512        assert_eq!(
12513            vals[1],
12514            OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
12515        );
12516        // And the absolute identities pin the projection's arms:
12517        assert_eq!(vals[0], Sexp::Nil);
12518        assert_eq!(vals[1], Sexp::int(5));
12519    }
12520
12521    #[test]
12522    fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
12523        // End-to-end path-uniformity: a macro with both an `&optional
12524        // (g "hi")` (with-default) and an `&optional h` (bare) param
12525        // expands the same way under bytecode AND substitute
12526        // strategies, because both strategies route through the SHARED
12527        // `MacroParams::bind` which now consults `resolved_default`
12528        // for absent slots. Pins that the accessor's contract is
12529        // structurally observable at the strategy boundary — a
12530        // regression that bifurcated the accessor's behavior between
12531        // the two paths (impossible, since they share `bind`) would
12532        // surface here.
12533        let src = r#"
12534            (defmacro greet (n &optional (g "hi") h)
12535              `(list ,g ,n ,h))
12536            (greet world)
12537        "#;
12538        let expected = vec![Sexp::List(vec![
12539            Sexp::symbol("list"),
12540            Sexp::string("hi"),
12541            Sexp::symbol("world"),
12542            Sexp::Nil,
12543        ])];
12544        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12545        let substitute = Expander::new_substitute_only()
12546            .expand_program(read(src).unwrap())
12547            .unwrap();
12548        assert_eq!(
12549            bytecode, expected,
12550            "bytecode resolved_default expansion drifted"
12551        );
12552        assert_eq!(
12553            substitute, expected,
12554            "substitute resolved_default expansion drifted"
12555        );
12556        assert_eq!(
12557            bytecode, substitute,
12558            "the two strategies disagree on resolved_default expansion"
12559        );
12560    }
12561
12562    #[test]
12563    fn resolved_default_supplied_optional_does_not_consult_accessor() {
12564        // A SUPPLIED optional binds to its CALL ARG, never to the
12565        // accessor's projection. Pins the contract: `resolved_default`
12566        // is the absence-only fallback; a present arg shadows the
12567        // accessor at the binder. Sibling negative control to
12568        // `resolved_default_is_the_binders_absent_optional_projection`:
12569        // that test exercises the absence arm at every slot; this test
12570        // exercises the presence arm at every slot and proves the
12571        // accessor's `Sexp::int(5)` projection is NOT consulted when
12572        // the optional is supplied with `Sexp::int(42)`. A regression
12573        // that wired the binder to always consult the accessor (the
12574        // wrong direction — the default would shadow supplied args) is
12575        // caught here.
12576        let params = MacroParams {
12577            required: Vec::new(),
12578            optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12579            rest: None,
12580        };
12581        let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12582        assert_eq!(vals, vec![Sexp::int(42)]);
12583        // And the accessor's would-be projection IS NOT the bound value:
12584        let p = OptionalParam::with_default("b", Sexp::int(5));
12585        assert_ne!(vals[0], p.resolved_default());
12586    }
12587
12588    #[test]
12589    fn optional_default_macro_expands_end_to_end_under_both_strategies() {
12590        // The end-to-end path: a macro with `&optional (g "hi")` expands to
12591        // the default literal when unsupplied, and to the supplied arg when
12592        // present. Both the bytecode and substitute strategies must agree
12593        // (invariant 2 — free middle); they share `MacroParams::bind`, so
12594        // the default arm lands once in `bind` and both strategies inherit
12595        // it unable to drift. This is the test the prior run (611a682)
12596        // signposted as the next-change-that-benefits.
12597        let src = r#"
12598            (defmacro greet (n &optional (g "hi"))
12599              `(list ,g ,n))
12600            (greet world)
12601            (greet world there)
12602        "#;
12603        let expected = vec![
12604            Sexp::List(vec![
12605                Sexp::symbol("list"),
12606                Sexp::string("hi"),
12607                Sexp::symbol("world"),
12608            ]),
12609            Sexp::List(vec![
12610                Sexp::symbol("list"),
12611                Sexp::symbol("there"),
12612                Sexp::symbol("world"),
12613            ]),
12614        ];
12615        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12616        let substitute = Expander::new_substitute_only()
12617            .expand_program(read(src).unwrap())
12618            .unwrap();
12619        assert_eq!(
12620            bytecode, expected,
12621            "bytecode optional-default expansion drifted"
12622        );
12623        assert_eq!(
12624            substitute, expected,
12625            "substitute optional-default expansion drifted"
12626        );
12627        assert_eq!(
12628            bytecode, substitute,
12629            "the two strategies disagree on optional-default expansion"
12630        );
12631    }
12632
12633    #[test]
12634    fn optional_macro_expands_end_to_end_under_both_strategies() {
12635        // The end-to-end path: a macro with an `&optional` param expands
12636        // correctly whether the optional is supplied or defaulted, and the
12637        // bytecode and substitute strategies agree (invariant 2 — free
12638        // middle). `,b` resolves to the supplied arg when present, to
12639        // `Sexp::Nil` when absent (CL's `&optional` default).
12640        let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
12641        // (cons 1 2) — optional supplied; (cons 3 <Nil>) — optional defaulted.
12642        // The defaulted slot is the canonical `Sexp::Nil`, distinct in the AST
12643        // from a reader-produced empty list `()` even though both Display as
12644        // `()`.
12645        let expected = vec![
12646            Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
12647            Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
12648        ];
12649        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12650        let substitute = Expander::new_substitute_only()
12651            .expand_program(read(src).unwrap())
12652            .unwrap();
12653        assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
12654        assert_eq!(
12655            substitute, expected,
12656            "substitute optional expansion drifted"
12657        );
12658        assert_eq!(
12659            bytecode, substitute,
12660            "the two strategies disagree on optional expansion"
12661        );
12662    }
12663
12664    // ── MacroDef::template_body: the shared body-projection primitive ──
12665    //
12666    // `template_body` lifts the `match &def.body { Sexp::Quasiquote(inner)
12667    // => inner.as_ref(), other => other }` inline peel — present
12668    // byte-identically at the bytecode (`compile_template`) AND substitute
12669    // (`apply`'s fallback) path entries — into ONE named projection both
12670    // strategies share. The existing `compiled_template_matches_substitute
12671    // _path` and `expansion_layers_agree_on_output_and_cache_wins` tests
12672    // are the path-uniformity guards covering the SHARED-PROJECTION shape;
12673    // the four tests below pin the projection's contract DIRECTLY:
12674    // (a) a quasi-quoted body unwraps to the inner; (b) a non-quasi-quoted
12675    // body returns the body verbatim; (c) the borrow is rooted in the
12676    // body field (single-level peel); (d) the projection is the same
12677    // `&Sexp` both strategies route on (so a regression that drifts the
12678    // body-peel from one strategy to the other becomes a type-level
12679    // change at this helper, not a silent two-site divergence).
12680
12681    #[test]
12682    fn template_body_unwraps_outer_quasiquote_to_inner() {
12683        // The canonical authoring shape: `(defmacro f (a) `(list ,a))` —
12684        // the reader wraps the `` ` `` form into `Sexp::Quasiquote(inner)`,
12685        // and `template_body` peels the outer marker. The returned `&Sexp`
12686        // is the inner walker-payload both expansion strategies consume.
12687        let inner = Sexp::List(vec![
12688            Sexp::symbol("list"),
12689            Sexp::Unquote(Box::new(Sexp::symbol("a"))),
12690        ]);
12691        let def = MacroDef {
12692            name: "f".into(),
12693            params: MacroParams::default(),
12694            body: Sexp::Quasiquote(Box::new(inner.clone())),
12695        };
12696        assert_eq!(def.template_body(), &inner);
12697    }
12698
12699    #[test]
12700    fn template_body_returns_non_quasiquote_body_verbatim() {
12701        // A body authored WITHOUT the outer `` ` `` affordance — a bare
12702        // `Sexp::List` body — returns verbatim. The "other" arm of the
12703        // legacy match. Pin parity with the pre-lift code path so a
12704        // regression that drifts the body-peel into "always peel
12705        // something" (which would break literal-body macros) fails here.
12706        let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
12707        let def = MacroDef {
12708            name: "f".into(),
12709            params: MacroParams::default(),
12710            body: body.clone(),
12711        };
12712        assert_eq!(def.template_body(), &body);
12713        // Atom bodies too — the projection is a single-arm match, not a
12714        // recursive descent. A `Sexp::Atom` body is its own template payload.
12715        let atom_def = MacroDef {
12716            name: "g".into(),
12717            params: MacroParams::default(),
12718            body: Sexp::symbol("nil-template"),
12719        };
12720        assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
12721    }
12722
12723    #[test]
12724    fn template_body_peels_single_level_only() {
12725        // A nested `` ``form `` body — `Sexp::Quasiquote(Box::new(
12726        // Sexp::Quasiquote(...)))` — unwraps ONE outer quasi-quote and
12727        // returns the inner `Sexp::Quasiquote(...)` as-is. The v0 module
12728        // preamble declares "Nested quasi-quotes: Not yet supported"; the
12729        // single-level peel matches the legacy inline match's posture
12730        // (which only matched ONE outer `Sexp::Quasiquote(_)` arm, not a
12731        // recursive loop). A regression that drifts to a recursive peel
12732        // would project too far and the inner `Sexp::Quasiquote` marker —
12733        // which the substitute walker treats as an atomic leaf returned
12734        // verbatim (line ~830, `Sexp::Quote(_) | Sexp::Quasiquote(_)
12735        // => Ok(form.clone())`) — would silently disappear from the
12736        // expansion's emitted form. Pin the single-level contract here.
12737        let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
12738        let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
12739        let def = MacroDef {
12740            name: "nested".into(),
12741            params: MacroParams::default(),
12742            body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
12743        };
12744        // Outer peel returns the INNER quasi-quote, NOT its inner payload.
12745        assert_eq!(def.template_body(), &inner_qq);
12746        assert_ne!(def.template_body(), &inner_payload);
12747    }
12748
12749    #[test]
12750    fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
12751        // A `Sexp::Quote(_)` body — not a `Sexp::Quasiquote(_)` — returns
12752        // verbatim through the "other" arm. The two close-cousin shapes
12753        // share an outer-marker character (`'` vs `` ` ``) at the reader
12754        // boundary but differ semantically: a `Quote` body is a literal
12755        // template (no substitution semantics), a `Quasiquote` body is a
12756        // substitution-walker entry. A regression that conflated the two
12757        // — peeling Quote as if it were Quasiquote — would silently turn
12758        // every quoted-body macro into a template-walked macro. Pin the
12759        // discrimination.
12760        let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
12761        let body = Sexp::Quote(Box::new(inner.clone()));
12762        let def = MacroDef {
12763            name: "quoted".into(),
12764            params: MacroParams::default(),
12765            body: body.clone(),
12766        };
12767        assert_eq!(def.template_body(), &body);
12768        // The inner is NOT what comes back — only Quasiquote-bodied macros
12769        // would peel to the inner.
12770        assert_ne!(def.template_body(), &inner);
12771    }
12772
12773    #[test]
12774    fn template_body_is_the_shared_projection_both_strategies_walk() {
12775        // End-to-end path-uniformity at the projection boundary: a macro
12776        // authored with the canonical quasi-quoted body expands
12777        // IDENTICALLY under bytecode and substitute strategies because
12778        // both route their walker's body through `template_body()` — the
12779        // SAME `&Sexp` projection. Sibling of
12780        // `compiled_template_matches_substitute_path` (which observes
12781        // agreement on the EMITTED form); this test pins agreement on
12782        // the projection ENTRY: `compile_template` and the substitute
12783        // fallback now consume the same `&Sexp` (`def.template_body()`),
12784        // never a divergent inline match the two paths could regress
12785        // independently.
12786        let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
12787        let expected = vec![Sexp::List(vec![
12788            Sexp::symbol("list"),
12789            Sexp::int(5),
12790            Sexp::int(5),
12791        ])];
12792        let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12793        let substitute = Expander::new_substitute_only()
12794            .expand_program(read(src).unwrap())
12795            .unwrap();
12796        assert_eq!(bytecode, expected, "bytecode body-projection drifted");
12797        assert_eq!(substitute, expected, "substitute body-projection drifted");
12798        assert_eq!(
12799            bytecode, substitute,
12800            "the two strategies disagree on the body-projection's emission"
12801        );
12802    }
12803
12804    // ── Expander::expand: macro-call dispatch routes through `as_call_to_any` ──
12805    //
12806    // `expand` lifts its macro-call recognition to route through the
12807    // substrate's typed-decoded call decomposition: `as_call_to_any(|h|
12808    // self.macros.get(h))` answers "is this form an invocation of any
12809    // registered macro?" in ONE structural query on the Sexp algebra,
12810    // and a HashMap-backed lookup as its classifier. Sibling consumer to
12811    // `macro_def_from` (the typed-macro-definition dispatcher already
12812    // routing through `as_call_to_any(MacroDefHead::from_keyword)` with
12813    // a closed-set enum classifier). With both in place, BOTH dispatch
12814    // sites in the macro expander project through the SAME family
12815    // primitive — each binding the classifier that fits its candidate
12816    // set. The tests below pin the consumer's path-uniformity contract
12817    // at the new boundary: a hand-rolled `as_call_to_any(|h| macros.get
12818    // (h))` dispatch observes the SAME `(def, args)` decomposition the
12819    // `Expander::expand` consumer routes through.
12820
12821    #[test]
12822    fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
12823        // Structural identity: on a registered-macro call, the consumer's
12824        // expansion is observably equivalent to: classify the form via
12825        // `as_call_to_any(|h| macros.get(h))` → some `(def, args)` →
12826        // apply the def to args. Pin path-uniformity: a hand-rolled
12827        // `as_call_to_any` lookup against the same registry the expander
12828        // walks produces the SAME `MacroDef` reference for the SAME
12829        // input form. A regression that drifts the consumer back to an
12830        // inline `as_list + as_call + macros.get` chain (which would
12831        // fragment the family adoption) is caught structurally — the
12832        // hand-rolled `as_call_to_any` and the consumer's dispatch must
12833        // observe the same decomposition.
12834        let mut e = Expander::new();
12835        e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12836            .unwrap();
12837        let call_form = parse("(wrap 42)");
12838
12839        // Hand-rolled family-primitive lookup mirrors the lifted consumer.
12840        let (def_via_family, args_via_family) = call_form
12841            .as_call_to_any(|h| e.macros.get(h))
12842            .expect("registered macro call must decompose via as_call_to_any");
12843        assert_eq!(def_via_family.name, "wrap");
12844        assert_eq!(args_via_family, &[Sexp::int(42)]);
12845
12846        // Consumer's expand observes the SAME decomposition: the expanded
12847        // form is `(list 42 42)`, derived from the SAME def + args the
12848        // hand-rolled lookup found. Path-uniform with the family
12849        // primitive at the dispatch boundary.
12850        let expanded = e.expand(&call_form).unwrap();
12851        assert_eq!(
12852            expanded,
12853            Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
12854        );
12855    }
12856
12857    #[test]
12858    fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
12859        // Path-uniformity for the non-registered-head path: `as_call_to_any
12860        // (|h| macros.get(h))` returns `None` for a call whose head ISN'T
12861        // a registered macro, and the consumer falls through to the
12862        // children-walk (which expands any nested macro calls). Pin
12863        // both halves: the hand-rolled lookup returns `None`, AND the
12864        // consumer's expand walks into the children. A regression that
12865        // accidentally short-circuits the children-walk for non-macro
12866        // calls (e.g. by treating `as_call_to_any` = `None` as
12867        // "non-expandable" globally) would fail here.
12868        let mut e = Expander::new();
12869        e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12870            .unwrap();
12871        let outer = parse("(foo (wrap 5))");
12872
12873        // Hand-rolled family-primitive lookup rejects the outer head.
12874        assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
12875
12876        // Consumer walks children — the inner `(wrap 5)` IS a macro call
12877        // and expands to `(list 5 5)`; the outer `foo` head is preserved.
12878        let expanded = e.expand(&outer).unwrap();
12879        assert_eq!(
12880            expanded,
12881            Sexp::List(vec![
12882                Sexp::symbol("foo"),
12883                Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
12884            ])
12885        );
12886    }
12887
12888    #[test]
12889    fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
12890        // Path-uniformity for the non-call path: every shape `as_call`
12891        // rejects (atoms across all 6 kinds, Nil, Quote-family wrappers)
12892        // ALSO routes past `as_call_to_any` into the `as_list()`
12893        // fallthrough, where the not-a-list arm returns `form.clone()`
12894        // verbatim. Pin both halves: the hand-rolled lookup rejects
12895        // every non-call shape regardless of decoder, AND the consumer
12896        // preserves each shape unchanged. A regression that drifts the
12897        // consumer's dispatch order (e.g. checking `as_list()` BEFORE
12898        // `as_call_to_any` in a way that mis-handles Quote-family
12899        // wrappers) would fail here.
12900        let e = Expander::new();
12901        let shapes = [
12902            Sexp::symbol("foo"),
12903            Sexp::int(5),
12904            Sexp::keyword("k"),
12905            Sexp::string("s"),
12906            Sexp::boolean(true),
12907            Sexp::float(1.5),
12908            Sexp::Nil,
12909            Sexp::Quote(Box::new(Sexp::symbol("x"))),
12910            Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
12911            Sexp::Unquote(Box::new(Sexp::symbol("x"))),
12912            Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
12913        ];
12914        for s in &shapes {
12915            // Hand-rolled family-primitive lookup rejects non-call shapes
12916            // even for a promiscuous decoder — the call-shape gate fires
12917            // BEFORE the decoder runs.
12918            assert!(
12919                s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
12920                "non-call shape must yield None for as_call_to_any: {s}"
12921            );
12922            // Consumer preserves the shape verbatim — the not-a-list
12923            // arm at the fallthrough returns `form.clone()`.
12924            assert_eq!(
12925                e.expand(s).unwrap(),
12926                s.clone(),
12927                "non-call shape must round-trip unchanged through expand: {s}"
12928            );
12929        }
12930    }
12931
12932    #[test]
12933    fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
12934        // The empty list `()` has no operator and no children. Pin that
12935        // `as_call_to_any` rejects it (no head to feed the decoder), the
12936        // consumer falls through to `as_list()` which returns `Some(&[])`,
12937        // and the children-walk emits `Sexp::List(vec![])` (an empty
12938        // list, not `form.clone()` of `Sexp::List(vec![])` — both happen
12939        // to be observationally identical, but the path is the
12940        // children-walk arm, NOT the not-a-list arm). Path-uniformity
12941        // gate for the singleton-list edge case the `compile_named_from_
12942        // forms` rejection chain relies on `as_call_to(KEYWORD)` to yield
12943        // `Some(&[])` for — same posture, different family member.
12944        let e = Expander::new();
12945        let empty = Sexp::List(vec![]);
12946
12947        // Hand-rolled family-primitive lookup rejects the empty list.
12948        assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
12949
12950        // Consumer walks children (zero of them) — output is the empty
12951        // list, same as input.
12952        assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
12953    }
12954
12955    // ── Expander::expand_and_collect_calls_to: program-level walk ───────
12956    //
12957    // `expand_and_collect_calls_to(forms, keyword, project)` composes the
12958    // expander's program-level expansion (`expand_program`) with the
12959    // substrate's slice-side typed-keyword projection (`iter_calls_to`)
12960    // and a caller-supplied per-form mapper into ONE method on the
12961    // `Expander` surface. Both `compile_typed::<T>` and
12962    // `compile_named_from_forms::<T>` (compile.rs) route through it; the
12963    // tests below pin its contract directly. The existing compile.rs
12964    // dispatch tests are the path-uniformity guards proving the two
12965    // production sites route through it without behavior drift.
12966
12967    #[test]
12968    fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
12969        // Three forms — two match `defmonitor`, one matches `defalert`.
12970        // The mapper records `args.len()` from each matching form's tail.
12971        // Pin: only the two `defmonitor` matches flow through `project`,
12972        // in source order (so the recorded lengths are 2, 1).
12973        let forms = vec![
12974            Sexp::List(vec![
12975                Sexp::symbol("defmonitor"),
12976                Sexp::keyword("name"),
12977                Sexp::string("first"),
12978            ]),
12979            Sexp::List(vec![
12980                Sexp::symbol("defalert"),
12981                Sexp::keyword("name"),
12982                Sexp::string("not-a-match"),
12983            ]),
12984            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
12985        ];
12986        let mut e = Expander::new();
12987        let lengths: Vec<usize> = e
12988            .expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
12989            .expect("matching forms must compose");
12990        assert_eq!(lengths, vec![2, 1]);
12991    }
12992
12993    #[test]
12994    fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
12995        // Path-uniformity for the soft-projection posture inherited from
12996        // `iter_calls_to`: non-matching forms are skipped silently, and
12997        // `project` is never invoked on them. Pin via a counter the
12998        // closure increments per invocation — the counter is the
12999        // observable that distinguishes "skipped silently" from
13000        // "projected to a no-op".
13001        let forms = vec![
13002            Sexp::symbol("bare-atom"),
13003            Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
13004            Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
13005            Sexp::List(vec![]),
13006            Sexp::Nil,
13007        ];
13008        let mut count = 0usize;
13009        let mut e = Expander::new();
13010        let out: Vec<()> = e
13011            .expand_and_collect_calls_to(forms, "defmonitor", |_args| {
13012                count += 1;
13013                Ok(())
13014            })
13015            .expect("non-matching-only slice must collect to empty Vec");
13016        assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13017        assert_eq!(count, 0, "project must never run for non-matching forms");
13018    }
13019
13020    #[test]
13021    fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13022        // Three matching forms; the closure errors on the SECOND. Pin: the
13023        // collect's `Result<Vec<_>, _>` short-circuit fires at the second
13024        // form's error, the third form's `project` is NEVER invoked, and
13025        // the returned error is exactly the one the closure raised. This
13026        // mirrors `compile_named_from_forms`'s short-circuit on the first
13027        // `NamedFormMissingName` / `NamedFormNonSymbolName` / typed-entry
13028        // kwargs rejection.
13029        let forms = vec![
13030            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13031            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
13032            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13033        ];
13034        let mut seen = Vec::new();
13035        let mut e = Expander::new();
13036        let err = e
13037            .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
13038                let n = args[0].as_int().expect("test args are ints");
13039                seen.push(n);
13040                if n == 2 {
13041                    Err(LispError::Compile {
13042                        form: "test".to_string(),
13043                        message: "stop at two".to_string(),
13044                    })
13045                } else {
13046                    Ok(())
13047                }
13048            })
13049            .expect_err("project's error must short-circuit collect");
13050        // Only the first two forms were ever projected — the third
13051        // never reached `project`.
13052        assert_eq!(seen, vec![1, 2]);
13053        assert!(
13054            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13055            "short-circuit must propagate the project's error verbatim, got {err:?}"
13056        );
13057    }
13058
13059    #[test]
13060    fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
13061        // The `expand_program` step runs BEFORE `iter_calls_to` walks
13062        // anything — so an `expand_program` error (e.g., a malformed
13063        // `defmacro` head) must short-circuit the entire composition
13064        // and `project` must NEVER run. Pin via the closure-running
13065        // counter: an error from the FIRST step never invokes `project`.
13066        // `(defmacro 5 (x) `,x)` — the macro NAME slot is an int, not a
13067        // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13068        // fires during `expand_program`.
13069        let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
13070        let mut count = 0usize;
13071        let mut e = Expander::new();
13072        let err = e
13073            .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
13074                count += 1;
13075                Ok(())
13076            })
13077            .expect_err("expand_program error must short-circuit before project");
13078        assert_eq!(
13079            count, 0,
13080            "project must never run when expand_program errors"
13081        );
13082        // Sanity: the error IS the expand_program-stage rejection, NOT
13083        // a project-stage error (path-uniformity for the ordering).
13084        let rendered = format!("{err}");
13085        assert!(
13086            rendered.contains("NAME") || rendered.contains("symbol"),
13087            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13088        );
13089    }
13090
13091    #[test]
13092    fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
13093        // Boundary: empty forms slice yields zero items regardless of
13094        // keyword. Pin the degenerate boundary — empty in, empty out —
13095        // and the closure is never invoked. Sibling of
13096        // `iter_calls_to_yields_nothing_for_empty_slice` in ast.rs,
13097        // one level up the composition: the program-level walk is
13098        // fused-empty when `expand_program` yields an empty `Vec<Sexp>`.
13099        let mut count = 0usize;
13100        let mut e = Expander::new();
13101        let out: Vec<()> = e
13102            .expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
13103                count += 1;
13104                Ok(())
13105            })
13106            .expect("empty forms is not an error");
13107        assert!(out.is_empty());
13108        assert_eq!(count, 0);
13109    }
13110
13111    #[test]
13112    fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13113        // Critical ordering: `expand_program` runs BEFORE the keyword
13114        // filter, so a `defmacro` whose expansion produces a form with
13115        // a matching head IS visible to `project`. Pin the pipeline
13116        // order: `(defmacro emit-monitor (n) `(defmonitor :name ,n))
13117        //         (emit-monitor "alpha") (defmonitor :name "beta")`
13118        // expands the macro call to `(defmonitor :name "alpha")` and
13119        // the keyword walk then sees TWO `defmonitor` forms (the
13120        // macro-emitted one AND the directly-authored one). A
13121        // pre-expansion filter would miss the macro-emitted form.
13122        let forms = read(
13123            "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13124             (emit-monitor \"alpha\")
13125             (defmonitor :name \"beta\")",
13126        )
13127        .unwrap();
13128        let mut e = Expander::new();
13129        let names: Vec<String> = e
13130            .expand_and_collect_calls_to(forms, "defmonitor", |args| {
13131                // Each defmonitor form's tail is `(:name "X")`; project
13132                // the second element's string to capture the name.
13133                Ok(args[1].as_string().unwrap().to_string())
13134            })
13135            .expect("macroexpanded + directly-authored forms must both flow");
13136        assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13137    }
13138
13139    #[test]
13140    fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13141        // The `keyword` argument flows straight into `iter_calls_to`'s
13142        // head-comparison gate — a regression that drifts the keyword
13143        // (e.g., uses `T::KEYWORD` from a different `T` at the helper,
13144        // or lowercases / trims the keyword en route) fails here. Pin
13145        // by passing TWO different keywords against the SAME forms
13146        // input and asserting each picks up only its matching forms.
13147        let forms = vec![
13148            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13149            Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
13150            Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13151            Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
13152        ];
13153
13154        let mut e = Expander::new();
13155        let monitors: Vec<i64> = e
13156            .expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
13157                Ok(args[0].as_int().unwrap())
13158            })
13159            .unwrap();
13160        assert_eq!(monitors, vec![1, 3]);
13161
13162        let mut e2 = Expander::new();
13163        let alerts: Vec<i64> = e2
13164            .expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
13165                Ok(args[0].as_int().unwrap())
13166            })
13167            .unwrap();
13168        assert_eq!(alerts, vec![2]);
13169
13170        // A keyword nothing matches collects to the empty Vec — same
13171        // shape as the empty-forms case at the slice level.
13172        let mut e3 = Expander::new();
13173        let none: Vec<i64> = e3
13174            .expand_and_collect_calls_to(forms, "missing-keyword", |args| {
13175                Ok(args[0].as_int().unwrap())
13176            })
13177            .unwrap();
13178        assert!(none.is_empty());
13179    }
13180
13181    #[test]
13182    fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
13183        // Structural identity: for any (forms, keyword, project) triple
13184        // whose pieces succeed, the method's result equals the inlined
13185        // pre-lift pipeline `expand_program + iter_calls_to + map +
13186        // collect`. Pin shape AND ordering on a mixed forms input
13187        // (matching + non-matching + macroexpand-emitted matches) so a
13188        // regression that drifts the method's pipeline from the inlined
13189        // closed-form fails here. This is the lift's load-bearing
13190        // path-uniformity gate — the SAME assertion that holds for
13191        // both production consumers (compile.rs) holds at the
13192        // primitive's contract level.
13193        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13194                   (foo :idx 1)
13195                   (emit-foo 2)
13196                   (bar :idx 99)
13197                   (foo :idx 3)";
13198        let forms = read(src).unwrap();
13199
13200        // Inlined pre-lift pipeline.
13201        let mut exp_inline = Expander::new();
13202        let expanded = exp_inline.expand_program(forms.clone()).unwrap();
13203        let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
13204            .map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
13205            .collect::<Result<Vec<_>>>()
13206            .unwrap();
13207
13208        // Method pipeline.
13209        let mut exp_method = Expander::new();
13210        let via_method: Vec<i64> = exp_method
13211            .expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13212            .unwrap();
13213
13214        assert_eq!(via_inline, via_method);
13215        assert_eq!(via_inline, vec![1, 2, 3]);
13216    }
13217
13218    // ── Expander::expand_and_collect_calls_to_any: typed-decoded sibling ──
13219    //
13220    // The typed-decoded classifier sibling of `expand_and_collect_calls_to`:
13221    // composes `expand_program` with `iter_calls_to_any` and a per-form
13222    // mapper that receives BOTH the typed witness AND the args tail.
13223    // Closes the (keyword, classifier) 2×2 of expander-surface
13224    // compose-on-iter projections at the typed-decoded corner the prior
13225    // runs' slice-side `iter_calls_to_any` lift left open. The keyword
13226    // sibling now ROUTES through this primitive via a constant-classifier
13227    // composition — the tests below pin both the typed-decoded primitive's
13228    // contract directly AND the routing identity binding the keyword
13229    // sibling to it.
13230    //
13231    // The existing `expand_and_collect_calls_to_*` tests above remain
13232    // load-bearing — they pin the keyword sibling's contract through
13233    // its ROUTED implementation, so a regression that drifts the routing
13234    // composition surfaces at the existing keyword tests too.
13235
13236    #[test]
13237    fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
13238    {
13239        // The typed-decoded primitive's happy path: a closed-set
13240        // classifier decodes head symbols to a typed enum, and the
13241        // per-form projection receives `(decoded, args)` for every
13242        // matched form in source order. Sweep across THREE distinct
13243        // classifier outcomes — `Foo` / `Bar` / `Baz` — interleaved with
13244        // a non-matching form to pin both the typed-witness threading
13245        // AND the source-order yield AND the rejection of non-classifier
13246        // forms in ONE assertion. The decoder runs once per call form
13247        // (`FnMut`); the projection runs once per matched form.
13248        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
13249        enum Op {
13250            Foo,
13251            Bar,
13252            Baz,
13253        }
13254        let src = "(foo 1)
13255                   (bar 2)
13256                   (other 99)
13257                   (baz 3)
13258                   (foo 4)";
13259        let forms = read(src).unwrap();
13260        let mut e = Expander::new();
13261        let pairs: Vec<(Op, i64)> = e
13262            .expand_and_collect_calls_to_any(
13263                forms,
13264                |h| match h {
13265                    "foo" => Some(Op::Foo),
13266                    "bar" => Some(Op::Bar),
13267                    "baz" => Some(Op::Baz),
13268                    _ => None,
13269                },
13270                |op, args| Ok((op, args[0].as_int().unwrap())),
13271            )
13272            .unwrap();
13273        assert_eq!(
13274            pairs,
13275            vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
13276        );
13277    }
13278
13279    #[test]
13280    fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13281        // The soft-projection contract — every shape `iter_calls_to_any`
13282        // rejects (non-list, empty list, list whose head is not a
13283        // symbol, list whose head decodes through the classifier to
13284        // `None`) skips the projection silently. Pin a deliberately-
13285        // panicking projection so the assertion fires loudly if a
13286        // non-matching form reaches it. The decoder accepts only
13287        // `"defmonitor"`; every other shape (atom, list whose head is a
13288        // keyword, list whose head is a symbol the decoder rejects)
13289        // short-circuits before the projection runs.
13290        let src = r#":bare-keyword
13291                     "bare-string"
13292                     42
13293                     ()
13294                     (foo bar)
13295                     (defmonitor :name "matches")"#;
13296        let forms = read(src).unwrap();
13297        let mut e = Expander::new();
13298        let lengths: Vec<usize> = e
13299            .expand_and_collect_calls_to_any(
13300                forms,
13301                |h| (h == "defmonitor").then_some(()),
13302                |(), args| {
13303                    // The projection must run EXACTLY once — for the
13304                    // single matched form. Any non-matching form that
13305                    // reaches the projection panics here.
13306                    assert_eq!(args.len(), 2, "projection ran on non-matching form");
13307                    Ok(args.len())
13308                },
13309            )
13310            .unwrap();
13311        assert_eq!(lengths, vec![2]);
13312    }
13313
13314    #[test]
13315    fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13316        // The Result projection's short-circuit contract — when the
13317        // projection returns `Err` on a matched form, the walk
13318        // short-circuits and subsequent matched forms are NOT projected.
13319        // Pin the source-order short-circuit via a counter the
13320        // projection increments before deciding to fail: the counter
13321        // sits at exactly the index of the failing form (the second
13322        // match), proving the third match never reached the projection.
13323        let src = "(foo 1) (foo 2) (foo 3)";
13324        let forms = read(src).unwrap();
13325        let mut count = 0usize;
13326        let mut e = Expander::new();
13327        let err = e
13328            .expand_and_collect_calls_to_any::<i64, _, _, ()>(
13329                forms,
13330                |h| (h == "foo").then_some(()),
13331                |(), args| {
13332                    count += 1;
13333                    let v = args[0].as_int().unwrap();
13334                    if v == 2 {
13335                        return Err(crate::error::LispError::Missing("test-failure"));
13336                    }
13337                    Ok(v)
13338                },
13339            )
13340            .expect_err("projection must short-circuit on first Err");
13341        assert_eq!(
13342            count, 2,
13343            "projection must have run on first AND failing form, then stopped",
13344        );
13345        // Sanity: the error is the project-stage rejection — the same
13346        // typed `LispError` the projection returned, propagated verbatim.
13347        assert!(
13348            matches!(err, crate::error::LispError::Missing("test-failure")),
13349            "expected the projection's typed Err verbatim, got {err:?}",
13350        );
13351    }
13352
13353    #[test]
13354    fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
13355        // Ordering contract — `expand_program` runs BEFORE
13356        // `iter_calls_to_any` walks the slice. A `(defmacro …)` form
13357        // that emits a classifier-decoded body must have its expanded
13358        // body decoded by the classifier (not the unexpanded macro-call
13359        // head). Pin the ordering with a macro `(emit-foo n)` that
13360        // expands to `(foo :idx n)` — the classifier sees the expanded
13361        // `foo` head, not `emit-foo`. Mirrors the parallel keyword test
13362        // `expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword`
13363        // — the SAME composition contract holds on the typed-decoded
13364        // sibling.
13365        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13366                   (foo :idx 1)
13367                   (emit-foo 2)
13368                   (bar :idx 99)
13369                   (foo :idx 3)";
13370        let forms = read(src).unwrap();
13371        let mut e = Expander::new();
13372        // Classifier returns `()` for "foo" only; the keyword sibling's
13373        // routing identity is the same composition we exercise here
13374        // directly.
13375        let idxs: Vec<i64> = e
13376            .expand_and_collect_calls_to_any(
13377                forms,
13378                |h| (h == "foo").then_some(()),
13379                |(), args| Ok(args[1].as_int().unwrap()),
13380            )
13381            .unwrap();
13382        // 1 from the literal call, 2 from the macro-emitted call, 3 from
13383        // the final literal call. The macro-emitted `(foo :idx 2)` is
13384        // present BECAUSE `expand_program` ran first.
13385        assert_eq!(idxs, vec![1, 2, 3]);
13386    }
13387
13388    #[test]
13389    fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
13390        // The `FnMut` constraint — the slice walk calls the classifier
13391        // once per call form, and a decoder that captures mutable state
13392        // (a counter, a registry cache, a visited-set) maintains that
13393        // state across the walk. Pin the `FnMut` contract with a counter
13394        // closure that increments per shape-gate-passing form (calls
13395        // whose head is a symbol), asserting the counter equals the
13396        // total call count regardless of whether the classifier accepts
13397        // each form. Mirrors the parallel slice-side test
13398        // `iter_calls_to_any_admits_fnmut_classifier_maintaining_state_across_batch_walk`
13399        // — the SAME `FnMut` contract holds on the expander surface.
13400        let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
13401        let forms = read(src).unwrap();
13402        let mut e = Expander::new();
13403        let mut classifier_calls = 0usize;
13404        let projected: Vec<i64> = e
13405            .expand_and_collect_calls_to_any(
13406                forms,
13407                |h| {
13408                    classifier_calls += 1;
13409                    (h == "foo").then_some(())
13410                },
13411                |(), args| Ok(args[0].as_int().unwrap()),
13412            )
13413            .unwrap();
13414        assert_eq!(
13415            classifier_calls, 5,
13416            "classifier must run once per call form (5 forms, all calls with symbol heads)",
13417        );
13418        assert_eq!(
13419            projected,
13420            vec![1, 3, 5],
13421            "projection must run only for classifier-accepted forms in source order",
13422        );
13423    }
13424
13425    #[test]
13426    fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
13427    ) {
13428        // Structural identity: the keyword sibling
13429        // `expand_and_collect_calls_to` ROUTES through the typed-decoded
13430        // primitive via a constant-classifier composition — the
13431        // post-lift composition law binding the two siblings:
13432        //
13433        //   expand_and_collect_calls_to(forms, k, p) ==
13434        //       expand_and_collect_calls_to_any(forms,
13435        //           |h| (h == k).then_some(()),
13436        //           |(), args| p(args))
13437        //
13438        // Pin shape AND ordering across THREE representative keyword
13439        // values: one that matches multiple forms, one that matches
13440        // none, one that matches exactly one form. Same mixed input
13441        // (literal + macroexpand-emitted + non-matching) the keyword
13442        // path-uniformity test exercises, so the routing identity holds
13443        // on the SAME input shape the keyword sibling's contract pins.
13444        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13445                   (foo :idx 1)
13446                   (emit-foo 2)
13447                   (bar :idx 99)
13448                   (foo :idx 3)";
13449        let forms = read(src).unwrap();
13450
13451        for keyword in ["foo", "bar", "absent"] {
13452            let mut exp_keyword = Expander::new();
13453            let via_keyword: Vec<i64> = exp_keyword
13454                .expand_and_collect_calls_to(forms.clone(), keyword, |args| {
13455                    Ok(args[1].as_int().unwrap())
13456                })
13457                .unwrap();
13458            let mut exp_classifier = Expander::new();
13459            let via_classifier: Vec<i64> = exp_classifier
13460                .expand_and_collect_calls_to_any(
13461                    forms.clone(),
13462                    |h| (h == keyword).then_some(()),
13463                    |(), args| Ok(args[1].as_int().unwrap()),
13464                )
13465                .unwrap();
13466            assert_eq!(
13467                via_keyword, via_classifier,
13468                "routing identity drifted for keyword {keyword:?}",
13469            );
13470        }
13471    }
13472
13473    #[test]
13474    fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
13475    {
13476        // The expand_program-stage short-circuit contract — when
13477        // `expand_program` rejects (e.g. a `(defmacro …)` form whose
13478        // param list is malformed), the walk short-circuits BEFORE the
13479        // classifier or projection runs. Pin the ordering with a
13480        // deliberately-panicking classifier AND projection: any
13481        // post-expand-program execution fires the panic. Mirrors the
13482        // parallel keyword test
13483        // `expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs`
13484        // — the SAME composition contract holds on the typed-decoded
13485        // sibling.
13486        // `(defmacro 5 (x) `,x)` — macro NAME slot is an int, not a
13487        // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13488        // fires during `expand_program`. Same rejection shape the
13489        // parallel keyword test uses, so the ordering contract observed
13490        // here is structurally identical to the keyword sibling's.
13491        let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
13492        let mut e = Expander::new();
13493        let err = e
13494            .expand_and_collect_calls_to_any::<(), _, _, ()>(
13495                forms,
13496                |_h| -> Option<()> {
13497                    panic!("classifier must not run when expand_program errors");
13498                },
13499                |(), _args| {
13500                    panic!("project must not run when expand_program errors");
13501                },
13502            )
13503            .expect_err("expand_program error must short-circuit before classifier or project");
13504        // Sanity: the error IS an expand_program-stage rejection — not a
13505        // classifier or projection error — so the ordering is observable.
13506        let rendered = format!("{err}");
13507        assert!(
13508            rendered.contains("NAME") || rendered.contains("symbol"),
13509            "expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
13510        );
13511    }
13512
13513    // ── Expander::expand_source_and_collect_calls_to: from-source walk ──
13514    //
13515    // The from-source sibling of `expand_and_collect_calls_to`: composes
13516    // `crate::reader::read(src)?` with the from-forms primitive in ONE
13517    // method on the `Expander` surface. All four typed-program dispatchers
13518    // (free-function `compile_typed` / `compile_named`, preloaded-expander
13519    // `RealizedCompiler::compile_typed` / `compile_named`) route through
13520    // it; the tests below pin its contract directly. The existing
13521    // compile.rs + compiler_spec.rs dispatch tests are the path-uniformity
13522    // guards proving the four production sites route through it without
13523    // behavior drift.
13524
13525    #[test]
13526    fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
13527        // Happy path: a source string with three top-level forms (two
13528        // matching `defmonitor`, one matching `defalert`) flows through
13529        // `read` then through the from-forms primitive. Pin the SAME
13530        // emission shape the from-forms test pins for the matching
13531        // sub-set, sourced from a `&str` rather than a `Vec<Sexp>`.
13532        let src = r#"(defmonitor :name "first")
13533                     (defalert :name "not-a-match")
13534                     (defmonitor :solo)"#;
13535        let mut e = Expander::new();
13536        let lengths: Vec<usize> = e
13537            .expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
13538            .expect("matching forms must compose");
13539        assert_eq!(lengths, vec![2, 1]);
13540    }
13541
13542    #[test]
13543    fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
13544        // The reader runs BEFORE `expand_program` — so a reader error (an
13545        // unterminated string, an unbalanced paren, an unknown escape)
13546        // must short-circuit the entire composition and `expand_program`
13547        // / the keyword filter / `project` must NEVER run. Pin the
13548        // ordering: an unbalanced open-paren is rejected by the reader
13549        // at parse time, never reaches the from-forms primitive.
13550        let mut count = 0usize;
13551        let mut e = Expander::new();
13552        let err = e
13553            .expand_source_and_collect_calls_to::<(), _>(
13554                "(defmonitor :name \"unbalanced",
13555                "defmonitor",
13556                |_args| {
13557                    count += 1;
13558                    Ok(())
13559                },
13560            )
13561            .expect_err("reader error must short-circuit before expand_program");
13562        assert_eq!(
13563            count, 0,
13564            "project must never run when reader errors at parse time"
13565        );
13566        // Sanity: the error IS a reader-stage rejection — the rendered
13567        // diagnostic mentions the lexer's gate, not the expander's or
13568        // projector's (path-uniformity for the read-then-walk ordering).
13569        let rendered = format!("{err}");
13570        assert!(
13571            rendered.to_lowercase().contains("string")
13572                || rendered.to_lowercase().contains("paren")
13573                || rendered.to_lowercase().contains("eof")
13574                || rendered.to_lowercase().contains("unexpected")
13575                || rendered.to_lowercase().contains("unterminated")
13576                || rendered.to_lowercase().contains("unclosed"),
13577            "error must be the reader-stage rejection, got: {rendered}"
13578        );
13579    }
13580
13581    #[test]
13582    fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
13583    ) {
13584        // Reader succeeds, but `expand_program` rejects a malformed
13585        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13586        // ordering: `expand_program` rejects BEFORE the keyword filter
13587        // walks anything, `project` must NEVER run. Sibling of the
13588        // from-forms test of the same name — one level up the
13589        // composition (sourced from `&str` rather than `Vec<Sexp>`).
13590        let mut count = 0usize;
13591        let mut e = Expander::new();
13592        let err = e
13593            .expand_source_and_collect_calls_to::<(), _>(
13594                "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13595                "defmonitor",
13596                |_args| {
13597                    count += 1;
13598                    Ok(())
13599                },
13600            )
13601            .expect_err("expand_program error must short-circuit before project");
13602        assert_eq!(
13603            count, 0,
13604            "project must never run when expand_program errors"
13605        );
13606        let rendered = format!("{err}");
13607        assert!(
13608            rendered.contains("NAME") || rendered.contains("symbol"),
13609            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13610        );
13611    }
13612
13613    #[test]
13614    fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13615        // Reader + `expand_program` both succeed; the per-form `project`
13616        // errors on the SECOND matched form. Pin: the collect's short-
13617        // circuit fires at the second match's error, the third match's
13618        // `project` is NEVER invoked, and the returned error is exactly
13619        // the one the closure raised. Mirrors `compile_named`'s
13620        // short-circuit on the first `NamedFormMissingName` /
13621        // `NamedFormNonSymbolName` / typed-entry rejection — sourced
13622        // from `&str`.
13623        let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
13624        let mut seen = Vec::new();
13625        let mut e = Expander::new();
13626        let err = e
13627            .expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
13628                let n = args[1].as_int().expect("test args are ints");
13629                seen.push(n);
13630                if n == 2 {
13631                    Err(LispError::Compile {
13632                        form: "test".to_string(),
13633                        message: "stop at two".to_string(),
13634                    })
13635                } else {
13636                    Ok(())
13637                }
13638            })
13639            .expect_err("project's error must short-circuit collect");
13640        assert_eq!(seen, vec![1, 2]);
13641        assert!(
13642            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13643            "short-circuit must propagate the project's error verbatim, got {err:?}"
13644        );
13645    }
13646
13647    #[test]
13648    fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
13649        // Boundary: empty source string yields zero items regardless of
13650        // keyword. Pin the degenerate boundary — empty in, empty out —
13651        // and the closure is never invoked. Sibling of the from-forms
13652        // test of the same name, one level up the composition: the
13653        // from-source posture is fused-empty when `read` yields an
13654        // empty `Vec<Sexp>` (which, fed into `expand_program`, yields
13655        // an empty `Vec<Sexp>`, which iter_calls_to walks to zero
13656        // matches).
13657        let mut count = 0usize;
13658        let mut e = Expander::new();
13659        let out: Vec<()> = e
13660            .expand_source_and_collect_calls_to("", "anything", |_args| {
13661                count += 1;
13662                Ok(())
13663            })
13664            .expect("empty source is not an error");
13665        assert!(out.is_empty());
13666        assert_eq!(count, 0);
13667    }
13668
13669    #[test]
13670    fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13671        // Critical ordering preserved across the from-source posture:
13672        // `defmacro` source registers in `expand_program`, its call
13673        // sites expand to the matching keyword, AND the keyword walk
13674        // sees both the macro-emitted form and any directly-authored
13675        // matches in source order. Sibling of the from-forms test of
13676        // the same name, sourced from `&str`.
13677        let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13678                   (emit-monitor \"alpha\")
13679                   (defmonitor :name \"beta\")";
13680        let mut e = Expander::new();
13681        let names: Vec<String> = e
13682            .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13683                Ok(args[1].as_string().unwrap().to_string())
13684            })
13685            .expect("macroexpanded + directly-authored forms must both flow");
13686        assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13687    }
13688
13689    #[test]
13690    fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
13691        // Structural identity: for any (src, keyword, project) triple
13692        // whose pieces succeed, the from-source method's result equals
13693        // the inlined pre-lift pipeline `read + expand_and_collect_
13694        // calls_to`. Pin shape AND ordering on a mixed source
13695        // (matching + non-matching + macroexpand-emitted matches) so a
13696        // regression that drifts the method's pipeline from the
13697        // inlined closed-form fails here. This is the lift's
13698        // load-bearing path-uniformity gate — the SAME assertion that
13699        // holds for all four production consumers (free-function
13700        // `compile_typed`/`compile_named` + `RealizedCompiler::compile_typed`/`compile_named`)
13701        // holds at the primitive's contract level.
13702        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13703                   (foo :idx 1)
13704                   (emit-foo 2)
13705                   (bar :idx 99)
13706                   (foo :idx 3)";
13707
13708        // Inlined pre-lift pipeline.
13709        let mut exp_inline = Expander::new();
13710        let inline_forms = read(src).unwrap();
13711        let via_inline: Vec<i64> = exp_inline
13712            .expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13713            .unwrap();
13714
13715        // From-source method pipeline.
13716        let mut exp_method = Expander::new();
13717        let via_method: Vec<i64> = exp_method
13718            .expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
13719            .unwrap();
13720
13721        assert_eq!(via_inline, via_method);
13722        assert_eq!(via_inline, vec![1, 2, 3]);
13723    }
13724
13725    #[test]
13726    fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13727        // The `keyword` argument flows straight into the from-forms
13728        // primitive's head-comparison gate, which inherits from
13729        // `iter_calls_to` — a regression that drifts the keyword (e.g.,
13730        // lowercases / trims it en route through the read step) fails
13731        // here. Pin by passing TWO different keywords against the SAME
13732        // source and asserting each picks up only its matching forms.
13733        let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
13734
13735        let mut e1 = Expander::new();
13736        let monitors: Vec<i64> = e1
13737            .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13738                Ok(args[1].as_int().unwrap())
13739            })
13740            .unwrap();
13741        assert_eq!(monitors, vec![1, 3]);
13742
13743        let mut e2 = Expander::new();
13744        let alerts: Vec<i64> = e2
13745            .expand_source_and_collect_calls_to(src, "defalert", |args| {
13746                Ok(args[1].as_int().unwrap())
13747            })
13748            .unwrap();
13749        assert_eq!(alerts, vec![2]);
13750
13751        let mut e3 = Expander::new();
13752        let none: Vec<i64> = e3
13753            .expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
13754                Ok(args[1].as_int().unwrap())
13755            })
13756            .unwrap();
13757        assert!(none.is_empty());
13758    }
13759
13760    #[test]
13761    fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13762        // Path-uniformity for the soft-projection posture inherited
13763        // from `iter_calls_to`: non-matching forms in the source are
13764        // skipped silently, and `project` is never invoked on them.
13765        // Sibling of the from-forms test of the same name, sourced
13766        // from `&str` with a mix of atoms-and-lists that the reader
13767        // emits at the top level.
13768        let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
13769        let mut count = 0usize;
13770        let mut e = Expander::new();
13771        let out: Vec<()> = e
13772            .expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
13773                count += 1;
13774                Ok(())
13775            })
13776            .expect("non-matching-only source must collect to empty Vec");
13777        assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13778        assert_eq!(count, 0, "project must never run for non-matching forms");
13779    }
13780
13781    // ── Expander::expand_source_and_collect_calls_to_any — from-source × classifier ─
13782    //
13783    // The from-source posture of the typed-decoded classifier sibling on
13784    // the `Expander` surface — closes the (from-forms, from-source) ×
13785    // (keyword, classifier) 2×2 of compose-on-iter projections at the
13786    // from-source classifier corner the prior runs left open. The
13787    // pre-existing `expand_source_and_collect_calls_to` (from-source ×
13788    // keyword) is the constant-classifier projection of this primitive:
13789    // its body composes this function with a `|h| (h == keyword).
13790    // then_some(())` decoder, so the from-source pipeline lives at ONE
13791    // site. These tests pin both the typed-decoded primitive's contract
13792    // directly AND the post-lift routing of
13793    // `expand_source_and_collect_calls_to` through it.
13794
13795    #[test]
13796    fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
13797    ) {
13798        // Happy path: a source string with four top-level forms (two
13799        // matching "foo", one matching "bar", one rejected-head
13800        // "defmonitor") flows through `read` → `expand_program` → the
13801        // typed-decoded classifier walk. The closed-set classifier
13802        // `Op::from_keyword` admits "foo" and "bar" but rejects
13803        // "defmonitor". Pin the typed-decoded yield shape (decoded
13804        // witness alongside args tail) AND source-order preservation
13805        // sourced from `&str` rather than a pre-parsed `Vec<Sexp>`.
13806        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13807        enum Op {
13808            Foo,
13809            Bar,
13810        }
13811        fn op_from_keyword(head: &str) -> Option<Op> {
13812            match head {
13813                "foo" => Some(Op::Foo),
13814                "bar" => Some(Op::Bar),
13815                _ => None,
13816            }
13817        }
13818        let src = r#"(foo :idx 1)
13819                     (defmonitor :idx 99)
13820                     (bar :idx 2)
13821                     (foo :idx 3)"#;
13822        let mut e = Expander::new();
13823        let yielded: Vec<(Op, i64)> = e
13824            .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
13825                Ok((op, args[1].as_int().expect("test args are ints")))
13826            })
13827            .expect("matching forms must compose through from-source classifier walk");
13828        assert_eq!(
13829            yielded,
13830            vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
13831            "yields must be in source order, decoded witness paired with per-form mapper output"
13832        );
13833    }
13834
13835    #[test]
13836    fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
13837    ) {
13838        // The reader runs BEFORE `expand_program` AND BEFORE the
13839        // classifier — so a reader error (an unterminated string)
13840        // short-circuits the entire composition and both the classifier
13841        // AND `project` must NEVER run. Pin the ordering with BOTH
13842        // decoder and projection explicitly panicking — any post-reader
13843        // execution fires the panic. Sibling of
13844        // `expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program`
13845        // one corner over in the 2×2 (the classifier version of the
13846        // same ordering pin).
13847        let mut e = Expander::new();
13848        let err = e
13849            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13850                "(defmonitor :name \"unbalanced",
13851                |_h: &str| -> Option<()> {
13852                    panic!("classifier must not run when reader errors at parse time")
13853                },
13854                |(), _args| -> Result<()> { panic!("project must not run when reader errors") },
13855            )
13856            .expect_err("reader error must short-circuit before classifier");
13857        // Sanity: the error is a reader-stage rejection.
13858        let rendered = format!("{err}");
13859        assert!(
13860            rendered.to_lowercase().contains("string")
13861                || rendered.to_lowercase().contains("paren")
13862                || rendered.to_lowercase().contains("eof")
13863                || rendered.to_lowercase().contains("unexpected")
13864                || rendered.to_lowercase().contains("unterminated")
13865                || rendered.to_lowercase().contains("unclosed"),
13866            "error must be the reader-stage rejection, got: {rendered}"
13867        );
13868    }
13869
13870    #[test]
13871    fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
13872    ) {
13873        // Reader succeeds, but `expand_program` rejects a malformed
13874        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13875        // ordering: `expand_program` rejects BEFORE the classifier walks
13876        // anything, both decoder AND `project` must NEVER run. Sibling
13877        // of the keyword-from-source variant — one corner over in the
13878        // 2×2 with both classifier and projection explicitly panicking.
13879        let mut e = Expander::new();
13880        let err = e
13881            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13882                "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13883                |_h: &str| -> Option<()> {
13884                    panic!("classifier must not run when expand_program errors")
13885                },
13886                |(), _args| -> Result<()> {
13887                    panic!("project must not run when expand_program errors")
13888                },
13889            )
13890            .expect_err("expand_program error must short-circuit before classifier");
13891        let rendered = format!("{err}");
13892        assert!(
13893            rendered.contains("NAME") || rendered.contains("symbol"),
13894            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13895        );
13896    }
13897
13898    #[test]
13899    fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13900        // Path-uniformity for the soft-projection posture inherited
13901        // from `iter_calls_to_any`: forms whose head the classifier
13902        // rejects are skipped silently, and `project` is never invoked
13903        // on them. Pin via an explicitly-panicking projection across a
13904        // mix of forms (atom, list-with-non-symbol-head, list with
13905        // unrecognized-by-classifier symbol head). A regression that
13906        // wrongly invokes `project` on a rejected form fires the panic.
13907        let src = r#"bare-atom
13908                     (5 :not-a-symbol-head)
13909                     (defmonitor :name "decoder-rejects-me")"#;
13910        let mut e = Expander::new();
13911        // The closed-set classifier admits NOTHING in this source.
13912        let out: Vec<()> = e
13913            .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13914                src,
13915                |_h: &str| -> Option<()> { None },
13916                |(), _args| -> Result<()> {
13917                    panic!("project must not run for classifier-rejected forms")
13918                },
13919            )
13920            .expect("classifier-rejects-all source must collect to empty Vec");
13921        assert!(
13922            out.is_empty(),
13923            "no classifier-accepted forms — empty Vec, got {out:?}"
13924        );
13925    }
13926
13927    #[test]
13928    fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13929        // Reader + `expand_program` + classifier all succeed; the
13930        // per-form `project` errors on the SECOND matched form. Pin: the
13931        // collect's short-circuit fires at the second match's error, the
13932        // third match's `project` is NEVER invoked, and the returned
13933        // error is exactly the one the closure raised. Sibling of the
13934        // keyword-from-source variant — one corner over in the 2×2.
13935        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13936        enum Op {
13937            Foo,
13938        }
13939        fn op_from_keyword(head: &str) -> Option<Op> {
13940            (head == "foo").then_some(Op::Foo)
13941        }
13942        let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
13943        let mut seen = Vec::new();
13944        let mut e = Expander::new();
13945        let err = e
13946            .expand_source_and_collect_calls_to_any::<(), _, _, _>(
13947                src,
13948                op_from_keyword,
13949                |_op: Op, args: &[Sexp]| -> Result<()> {
13950                    let n = args[1].as_int().expect("test args are ints");
13951                    seen.push(n);
13952                    if n == 2 {
13953                        Err(LispError::Compile {
13954                            form: "test".to_string(),
13955                            message: "stop at two".to_string(),
13956                        })
13957                    } else {
13958                        Ok(())
13959                    }
13960                },
13961            )
13962            .expect_err("project's error must short-circuit collect");
13963        assert_eq!(seen, vec![1, 2], "third match's project must never run");
13964        assert!(
13965            matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13966            "short-circuit must propagate the project's error verbatim, got {err:?}"
13967        );
13968    }
13969
13970    #[test]
13971    fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
13972    ) {
13973        // The post-lift composition law binding the keyword-from-source
13974        // sibling to the typed-decoded primitive:
13975        //   `expand_source_and_collect_calls_to(src, k, project) ==
13976        //    expand_source_and_collect_calls_to_any(src,
13977        //        |h| (h == k).then_some(()),
13978        //        |(), args| project(args))`
13979        // Pin shape AND ordering across THREE representative keywords
13980        // (matching some, matching exactly one, matching none) on the
13981        // SAME mixed source the keyword path-uniformity test exercises.
13982        // A regression that re-implements the keyword-from-source filter
13983        // as a parallel `read + expand_and_collect_calls_to` pipeline
13984        // (rather than routing through the typed-decoded primitive)
13985        // fails the value pin.
13986        let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13987                   (foo :idx 1)
13988                   (emit-foo 2)
13989                   (bar :idx 99)
13990                   (foo :idx 3)";
13991        for k in ["foo", "bar", "missing"] {
13992            let mut e_keyword = Expander::new();
13993            let via_keyword: Vec<i64> = e_keyword
13994                .expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
13995                .unwrap();
13996
13997            let mut e_classifier = Expander::new();
13998            let via_classifier: Vec<i64> = e_classifier
13999                .expand_source_and_collect_calls_to_any(
14000                    src,
14001                    |h: &str| (h == k).then_some(()),
14002                    |(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
14003                )
14004                .unwrap();
14005
14006            assert_eq!(
14007                via_keyword, via_classifier,
14008                "keyword from-source path must equal classifier from-source path for {k:?}"
14009            );
14010        }
14011    }
14012
14013    #[test]
14014    fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
14015        // Critical ordering preserved across the typed-decoded
14016        // from-source posture: `defmacro` source registers in
14017        // `expand_program`, its call sites expand to a classifier-
14018        // accepted form, AND the classifier walk sees both the
14019        // macro-emitted form and any directly-authored matches in
14020        // source order. Sibling of the keyword-from-source ordering
14021        // test, one corner over in the 2×2.
14022        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14023        enum Op {
14024            Foo,
14025        }
14026        fn op_from_keyword(head: &str) -> Option<Op> {
14027            (head == "foo").then_some(Op::Foo)
14028        }
14029        let src = "(defmacro emit-foo (n) `(foo :name ,n))
14030                   (emit-foo \"alpha\")
14031                   (foo :name \"beta\")";
14032        let mut e = Expander::new();
14033        let names: Vec<(Op, String)> = e
14034            .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
14035                Ok((op, args[1].as_string().unwrap().to_string()))
14036            })
14037            .expect("macroexpanded + directly-authored forms must both flow through classifier");
14038        assert_eq!(
14039            names,
14040            vec![
14041                (Op::Foo, "alpha".to_string()),
14042                (Op::Foo, "beta".to_string()),
14043            ]
14044        );
14045    }
14046
14047    // ── expand_source_program — from-source × yield-all primitive ───
14048    //
14049    // Closes the 2×2 program-level walk family on the `Expander` surface:
14050    // from-forms × yield-all (`expand_program`), from-forms × keyword-
14051    // projected (`expand_and_collect_calls_to`), from-source × keyword-
14052    // projected (`expand_source_and_collect_calls_to`), AND now
14053    // from-source × yield-all (`expand_source_program`). Pins (a) the
14054    // happy path (mixed forms flow through reader then expand_program),
14055    // (b) reader-stage short-circuit (no expand_program side-effects),
14056    // (c) expand_program-stage short-circuit (defmacro-NAME-not-symbol
14057    // is rejected at its offending form), (d) defmacro absorption (the
14058    // primitive's side-effect on `self.macros` matches the from-forms
14059    // posture's side-effect verbatim), (e) the empty-source boundary,
14060    // and (f) structural identity vs. the inlined pre-lift `read +
14061    // expand_program` pipeline (path-uniformity gate).
14062
14063    #[test]
14064    fn expand_source_program_routes_through_reader_then_expand_program() {
14065        // Happy path: a source string with mixed forms (a `defmacro`
14066        // definition, a `defmonitor` form, a plain symbol literal) flows
14067        // through `read` then through the from-forms primitive. Pin the
14068        // SAME emission shape the from-forms test pins, sourced from a
14069        // `&str` rather than a `Vec<Sexp>`.
14070        let src = r#"(defmacro id (x) `,x)
14071                     (defmonitor :name "alpha")
14072                     bare-symbol"#;
14073        let mut e = Expander::new();
14074        let out = e
14075            .expand_source_program(src)
14076            .expect("mixed forms must compose");
14077        // `(defmacro id …)` is consumed as a side-effect (registers `id`
14078        // in `e.macros`), so the returned `Vec<Sexp>` contains the
14079        // remaining two top-level forms in source order.
14080        assert_eq!(out.len(), 2);
14081        assert_eq!(
14082            out[0].as_call_to("defmonitor").map(<[_]>::len),
14083            Some(2),
14084            "first surviving form is the defmonitor with two args, got: {:?}",
14085            out[0]
14086        );
14087        assert_eq!(
14088            out[1].as_symbol(),
14089            Some("bare-symbol"),
14090            "second surviving form is the bare symbol literal, got: {:?}",
14091            out[1]
14092        );
14093        // The defmacro side-effect landed in the expander's macro table.
14094        assert!(
14095            e.has("id"),
14096            "defmacro must register `id` in the expander's macro table"
14097        );
14098    }
14099
14100    #[test]
14101    fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
14102        // The reader runs BEFORE `expand_program` — so a reader error (an
14103        // unterminated string, an unbalanced paren) must short-circuit
14104        // the entire composition and `expand_program` must NEVER run.
14105        // Pin the ordering: a `defmacro` BEFORE the unterminated string
14106        // is NOT registered, because the reader fails at parse time
14107        // before `expand_program` sees any form.
14108        let mut e = Expander::new();
14109        let err = e
14110            .expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
14111            .expect_err("reader error must short-circuit before expand_program");
14112        // Sanity: the error IS a reader-stage rejection.
14113        let rendered = format!("{err}").to_lowercase();
14114        assert!(
14115            rendered.contains("string")
14116                || rendered.contains("paren")
14117                || rendered.contains("eof")
14118                || rendered.contains("unexpected")
14119                || rendered.contains("unterminated")
14120                || rendered.contains("unclosed"),
14121            "error must be the reader-stage rejection, got: {err}"
14122        );
14123        // Path-uniformity gate: the `defmacro` lexically BEFORE the
14124        // unterminated string must NOT have been absorbed — `read`'s
14125        // failure short-circuits the entire pipeline before
14126        // `expand_program` walks any forms.
14127        assert!(
14128            !e.has("should-not-register"),
14129            "reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
14130        );
14131    }
14132
14133    #[test]
14134    fn expand_source_program_short_circuits_on_expand_program_error() {
14135        // Reader succeeds, but `expand_program` rejects a malformed
14136        // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14137        // ordering: the expand_program-stage rejection bubbles up
14138        // verbatim, sourced from `&str` rather than `Vec<Sexp>`.
14139        let mut e = Expander::new();
14140        let err = e
14141            .expand_source_program("(defmacro 5 (x) `,x)")
14142            .expect_err("expand_program error must propagate");
14143        let rendered = format!("{err}");
14144        assert!(
14145            rendered.contains("NAME") || rendered.contains("symbol"),
14146            "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14147        );
14148    }
14149
14150    #[test]
14151    fn expand_source_program_yields_empty_vec_for_empty_source() {
14152        // Boundary: empty source string yields zero items. Pin the
14153        // degenerate boundary — empty in, empty out — sibling of the
14154        // from-source × keyword-projected test of the same name, one
14155        // level down the composition (no keyword filter).
14156        let mut e = Expander::new();
14157        let out = e
14158            .expand_source_program("")
14159            .expect("empty source is not an error");
14160        assert!(
14161            out.is_empty(),
14162            "empty source must yield empty Vec, got: {out:?}"
14163        );
14164    }
14165
14166    #[test]
14167    fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
14168        // Critical pipeline order: a `defmacro` in the source registers
14169        // AND a subsequent call to it expands AND the expanded form
14170        // surfaces in the returned Vec. Sourced from `&str`, the
14171        // from-source posture preserves the side-effect-then-expansion
14172        // semantics of the from-forms primitive verbatim.
14173        let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
14174                     (emit-monitor "alpha")
14175                     (emit-monitor "beta")"#;
14176        let mut e = Expander::new();
14177        let out = e
14178            .expand_source_program(src)
14179            .expect("defmacro absorption then expansion must compose");
14180        // Two surviving forms (the `defmacro` itself is consumed as a
14181        // side-effect), each expanded to a `(defmonitor :name "<n>")`.
14182        assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
14183        for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
14184            let args = out[i]
14185                .as_call_to("defmonitor")
14186                .unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
14187            assert_eq!(args[0].as_keyword(), Some("name"));
14188            assert_eq!(args[1].as_string(), Some(expected_name));
14189        }
14190        // The macro is now in the table — a subsequent call would
14191        // expand against this SAME expander.
14192        assert!(e.has("emit-monitor"));
14193    }
14194
14195    #[test]
14196    fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
14197        // Structural identity: for any `src` whose pieces succeed, the
14198        // from-source method's result equals the inlined pre-lift
14199        // pipeline `read + expand_program`. Pin shape AND ordering on a
14200        // mixed source (defmacro definition + macro call + plain form)
14201        // so a regression that drifts the method's pipeline from the
14202        // inlined closed-form fails here. This is the lift's
14203        // load-bearing path-uniformity gate — the SAME assertion holds
14204        // for both production consumers (`RealizedCompiler::compile` +
14205        // `realize_in_memory`'s `:macros` loop) at the primitive's
14206        // contract level.
14207        let src = r#"(defmacro id (x) `,x)
14208                     (id (foo 1 2))
14209                     (bar)"#;
14210
14211        // Inlined pre-lift pipeline.
14212        let mut e_inline = Expander::new();
14213        let inline_forms = read(src).unwrap();
14214        let via_inline = e_inline
14215            .expand_program(inline_forms)
14216            .expect("inlined pipeline must succeed");
14217
14218        // From-source method pipeline.
14219        let mut e_method = Expander::new();
14220        let via_method = e_method
14221            .expand_source_program(src)
14222            .expect("from-source method pipeline must succeed");
14223
14224        assert_eq!(
14225            via_inline, via_method,
14226            "from-source method must emit byte-identical result to inlined read+expand_program"
14227        );
14228        // Both pipelines registered the same defmacro side-effect.
14229        assert_eq!(e_inline.has("id"), e_method.has("id"));
14230        assert!(e_method.has("id"));
14231    }
14232
14233    #[test]
14234    fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
14235        // Per-`&mut self` absorption posture pin: a `defmacro` registered
14236        // in call 1 SURVIVES into call 2 on the SAME `Expander`. This is
14237        // the load-bearing semantic `realize_in_memory`'s per-spec-macro
14238        // build-up depends on — every iteration's `expand_source_program(
14239        // macro_src)?` lands its `defmacro` heads into the SAME mutable
14240        // `preloaded` expander, so a follow-up `:macros` source can
14241        // invoke macros defined in earlier ones.
14242        let mut e = Expander::new();
14243        let _ = e
14244            .expand_source_program("(defmacro outer (n) `(inner ,n))")
14245            .unwrap();
14246        assert!(e.has("outer"));
14247        // Call 2: defines `inner` AND invokes `outer`, which expands to
14248        // `(inner <n>)`. The expansion proves call 1's `outer` survived
14249        // into call 2's expansion.
14250        let out = e
14251            .expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
14252            .unwrap();
14253        assert_eq!(out.len(), 1, "call 2 yields one expanded form");
14254        // (outer 42) → (inner 42) → (wrapped 42)
14255        let args = out[0]
14256            .as_call_to("wrapped")
14257            .expect("nested expansion must reach `wrapped`");
14258        assert_eq!(args[0].as_int(), Some(42));
14259    }
14260
14261    // ── Expander::register_macro_def: the macro-registration primitive ──
14262    //
14263    // `register_macro_def(&mut self, def: MacroDef) -> Result<()>` lifts
14264    // the byte-identical two-step block —
14265    //
14266    //   if self.compile_templates {
14267    //       self.templates.insert(def.name.clone(), compile_template(&def)?);
14268    //   }
14269    //   self.macros.insert(def.name.clone(), def);
14270    //
14271    // — that lived inline at `with_macros` (the bulk-from-iterator
14272    // constructor) AND `expand_program`'s `(defmacro …)`-head arm (the
14273    // program-walk-time registration site) into ONE named method on the
14274    // `Expander` surface. The tests below pin:
14275    //
14276    //   (a) the bytecode-default expander (`Expander::new()`) populates
14277    //       BOTH `macros` AND `templates` keyed by `def.name`,
14278    //   (b) the substitute-only expander (`Expander::new_substitute_only()`)
14279    //       populates `macros` but SKIPS `templates` — the
14280    //       `compile_templates: false` gate fires structurally,
14281    //   (c) a template that fails to compile (`,foo` against `params:
14282    //       []`) short-circuits BEFORE `self.macros.insert` runs, so the
14283    //       failed registration leaves both tables untouched — no
14284    //       partial-write window in which `self.macros.has(name)` is
14285    //       true but `self.templates.has(name)` is missing,
14286    //   (d) `with_macros([def])` and `register_macro_def(def)` on a fresh
14287    //       expander produce structurally identical state (both tables'
14288    //       sets of keys + the same `MacroDef` body under each key),
14289    //   (e) `expand_program` of one `(defmacro …)` form and
14290    //       `register_macro_def` of the parsed `MacroDef` produce
14291    //       structurally identical state — closing path-uniformity
14292    //       across the lift's two consumers AT the registration site.
14293    //
14294    // The tests bind on `Expander::has(name)` / `Expander::len()` for
14295    // the macros table and on bytecode-vs-substitute output equivalence
14296    // (a registered macro must expand to the SAME result regardless of
14297    // whether the bytecode path's `self.templates` entry exists) for the
14298    // templates table.
14299
14300    fn macro_def_id() -> MacroDef {
14301        // `(defmacro id (x) `,x)` — the simplest well-formed MacroDef:
14302        // one required param, a single-symbol unquote body. Compiles to
14303        // a valid `CompiledTemplate` (no unbound vars), so
14304        // `register_macro_def` succeeds on every Expander posture.
14305        MacroDef {
14306            name: "id".into(),
14307            params: MacroParams {
14308                required: vec!["x".into()],
14309                optional: vec![],
14310                rest: None,
14311            },
14312            // ` ` quasi-quoted `,x` — `Quasiquote(Unquote(Symbol("x")))`.
14313            body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14314        }
14315    }
14316
14317    fn macro_def_bad_template() -> MacroDef {
14318        // `(defmacro bad () `,unbound)` — a quasi-quote body with `,unbound`
14319        // against an EMPTY required-params list. `compile_template`
14320        // rejects it with `UnboundTemplateVar` because `unbound` is not
14321        // in `params.names()`. Used to exercise the
14322        // `compile_template`-failed-but-self.macros-still-pristine
14323        // path-uniformity pin.
14324        MacroDef {
14325            name: "bad".into(),
14326            params: MacroParams::default(),
14327            body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
14328        }
14329    }
14330
14331    #[test]
14332    fn register_macro_def_bytecode_default_populates_macros_and_templates() {
14333        // The bytecode-default `Expander::new()` carries
14334        // `compile_templates: true`; `register_macro_def` MUST populate
14335        // BOTH `self.macros` (the substitute path's registry) AND
14336        // `self.templates` (the bytecode path's pre-compiled bytecode
14337        // index) keyed by `def.name`. Fail-before-pass-after: this
14338        // assert requires the new method to exist AND to compose the
14339        // two side-effects in canonical order on a single-call
14340        // registration — pre-lift `register_macro_def` did not exist.
14341        let mut e = Expander::new();
14342        e.register_macro_def(macro_def_id())
14343            .expect("well-formed MacroDef must register");
14344        assert!(
14345            e.has("id"),
14346            "self.macros must carry the registered name after register_macro_def"
14347        );
14348        assert!(
14349            e.templates.contains_key("id"),
14350            "self.templates must carry the compiled bytecode under the bytecode-default posture"
14351        );
14352        // The registered macro must expand correctly through the
14353        // bytecode strategy — proves the inserted template is the right
14354        // bytecode for the body.
14355        let out = e
14356            .expand_program(read("(id 42)").unwrap())
14357            .expect("registered macro must expand");
14358        assert_eq!(out.len(), 1);
14359        assert_eq!(out[0], Sexp::int(42));
14360    }
14361
14362    #[test]
14363    fn register_macro_def_substitute_only_skips_templates() {
14364        // `Expander::new_substitute_only()` carries `compile_templates:
14365        // false`; `register_macro_def` MUST populate `self.macros` for
14366        // the substitute path but SKIP `self.templates` (no bytecode
14367        // pre-compile). Pin the `compile_templates: false` gate fires
14368        // structurally — a regression that drifts the conditional from
14369        // the registration primitive (e.g. a future emitter that
14370        // unconditionally pre-compiles) would silently double the
14371        // benchmark baseline's allocation footprint.
14372        let mut e = Expander::new_substitute_only();
14373        e.register_macro_def(macro_def_id())
14374            .expect("well-formed MacroDef must register under substitute-only");
14375        assert!(
14376            e.has("id"),
14377            "self.macros must carry the registered name even under substitute-only"
14378        );
14379        assert!(
14380            !e.templates.contains_key("id"),
14381            "self.templates MUST be empty under compile_templates: false — the gate fires"
14382        );
14383        // The registered macro must still expand correctly through the
14384        // substitute strategy — proves the inserted MacroDef is the
14385        // right body for substitute-walking.
14386        let out = e
14387            .expand_program(read("(id 42)").unwrap())
14388            .expect("registered macro must expand via substitute path");
14389        assert_eq!(out.len(), 1);
14390        assert_eq!(out[0], Sexp::int(42));
14391    }
14392
14393    #[test]
14394    fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
14395        // The structural ordering pin: when `compile_template(&def)?`
14396        // rejects (e.g. `,unbound` against empty params), the `?` MUST
14397        // short-circuit BEFORE `self.macros.insert(def.name.clone(),
14398        // def)` runs, so a failed registration leaves BOTH tables
14399        // exactly as they were. Without this ordering a future
14400        // bytecode-strategy lookup would resolve `self.macros.get(name)`
14401        // (the body was inserted) AND find `self.templates.get(name)`
14402        // returning `None` (the pre-compile failed), silently coercing
14403        // the macro onto the substitute fallback path despite
14404        // `compile_templates: true`. The lift preserves the pre-lift
14405        // ordering (`compile_template` precedes `self.macros.insert`)
14406        // structurally — the test pins it across the lift.
14407        let mut e = Expander::new();
14408        let err = e
14409            .register_macro_def(macro_def_bad_template())
14410            .expect_err("unbound-template body must reject");
14411        // The rejection is the structural variant the bytecode
14412        // strategy's template-gate emits.
14413        assert!(
14414            matches!(err, LispError::UnboundTemplateVar { .. }),
14415            "expected UnboundTemplateVar, got: {err:?}"
14416        );
14417        // Both tables MUST be pristine — no partial write.
14418        assert!(
14419            !e.has("bad"),
14420            "self.macros must be untouched after compile_template failure"
14421        );
14422        assert!(
14423            !e.templates.contains_key("bad"),
14424            "self.templates must be untouched after compile_template failure"
14425        );
14426    }
14427
14428    #[test]
14429    fn with_macros_routes_through_register_macro_def_path_uniformity() {
14430        // Path-uniformity pin across the bulk-constructor consumer:
14431        // `with_macros([def])` MUST produce the same final state as
14432        // `Expander::new()` + `register_macro_def(def)`. A regression
14433        // that drifts the constructor's per-MacroDef inline block from
14434        // the registration primitive (e.g. a future emitter that
14435        // re-inlines the two-step block at `with_macros` rather than
14436        // delegating) would fail loudly here.
14437        let mut via_register = Expander::new();
14438        via_register
14439            .register_macro_def(macro_def_id())
14440            .expect("register must succeed");
14441        let mut via_with_macros =
14442            Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
14443        assert_eq!(via_register.len(), via_with_macros.len());
14444        assert!(via_register.has("id"));
14445        assert!(via_with_macros.has("id"));
14446        // Both tables key on the same set across both postures.
14447        assert_eq!(
14448            via_register.templates.contains_key("id"),
14449            via_with_macros.templates.contains_key("id"),
14450            "self.templates key-presence must agree across with_macros and register_macro_def"
14451        );
14452        // Both registered expanders expand the registered macro
14453        // identically — the strongest behavioral parity.
14454        let out_a = via_register
14455            .expand_program(read("(id 99)").unwrap())
14456            .unwrap();
14457        let out_b = via_with_macros
14458            .expand_program(read("(id 99)").unwrap())
14459            .unwrap();
14460        assert_eq!(out_a, out_b);
14461        assert_eq!(out_a, vec![Sexp::int(99)]);
14462    }
14463
14464    #[test]
14465    fn expand_program_routes_through_register_macro_def_path_uniformity() {
14466        // Path-uniformity pin across the program-walk consumer:
14467        // `expand_program` of `(defmacro id (x) `,x)` MUST produce the
14468        // same final state as a direct
14469        // `register_macro_def(macro_def_id())`. A regression that
14470        // drifts `expand_program`'s `(defmacro …)`-head arm from the
14471        // registration primitive (e.g. a future emitter that re-inlines
14472        // the two-step block at `expand_program` rather than delegating)
14473        // would fail loudly here.
14474        let mut via_register = Expander::new();
14475        via_register
14476            .register_macro_def(macro_def_id())
14477            .expect("register must succeed");
14478        let mut via_expand_program = Expander::new();
14479        let yielded = via_expand_program
14480            .expand_program(read("(defmacro id (x) `,x)").unwrap())
14481            .expect("expand_program of one defmacro must succeed");
14482        // expand_program drops the (defmacro …) form from its returned
14483        // Vec<Sexp> (defmacro is a definition, not a program form), so
14484        // the yielded list is empty — pin the side-effect-only posture.
14485        assert!(
14486            yielded.is_empty(),
14487            "(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
14488        );
14489        assert!(via_register.has("id"));
14490        assert!(via_expand_program.has("id"));
14491        assert_eq!(via_register.len(), via_expand_program.len());
14492        // Both tables key on the same set across both postures.
14493        assert_eq!(
14494            via_register.templates.contains_key("id"),
14495            via_expand_program.templates.contains_key("id"),
14496            "self.templates key-presence must agree across expand_program and register_macro_def"
14497        );
14498        // Both registered expanders expand the registered macro
14499        // identically — the strongest behavioral parity, closing
14500        // path-uniformity across BOTH consumers (with_macros above,
14501        // expand_program here) at ONE primitive.
14502        let out_a = via_register
14503            .expand_program(read("(id 7)").unwrap())
14504            .unwrap();
14505        let out_b = via_expand_program
14506            .expand_program(read("(id 7)").unwrap())
14507            .unwrap();
14508        assert_eq!(out_a, out_b);
14509        assert_eq!(out_a, vec![Sexp::int(7)]);
14510    }
14511
14512    // ── as_unquote: path-uniformity across the three substitute / compile_node sites ──
14513    //
14514    // The new typed-marker projection `Sexp::as_unquote` lifts the
14515    // (Sexp::Unquote/UnquoteSplice variant, UnquoteForm::Unquote/Splice
14516    // literal) pair into ONE structural query. These tests pin behavioral
14517    // parity end-to-end across BOTH expansion strategies:
14518    //   * `compile_node` — the bytecode-template strategy's typed marker
14519    //     dispatch routes through as_unquote at the compile step.
14520    //   * `substitute` (top-level + list-inner) — the substitute-walker
14521    //     fallback strategy's typed marker dispatch routes through
14522    //     as_unquote at both per-form sites.
14523    // The structural invariant the prior runs' `expansion_layers_agree_on_
14524    // output_and_cache_wins` benchmark observes — bytecode AND substitute
14525    // produce byte-identical output — is anchored here at the macro-template
14526    // level for every distinct unquote-family shape the substitute and
14527    // bytecode strategies discriminate.
14528
14529    #[test]
14530    fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
14531        // A template body whose only marker is a top-level `,x`. Both
14532        // expansion strategies route through `as_unquote` (compile_node for
14533        // bytecode, substitute for the fallback walker), each pairing
14534        // Sexp::Unquote ↔ UnquoteForm::Unquote at ONE typed projection.
14535        // Pin byte-identical output across both strategies — the
14536        // structural-invariant the new projection's lift was designed to
14537        // make load-bearing structural rather than per-site discipline.
14538        let src = "(defmacro id (x) ,x) (id 42)";
14539        let mut bc = Expander::new();
14540        let mut sub = Expander::new_substitute_only();
14541        let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14542        let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14543        assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
14544        assert_eq!(out_bc, vec![Sexp::int(42)]);
14545    }
14546
14547    #[test]
14548    fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
14549        // A template body whose marker is a list-inner `,@xs`. The substitute
14550        // strategy's list-inner Splice arm routes through `as_unquote`
14551        // matching Some((UnquoteForm::Splice, inner)); the bytecode strategy's
14552        // compile_node Splice arm routes through `as_unquote` matching the
14553        // same. Pin byte-identical output across both strategies for the
14554        // splice path — the projection's typed-marker pairing
14555        // Sexp::UnquoteSplice ↔ UnquoteForm::Splice is structurally
14556        // identical at every consumer post-lift.
14557        let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
14558        let mut bc = Expander::new();
14559        let mut sub = Expander::new_substitute_only();
14560        let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14561        let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14562        assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
14563        let expected = Sexp::List(vec![
14564            Sexp::symbol("list"),
14565            Sexp::int(0),
14566            Sexp::int(1),
14567            Sexp::int(2),
14568            Sexp::int(3),
14569            Sexp::int(99),
14570        ]);
14571        assert_eq!(out_bc, vec![expected]);
14572    }
14573
14574    #[test]
14575    fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
14576        // The substitute strategy's top-level `,@x` arm rejects with
14577        // LispError::SpliceOutsideList (a splice marker with no containing
14578        // list to flatten into). Pre-lift the arm was
14579        // `Sexp::UnquoteSplice(inner) => Err(splice_outside_list(inner))`;
14580        // post-lift the arm routes through `as_unquote` matching
14581        // Some((UnquoteForm::Splice, inner)) and dispatches on the typed
14582        // marker. Pin path-uniformity: the rejection MUST fire identically
14583        // through the new projection. The substitute-only strategy bypasses
14584        // the bytecode path, exposing this gate directly.
14585        //
14586        // `(defmacro bad (xs) ,@xs) (bad (1 2 3))` — the body is a bare
14587        // `,@xs` at top level (NOT wrapped in a containing list), so the
14588        // substitute fallback's top-level Splice arm fires.
14589        let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
14590        let mut sub = Expander::new_substitute_only();
14591        let err = sub.expand_program(read(src).unwrap()).unwrap_err();
14592        assert!(
14593            matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14594            "expected SpliceOutsideList through as_unquote, got: {err:?}"
14595        );
14596    }
14597
14598    #[test]
14599    fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
14600        // A `,unbound` template body — the inner symbol isn't a param —
14601        // fires gate-2 (must-be-bound-in-scope). The typed marker
14602        // `UnquoteForm::Unquote` MUST thread through `as_unquote` →
14603        // `resolve_unquote_in_params(inner, params, form)` → gate-2's
14604        // rejection-builder. Pre-lift the marker was bound at the per-arm
14605        // literal `UnquoteForm::Unquote`; post-lift it's bound at the
14606        // typed projection. Pin: the rejection's `prefix` slot is
14607        // UnquoteForm::Unquote, structurally derived from the typed
14608        // projection's typed marker, NOT a literal at the arm body.
14609        let src = "(defmacro bad (x) ,unbound)";
14610        let mut bc = Expander::new();
14611        let err = bc.expand_program(read(src).unwrap()).unwrap_err();
14612        match err {
14613            crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14614                assert_eq!(
14615                    prefix,
14616                    UnquoteForm::Unquote,
14617                    "typed marker drifted from UnquoteForm::Unquote at gate-2"
14618                );
14619            }
14620            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14621        }
14622        // Sibling negative control: `,@unbound` inside a containing list
14623        // threads UnquoteForm::Splice through the same projection. The
14624        // splice MUST be inside a list — `compile_template` rejects
14625        // top-level `,@X` bodies with SpliceOutsideList before compile_node
14626        // runs, so the typed-marker dispatch on Splice is observable only
14627        // for well-positioned splice bodies. Closes path-uniformity across
14628        // BOTH typed marker variants at the compile path.
14629        let src_splice = "(defmacro bad (x) (foo ,@unbound))";
14630        let mut bc2 = Expander::new();
14631        let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
14632        match err_splice {
14633            crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14634                assert_eq!(
14635                    prefix,
14636                    UnquoteForm::Splice,
14637                    "typed marker drifted from UnquoteForm::Splice at gate-2"
14638                );
14639            }
14640            other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14641        }
14642    }
14643
14644    // ── compile_template + contains_unquote path-uniformity through as_unquote ──
14645    //
14646    // The prior `as_unquote` projection lift (eb00684) routed `compile_node`'s
14647    // two arms and `substitute`'s top-level + list-inner arms through the
14648    // typed-marker projection but LEFT `compile_template`'s top-level
14649    // splice-outside-list gate and `contains_unquote`'s family check inline
14650    // matching `Sexp::UnquoteSplice(inner)` / `Sexp::Unquote(_) |
14651    // Sexp::UnquoteSplice(_)`. After this lift both production sites route
14652    // through `as_unquote`: `compile_template` matches `Some((UnquoteForm::
14653    // Splice, inner))` (same shape as `substitute`'s list-inner Splice arm),
14654    // `contains_unquote` uses `as_unquote().is_some()` for the family check.
14655    // Every production-site recognizer of an unquote-family wrapper now
14656    // shares ONE typed-marker projection — the (Sexp variant, UnquoteForm
14657    // variant) pairing for `,@-outside-list` is bound at ONE structural
14658    // query across all FOUR reachable splice-recognition sites, and a future
14659    // regression that drifts the marker pairing at any production site
14660    // becomes a type error at the helper boundary rather than a silent
14661    // per-site divergence.
14662
14663    #[test]
14664    fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
14665        // The bytecode path's `compile_template` gate pre-rejects top-level
14666        // `,@X` bodies via `as_unquote()` matching `Some((UnquoteForm::Splice,
14667        // inner))`. Pre-lift the arm was `if let Sexp::UnquoteSplice(inner) =
14668        // body` — the LAST production-site inline `Sexp::UnquoteSplice(_)`
14669        // match outside the projection itself. Post-lift the (Sexp variant,
14670        // UnquoteForm variant) pairing for the splice-outside-list gate is
14671        // bound at ONE projection function across all three reachable
14672        // emission sites (compile_template top-level, substitute top-level,
14673        // substitute list-inner). Path-uniformity guard at the new boundary:
14674        // the bytecode-path compile-time gate now routes through the same
14675        // shape `substitute`'s list-inner Splice arm uses.
14676        //
14677        // `(defmacro bad (xs) \`,@xs)` — the body's outer quasi-quote is
14678        // peeled by `template_body`, leaving `,@xs` at top level for
14679        // `compile_template` to gate before `compile_node` walks anything.
14680        // The bytecode strategy is default-on (`Expander::new()` sets
14681        // `compile_templates: true`), so the gate fires at
14682        // `register_macro_def` time.
14683        let mut e = Expander::new();
14684        let err = e
14685            .expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
14686            .expect_err("compile_template must reject top-level ,@X via as_unquote");
14687        assert!(
14688            matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14689            "expected SpliceOutsideList through as_unquote, got: {err:?}"
14690        );
14691    }
14692
14693    #[test]
14694    fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
14695        // Negative control on the typed-marker dispatch at `compile_template`'s
14696        // top-level gate. A top-level `,X` body (Unquote, NOT Splice) is a
14697        // valid template — `compile_node` lowers it to a single `Subst(idx)`
14698        // op. The new `Some((UnquoteForm::Splice, inner))` pattern at the
14699        // gate MUST NOT fire on the Unquote variant — only Splice — so the
14700        // typed marker is observably load-bearing: a regression that drifts
14701        // the pattern from `UnquoteForm::Splice` to the wider
14702        // `Some((_, inner))` would mis-reject `(defmacro id (x) ,x)` here.
14703        //
14704        // `(defmacro id (x) ,x) (id 42)` — `id`'s body is bare `,x` at top
14705        // level; `compile_template` admits it via the typed-marker gate,
14706        // `compile_node` emits `Subst(0)`, and the call expands to `42`.
14707        let src = "(defmacro id (x) ,x) (id 42)";
14708        let mut e = Expander::new();
14709        let expanded = e
14710            .expand_program(read(src).unwrap())
14711            .expect("top-level ,X body must compile through as_unquote-typed gate");
14712        assert_eq!(expanded, vec![Sexp::int(42)]);
14713    }
14714
14715    #[test]
14716    fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
14717        // The fast-path optimizer in `compile_node` short-circuits on
14718        // `contains_unquote(node)` — true iff the subtree carries an
14719        // unquote-family wrapper. Pre-lift the family check inlined
14720        // `Sexp::Unquote(_) | Sexp::UnquoteSplice(_) => true`; post-lift it
14721        // routes through `as_unquote().is_some()`. Pin path-uniformity at
14722        // the family-recognition boundary: every production-site unquote
14723        // recognizer (compile_template's gate, compile_node's per-arm
14724        // dispatch, substitute's top-level + list-inner arms, AND this
14725        // fast-path predicate) now shares ONE typed-marker projection.
14726        //
14727        // Both variants must trigger the fast-path bail — the optimizer's
14728        // gate keys on the family, not the inner. The recursion into
14729        // Quote/Quasiquote/List subtrees ALSO observes the same family
14730        // gate at every level, so a `,@xs` buried under a `\`...` outer
14731        // wrapper still fires it.
14732        let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14733        let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
14734        let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
14735            Sexp::symbol("foo"),
14736            Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
14737        ])));
14738        // The projection's `is_some()` face MUST agree with the pre-lift
14739        // `matches!()` discriminant on every variant — closed-set guarantee
14740        // shared with `as_unquote`'s contract pin in `ast.rs`.
14741        assert!(super::contains_unquote(&bare_unquote));
14742        assert!(super::contains_unquote(&bare_splice));
14743        assert!(super::contains_unquote(&nested));
14744        // Negative control: shapes the projection rejects (Nil, Atom,
14745        // bare List, Quote-family without inner unquote) must NOT trigger
14746        // the fast-path bail — the projection's None face stays observably
14747        // distinct from its Some face.
14748        assert!(!super::contains_unquote(&Sexp::Nil));
14749        assert!(!super::contains_unquote(&Sexp::symbol("plain")));
14750        assert!(!super::contains_unquote(&Sexp::int(5)));
14751        assert!(!super::contains_unquote(&Sexp::List(vec![
14752            Sexp::symbol("plain"),
14753            Sexp::int(1),
14754        ])));
14755        assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
14756            Sexp::symbol("inert")
14757        ))));
14758    }
14759
14760    #[test]
14761    fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
14762        // Pin the lift: `contains_unquote`'s quote-family recognition
14763        // now routes through `Sexp::as_quote_form` (the 4-of-4 wrapper
14764        // projection) AND `QuoteForm::as_unquote_form` (the 2-of-4
14765        // substitution-subset gate) at ONE site — not through the pair
14766        // of `as_unquote().is_some()` + inline
14767        // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm. The
14768        // path-uniformity assertion: for every quote-family wrapper, the
14769        // function's behavior agrees with the manual composition through
14770        // the two typed-marker projections. A regression that re-inlines
14771        // either arm (e.g. drops the `as_unquote_form().is_some()` gate
14772        // and just returns true for any `as_quote_form().is_some()`, or
14773        // restores the per-variant `Quote | Quasiquote` arm) drifts from
14774        // this composition and surfaces here.
14775        use crate::ast::QuoteForm;
14776
14777        // Sweep every QuoteForm variant under both axes:
14778        //   * unquote-only-inner (inner = symbol):
14779        //       Unquote(s)        → true via `as_unquote_form() == Some`
14780        //       UnquoteSplice(s)  → true via `as_unquote_form() == Some`
14781        //       Quote(s)          → false (inner has no unquote, recurses to false)
14782        //       Quasiquote(s)     → false (same)
14783        //   * unquote-inner-inside-quote-wrapper:
14784        //       Quote(Unquote(s)) → true (Quote arm recurses via `as_quote_form`,
14785        //                                 inner `Unquote` returns true through the
14786        //                                 SAME projection — both arms share the
14787        //                                 ONE typed-marker site post-lift)
14788        let inner_plain = Sexp::symbol("x");
14789        let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14790
14791        for qf in QuoteForm::ALL {
14792            let wrapped_plain = match qf {
14793                QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
14794                QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
14795                QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
14796                QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
14797            };
14798            // Path-uniformity: behavior derives from the manual two-marker
14799            // composition through `as_quote_form` + `as_unquote_form`.
14800            // The substitution-subset gate (`as_unquote_form().is_some()`)
14801            // returns true for Unquote/UnquoteSplice and false for
14802            // Quote/Quasiquote — directly encoding the 2-of-4 partition.
14803            let via_manual =
14804                qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
14805            assert_eq!(
14806                super::contains_unquote(&wrapped_plain),
14807                via_manual,
14808                "contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
14809            );
14810
14811            let wrapped_unquote = match qf {
14812                QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
14813                QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
14814                QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
14815                QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
14816            };
14817            // EVERY quote-family wrapper around an inner `Unquote` must
14818            // contain unquote — either the outer projects as `Some` on
14819            // the substitution subset (Unquote/UnquoteSplice arms), OR
14820            // the outer is Quote/Quasiquote and the recursion into the
14821            // inner re-fires the projection. The pre-lift Quote arm
14822            // recursed via the inline `Sexp::Quote(inner)` match;
14823            // post-lift the SAME recursion fires via `as_quote_form`'s
14824            // typed projection. Both yield `true`.
14825            assert!(
14826                super::contains_unquote(&wrapped_unquote),
14827                "contains_unquote missed an inner Unquote under {qf:?} — \
14828                 the quote-family recursion through as_quote_form drifted"
14829            );
14830        }
14831
14832        // Negative control: a non-quote-family wrapper (List, Atom,
14833        // Nil) must NOT route through `as_quote_form` at all. The
14834        // `if let Some(...) = node.as_quote_form()` gate stays `None`
14835        // and falls through to the List/_  arms. This is the structural
14836        // partition: the projection's `None` face MUST agree with
14837        // `!matches!(node, Sexp::Quote(_) | Sexp::Quasiquote(_) |
14838        // Sexp::Unquote(_) | Sexp::UnquoteSplice(_))`.
14839        let nested_in_list = Sexp::List(vec![
14840            Sexp::symbol("outer"),
14841            Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14842        ]);
14843        assert!(
14844            super::contains_unquote(&nested_in_list),
14845            "contains_unquote failed to descend into a List subtree containing a \
14846             Quasiquote(Unquote(_)) — list recursion arm drifted"
14847        );
14848    }
14849
14850    // ── expand_and_collect_named_calls_to_any (from-forms × named ────
14851    // × typed-decoded classifier) ─────────────────────────────────────
14852    //
14853    // The (named NAME-then-kwargs × typed-decoded classifier) cell of
14854    // the dispatcher matrix on the `Expander` surface — pre-lift the
14855    // matrix had the bare-kwargs row closed (expand_and_collect_calls_to
14856    // + expand_and_collect_calls_to_any) AND the constant-keyword
14857    // column closed on the named axis (expand_to_named +
14858    // expand_source_to_named) but the (named × classifier) cell was
14859    // open — every consumer that wanted to walk a program by typed-
14860    // decoded classifier AND extract the NAME slot per matched form
14861    // had to compose `expand_and_collect_calls_to_any` with
14862    // `split_name_slot` inline. Post-lift the cell is ONE named primitive
14863    // on the `Expander` surface, and the constant-`T::KEYWORD` named
14864    // sibling routes through it as the (constant-classifier × named)
14865    // specialization.
14866    //
14867    // The tests below pin: (a) the happy path yields the decoded triple
14868    // `(T, &str, &[Sexp])` for every matched form in source order; (b)
14869    // non-matching forms are skipped (soft-projection contract inherited
14870    // from the typed-decoded primitive); (c) the `NamedFormMissingName`
14871    // variant fires for matched forms with no NAME slot, threading the
14872    // classifier-supplied `&'static str` keyword; (d) the
14873    // `NamedFormNonSymbolName` variant fires for matched forms with a
14874    // non-symbol-or-string NAME slot, again threading the classifier-
14875    // supplied keyword; (e) the projection's `Err` short-circuits at
14876    // the first failing matched form; (f) `expand_program` runs BEFORE
14877    // the classifier filter walks — a `(defmacro …)` emitting a named
14878    // form is decoded by the classifier on the EXPANDED head, not the
14879    // macro-call head; (g) the from-source sibling agrees with `read +
14880    // from-forms` on the same source.
14881
14882    #[test]
14883    fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
14884    ) {
14885        // The typed-decoded named primitive's happy path: a closed-set
14886        // classifier decodes head symbols to a typed enum PAIRED with
14887        // its canonical static keyword, and the per-form projection
14888        // receives `(decoded, name, spec_args)` for every matched form
14889        // in source order. Sweep across TWO distinct classifier outcomes
14890        // — `Monitor` / `Notify` — interleaved with a non-matching form
14891        // to pin the typed-witness threading AND the NAME-slot
14892        // projection AND the source-order yield AND the rejection of
14893        // non-classifier forms in ONE assertion.
14894        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14895        enum Kind {
14896            Monitor,
14897            Notify,
14898        }
14899        let src = r#"(defmonitor cpu :threshold 80)
14900                     (other-form 99)
14901                     (defnotify email :to "ops@example.com")
14902                     (defmonitor mem :threshold 90)"#;
14903        let forms = read(src).unwrap();
14904        let mut e = Expander::new();
14905        let rows: Vec<(Kind, String, usize)> = e
14906            .expand_and_collect_named_calls_to_any(
14907                forms,
14908                |h| match h {
14909                    "defmonitor" => Some((Kind::Monitor, "defmonitor")),
14910                    "defnotify" => Some((Kind::Notify, "defnotify")),
14911                    _ => None,
14912                },
14913                |kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
14914            )
14915            .unwrap();
14916        assert_eq!(
14917            rows,
14918            vec![
14919                (Kind::Monitor, "cpu".to_string(), 2),
14920                (Kind::Notify, "email".to_string(), 2),
14921                (Kind::Monitor, "mem".to_string(), 2),
14922            ],
14923        );
14924    }
14925
14926    #[test]
14927    fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
14928        // Soft-projection contract — every shape the typed-decoded
14929        // primitive rejects (non-list, empty list, list whose head is
14930        // not a symbol, list whose head decodes to `None`) skips the
14931        // projection silently. The named gate inside the wrapper
14932        // projection runs ONLY for classifier-matched forms; a non-
14933        // matching form that reaches the projection panics here.
14934        let src = r#":bare-keyword
14935                     "bare-string"
14936                     42
14937                     ()
14938                     (foo bar)
14939                     (defmonitor cpu :threshold 80)"#;
14940        let forms = read(src).unwrap();
14941        let mut e = Expander::new();
14942        let names: Vec<String> = e
14943            .expand_and_collect_named_calls_to_any(
14944                forms,
14945                |h| (h == "defmonitor").then_some(((), "defmonitor")),
14946                |(), name, _args| Ok(name.to_string()),
14947            )
14948            .unwrap();
14949        assert_eq!(names, vec!["cpu".to_string()]);
14950    }
14951
14952    #[test]
14953    fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
14954    ) {
14955        // `(defmonitor)` matches the classifier but has no NAME slot —
14956        // `split_name_slot` fires `NamedFormMissingName { keyword:
14957        // "defmonitor" }`. The classifier's `&'static str` keyword is
14958        // threaded VERBATIM through the named gate; a regression that
14959        // hardcodes a different keyword (e.g. the matched head text or
14960        // a default) would fail the structural variant identity
14961        // assertion. Fail-before-pass-after: pre-lift the primitive did
14962        // not exist; the inline `expand_and_collect_calls_to_any +
14963        // split_name_slot` composition at the consumer site was the
14964        // only path, with the keyword bound at the consumer's call
14965        // boundary rather than at the named primitive's body.
14966        let forms = read("(defmonitor)").unwrap();
14967        let mut e = Expander::new();
14968        let err = e
14969            .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
14970                forms,
14971                |h| (h == "defmonitor").then_some(((), "defmonitor")),
14972                |(), _name, _args| Ok(()),
14973            )
14974            .unwrap_err();
14975        assert!(
14976            matches!(
14977                err,
14978                crate::error::LispError::NamedFormMissingName {
14979                    keyword: "defmonitor"
14980                }
14981            ),
14982            "expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
14983        );
14984    }
14985
14986    #[test]
14987    fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
14988    ) {
14989        // `(defmonitor 42 :threshold 80)` matches the classifier but
14990        // the NAME slot is an int — `split_name_slot` fires
14991        // `NamedFormNonSymbolName { keyword: "defmonitor", got:
14992        // SexpShape::Int }`. The classifier's `&'static str` keyword is
14993        // threaded through the structural rejection variant verbatim,
14994        // and the typed `SexpShape` projection on the offending slot
14995        // is preserved across the named primitive.
14996        let forms = read("(defmonitor 42 :threshold 80)").unwrap();
14997        let mut e = Expander::new();
14998        let err = e
14999            .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15000                forms,
15001                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15002                |(), _name, _args| Ok(()),
15003            )
15004            .unwrap_err();
15005        assert!(
15006            matches!(
15007                err,
15008                crate::error::LispError::NamedFormNonSymbolName {
15009                    keyword: "defmonitor",
15010                    got: crate::error::SexpShape::Int,
15011                }
15012            ),
15013            "expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
15014        );
15015    }
15016
15017    #[test]
15018    fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
15019        // Result projection's short-circuit contract — when the
15020        // projection returns `Err` on a matched named form, the walk
15021        // stops and subsequent matched forms are NOT projected. Pin
15022        // source-order short-circuit via a counter the projection
15023        // increments before deciding to fail: the counter sits at
15024        // exactly the index of the failing form (second match),
15025        // proving the third match never reached the projection.
15026        let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
15027        let mut count = 0usize;
15028        let mut e = Expander::new();
15029        let err = e
15030            .expand_and_collect_named_calls_to_any::<String, _, _, ()>(
15031                forms,
15032                |h| (h == "defmon").then_some(((), "defmon")),
15033                |(), name, _args| {
15034                    count += 1;
15035                    if name == "b" {
15036                        return Err(crate::error::LispError::Missing("test-failure"));
15037                    }
15038                    Ok(name.to_string())
15039                },
15040            )
15041            .expect_err("projection must short-circuit on first Err");
15042        assert_eq!(
15043            count, 2,
15044            "projection must have run on first matched form AND the failing form, then stopped"
15045        );
15046        assert!(
15047            matches!(err, crate::error::LispError::Missing("test-failure")),
15048            "expected the projection's typed Err verbatim, got: {err:?}"
15049        );
15050    }
15051
15052    #[test]
15053    fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
15054        // Ordering contract — `expand_program` runs BEFORE the
15055        // typed-decoded named walk. A `(defmacro …)` form that emits a
15056        // classifier-decoded named form must have its EXPANDED head
15057        // decoded by the classifier, not the macro-call head. Pin the
15058        // ordering with a macro `(emit-mon n thr)` expanding to
15059        // `(defmonitor ,n :threshold ,thr)` — the classifier sees the
15060        // expanded `defmonitor` head and the NAME slot's symbol value
15061        // from the macro arg.
15062        let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15063                     (defmonitor cpu :threshold 80)
15064                     (emit-mon mem 90)
15065                     (other-form 99)
15066                     (emit-mon disk 70)"#;
15067        let forms = read(src).unwrap();
15068        let mut e = Expander::new();
15069        let names: Vec<String> = e
15070            .expand_and_collect_named_calls_to_any(
15071                forms,
15072                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15073                |(), name, _args| Ok(name.to_string()),
15074            )
15075            .unwrap();
15076        // `cpu` from the literal call, `mem` from the first macro
15077        // emit, `disk` from the second macro emit. The macro-emitted
15078        // forms are present BECAUSE `expand_program` ran first.
15079        assert_eq!(names, vec!["cpu", "mem", "disk"]);
15080    }
15081
15082    #[test]
15083    fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
15084        // From-source posture parity: feeding source through
15085        // `expand_source_and_collect_named_calls_to_any` is byte-
15086        // identical to feeding `read(src)?` through the from-forms
15087        // sibling on a fresh expander, because the from-source posture
15088        // is `read(src)? + from-forms` by construction. A regression
15089        // that drifts the from-source posture's pipeline from the
15090        // from-forms posture's pipeline would fail here.
15091        let src = r#"(defmonitor cpu :threshold 80)
15092                     (defmonitor mem :threshold 90)"#;
15093        let via_src: Vec<String> = Expander::new()
15094            .expand_source_and_collect_named_calls_to_any(
15095                src,
15096                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15097                |(), name, _args| Ok(name.to_string()),
15098            )
15099            .unwrap();
15100        let forms = read(src).unwrap();
15101        let via_forms: Vec<String> = Expander::new()
15102            .expand_and_collect_named_calls_to_any(
15103                forms,
15104                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15105                |(), name, _args| Ok(name.to_string()),
15106            )
15107            .unwrap();
15108        assert_eq!(via_src, via_forms);
15109        assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15110    }
15111
15112    #[test]
15113    fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
15114    ) {
15115        // `?`-routing through `read` short-circuits BEFORE the
15116        // classifier or the named gate runs. Unbalanced paren — reader
15117        // error fires; classifier panic-decoder MUST NOT execute,
15118        // proving the read step gates the entire pipeline.
15119        let mut e = Expander::new();
15120        let err = e
15121            .expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
15122                "(defmonitor cpu :threshold 80",
15123                |_h| panic!("classifier must not run when reader fails"),
15124                |(), _name, _args| Ok(()),
15125            )
15126            .unwrap_err();
15127        // The named gate's variants MUST NOT fire — the reader error
15128        // is structurally distinct.
15129        assert!(
15130            !matches!(
15131                err,
15132                crate::error::LispError::NamedFormMissingName { .. }
15133                    | crate::error::LispError::NamedFormNonSymbolName { .. }
15134            ),
15135            "expected reader error, not named-gate variant; got: {err:?}"
15136        );
15137    }
15138
15139    // ── expand_and_collect_named_calls_to (from-forms × named × ──────
15140    // constant-keyword × untyped R) ─────────────────────────────────
15141    //
15142    // The (named NAME-then-kwargs × constant-keyword × untyped `R`)
15143    // cell of the `Expander` typed-walk family — pre-lift the cell was
15144    // reachable ONLY through `expand_to_named<T>` with the
15145    // `T: TataraDomain` parameter baking BOTH the `T::KEYWORD` filter
15146    // AND the `T::compile_from_args`-based projection through
15147    // `expand_and_collect_calls_to(forms, T::KEYWORD,
15148    // named_form_projection::<T>)`. Post-lift the cell surfaces as ONE
15149    // method that takes the keyword and the `(name, args) -> R`
15150    // projection as caller-supplied parameters, AND
15151    // `expand_to_named<T>` routes through it as the typed
15152    // `T::KEYWORD`-constant specialization — so the named-form
15153    // `split_name_slot` composition lives at ONE site
15154    // (`expand_and_collect_named_calls_to_any` body) post-lift rather
15155    // than at TWO sites pre-lift (the bare-kwargs path through
15156    // `named_form_projection<T>` AND the classifier path).
15157    //
15158    // The tests below pin: (a) the happy path yields the `(name, args)`
15159    // pair for every matched form in source order; (b) non-matching
15160    // forms are skipped (soft-projection contract inherited from the
15161    // named typed-decoded primitive); (c) the `NamedFormMissingName`
15162    // variant fires for matched forms with no NAME slot, threading the
15163    // primitive's `&'static str` keyword; (d) the
15164    // `NamedFormNonSymbolName` variant fires for matched forms with a
15165    // non-symbol-or-string NAME slot, again threading the primitive's
15166    // keyword; (e) the projection's `Err` short-circuits at the first
15167    // failing matched form; (f) `expand_program` runs BEFORE the
15168    // keyword filter walks — a `(defmacro …)` emitting a named form
15169    // has its EXPANDED head matched by the constant-keyword filter,
15170    // not the macro-call head; (g) the from-source sibling agrees with
15171    // `read + from-forms` on the same source; (h) the constant-
15172    // classifier composition law binds the runtime-keyword cell
15173    // (`expand_and_collect_named_calls_to`) to the typed-decoded
15174    // classifier cell (`expand_and_collect_named_calls_to_any`) via a
15175    // constant-classifier decoder.
15176
15177    #[test]
15178    fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
15179    ) {
15180        // The constant-keyword named primitive's happy path: a runtime
15181        // `&'static str` keyword filters matched forms, and the per-
15182        // form projection receives `(name, spec_args)` for every
15183        // matched form in source order. Three matched forms
15184        // interleaved with a non-matcher pin the source-order yield,
15185        // the NAME-slot projection, AND the rejection of non-keyword
15186        // forms in ONE assertion.
15187        let src = r#"(defmonitor cpu :threshold 80)
15188                     (other-form 99)
15189                     (defmonitor mem :threshold 90 :unit "MiB")
15190                     (defmonitor disk :threshold 70)"#;
15191        let forms = read(src).unwrap();
15192        let mut e = Expander::new();
15193        let rows: Vec<(String, usize)> = e
15194            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
15195                Ok((name.to_string(), spec_args.len()))
15196            })
15197            .unwrap();
15198        assert_eq!(
15199            rows,
15200            vec![
15201                ("cpu".to_string(), 2),
15202                ("mem".to_string(), 4),
15203                ("disk".to_string(), 2),
15204            ],
15205        );
15206    }
15207
15208    #[test]
15209    fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
15210        // Soft-projection contract — every shape the named typed-
15211        // decoded primitive rejects (non-list, empty list, list whose
15212        // head is not a symbol, list whose head doesn't match the
15213        // constant keyword) skips the projection silently. The named
15214        // gate inside the wrapper projection runs ONLY for matched
15215        // forms; a non-matching form that reaches the projection
15216        // panics here.
15217        let src = r#":bare-keyword
15218                     "bare-string"
15219                     42
15220                     ()
15221                     (foo bar)
15222                     (defmonitor cpu :threshold 80)"#;
15223        let forms = read(src).unwrap();
15224        let mut e = Expander::new();
15225        let names: Vec<String> = e
15226            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15227                Ok(name.to_string())
15228            })
15229            .unwrap();
15230        assert_eq!(names, vec!["cpu".to_string()]);
15231    }
15232
15233    #[test]
15234    fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
15235        // The named-form gate's missing-NAME variant must thread the
15236        // primitive's `&'static str` keyword (NOT a hardcoded literal,
15237        // NOT the projection's own copy) — the `NamedFormMissingName
15238        // { keyword }` slot inside `split_name_slot` is bound at the
15239        // call site BY THE PRIMITIVE itself, via the constant-
15240        // classifier decoder threading `((), keyword)` into
15241        // `expand_and_collect_named_calls_to_any`.
15242        let src = r#"(defmonitor)"#;
15243        let forms = read(src).unwrap();
15244        let mut e = Expander::new();
15245        let err = e
15246            .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15247            .unwrap_err();
15248        assert!(
15249            matches!(
15250                err,
15251                crate::error::LispError::NamedFormMissingName {
15252                    keyword: "defmonitor"
15253                }
15254            ),
15255            "expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
15256        );
15257    }
15258
15259    #[test]
15260    fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
15261    ) {
15262        // The named-form gate's non-symbol-NAME variant must thread
15263        // the primitive's `&'static str` keyword AND the typed-shape
15264        // witness (`SexpShape::Int` for an integer NAME slot). The
15265        // shape projection through `split_name_slot`'s
15266        // `as_symbol_or_string()` gate fires on the first matched
15267        // form whose NAME slot is not a symbol or string.
15268        let src = r#"(defmonitor 42 :threshold 80)"#;
15269        let forms = read(src).unwrap();
15270        let mut e = Expander::new();
15271        let err = e
15272            .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15273            .unwrap_err();
15274        match err {
15275            crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
15276                assert_eq!(keyword, "defmonitor");
15277                assert_eq!(got, crate::error::SexpShape::Int);
15278            }
15279            other => panic!(
15280                "expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
15281            ),
15282        }
15283    }
15284
15285    #[test]
15286    fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
15287        // `Iterator::collect::<Result<Vec<R>, _>>()` short-circuits at
15288        // the first failing projection. The second matched form's
15289        // projection error MUST fire AND the third matched form's
15290        // projection MUST NOT run. Pin both halves with a counter +
15291        // a fail-on-call sentinel.
15292        let src = r#"(defmonitor cpu :threshold 80)
15293                     (defmonitor mem :threshold 90)
15294                     (defmonitor disk :threshold 70)"#;
15295        let forms = read(src).unwrap();
15296        let mut count: usize = 0;
15297        let mut e = Expander::new();
15298        let err = e
15299            .expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
15300                count += 1;
15301                if name == "mem" {
15302                    Err(crate::error::LispError::Compile {
15303                        form: "test".into(),
15304                        message: format!("rejecting at NAME={name}"),
15305                    })
15306                } else {
15307                    Ok(name.to_string())
15308                }
15309            })
15310            .unwrap_err();
15311        assert_eq!(count, 2, "third matched form must not be projected");
15312        match err {
15313            crate::error::LispError::Compile { message, .. } => {
15314                assert!(message.contains("NAME=mem"));
15315            }
15316            other => panic!("expected projection-driven Compile error; got: {other:?}"),
15317        }
15318    }
15319
15320    #[test]
15321    fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
15322        // Ordering contract — `expand_program` runs BEFORE the
15323        // constant-keyword named walk. A `(defmacro …)` form that
15324        // emits a matched named form must have its EXPANDED head
15325        // matched by the constant-keyword filter, not the macro-call
15326        // head. Pin the ordering with a macro `(emit-mon n thr)`
15327        // expanding to `(defmonitor ,n :threshold ,thr)` — the
15328        // constant-keyword filter matches the expanded `defmonitor`
15329        // head and the NAME slot projects the symbol-author value.
15330        let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15331                     (defmonitor cpu :threshold 80)
15332                     (emit-mon mem 90)
15333                     (other-form 99)
15334                     (emit-mon disk 70)"#;
15335        let forms = read(src).unwrap();
15336        let mut e = Expander::new();
15337        let names: Vec<String> = e
15338            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15339                Ok(name.to_string())
15340            })
15341            .unwrap();
15342        // `cpu` from the literal call, `mem` from the first macro
15343        // emit, `disk` from the second macro emit. The macro-emitted
15344        // forms are present BECAUSE `expand_program` ran first.
15345        assert_eq!(names, vec!["cpu", "mem", "disk"]);
15346    }
15347
15348    #[test]
15349    fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
15350        // From-source posture parity: feeding source through
15351        // `expand_source_and_collect_named_calls_to` is byte-identical
15352        // to feeding `read(src)?` through the from-forms sibling on a
15353        // fresh expander, because the from-source posture is
15354        // `read(src)? + from-forms` by construction. A regression that
15355        // drifts the from-source posture's pipeline from the from-
15356        // forms posture's pipeline would fail here.
15357        let src = r#"(defmonitor cpu :threshold 80)
15358                     (defmonitor mem :threshold 90)"#;
15359        let via_src: Vec<String> = Expander::new()
15360            .expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
15361                Ok(name.to_string())
15362            })
15363            .unwrap();
15364        let forms = read(src).unwrap();
15365        let via_forms: Vec<String> = Expander::new()
15366            .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15367                Ok(name.to_string())
15368            })
15369            .unwrap();
15370        assert_eq!(via_src, via_forms);
15371        assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15372    }
15373
15374    #[test]
15375    fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
15376    ) {
15377        // `?`-routing through `read` short-circuits BEFORE the
15378        // constant-keyword filter, the named gate, or the projection
15379        // runs. Unbalanced paren — reader error fires; the projection
15380        // panic-payload MUST NOT execute, proving the read step gates
15381        // the entire pipeline.
15382        let mut e = Expander::new();
15383        let err = e
15384            .expand_source_and_collect_named_calls_to::<(), _>(
15385                "(defmonitor cpu :threshold 80",
15386                "defmonitor",
15387                |_name, _args| panic!("projection must not run when reader fails"),
15388            )
15389            .unwrap_err();
15390        // The named gate's variants MUST NOT fire — the reader error
15391        // is structurally distinct.
15392        assert!(
15393            !matches!(
15394                err,
15395                crate::error::LispError::NamedFormMissingName { .. }
15396                    | crate::error::LispError::NamedFormNonSymbolName { .. }
15397            ),
15398            "expected reader error, not named-gate variant; got: {err:?}",
15399        );
15400    }
15401
15402    #[test]
15403    fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
15404    ) {
15405        // Composition-identity test pinning the runtime-keyword named
15406        // cell (`expand_and_collect_named_calls_to`) to the typed-
15407        // decoded named-classifier cell
15408        // (`expand_and_collect_named_calls_to_any`) via the constant-
15409        // classifier decoder shape. Post-lift the identity:
15410        //
15411        //   expand_and_collect_named_calls_to(forms, kw, project) ==
15412        //       expand_and_collect_named_calls_to_any(forms,
15413        //           |h| (h == kw).then_some(((), kw)),
15414        //           |(), name, args| project(name, args))
15415        //
15416        // Pinning the identity here makes the typed-decoded named-
15417        // classifier primitive the CANONICAL composition point the
15418        // runtime-keyword sibling routes through — parallel to how
15419        // `expand_and_collect_calls_to` routes through
15420        // `expand_and_collect_calls_to_any` via a `|h| (h == k).
15421        // then_some(())` decoder on the bare-kwargs axis. A future
15422        // regression that drifts ONE cell's NAME-slot rejection chain
15423        // from the other becomes loudly visible at this assertion.
15424        let src = r#"(defmonitor cpu :threshold 80)
15425                     (other-form 99)
15426                     (defmonitor mem :threshold 90)"#;
15427        let forms = read(src).unwrap();
15428        let via_constant_keyword: Vec<(String, usize)> = Expander::new()
15429            .expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
15430                Ok((name.to_string(), args.len()))
15431            })
15432            .unwrap();
15433        let via_classifier: Vec<(String, usize)> = Expander::new()
15434            .expand_and_collect_named_calls_to_any(
15435                forms,
15436                |h| (h == "defmonitor").then_some(((), "defmonitor")),
15437                |(), name, args| Ok((name.to_string(), args.len())),
15438            )
15439            .unwrap();
15440        assert_eq!(via_constant_keyword, via_classifier);
15441        assert_eq!(
15442            via_constant_keyword,
15443            vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
15444        );
15445    }
15446
15447    #[test]
15448    fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
15449    ) {
15450        // End-to-end path-uniformity: `Expander::expand_to_named<T>`
15451        // (the typed constant-keyword named cell) must yield the same
15452        // payload as `expand_and_collect_named_calls_to(forms,
15453        // T::KEYWORD, |name, args| { T::compile_from_args(args)? +
15454        // NamedDefinition })` — the lift's structural identity.
15455        //
15456        // Pre-lift `expand_to_named<T>` routed through
15457        // `expand_and_collect_calls_to(forms, T::KEYWORD,
15458        // named_form_projection::<T>)` (the bare-kwargs constant
15459        // primitive with `named_form_projection<T>` doing the NAME
15460        // extraction internally). Post-lift `expand_to_named<T>`
15461        // routes through the named constant-keyword primitive (which
15462        // itself routes through the classifier primitive via a
15463        // constant-classifier decoder) — so the `split_name_slot`
15464        // composition lives at ONE site (the `_any` primitive body)
15465        // post-lift rather than at TWO sites.
15466        use crate::compile::NamedDefinition;
15467        use crate::compiler_spec::CompilerSpec;
15468        let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
15469                     (defcompiler beta-compiler  :name "y" :dialect "standard")"#;
15470        let forms = read(src).unwrap();
15471        let via_expand_to_named = Expander::new()
15472            .expand_to_named::<CompilerSpec>(forms.clone())
15473            .unwrap();
15474        let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
15475            .expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
15476                let spec =
15477                    <CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
15478                Ok(NamedDefinition {
15479                    name: name.to_string(),
15480                    spec,
15481                })
15482            })
15483            .unwrap();
15484        assert_eq!(via_expand_to_named.len(), 2);
15485        assert_eq!(via_expand_to_named.len(), via_named_constant.len());
15486        for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
15487            assert_eq!(a.name, b.name, "NAME slot must agree across cells");
15488            assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
15489        }
15490        assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
15491        assert_eq!(via_expand_to_named[0].spec.name, "x");
15492        assert_eq!(via_expand_to_named[1].name, "beta-compiler");
15493        assert_eq!(via_expand_to_named[1].spec.name, "y");
15494    }
15495
15496    // ── ExpansionDepthExceeded: runaway macro rejected instead of aborting ──
15497
15498    #[test]
15499    fn expander_default_max_expansion_depth_is_the_module_constant() {
15500        // Both constructors seed `max_expansion_depth` from the module
15501        // constant — a regression that drifts one constructor's default
15502        // from the constant (e.g. a hardcoded `256` at ONE site with the
15503        // other left uninitialized after a `Default` derive addition)
15504        // fails this pin.
15505        assert_eq!(
15506            Expander::new().max_expansion_depth(),
15507            DEFAULT_MAX_EXPANSION_DEPTH
15508        );
15509        assert_eq!(
15510            Expander::new_substitute_only().max_expansion_depth(),
15511            DEFAULT_MAX_EXPANSION_DEPTH
15512        );
15513        assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
15514    }
15515
15516    #[test]
15517    fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
15518        let mut e = Expander::new();
15519        e.set_max_expansion_depth(7);
15520        assert_eq!(e.max_expansion_depth(), 7);
15521    }
15522
15523    #[test]
15524    fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
15525        // `(defmacro loop (x) `(loop ,x))` applied to `(loop hello)`
15526        // pre-lift stack-overflowed the process — a below-floor abort.
15527        // Post-lift the expander returns a typed
15528        // `LispError::ExpansionDepthExceeded` at the ceiling with
15529        // `macro_name` populated from the offending call's head.
15530        let mut e = Expander::new();
15531        e.set_max_expansion_depth(4);
15532        let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15533        let err = e.expand_program(forms).unwrap_err();
15534        match err {
15535            LispError::ExpansionDepthExceeded { macro_name, limit } => {
15536                assert_eq!(macro_name, "loop");
15537                assert_eq!(limit, 4);
15538            }
15539            other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15540        }
15541    }
15542
15543    #[test]
15544    fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
15545        // The substitute strategy shares the same `expand` outer walker
15546        // with the bytecode strategy, so the ceiling fires at the SAME
15547        // gate regardless of which apply-layer is active. The pin makes
15548        // that shared-outer-walker property structural: a regression
15549        // that ever branches the depth guard on strategy (e.g. moves it
15550        // into `apply_compiled` alone) fails this test on the
15551        // substitute expander.
15552        let mut e = Expander::new_substitute_only();
15553        e.set_max_expansion_depth(4);
15554        let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15555        let err = e.expand_program(forms).unwrap_err();
15556        match err {
15557            LispError::ExpansionDepthExceeded { macro_name, limit } => {
15558                assert_eq!(macro_name, "loop");
15559                assert_eq!(limit, 4);
15560            }
15561            other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15562        }
15563    }
15564
15565    #[test]
15566    fn expand_lawful_nested_macros_within_ceiling_succeed() {
15567        // Lawful nested macros (`(twice (twice hey))`) accrete depth in
15568        // the low single digits because the tree-child arm doesn't
15569        // count — only the post-apply re-expansion path bumps the
15570        // counter. A ceiling of 3 is enough for a two-deep nested
15571        // expansion. This pin is the fail-before/pass-after negative
15572        // control for the recursive-macro rejection tests above: the
15573        // depth counter must NOT reject lawful macro-nesting within
15574        // the ceiling, and only reject the runaway shape.
15575        let mut e = Expander::new();
15576        e.set_max_expansion_depth(3);
15577        let forms = read(
15578            "(defmacro twice (x) `(list ,x ,x))
15579             (twice (twice hey))",
15580        )
15581        .unwrap();
15582        let out = e.expand_program(forms).unwrap();
15583        assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
15584    }
15585
15586    #[test]
15587    fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
15588        // A macro-free lawful deep tree — five levels of plain nesting —
15589        // is expanded at a ceiling well below the tree's height. The
15590        // tree-child arm does not accrete depth, so tree height is
15591        // orthogonal to the ceiling; only macro re-expansion counts.
15592        // A regression that ever bumps depth on tree-child descent
15593        // fails this pin.
15594        let mut e = Expander::new();
15595        e.set_max_expansion_depth(2);
15596        let forms = read("(a (b (c (d (e f)))))").unwrap();
15597        let out = e.expand_program(forms).unwrap();
15598        assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
15599    }
15600
15601    #[test]
15602    fn expansion_depth_exceeded_position_is_none() {
15603        // The variant joins the non-positional cohort — the offending
15604        // macro's byte offset is not what a runaway diagnostic anchors
15605        // to; the OFFENDING MACRO NAME (`macro_name`) is. This pin
15606        // keeps the variant in the `position() -> None` cohort so a
15607        // future span-carrying edit lands the field in ONE place
15608        // across every non-positional variant simultaneously.
15609        let err = LispError::ExpansionDepthExceeded {
15610            macro_name: "loop".to_string(),
15611            limit: 256,
15612        };
15613        assert_eq!(err.position(), None);
15614    }
15615
15616    #[test]
15617    fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
15618        // Display projection carries both `macro_name` and `limit`, so
15619        // authoring surfaces that substring-grep the rendered
15620        // diagnostic (`tatara-check`, REPL, LSP) see BOTH halves at
15621        // the variant boundary and never need to substring-parse
15622        // free-form text to recover the identity of the offending
15623        // macro or the ceiling it breached.
15624        let err = LispError::ExpansionDepthExceeded {
15625            macro_name: "loop".to_string(),
15626            limit: 256,
15627        };
15628        let rendered = err.to_string();
15629        assert!(rendered.contains("loop"), "rendered: {rendered}");
15630        assert!(rendered.contains("256"), "rendered: {rendered}");
15631        assert!(
15632            rendered.contains("macro expansion depth exceeded"),
15633            "rendered: {rendered}"
15634        );
15635    }
15636
15637    // ── max_cache_entries: bounded-memoization ceiling ──────────────────
15638
15639    #[test]
15640    fn expander_default_max_cache_entries_is_the_module_constant() {
15641        // Both cache-enabling constructors seed `max_cache_entries` from
15642        // the module constant — a regression that drifts one
15643        // constructor's default from the constant (e.g. a hardcoded
15644        // `8192` at ONE site with the other left uninitialized after a
15645        // `Default` derive addition) fails this pin. The peer
15646        // constructor `new_substitute_only` seeds the same ceiling so
15647        // the field stays initialized under `#[derive(Clone)]` (a
15648        // zero-initialized clone would be a silent bifurcation of the
15649        // cache-ceiling surface).
15650        assert_eq!(
15651            Expander::new().max_cache_entries(),
15652            DEFAULT_MAX_CACHE_ENTRIES
15653        );
15654        assert_eq!(
15655            Expander::new_substitute_only().max_cache_entries(),
15656            DEFAULT_MAX_CACHE_ENTRIES
15657        );
15658        assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
15659    }
15660
15661    #[test]
15662    fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
15663        // Setter mirrors accessor — a regression that ever forgets to
15664        // write through to the field, or writes to a shadow field the
15665        // accessor does not read, fails this pin.
15666        let mut e = Expander::new();
15667        e.set_max_cache_entries(7);
15668        assert_eq!(e.max_cache_entries(), 7);
15669    }
15670
15671    #[test]
15672    fn expand_cache_size_is_bounded_by_max_cache_entries() {
15673        // Five distinct `(name, args)` pairs expanded against a cache
15674        // ceiling of `2` populate the cache with EXACTLY the first two
15675        // insertions and skip the remaining three. Every expansion
15676        // still returns the correct result — the cache is a pure
15677        // PERFORMANCE optimization; the ceiling gates memoization,
15678        // never correctness. LOAD-BEARING true-arm catch on the
15679        // bounded-cache contract: a regression that ignored the
15680        // ceiling (unbounded insert) would land `cache_size() == 5`
15681        // here; a regression that skipped insertion entirely (broken
15682        // cache) would still see correct expansion output but would
15683        // fail the interior `cache_size() == 2` pin.
15684        let mut e = Expander::new();
15685        e.set_max_cache_entries(2);
15686        let src = "
15687            (defmacro id (x) `,x)
15688            (id one)
15689            (id two)
15690            (id three)
15691            (id four)
15692            (id five)
15693        ";
15694        let out = e.expand_program(read(src).unwrap()).unwrap();
15695        assert_eq!(out.len(), 5);
15696        assert_eq!(out[0], parse("one"));
15697        assert_eq!(out[1], parse("two"));
15698        assert_eq!(out[2], parse("three"));
15699        assert_eq!(out[3], parse("four"));
15700        assert_eq!(out[4], parse("five"));
15701        assert_eq!(
15702            e.cache_size(),
15703            2,
15704            "cache grew past the max_cache_entries ceiling",
15705        );
15706    }
15707
15708    #[test]
15709    fn expand_zero_cache_ceiling_disables_caching_effectively() {
15710        // Zero ceiling admits every fresh `(name, args)` pair through
15711        // the compute path and never grows the cache — the same
15712        // observable behavior as `set_cache_enabled(false)` but
15713        // reached through the ceiling knob. Correctness is preserved:
15714        // the miss path always recomputes the same value the cache
15715        // would have returned, so an operator that dials the ceiling
15716        // to `0` (e.g. to hunt a cache-related regression) never
15717        // bifurcates the output surface. Peer to
15718        // `set_max_expansion_depth(0)` one RESOURCE axis over — both
15719        // ceilings admit the zero endpoint as a well-defined
15720        // extremum.
15721        let mut e = Expander::new();
15722        e.set_max_cache_entries(0);
15723        let src = "
15724            (defmacro id (x) `,x)
15725            (id one)
15726            (id two)
15727        ";
15728        let out = e.expand_program(read(src).unwrap()).unwrap();
15729        assert_eq!(out.len(), 2);
15730        assert_eq!(out[0], parse("one"));
15731        assert_eq!(out[1], parse("two"));
15732        assert_eq!(
15733            e.cache_size(),
15734            0,
15735            "zero cache ceiling grew the cache — the ceiling must be respected on every insert",
15736        );
15737    }
15738
15739    #[test]
15740    fn expand_cached_hits_survive_past_the_ceiling() {
15741        // Once a `(name, args)` pair sits IN the cache under the
15742        // ceiling, subsequent calls hit it — the ceiling only gates
15743        // NEW-KEY inserts, never lookups. This test seeds the cache
15744        // with two entries under a ceiling of `2`, then re-issues the
15745        // SAME calls to prove the cached hits still fire (cache_size
15746        // stays at `2` because no new keys are inserted, and the
15747        // expansion output is stable). LOAD-BEARING true-arm catch
15748        // that the ceiling gates the INSERT path alone — a regression
15749        // that ever refused cache LOOKUPS at the ceiling would
15750        // silently bifurcate correctness on the second call.
15751        let mut e = Expander::new();
15752        e.set_max_cache_entries(2);
15753        let src = "
15754            (defmacro id (x) `,x)
15755            (id one)
15756            (id two)
15757            (id one)
15758            (id two)
15759        ";
15760        let out = e.expand_program(read(src).unwrap()).unwrap();
15761        assert_eq!(out.len(), 4);
15762        assert_eq!(out[0], parse("one"));
15763        assert_eq!(out[1], parse("two"));
15764        assert_eq!(out[2], parse("one"));
15765        assert_eq!(out[3], parse("two"));
15766        assert_eq!(
15767            e.cache_size(),
15768            2,
15769            "cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
15770        );
15771    }
15772
15773    #[test]
15774    fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
15775        // After the cache reaches its ceiling and subsequent inserts
15776        // are skipped, `clear_cache` empties the memo and RE-OPENS the
15777        // insert path — the next expansion populates the freshly-
15778        // empty cache without operator intervention on the ceiling
15779        // itself. The pin binds the operator-facing recovery path
15780        // documented on [`DEFAULT_MAX_CACHE_ENTRIES`]: "skip cache
15781        // insertion until the operator clears the cache via
15782        // [`Expander::clear_cache`]".
15783        let mut e = Expander::new();
15784        e.set_max_cache_entries(1);
15785        let src_fill = "
15786            (defmacro id (x) `,x)
15787            (id one)
15788            (id two)
15789        ";
15790        let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
15791        assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
15792        e.clear_cache();
15793        assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
15794        let out = e
15795            .expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
15796            .unwrap();
15797        assert_eq!(out.len(), 1);
15798        assert_eq!(out[0], parse("three"));
15799        assert_eq!(
15800            e.cache_size(),
15801            1,
15802            "clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
15803        );
15804    }
15805
15806    // ── max_expansion_size: bounded-output ceiling ─────────────────────
15807
15808    #[test]
15809    fn expander_default_max_expansion_size_is_the_module_constant() {
15810        // Both constructors seed `max_expansion_size` from the module
15811        // constant — a regression that drifts one constructor's
15812        // default from the constant (e.g. a hardcoded `65_536` at
15813        // ONE site with the other left uninitialized after a
15814        // `Default` derive addition) fails this pin. Peer to the
15815        // `max_expansion_depth` + `max_cache_entries` default-
15816        // coherence pins one RESOURCE-DIMENSION axis over.
15817        assert_eq!(
15818            Expander::new().max_expansion_size(),
15819            DEFAULT_MAX_EXPANSION_SIZE
15820        );
15821        assert_eq!(
15822            Expander::new_substitute_only().max_expansion_size(),
15823            DEFAULT_MAX_EXPANSION_SIZE
15824        );
15825        assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
15826    }
15827
15828    #[test]
15829    fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
15830        // Setter mirrors accessor — a regression that ever forgets to
15831        // write through to the field, or writes to a shadow field the
15832        // accessor does not read, fails this pin. Peer to the
15833        // setter/accessor coherence pins on the depth + cache axes.
15834        let mut e = Expander::new();
15835        e.set_max_expansion_size(64);
15836        assert_eq!(e.max_expansion_size(), 64);
15837    }
15838
15839    #[test]
15840    fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
15841        // `(defmacro bomb (x) `(list ,x ,x ,x ,x))` applied to
15842        // `(bomb hey)` produces `(list hey hey hey hey)` — 6 nodes
15843        // (outer List + 5 children). With a ceiling of `4` this
15844        // crosses the OUTPUT-SIZE threshold at the first apply and
15845        // the expander returns a typed
15846        // `LispError::ExpansionSizeExceeded` with `macro_name`,
15847        // `size`, and `limit` all populated. Pre-lift the runaway
15848        // just accumulated in a growing tree until the process
15849        // heap ran out; post-lift the rejection sits at the same
15850        // call boundary as every other macro-expansion failure.
15851        let mut e = Expander::new();
15852        e.set_max_expansion_size(4);
15853        let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15854        let err = e.expand_program(forms).unwrap_err();
15855        match err {
15856            LispError::ExpansionSizeExceeded {
15857                macro_name,
15858                size,
15859                limit,
15860            } => {
15861                assert_eq!(macro_name, "bomb");
15862                assert_eq!(size, 6);
15863                assert_eq!(limit, 4);
15864            }
15865            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15866        }
15867    }
15868
15869    #[test]
15870    fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
15871        // The substitute strategy shares the same `expand_with_depth`
15872        // outer walker with the bytecode strategy, so the ceiling
15873        // fires at the SAME gate regardless of which apply-layer is
15874        // active. The pin makes that shared-outer-walker property
15875        // structural: a regression that ever branches the size guard
15876        // on strategy (e.g. moves it into `apply_compiled` alone)
15877        // fails this test on the substitute expander. Peer to
15878        // `expand_recursive_macro_rejects_at_depth_limit_substitute_path`
15879        // one RESOURCE axis over.
15880        let mut e = Expander::new_substitute_only();
15881        e.set_max_expansion_size(4);
15882        let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15883        let err = e.expand_program(forms).unwrap_err();
15884        match err {
15885            LispError::ExpansionSizeExceeded {
15886                macro_name,
15887                size,
15888                limit,
15889            } => {
15890                assert_eq!(macro_name, "bomb");
15891                assert_eq!(size, 6);
15892                assert_eq!(limit, 4);
15893            }
15894            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15895        }
15896    }
15897
15898    #[test]
15899    fn expand_lawful_output_within_size_ceiling_succeeds() {
15900        // `(defmacro twice (x) `(list ,x ,x))` applied to `(twice hey)`
15901        // produces `(list hey hey)` — 4 nodes. With a ceiling of `4`
15902        // the output sits AT the ceiling (`<=` admits equality; only
15903        // `>` rejects), so the expansion succeeds. Fail-before/pass-
15904        // after negative control for the bomb rejection tests: a
15905        // regression that ever rejected at-ceiling output would fail
15906        // this pin. Peer to `expand_lawful_nested_macros_within_ceiling_succeed`
15907        // one RESOURCE axis over.
15908        let mut e = Expander::new();
15909        e.set_max_expansion_size(4);
15910        let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
15911        let out = e.expand_program(forms).unwrap();
15912        assert_eq!(out[0], parse("(list hey hey)"));
15913    }
15914
15915    #[test]
15916    fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
15917        // A macro-free lawful large tree is NOT gated by the size
15918        // ceiling — the ceiling gates macro-`apply` outputs alone,
15919        // not tree-child descents through non-macro forms. A `usize::MAX`
15920        // ceiling would trivially admit anything; this pin uses a
15921        // deliberately small ceiling of `4` on a tree with 6 nodes
15922        // to catch a regression that ever bumps the ceiling on tree
15923        // descent. A tree-only walk should NEVER fire the ceiling.
15924        // Peer to `expand_depth_ceiling_ignores_lawful_tree_nesting_depth`
15925        // one RESOURCE axis over.
15926        let mut e = Expander::new();
15927        e.set_max_expansion_size(4);
15928        // `(a (b (c d)))` — 6 nodes, no macros. Passes cleanly.
15929        let forms = read("(a (b (c d)))").unwrap();
15930        let out = e.expand_program(forms).unwrap();
15931        assert_eq!(out[0], parse("(a (b (c d)))"));
15932    }
15933
15934    #[test]
15935    fn expansion_size_exceeded_position_is_none() {
15936        // The variant joins the non-positional cohort — the offending
15937        // macro's byte offset is not what an expansion-bomb diagnostic
15938        // anchors to; the (offending macro name, observed size,
15939        // configured ceiling) triple is. This pin keeps the variant
15940        // in the `position() -> None` cohort so a future span-carrying
15941        // edit lands the field in ONE place across every non-positional
15942        // variant simultaneously. Peer to
15943        // `expansion_depth_exceeded_position_is_none` one RESOURCE
15944        // axis over.
15945        let err = LispError::ExpansionSizeExceeded {
15946            macro_name: "bomb".to_string(),
15947            size: 512,
15948            limit: 256,
15949        };
15950        assert_eq!(err.position(), None);
15951    }
15952
15953    #[test]
15954    fn expansion_size_exceeded_display_matches_typed_variant_shape() {
15955        // Display projection carries `macro_name`, `size`, AND
15956        // `limit`, so authoring surfaces that substring-grep the
15957        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
15958        // THREE halves at the variant boundary and never need to
15959        // substring-parse free-form text to recover the identity of
15960        // the offending macro, the observed size, or the ceiling it
15961        // breached. Peer to `expansion_depth_exceeded_display_matches_typed_variant_shape`
15962        // one RESOURCE axis over.
15963        let err = LispError::ExpansionSizeExceeded {
15964            macro_name: "bomb".to_string(),
15965            size: 512,
15966            limit: 256,
15967        };
15968        let rendered = err.to_string();
15969        assert!(rendered.contains("bomb"), "rendered: {rendered}");
15970        assert!(rendered.contains("512"), "rendered: {rendered}");
15971        assert!(rendered.contains("256"), "rendered: {rendered}");
15972        assert!(
15973            rendered.contains("macro expansion output size exceeded"),
15974            "rendered: {rendered}"
15975        );
15976    }
15977
15978    #[test]
15979    fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
15980        // A lawful outer macro that expands to a call of a bomb inner
15981        // macro rejects at the INNER call — `macro_name` at the
15982        // variant boundary is the name of the macro whose `apply`
15983        // output crossed the ceiling, not the outermost caller.
15984        // Load-bearing name-provenance pin: an operator whose bomb
15985        // sits N levels deep in a compose chain needs to see the
15986        // INNER macro's name in the diagnostic, not the wrapping
15987        // shape. A regression that ever populates `macro_name` from
15988        // the outermost caller's head fails this pin.
15989        let mut e = Expander::new();
15990        e.set_max_expansion_size(4);
15991        // `wrapper` re-expands into `bomb`; `bomb` produces a size-6
15992        // output that crosses the ceiling. Expected: the diagnostic
15993        // names `bomb`, not `wrapper`.
15994        let src = "
15995            (defmacro bomb (x) `(list ,x ,x ,x ,x))
15996            (defmacro wrapper (x) `(bomb ,x))
15997            (wrapper hey)
15998        ";
15999        let err = e.expand_program(read(src).unwrap()).unwrap_err();
16000        match err {
16001            LispError::ExpansionSizeExceeded {
16002                macro_name,
16003                size,
16004                limit,
16005            } => {
16006                assert_eq!(macro_name, "bomb");
16007                assert_eq!(size, 6);
16008                assert_eq!(limit, 4);
16009            }
16010            other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16011        }
16012    }
16013
16014    // ── max_macro_body_size: REGISTRATION-time body-size ceiling ──────
16015    // Peer to depth (recursion length) + cache (memoization width) +
16016    // expansion size (single-`apply` OUTPUT size) one PIPELINE-STAGE
16017    // axis over — the three prior guards fire at EXPAND time; this
16018    // one fires at REGISTER time. Closes the expander's RESOURCE
16019    // surface at a fourth typed dimension across two pipeline stages.
16020
16021    #[test]
16022    fn expander_default_max_macro_body_size_is_the_module_constant() {
16023        // Both constructors seed `max_macro_body_size` from the
16024        // module constant — a regression that drifts one
16025        // constructor's default from the constant (e.g. a hardcoded
16026        // `16_384` at ONE site with the other left uninitialized
16027        // after a `Default` derive addition) fails this pin. Peer
16028        // to the `expander_default_max_expansion_{depth,size}_is_the_module_constant`
16029        // pins one PIPELINE-STAGE axis over.
16030        assert_eq!(
16031            Expander::new().max_macro_body_size(),
16032            DEFAULT_MAX_MACRO_BODY_SIZE
16033        );
16034        assert_eq!(
16035            Expander::new_substitute_only().max_macro_body_size(),
16036            DEFAULT_MAX_MACRO_BODY_SIZE
16037        );
16038        assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
16039    }
16040
16041    #[test]
16042    fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
16043        // Setter mirrors accessor — a regression that ever forgets to
16044        // write through to the field, or writes to a shadow field the
16045        // accessor does not read, fails this pin. Peer to the
16046        // setter/accessor coherence pins on the depth + cache +
16047        // expansion-size axes.
16048        let mut e = Expander::new();
16049        e.set_max_macro_body_size(64);
16050        assert_eq!(e.max_macro_body_size(), 64);
16051    }
16052
16053    #[test]
16054    fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
16055        // `(defmacro huge (x) `(list a b c d e))` has body
16056        // `` `(list a b c d e) `` — a `Sexp::Quasiquote` wrapping a
16057        // list of 6 nodes (outer List + 5 children); the outer
16058        // quasi-quote wrapper adds one → 8 nodes total for
16059        // `def.body.node_count()`. With a ceiling of `4` this
16060        // crosses the REGISTRATION-time BODY-SIZE threshold and the
16061        // expander returns a typed `LispError::MacroBodySizeExceeded`
16062        // with `macro_name`, `size`, and `limit` all populated. Peer
16063        // to `expand_expansion_bomb_rejects_at_size_limit_bytecode_path`
16064        // one PIPELINE-STAGE axis over.
16065        let mut e = Expander::new();
16066        e.set_max_macro_body_size(4);
16067        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16068        let err = e.expand_program(forms).unwrap_err();
16069        match err {
16070            LispError::MacroBodySizeExceeded {
16071                macro_name,
16072                size,
16073                limit,
16074            } => {
16075                assert_eq!(macro_name, "huge");
16076                assert_eq!(size, 8);
16077                assert_eq!(limit, 4);
16078            }
16079            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16080        }
16081    }
16082
16083    #[test]
16084    fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
16085        // The substitute strategy shares the same
16086        // `register_macro_def` outer registration entry with the
16087        // bytecode strategy, so the REGISTRATION-time BODY-SIZE
16088        // ceiling fires at the SAME gate regardless of which
16089        // apply-layer is active. The pin makes that shared-registration
16090        // property structural: a regression that ever branches the
16091        // body-size guard on strategy (e.g. moves it into the
16092        // `compile_templates` arm alone) fails this test on the
16093        // substitute expander. Peer to
16094        // `expand_expansion_bomb_rejects_at_size_limit_substitute_path`
16095        // one PIPELINE-STAGE axis over.
16096        let mut e = Expander::new_substitute_only();
16097        e.set_max_macro_body_size(4);
16098        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16099        let err = e.expand_program(forms).unwrap_err();
16100        match err {
16101            LispError::MacroBodySizeExceeded {
16102                macro_name,
16103                size,
16104                limit,
16105            } => {
16106                assert_eq!(macro_name, "huge");
16107                assert_eq!(size, 8);
16108                assert_eq!(limit, 4);
16109            }
16110            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16111        }
16112    }
16113
16114    #[test]
16115    fn register_macro_body_at_ceiling_admits() {
16116        // `(defmacro twice (x) `(list ,x))` has body
16117        // `` `(list ,x) `` — `Sexp::Quasiquote` wrapping a 3-node
16118        // list (List + `list` atom + `,x` unquote which is
16119        // 1 + inner atom = 2 nodes) = 1 + (1 + 1 + 2) = 5 nodes.
16120        // With a ceiling of `5` the body sits AT the ceiling
16121        // (`<=` admits equality; only `>` rejects), so the
16122        // registration succeeds and the subsequent expansion
16123        // proceeds normally. Fail-before/pass-after negative
16124        // control for the body-bomb rejection tests: a regression
16125        // that ever rejected at-ceiling bodies would fail this pin.
16126        let mut e = Expander::new();
16127        e.set_max_macro_body_size(5);
16128        let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
16129        let out = e.expand_program(forms).unwrap();
16130        assert_eq!(out[0], parse("(list hey)"));
16131    }
16132
16133    #[test]
16134    fn register_failure_leaves_both_tables_pristine() {
16135        // A rejected registration MUST leave BOTH `self.macros`
16136        // AND `self.templates` exactly as they were — no
16137        // partial-write window in which one table carries the
16138        // over-ceiling entry while the other does not, or in
16139        // which the template pre-compile already ran. This pin
16140        // catches a regression that ever moves the body-size
16141        // gate below `self.templates.insert` or below
16142        // `self.macros.insert`.
16143        let mut e = Expander::new();
16144        e.set_max_macro_body_size(4);
16145        let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16146        assert!(e.expand_program(forms).is_err());
16147        assert!(
16148            !e.has("huge"),
16149            "macros table must stay pristine after rejection"
16150        );
16151        assert_eq!(e.len(), 0);
16152    }
16153
16154    #[test]
16155    fn macro_body_size_exceeded_position_is_none() {
16156        // The variant joins the non-positional cohort — the
16157        // offending macro's byte offset is not what a body-bomb
16158        // diagnostic anchors to; the (offending macro name,
16159        // observed body size, configured ceiling) triple is.
16160        // This pin keeps the variant in the `position() -> None`
16161        // cohort so a future span-carrying edit lands the field
16162        // in ONE place across every non-positional variant
16163        // simultaneously. Peer to
16164        // `expansion_{depth,size}_exceeded_position_is_none` one
16165        // PIPELINE-STAGE axis over.
16166        let err = LispError::MacroBodySizeExceeded {
16167            macro_name: "huge".to_string(),
16168            size: 512,
16169            limit: 256,
16170        };
16171        assert_eq!(err.position(), None);
16172    }
16173
16174    #[test]
16175    fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
16176        // Display projection carries `macro_name`, `size`, AND
16177        // `limit`, so authoring surfaces that substring-grep the
16178        // rendered diagnostic (`tatara-check`, REPL, LSP) see
16179        // ALL THREE halves at the variant boundary and never need
16180        // to substring-parse free-form text to recover the
16181        // identity of the offending macro, the observed body
16182        // size, or the ceiling it breached. Peer to
16183        // `expansion_size_exceeded_display_matches_typed_variant_shape`
16184        // one PIPELINE-STAGE axis over.
16185        let err = LispError::MacroBodySizeExceeded {
16186            macro_name: "huge".to_string(),
16187            size: 512,
16188            limit: 256,
16189        };
16190        let rendered = err.to_string();
16191        assert!(rendered.contains("huge"), "rendered: {rendered}");
16192        assert!(rendered.contains("512"), "rendered: {rendered}");
16193        assert!(rendered.contains("256"), "rendered: {rendered}");
16194        assert!(
16195            rendered.contains("macro body size exceeded"),
16196            "rendered: {rendered}"
16197        );
16198    }
16199
16200    #[test]
16201    fn lawful_macro_body_within_ceiling_registers_and_expands() {
16202        // A lawful hand-authored macro body sits well below any
16203        // realistic ceiling — the default `DEFAULT_MAX_MACRO_BODY_SIZE`
16204        // is deliberately generous so operators never brush it on
16205        // real authoring workloads. This pin locks the default
16206        // ceiling's non-interference posture: a regression that
16207        // ever lowered the default enough to reject the reference
16208        // `(defmacro when (cond x) `(if ,cond ,x))` shape would
16209        // fail here.
16210        let mut e = Expander::new();
16211        let forms = read(
16212            "(defmacro when (cond x) `(if ,cond ,x))
16213             (when #t hey)",
16214        )
16215        .unwrap();
16216        let out = e.expand_program(forms).unwrap();
16217        assert_eq!(out[0], parse("(if #t hey)"));
16218    }
16219
16220    #[test]
16221    fn register_macro_def_direct_call_respects_body_size_ceiling() {
16222        // `register_macro_def` is the substrate primitive both
16223        // `expand_program`'s `defmacro` recognition AND
16224        // `with_macros`'s bulk load route through. The body-size
16225        // ceiling fires on the primitive itself, not on any one
16226        // consumer — a regression that ever moved the gate into
16227        // `expand_program`'s arm alone would fail this pin.
16228        let mut e = Expander::new();
16229        e.set_max_macro_body_size(4);
16230        let def = MacroDef {
16231            name: "huge".to_string(),
16232            params: MacroParams {
16233                required: vec!["x".to_string()],
16234                optional: Vec::new(),
16235                rest: None,
16236            },
16237            body: Sexp::List(vec![
16238                Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
16239                Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
16240                Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
16241                Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
16242                Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
16243            ]),
16244        };
16245        // Body node_count: List(5 atoms) = 1 + 5 = 6 nodes.
16246        let err = e.register_macro_def(def).unwrap_err();
16247        match err {
16248            LispError::MacroBodySizeExceeded {
16249                macro_name,
16250                size,
16251                limit,
16252            } => {
16253                assert_eq!(macro_name, "huge");
16254                assert_eq!(size, 6);
16255                assert_eq!(limit, 4);
16256            }
16257            other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16258        }
16259        assert!(!e.has("huge"));
16260    }
16261
16262    // ── max_registered_macros: REGISTRATION-time table-count ceiling ──
16263    // Peer to body-size (REGISTER-time SIZE) one RESOURCE-DIMENSION
16264    // axis over — where body-size bounds a per-registration SIZE,
16265    // this ceiling bounds cumulative registration COUNT. Peer to
16266    // cache-entries (EXPAND-time COUNT) one PIPELINE-STAGE axis over
16267    // — the two entry-count guards close the two pipeline stages
16268    // symmetrically. Closes the expander's RESOURCE surface at a
16269    // FIFTH typed dimension across two pipeline stages: the
16270    // REGISTER-time face is now a two-dimensional (size × count)
16271    // closure symmetric with the EXPAND-time (depth + count + size)
16272    // triple.
16273
16274    #[test]
16275    fn expander_default_max_registered_macros_is_the_module_constant() {
16276        // Both constructors seed `max_registered_macros` from the
16277        // module constant — a regression that drifts one
16278        // constructor's default from the constant (e.g. a hardcoded
16279        // `4096` at ONE site with the other left uninitialized
16280        // after a `Default` derive addition) fails this pin. Peer
16281        // to the four prior default-coherence pins on depth,
16282        // cache-entries, expansion-size, and body-size.
16283        assert_eq!(
16284            Expander::new().max_registered_macros(),
16285            DEFAULT_MAX_REGISTERED_MACROS
16286        );
16287        assert_eq!(
16288            Expander::new_substitute_only().max_registered_macros(),
16289            DEFAULT_MAX_REGISTERED_MACROS
16290        );
16291        assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
16292    }
16293
16294    #[test]
16295    fn set_max_registered_macros_takes_effect_on_subsequent_register() {
16296        // Setter mirrors accessor — a regression that ever forgets
16297        // to write through to the field, or writes to a shadow
16298        // field the accessor does not read, fails this pin. Peer
16299        // to the four prior setter/accessor coherence pins.
16300        let mut e = Expander::new();
16301        e.set_max_registered_macros(64);
16302        assert_eq!(e.max_registered_macros(), 64);
16303    }
16304
16305    #[test]
16306    fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
16307        // With a ceiling of `2`, the first two fresh registrations
16308        // land in the table and the third fresh key rejects with
16309        // `LispError::RegisteredMacrosExceeded { macro_name: "c",
16310        // count: 2, limit: 2 }`. `count` is the pre-insert table
16311        // length (which equals `limit` at rejection in the common
16312        // case); `macro_name` is the OFFENDING call's name (the
16313        // fresh key we could not admit), not the outermost caller.
16314        let mut e = Expander::new();
16315        e.set_max_registered_macros(2);
16316        let forms = read(
16317            "(defmacro a (x) `(list ,x))
16318             (defmacro b (x) `(list ,x))
16319             (defmacro c (x) `(list ,x))",
16320        )
16321        .unwrap();
16322        let err = e.expand_program(forms).unwrap_err();
16323        match err {
16324            LispError::RegisteredMacrosExceeded {
16325                macro_name,
16326                count,
16327                limit,
16328            } => {
16329                assert_eq!(macro_name, "c");
16330                assert_eq!(count, 2);
16331                assert_eq!(limit, 2);
16332            }
16333            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16334        }
16335        // First two admitted; third rejected leaves the table at
16336        // its pre-third-call size.
16337        assert!(e.has("a"));
16338        assert!(e.has("b"));
16339        assert!(!e.has("c"));
16340        assert_eq!(e.len(), 2);
16341    }
16342
16343    #[test]
16344    fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
16345        // The substitute strategy shares the same
16346        // `register_macro_def` outer registration entry with the
16347        // bytecode strategy, so the REGISTRATION-time TABLE-COUNT
16348        // ceiling fires at the SAME gate regardless of which
16349        // apply-layer is active. The pin makes that shared-
16350        // registration property structural: a regression that ever
16351        // branches the table-count guard on strategy (e.g. moves
16352        // it into the `compile_templates` arm alone) fails this
16353        // test on the substitute expander. Peer to the four prior
16354        // shared-registration pins.
16355        let mut e = Expander::new_substitute_only();
16356        e.set_max_registered_macros(2);
16357        let forms = read(
16358            "(defmacro a (x) `(list ,x))
16359             (defmacro b (x) `(list ,x))
16360             (defmacro c (x) `(list ,x))",
16361        )
16362        .unwrap();
16363        let err = e.expand_program(forms).unwrap_err();
16364        match err {
16365            LispError::RegisteredMacrosExceeded {
16366                macro_name,
16367                count,
16368                limit,
16369            } => {
16370                assert_eq!(macro_name, "c");
16371                assert_eq!(count, 2);
16372                assert_eq!(limit, 2);
16373            }
16374            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16375        }
16376        assert!(e.has("a"));
16377        assert!(e.has("b"));
16378        assert!(!e.has("c"));
16379        assert_eq!(e.len(), 2);
16380    }
16381
16382    #[test]
16383    fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
16384        // OVERWRITES (a re-registration of an already-registered
16385        // key) do not grow the table and MUST be admitted at any
16386        // ceiling — operators redefining a macro at capacity is a
16387        // legitimate authoring pattern. This pin catches a
16388        // regression that ever treats overwrites the same as fresh
16389        // keys (a `contains_key` check that inverts, or a
16390        // `set_max_registered_macros(0)` that would reject even
16391        // pre-registered overwrites).
16392        let mut e = Expander::new();
16393        // Land two macros with a permissive ceiling.
16394        e.expand_program(
16395            read(
16396                "(defmacro a (x) `(list ,x))
16397                 (defmacro b (x) `(list ,x))",
16398            )
16399            .unwrap(),
16400        )
16401        .unwrap();
16402        assert_eq!(e.len(), 2);
16403        // Drop the ceiling to exactly the current size — a fresh
16404        // third key would now reject, but an overwrite of `a` MUST
16405        // admit.
16406        e.set_max_registered_macros(2);
16407        let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
16408        e.expand_program(forms).unwrap();
16409        assert_eq!(e.len(), 2, "overwrite must not grow the table");
16410        // Re-expand a call to `a` — the NEW definition (two
16411        // required params) is active, and the OLD definition (one
16412        // required param) is gone. Under the new definition,
16413        // `(a 1 2)` expands to `(pair 1 2)`.
16414        let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
16415        assert_eq!(expanded[0], parse("(pair foo bar)"));
16416    }
16417
16418    #[test]
16419    fn register_at_table_ceiling_admits_up_to_but_not_past() {
16420        // Fail-before/pass-after negative control on the exact
16421        // boundary: with a ceiling of `3`, exactly three fresh keys
16422        // admit and the fourth rejects. A regression that ever
16423        // shifted the boundary by one (e.g. keyed on `>` instead of
16424        // `>=`, or checked `len() > cap` before insert instead of
16425        // `len() >= cap`) would fail one of the two arms — either
16426        // the third admission or the fourth rejection.
16427        let mut e = Expander::new();
16428        e.set_max_registered_macros(3);
16429        // Three fresh keys admit.
16430        e.expand_program(
16431            read(
16432                "(defmacro a (x) `(list ,x))
16433                 (defmacro b (x) `(list ,x))
16434                 (defmacro c (x) `(list ,x))",
16435            )
16436            .unwrap(),
16437        )
16438        .unwrap();
16439        assert_eq!(e.len(), 3);
16440        // Fourth fresh key rejects.
16441        let err = e
16442            .expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
16443            .unwrap_err();
16444        match err {
16445            LispError::RegisteredMacrosExceeded {
16446                macro_name,
16447                count,
16448                limit,
16449            } => {
16450                assert_eq!(macro_name, "d");
16451                assert_eq!(count, 3);
16452                assert_eq!(limit, 3);
16453            }
16454            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16455        }
16456        // Table stays at pre-rejection size — no partial write.
16457        assert_eq!(e.len(), 3);
16458        assert!(!e.has("d"));
16459    }
16460
16461    #[test]
16462    fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
16463        // A rejected registration MUST leave BOTH `self.macros` AND
16464        // `self.templates` exactly as they were — no partial-write
16465        // window in which one table carries the over-ceiling entry
16466        // while the other does not, or in which the template
16467        // pre-compile already ran. Table-count gate fires BEFORE
16468        // body-size gate walks the body AND BEFORE any insert lands,
16469        // so a regression that ever moved the table-count gate
16470        // below `self.templates.insert` or below
16471        // `self.macros.insert` would fail this pin. Peer to
16472        // `register_failure_leaves_both_tables_pristine` on the
16473        // body-size axis.
16474        let mut e = Expander::new();
16475        e.set_max_registered_macros(1);
16476        // Land one macro at ceiling.
16477        e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16478            .unwrap();
16479        assert!(e.has("a"));
16480        // Reject a fresh second key.
16481        let err = e
16482            .expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
16483            .unwrap_err();
16484        assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
16485        // Table stays at pre-rejection state — `b` is NOT present
16486        // in either self.macros OR self.templates.
16487        assert_eq!(e.len(), 1);
16488        assert!(e.has("a"));
16489        assert!(!e.has("b"));
16490        // Expanding a call to `a` still works (templates table for
16491        // `a` untouched by the failed `b` registration).
16492        let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
16493        assert_eq!(expanded[0], parse("(list hey)"));
16494    }
16495
16496    #[test]
16497    fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
16498        // Zero ceiling is the "contract-test that this expander
16499        // accepts no new macros" posture — every fresh key rejects
16500        // at the first `defmacro` because `0 >= 0`. Peer to the
16501        // depth-ceiling zero-admits-and-rejects-every-macro-call
16502        // pin one RESOURCE axis over.
16503        let mut e = Expander::new();
16504        e.set_max_registered_macros(0);
16505        let err = e
16506            .expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16507            .unwrap_err();
16508        match err {
16509            LispError::RegisteredMacrosExceeded {
16510                macro_name,
16511                count,
16512                limit,
16513            } => {
16514                assert_eq!(macro_name, "a");
16515                assert_eq!(count, 0);
16516                assert_eq!(limit, 0);
16517            }
16518            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16519        }
16520        assert!(e.is_empty());
16521    }
16522
16523    #[test]
16524    fn registered_macros_exceeded_position_is_none() {
16525        // The variant joins the non-positional cohort — the
16526        // offending macro's byte offset is not what a table-count
16527        // diagnostic anchors to; the (offending fresh macro name,
16528        // observed table count, configured ceiling) triple is.
16529        // This pin keeps the variant in the `position() -> None`
16530        // cohort so a future span-carrying edit lands the field
16531        // in ONE place across every non-positional variant
16532        // simultaneously. Peer to the four prior
16533        // `*_exceeded_position_is_none` pins on the resource-
16534        // ceiling cohort.
16535        let err = LispError::RegisteredMacrosExceeded {
16536            macro_name: "fresh-1000".to_string(),
16537            count: 4096,
16538            limit: 4096,
16539        };
16540        assert_eq!(err.position(), None);
16541    }
16542
16543    #[test]
16544    fn registered_macros_exceeded_display_matches_typed_variant_shape() {
16545        // Display projection carries `macro_name`, `count`, AND
16546        // `limit`, so authoring surfaces that substring-grep the
16547        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16548        // THREE halves at the variant boundary and never need to
16549        // substring-parse free-form text to recover the identity
16550        // of the offending fresh key, the observed table count, or
16551        // the ceiling it breached. Peer to the four prior
16552        // `*_exceeded_display_matches_typed_variant_shape` pins.
16553        let err = LispError::RegisteredMacrosExceeded {
16554            macro_name: "fresh-1000".to_string(),
16555            count: 4096,
16556            limit: 4096,
16557        };
16558        let rendered = err.to_string();
16559        assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
16560        assert!(rendered.contains("4096"), "rendered: {rendered}");
16561        assert!(
16562            rendered.contains("registered macros count exceeded"),
16563            "rendered: {rendered}"
16564        );
16565    }
16566
16567    #[test]
16568    fn lawful_typescape_within_ceiling_registers_and_expands() {
16569        // A lawful hand-authored typescape (a handful of macros)
16570        // sits well below any realistic ceiling — the default
16571        // `DEFAULT_MAX_REGISTERED_MACROS` is deliberately generous
16572        // so operators never brush it on real authoring workloads.
16573        // This pin locks the default ceiling's non-interference
16574        // posture: a regression that ever lowered the default
16575        // enough to reject a small typescape would fail here. Peer
16576        // to `lawful_macro_body_within_ceiling_registers_and_expands`.
16577        let mut e = Expander::new();
16578        let forms = read(
16579            "(defmacro when (cond x) `(if ,cond ,x))
16580             (defmacro twice (x) `(list ,x ,x))
16581             (defmacro pair (x y) `(list ,x ,y))
16582             (when #t (twice hey))",
16583        )
16584        .unwrap();
16585        let out = e.expand_program(forms).unwrap();
16586        assert_eq!(out[0], parse("(if #t (list hey hey))"));
16587        assert_eq!(e.len(), 3);
16588    }
16589
16590    #[test]
16591    fn register_macro_def_direct_call_respects_table_ceiling() {
16592        // `register_macro_def` is the substrate primitive both
16593        // `expand_program`'s `defmacro` recognition AND
16594        // `with_macros`'s bulk load route through. The table-count
16595        // ceiling fires on the primitive itself, not on any one
16596        // consumer — a regression that ever moved the gate into
16597        // `expand_program`'s arm alone would fail this pin. Peer
16598        // to `register_macro_def_direct_call_respects_body_size_ceiling`
16599        // one RESOURCE-DIMENSION axis over.
16600        let mut e = Expander::new();
16601        e.set_max_registered_macros(1);
16602        let def_a = MacroDef {
16603            name: "a".to_string(),
16604            params: MacroParams {
16605                required: vec!["x".to_string()],
16606                optional: Vec::new(),
16607                rest: None,
16608            },
16609            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16610        };
16611        let def_b = MacroDef {
16612            name: "b".to_string(),
16613            params: MacroParams {
16614                required: vec!["x".to_string()],
16615                optional: Vec::new(),
16616                rest: None,
16617            },
16618            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16619        };
16620        // First admits — table sits at ceiling.
16621        e.register_macro_def(def_a).unwrap();
16622        assert!(e.has("a"));
16623        assert_eq!(e.len(), 1);
16624        // Second rejects — fresh key past ceiling.
16625        let err = e.register_macro_def(def_b).unwrap_err();
16626        match err {
16627            LispError::RegisteredMacrosExceeded {
16628                macro_name,
16629                count,
16630                limit,
16631            } => {
16632                assert_eq!(macro_name, "b");
16633                assert_eq!(count, 1);
16634                assert_eq!(limit, 1);
16635            }
16636            other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16637        }
16638        assert!(!e.has("b"));
16639        assert_eq!(e.len(), 1);
16640    }
16641
16642    #[test]
16643    fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
16644        // Direct `register_macro_def` calls must ALSO admit
16645        // overwrites at ceiling — the overwrite discipline lives
16646        // on the primitive, not on `expand_program`'s arm alone.
16647        // Sibling of the source-level overwrite admission test.
16648        let mut e = Expander::new();
16649        e.set_max_registered_macros(1);
16650        let def_a_v1 = MacroDef {
16651            name: "a".to_string(),
16652            params: MacroParams {
16653                required: vec!["x".to_string()],
16654                optional: Vec::new(),
16655                rest: None,
16656            },
16657            body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16658        };
16659        let def_a_v2 = MacroDef {
16660            name: "a".to_string(),
16661            params: MacroParams {
16662                required: vec!["x".to_string(), "y".to_string()],
16663                optional: Vec::new(),
16664                rest: None,
16665            },
16666            body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
16667        };
16668        e.register_macro_def(def_a_v1).unwrap();
16669        assert_eq!(e.len(), 1);
16670        // Overwrite of `a` at ceiling admits — no growth.
16671        e.register_macro_def(def_a_v2).unwrap();
16672        assert_eq!(e.len(), 1);
16673    }
16674
16675    // ── `MacroParams::total_arity` — the total-slots projection ──
16676    //
16677    // `total_arity()` names `required.len() + optional.len() +
16678    // rest.is_some() as usize` on the typed `MacroParams` algebra —
16679    // the `Vec`-free peer of `names().len()` AND the axis both
16680    // `MacroParams::bind`'s `Vec::with_capacity` hint AND the
16681    // REGISTER-time arity-ceiling gate consult. These pins cover:
16682    // structural identity vs. `names().len()`, the
16683    // `fixed_arity + rest.is_some() as usize` decomposition, and the
16684    // three canonical shapes (empty, all-required, mixed with rest).
16685    #[test]
16686    fn macro_params_total_arity_matches_names_len() {
16687        // The structural identity `total_arity() == names().len()`
16688        // — the `Vec`-free primitive's projection equals the flat-
16689        // index list's length. A regression that ever miscounts
16690        // one slot (drops rest, double-counts optional) fails this
16691        // pin. Sibling to `fixed_arity`'s peer-arithmetic pins.
16692        let params = MacroParams {
16693            required: vec!["a".into(), "b".into()],
16694            optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
16695            rest: Some("e".into()),
16696        };
16697        assert_eq!(params.total_arity(), params.names().len());
16698        assert_eq!(params.total_arity(), 5);
16699    }
16700
16701    #[test]
16702    fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
16703        // The `fixed_arity + rest.is_some() as usize` decomposition
16704        // — pins the composition on the [`MacroParams`] algebra so
16705        // a future consumer that wants "total slots" via
16706        // `fixed_arity + rest?` binds to the same arithmetic
16707        // `total_arity` names.
16708        let rest_none = MacroParams {
16709            required: vec!["a".into()],
16710            optional: vec![OptionalParam::bare("b")],
16711            rest: None,
16712        };
16713        assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
16714        assert_eq!(rest_none.total_arity(), 2);
16715        let rest_some = MacroParams {
16716            required: vec!["a".into()],
16717            optional: vec![OptionalParam::bare("b")],
16718            rest: Some("r".into()),
16719        };
16720        assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
16721        assert_eq!(rest_some.total_arity(), 3);
16722    }
16723
16724    #[test]
16725    fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
16726        // Nullary macro edge: `(defmacro nullary () BODY)` has
16727        // `total_arity() == 0`. Under a `set_max_macro_arity(0)`
16728        // ceiling the arity gate keys on `>` (equality admits), so
16729        // this nullary registration succeeds while any non-nullary
16730        // one rejects — the exact contract the arity-gate
16731        // zero-admits-nullary test below relies on.
16732        let params = MacroParams::default();
16733        assert_eq!(params.total_arity(), 0);
16734    }
16735
16736    // ── max_macro_arity: REGISTRATION-time param-count ceiling ──
16737    // Peer to body-size (REGISTER-time BODY SIZE) one RESOURCE-
16738    // DIMENSION axis over — where body-size bounds a per-registration
16739    // BODY node count, this ceiling bounds a per-registration PARAM
16740    // slot count. Peer to registered-macros (REGISTER-time TABLE
16741    // COUNT) one RESOURCE-DIMENSION axis over — where
16742    // registered-macros bounds cumulative registration COUNT, this
16743    // ceiling bounds a per-registration PARAM-LIST WIDTH. Together
16744    // the three REGISTER-time ceilings close the (per-body SIZE,
16745    // per-body ARITY, per-table COUNT) three-corner surface.
16746
16747    #[test]
16748    fn expander_default_max_macro_arity_is_the_module_constant() {
16749        // Both constructors seed `max_macro_arity` from the module
16750        // constant — a regression that drifts one constructor's
16751        // default from the constant (e.g. a hardcoded `128` at ONE
16752        // site with the other left uninitialized after a `Default`
16753        // derive addition) fails this pin. Peer to the five prior
16754        // default-coherence pins on depth, cache-entries,
16755        // expansion-size, body-size, and registered-macros.
16756        assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
16757        assert_eq!(
16758            Expander::new_substitute_only().max_macro_arity(),
16759            DEFAULT_MAX_MACRO_ARITY
16760        );
16761        assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
16762    }
16763
16764    #[test]
16765    fn set_max_macro_arity_takes_effect_on_subsequent_register() {
16766        // Setter mirrors accessor — a regression that ever forgets to
16767        // write through to the field, or writes to a shadow field the
16768        // accessor does not read, fails this pin. Peer to the five
16769        // prior setter/accessor coherence pins.
16770        let mut e = Expander::new();
16771        e.set_max_macro_arity(16);
16772        assert_eq!(e.max_macro_arity(), 16);
16773    }
16774
16775    #[test]
16776    fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
16777        // `(defmacro huge (a b c) `,a)` declares 3 required params.
16778        // Under `set_max_macro_arity(2)` the arity gate fires:
16779        // 3 > 2 → `LispError::MacroArityExceeded { macro_name: "huge",
16780        // arity: 3, limit: 2 }`. `arity` is the observed
16781        // `def.params.total_arity()`, NOT `required.len()` alone —
16782        // even an optional-only or rest-only overrun trips the gate
16783        // (pinned separately). Peer to the register-time body-size
16784        // rejection tests one RESOURCE-DIMENSION axis over.
16785        let mut e = Expander::new();
16786        e.set_max_macro_arity(2);
16787        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16788        let err = e.expand_program(forms).unwrap_err();
16789        match err {
16790            LispError::MacroArityExceeded {
16791                macro_name,
16792                arity,
16793                limit,
16794            } => {
16795                assert_eq!(macro_name, "huge");
16796                assert_eq!(arity, 3);
16797                assert_eq!(limit, 2);
16798            }
16799            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16800        }
16801        assert!(!e.has("huge"));
16802    }
16803
16804    #[test]
16805    fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
16806        // The substitute strategy shares the same
16807        // `register_macro_def` outer registration entry with the
16808        // bytecode strategy, so the REGISTRATION-time ARITY ceiling
16809        // fires at the SAME gate regardless of which apply-layer is
16810        // active. Peer to
16811        // `register_arity_bomb_rejects_at_arity_limit_bytecode_path`.
16812        let mut e = Expander::new_substitute_only();
16813        e.set_max_macro_arity(2);
16814        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16815        let err = e.expand_program(forms).unwrap_err();
16816        match err {
16817            LispError::MacroArityExceeded {
16818                macro_name,
16819                arity,
16820                limit,
16821            } => {
16822                assert_eq!(macro_name, "huge");
16823                assert_eq!(arity, 3);
16824                assert_eq!(limit, 2);
16825            }
16826            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16827        }
16828        assert!(!e.has("huge"));
16829    }
16830
16831    #[test]
16832    fn register_arity_gate_counts_optional_slots() {
16833        // `(defmacro foo (a &optional b c) `,a)` has 1 required + 2
16834        // optional + 0 rest = arity 3 via `total_arity`. Under a
16835        // ceiling of `2` the gate rejects, matching the required-only
16836        // arity-bomb — the arity axis is `total_arity()`, not
16837        // `required.len()`. A regression that drops the optional slots
16838        // from the arity computation would ADMIT this shape and fail
16839        // this pin.
16840        let mut e = Expander::new();
16841        e.set_max_macro_arity(2);
16842        let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
16843        let err = e.expand_program(forms).unwrap_err();
16844        match err {
16845            LispError::MacroArityExceeded {
16846                macro_name,
16847                arity,
16848                limit,
16849            } => {
16850                assert_eq!(macro_name, "foo");
16851                assert_eq!(arity, 3);
16852                assert_eq!(limit, 2);
16853            }
16854            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16855        }
16856    }
16857
16858    #[test]
16859    fn register_arity_gate_counts_rest_slot() {
16860        // `(defmacro spread (a b &rest r) `,a)` has 2 required + 0
16861        // optional + 1 rest = arity 3 via `total_arity`. Under a
16862        // ceiling of `2` the gate rejects, matching the required-only
16863        // arity-bomb — the arity axis includes the rest slot as ONE
16864        // named param, not zero. A regression that drops the rest slot
16865        // from the arity computation would ADMIT this shape and fail
16866        // this pin.
16867        let mut e = Expander::new();
16868        e.set_max_macro_arity(2);
16869        let forms = read("(defmacro spread (a b &rest r) `,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, "spread");
16878                assert_eq!(arity, 3);
16879                assert_eq!(limit, 2);
16880            }
16881            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16882        }
16883    }
16884
16885    #[test]
16886    fn register_arity_at_ceiling_admits() {
16887        // `(defmacro pair (x y) `(list ,x ,y))` has 2 required = arity
16888        // 2 via `total_arity`. Under a ceiling of `2` the arity gate
16889        // keys on `>` (equality admits; only strict overrun rejects),
16890        // so the registration succeeds and the expansion proceeds
16891        // normally. Fail-before/pass-after negative control on the
16892        // exact boundary: a regression that ever shifted the boundary
16893        // by one (e.g. keyed on `>=` instead of `>`) would fail this
16894        // pin. Peer to `register_macro_body_at_ceiling_admits` on the
16895        // body-size axis.
16896        let mut e = Expander::new();
16897        e.set_max_macro_arity(2);
16898        let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
16899        let out = e.expand_program(forms).unwrap();
16900        assert_eq!(out[0], parse("(list a b)"));
16901        assert_eq!(e.len(), 1);
16902    }
16903
16904    #[test]
16905    fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
16906        // A rejected arity-gated registration MUST leave BOTH
16907        // `self.macros` AND `self.templates` exactly as they were —
16908        // no partial-write window in which one table carries the
16909        // over-ceiling entry while the other does not, or in which
16910        // the template pre-compile already ran. Arity gate fires
16911        // BEFORE the body-size gate walks the body AND BEFORE any
16912        // insert lands, so a regression that ever moved the arity
16913        // gate below `self.templates.insert` or below
16914        // `self.macros.insert` would fail this pin. Peer to
16915        // `register_failure_leaves_both_tables_pristine` (body-size)
16916        // and `register_table_ceiling_leaves_both_tables_pristine_on_rejection`
16917        // (table-count).
16918        let mut e = Expander::new();
16919        e.set_max_macro_arity(1);
16920        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16921        assert!(e.expand_program(forms).is_err());
16922        assert!(
16923            !e.has("huge"),
16924            "macros table must stay pristine after rejection"
16925        );
16926        assert_eq!(e.len(), 0);
16927        // Expanding a NEW lawful macro afterwards still works — the
16928        // failed arity-gate rejection has not corrupted expander
16929        // state.
16930        let out = e
16931            .expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
16932            .unwrap();
16933        assert_eq!(out[0], parse("hey"));
16934    }
16935
16936    #[test]
16937    fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
16938        // Zero ceiling is the "contract-test that this expander
16939        // accepts only nullary macros" posture — every fresh
16940        // non-nullary key rejects at the arity gate because
16941        // `total_arity() > 0`, while nullary `(defmacro nullary ()
16942        // BODY)` still admits (its arity is exactly 0, and only
16943        // strict overrun rejects). Peer to the body-size zero-rejects-
16944        // every-non-empty-body pin one RESOURCE-DIMENSION axis over
16945        // (though body-size zero rejects even nullary-body shapes,
16946        // since `Sexp::Nil::node_count() == 1 > 0`; arity zero
16947        // preserves the nullary registration surface).
16948        let mut e = Expander::new();
16949        e.set_max_macro_arity(0);
16950        // Nullary admits — arity 0 <= 0.
16951        e.expand_program(read("(defmacro nullary () `unit)").unwrap())
16952            .unwrap();
16953        assert!(e.has("nullary"));
16954        // Non-nullary rejects — arity 1 > 0.
16955        let err = e
16956            .expand_program(read("(defmacro id (x) `,x)").unwrap())
16957            .unwrap_err();
16958        match err {
16959            LispError::MacroArityExceeded {
16960                macro_name,
16961                arity,
16962                limit,
16963            } => {
16964                assert_eq!(macro_name, "id");
16965                assert_eq!(arity, 1);
16966                assert_eq!(limit, 0);
16967            }
16968            other => panic!("expected MacroArityExceeded, got: {other:?}"),
16969        }
16970        assert!(!e.has("id"));
16971    }
16972
16973    #[test]
16974    fn macro_arity_exceeded_position_is_none() {
16975        // The variant joins the non-positional cohort — the offending
16976        // macro's byte offset is not what an arity-bomb diagnostic
16977        // anchors to; the (macro name, observed arity, configured
16978        // ceiling) triple is. This pin keeps the variant in the
16979        // `position() -> None` cohort so a future span-carrying edit
16980        // lands the field in ONE place across every non-positional
16981        // variant simultaneously. Peer to the five prior
16982        // `*_exceeded_position_is_none` pins on the resource-ceiling
16983        // cohort.
16984        let err = LispError::MacroArityExceeded {
16985            macro_name: "huge".to_string(),
16986            arity: 512,
16987            limit: 128,
16988        };
16989        assert_eq!(err.position(), None);
16990    }
16991
16992    #[test]
16993    fn macro_arity_exceeded_display_matches_typed_variant_shape() {
16994        // Display projection carries `macro_name`, `arity`, AND
16995        // `limit`, so authoring surfaces that substring-grep the
16996        // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16997        // THREE halves at the variant boundary and never need to
16998        // substring-parse free-form text to recover the identity of
16999        // the offending macro, the observed arity, or the ceiling it
17000        // breached. Peer to the five prior
17001        // `*_exceeded_display_matches_typed_variant_shape` pins.
17002        let err = LispError::MacroArityExceeded {
17003            macro_name: "huge".to_string(),
17004            arity: 512,
17005            limit: 128,
17006        };
17007        let rendered = err.to_string();
17008        assert!(rendered.contains("huge"), "rendered: {rendered}");
17009        assert!(rendered.contains("512"), "rendered: {rendered}");
17010        assert!(rendered.contains("128"), "rendered: {rendered}");
17011        assert!(
17012            rendered.contains("macro arity exceeded"),
17013            "rendered: {rendered}"
17014        );
17015    }
17016
17017    #[test]
17018    fn lawful_macro_arity_within_ceiling_registers_and_expands() {
17019        // A lawful hand-authored macro arity sits well below any
17020        // realistic ceiling — the default `DEFAULT_MAX_MACRO_ARITY`
17021        // is deliberately generous so operators never brush it on
17022        // real authoring workloads. This pin locks the default
17023        // ceiling's non-interference posture: a regression that
17024        // ever lowered the default enough to reject the reference
17025        // multi-slot `(defmacro when (cond x) …)` shape would fail
17026        // here. Peer to `lawful_macro_body_within_ceiling_registers_and_expands`
17027        // and `lawful_typescape_within_ceiling_registers_and_expands`.
17028        let mut e = Expander::new();
17029        let forms = read(
17030            "(defmacro when (cond x) `(if ,cond ,x))
17031             (defmacro triple (x y z) `(list ,x ,y ,z))
17032             (when #t (triple a b c))",
17033        )
17034        .unwrap();
17035        let out = e.expand_program(forms).unwrap();
17036        assert_eq!(out[0], parse("(if #t (list a b c))"));
17037    }
17038
17039    #[test]
17040    fn register_macro_def_direct_call_respects_arity_ceiling() {
17041        // `register_macro_def` is the substrate primitive both
17042        // `expand_program`'s `defmacro` recognition AND `with_macros`'s
17043        // bulk load route through. The arity ceiling fires on the
17044        // primitive itself, not on any one consumer — a regression
17045        // that ever moved the gate into `expand_program`'s arm alone
17046        // would fail this pin. Peer to
17047        // `register_macro_def_direct_call_respects_body_size_ceiling`
17048        // AND `register_macro_def_direct_call_respects_table_ceiling`
17049        // one RESOURCE-DIMENSION axis over.
17050        let mut e = Expander::new();
17051        e.set_max_macro_arity(2);
17052        let def = MacroDef {
17053            name: "huge".to_string(),
17054            params: MacroParams {
17055                required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
17056                optional: Vec::new(),
17057                rest: None,
17058            },
17059            body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
17060        };
17061        // Arity 3 > ceiling 2 — rejected before body-size gate.
17062        let err = e.register_macro_def(def).unwrap_err();
17063        match err {
17064            LispError::MacroArityExceeded {
17065                macro_name,
17066                arity,
17067                limit,
17068            } => {
17069                assert_eq!(macro_name, "huge");
17070                assert_eq!(arity, 3);
17071                assert_eq!(limit, 2);
17072            }
17073            other => panic!("expected MacroArityExceeded, got: {other:?}"),
17074        }
17075        assert!(!e.has("huge"));
17076    }
17077
17078    #[test]
17079    fn arity_gate_fires_before_body_size_gate() {
17080        // ORDERING PIN — the arity gate fires BEFORE the body-size
17081        // gate. With both `set_max_macro_arity(1)` AND
17082        // `set_max_macro_body_size(2)` set below a shape that violates
17083        // BOTH ceilings — `(defmacro dual (a b) `(list ,a ,b))` has
17084        // arity 2 AND body node count > 2 — the arity gate's
17085        // rejection wins, so the returned variant is
17086        // `MacroArityExceeded`, not `MacroBodySizeExceeded`. A
17087        // regression that ever swapped the two gates' order would fail
17088        // this pin. The ordering isn't cosmetic: arity is O(1)
17089        // (three-arm addition on `def.params` fields), body-size is
17090        // O(body_nodes) (`def.body.node_count()` walks the AST), so
17091        // the O(1)-first order minimises the cost of rejecting the
17092        // worst case (a large-body large-arity macro).
17093        let mut e = Expander::new();
17094        e.set_max_macro_arity(1);
17095        e.set_max_macro_body_size(2);
17096        let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
17097        let err = e.expand_program(forms).unwrap_err();
17098        assert!(
17099            matches!(err, LispError::MacroArityExceeded { .. }),
17100            "arity gate must fire before body-size gate; got: {err:?}"
17101        );
17102    }
17103
17104    #[test]
17105    fn arity_gate_fires_after_table_count_gate() {
17106        // ORDERING PIN — the table-count gate fires BEFORE the arity
17107        // gate. Under `set_max_registered_macros(1)` at capacity AND
17108        // `set_max_macro_arity(1)`, a fresh over-arity registration
17109        // trips the table-count gate FIRST (its `HashMap::len`
17110        // comparison is O(1) cache-friendly and gates cumulative
17111        // registration cost); the arity gate is O(1) but on `def`,
17112        // not on shared state, so it sits AFTER the table-count gate
17113        // on the O(1)-first ordering of the REGISTER-time chain. A
17114        // regression that ever swapped the two would fail this pin.
17115        let mut e = Expander::new();
17116        e.set_max_registered_macros(1);
17117        e.set_max_macro_arity(1);
17118        // Land one macro at table ceiling.
17119        e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
17120            .unwrap();
17121        assert_eq!(e.len(), 1);
17122        // Fresh registration with BOTH violations — table-count wins.
17123        let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17124        let err = e.expand_program(forms).unwrap_err();
17125        assert!(
17126            matches!(err, LispError::RegisteredMacrosExceeded { .. }),
17127            "table-count gate must fire before arity gate; got: {err:?}"
17128        );
17129    }
17130
17131    // ── ResourceLimits: bundled resource-ceiling snapshot ────────────
17132
17133    #[test]
17134    fn default_resource_limits_binds_each_field_to_matching_module_constant() {
17135        // Pin the (constant × aggregation) corner: the const-form
17136        // aggregate MUST bind every field to its individually-defined
17137        // module constant. A re-tuning of ONE module constant
17138        // (e.g. `DEFAULT_MAX_EXPANSION_DEPTH` raised from 256 to 512)
17139        // has to propagate through the aggregate at ONE site; this
17140        // test fires the divergence loudly if a future edit sets a
17141        // module constant to a fresh value but forgets to update the
17142        // matching aggregate slot. Field-by-field equality carries
17143        // the message better than one deep-equality assertion.
17144        assert_eq!(
17145            DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
17146            DEFAULT_MAX_EXPANSION_DEPTH
17147        );
17148        assert_eq!(
17149            DEFAULT_RESOURCE_LIMITS.max_cache_entries,
17150            DEFAULT_MAX_CACHE_ENTRIES
17151        );
17152        assert_eq!(
17153            DEFAULT_RESOURCE_LIMITS.max_expansion_size,
17154            DEFAULT_MAX_EXPANSION_SIZE
17155        );
17156        assert_eq!(
17157            DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
17158            DEFAULT_MAX_MACRO_BODY_SIZE
17159        );
17160        assert_eq!(
17161            DEFAULT_RESOURCE_LIMITS.max_registered_macros,
17162            DEFAULT_MAX_REGISTERED_MACROS
17163        );
17164        assert_eq!(
17165            DEFAULT_RESOURCE_LIMITS.max_macro_arity,
17166            DEFAULT_MAX_MACRO_ARITY
17167        );
17168    }
17169
17170    #[test]
17171    fn resource_limits_default_impl_matches_const_form() {
17172        // The two projections of the shipped posture — the `const`
17173        // form [`DEFAULT_RESOURCE_LIMITS`] and the runtime
17174        // `Default::default()` form — must reach the SAME value. Pins
17175        // that a future edit which flips one to a new default cannot
17176        // silently drift the other.
17177        assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
17178    }
17179
17180    #[test]
17181    fn expander_new_resource_limits_matches_shipped_defaults() {
17182        // Fresh-expander posture on the bundled-getter surface — the
17183        // six ceilings [`Expander::new`] seeds MUST snapshot to
17184        // [`DEFAULT_RESOURCE_LIMITS`] verbatim. Peer of the six
17185        // individual `expander_default_max_*_is_the_module_constant`
17186        // pins one AGGREGATION axis over — where each individual
17187        // pin catches ONE field drift, this pin catches ANY of the
17188        // six drifting AND the snapshot projection itself losing a
17189        // field.
17190        assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
17191    }
17192
17193    #[test]
17194    fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
17195        // Substitute-only expander seeds the SAME six ceilings from
17196        // the SAME module constants as [`Expander::new`] — the two
17197        // constructors share the resource-ceiling posture even
17198        // though they diverge on `compile_templates` +
17199        // `cache_enabled`. Pins that a future constructor which
17200        // forgets to seed one of the six ceilings from its module
17201        // constant fails loudly through the bundled getter.
17202        assert_eq!(
17203            Expander::new_substitute_only().resource_limits(),
17204            DEFAULT_RESOURCE_LIMITS
17205        );
17206    }
17207
17208    #[test]
17209    fn resource_limits_snapshot_reflects_each_individual_setter() {
17210        // Coherence pin between the six individual setters and the
17211        // bundled getter — flipping ONE ceiling via its individual
17212        // setter must project through [`Expander::resource_limits`]
17213        // to the SAME value the individual getter returns. A
17214        // regression that leaves ONE field out of the snapshot
17215        // projection (a copy-paste omission in the struct literal
17216        // constructor) fires here. Distinct low-non-default values
17217        // so a stray field carrying the default fails loudly.
17218        let mut e = Expander::new();
17219        e.set_max_expansion_depth(3);
17220        e.set_max_cache_entries(5);
17221        e.set_max_expansion_size(7);
17222        e.set_max_macro_body_size(11);
17223        e.set_max_registered_macros(13);
17224        e.set_max_macro_arity(17);
17225        let snap = e.resource_limits();
17226        assert_eq!(snap.max_expansion_depth, 3);
17227        assert_eq!(snap.max_cache_entries, 5);
17228        assert_eq!(snap.max_expansion_size, 7);
17229        assert_eq!(snap.max_macro_body_size, 11);
17230        assert_eq!(snap.max_registered_macros, 13);
17231        assert_eq!(snap.max_macro_arity, 17);
17232        // And the individual getters see the same values — the
17233        // snapshot is a projection, not a divergent copy.
17234        assert_eq!(e.max_expansion_depth(), 3);
17235        assert_eq!(e.max_cache_entries(), 5);
17236        assert_eq!(e.max_expansion_size(), 7);
17237        assert_eq!(e.max_macro_body_size(), 11);
17238        assert_eq!(e.max_registered_macros(), 13);
17239        assert_eq!(e.max_macro_arity(), 17);
17240    }
17241
17242    #[test]
17243    fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
17244        // Sibling pin to `resource_limits_snapshot_reflects_...` on the
17245        // WRITE axis — bulk-setting via [`Expander::set_resource_limits`]
17246        // must reach every individual `max_*` getter. A regression
17247        // that drops one field from the destructuring assignment in
17248        // the bulk setter fires here. Distinct low-non-default values
17249        // so a stray field carrying the previous value fails loudly.
17250        let mut e = Expander::new();
17251        e.set_resource_limits(ResourceLimits {
17252            max_expansion_depth: 2,
17253            max_cache_entries: 4,
17254            max_expansion_size: 8,
17255            max_macro_body_size: 16,
17256            max_registered_macros: 32,
17257            max_macro_arity: 64,
17258        });
17259        assert_eq!(e.max_expansion_depth(), 2);
17260        assert_eq!(e.max_cache_entries(), 4);
17261        assert_eq!(e.max_expansion_size(), 8);
17262        assert_eq!(e.max_macro_body_size(), 16);
17263        assert_eq!(e.max_registered_macros(), 32);
17264        assert_eq!(e.max_macro_arity(), 64);
17265    }
17266
17267    #[test]
17268    fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
17269        // Round-trip theorem — for any starting expander, the
17270        // composition `set_resource_limits(resource_limits())` is
17271        // the identity on the resource-ceiling surface. This is the
17272        // ONE typed statement that pins the (getter, setter) pair as
17273        // strict projections of the same six-field surface, no
17274        // hidden lossy field, no ordering asymmetry between the
17275        // struct literal and the destructuring pattern.
17276        let mut e = Expander::new();
17277        e.set_max_expansion_depth(9);
17278        e.set_max_cache_entries(19);
17279        e.set_max_expansion_size(29);
17280        e.set_max_macro_body_size(39);
17281        e.set_max_registered_macros(49);
17282        e.set_max_macro_arity(59);
17283        let snap = e.resource_limits();
17284        // Reset to defaults so any leftover state would show up in
17285        // the post-round-trip snapshot as the default rather than
17286        // the intended value.
17287        e.set_resource_limits(ResourceLimits::default());
17288        assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
17289        // Apply the captured snapshot — the expander must land at
17290        // the same six values `snap` recorded.
17291        e.set_resource_limits(snap);
17292        assert_eq!(e.resource_limits(), snap);
17293    }
17294
17295    #[test]
17296    fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
17297        // The `Copy` posture on [`ResourceLimits`] gives callers the
17298        // cheap ergonomic of a struct-update literal — take the
17299        // shipped posture and override ONE ceiling — that a chain of
17300        // six independent setters cannot express in ONE typed expression.
17301        // Pins the intended use-shape so a future refactor which
17302        // removes `Copy` from the struct fails to compile at THIS
17303        // site rather than at every downstream consumer.
17304        let mut e = Expander::new();
17305        e.set_resource_limits(ResourceLimits {
17306            max_macro_arity: 4,
17307            ..DEFAULT_RESOURCE_LIMITS
17308        });
17309        // Overridden field carries the fresh value.
17310        assert_eq!(e.max_macro_arity(), 4);
17311        // Every other field carries the shipped default — the
17312        // struct-update literal is a full projection, not a
17313        // selective mutation.
17314        assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17315        assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17316        assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17317        assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17318        assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17319    }
17320
17321    #[test]
17322    fn expander_default_derives_resource_limits_from_bundled_field_default() {
17323        // Post the [`Expander::limits`]-field consolidation the derived
17324        // `Default` on [`Expander`] projects through [`ResourceLimits`]'s
17325        // own `Default` — which returns [`DEFAULT_RESOURCE_LIMITS`] — so
17326        // `Expander::default().resource_limits()` lands at the shipped
17327        // ceiling posture verbatim. Pre-consolidation the six independent
17328        // `max_*: usize` fields defaulted to the bare `usize` zero, so
17329        // `Expander::default()` yielded a below-floor posture with every
17330        // ceiling clamped to `0` — a shape no consumer wanted and every
17331        // test path avoided by routing through [`Expander::new`] instead.
17332        // The consolidation is what turned that latent below-floor
17333        // derived-`Default` shape into a lawful shipped posture on the
17334        // structural axis. Peer of
17335        // `expander_new_resource_limits_matches_shipped_defaults` one
17336        // CONSTRUCTOR axis over — where that test pins the explicit
17337        // [`Expander::new`] constructor, this test pins the derived-
17338        // [`Default`] constructor route to the SAME six values through
17339        // the SAME bundled projection. Sibling of
17340        // `resource_limits_default_impl_matches_const_form` one
17341        // TYPE-LEVEL axis over — that test pins [`ResourceLimits::default`]
17342        // to [`DEFAULT_RESOURCE_LIMITS`]; this test pins [`Expander`]'s
17343        // derived `Default` to project through it.
17344        assert_eq!(
17345            Expander::default().resource_limits(),
17346            DEFAULT_RESOURCE_LIMITS
17347        );
17348    }
17349
17350    #[test]
17351    fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
17352        // Behavioral pin — the bundled setter is not a decorative
17353        // getter/setter pair; the six ceilings it propagates must
17354        // reach the SAME check sites the individual setters do. Set
17355        // `max_macro_arity` to 1 via the bulk setter and confirm the
17356        // arity gate fires on a two-arg macro registration. A
17357        // regression that wired the bulk setter to a shadow field
17358        // rather than the actual `Expander` state fails here.
17359        let mut e = Expander::new();
17360        e.set_resource_limits(ResourceLimits {
17361            max_macro_arity: 1,
17362            ..DEFAULT_RESOURCE_LIMITS
17363        });
17364        let forms = read("(defmacro two (a b) `,a)").unwrap();
17365        let err = e.expand_program(forms).unwrap_err();
17366        assert!(
17367            matches!(
17368                err,
17369                LispError::MacroArityExceeded {
17370                    arity: 2,
17371                    limit: 1,
17372                    ..
17373                }
17374            ),
17375            "bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
17376        );
17377    }
17378
17379    // ── Expander::with_limits — at-construction resource-posture ───────
17380
17381    #[test]
17382    fn expander_with_limits_seeds_the_provided_resource_posture() {
17383        // Round-trip pin at CONSTRUCTION time — the bundle the caller
17384        // passes into [`Expander::with_limits`] MUST project back
17385        // through [`Expander::resource_limits`] verbatim. Peer of the
17386        // `set_resource_limits(l); resource_limits() == l` round-trip
17387        // one CONSTRUCTOR-STAGE axis over — that pin covers the
17388        // POST-construction assignment; this pin covers the
17389        // AT-construction assignment. A regression that dropped the
17390        // `limits` argument on the floor (a copy-paste that left
17391        // `limits: DEFAULT_RESOURCE_LIMITS` in the struct literal
17392        // instead of `limits`) fires here on every non-default field.
17393        // Distinct low-non-default values so a stray field defaulting
17394        // fails loudly.
17395        let want = ResourceLimits {
17396            max_expansion_depth: 3,
17397            max_cache_entries: 5,
17398            max_expansion_size: 7,
17399            max_macro_body_size: 11,
17400            max_registered_macros: 13,
17401            max_macro_arity: 17,
17402        };
17403        assert_eq!(Expander::with_limits(want).resource_limits(), want);
17404    }
17405
17406    #[test]
17407    fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
17408        // Delegation identity — [`Expander::new`] delegates to
17409        // [`Expander::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`] so
17410        // the two constructors MUST agree on the resource-ceiling
17411        // projection. Pins the "no drift between the two paths to a
17412        // fresh default-posture expander" theorem: a future refactor
17413        // that breaks the delegation (an inline field literal reappearing
17414        // in [`Expander::new`], an accidental hardcoded const at ONE
17415        // site with the other unchanged) fires here. Peer of
17416        // `resource_limits_default_impl_matches_const_form` one
17417        // TYPE-LEVEL axis over — that test pins the two projections of
17418        // the shipped posture on [`ResourceLimits`] itself; this test
17419        // pins the two constructor paths on [`Expander`] to reach the
17420        // SAME six values through the SAME bundle.
17421        assert_eq!(
17422            Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
17423            Expander::new().resource_limits()
17424        );
17425    }
17426
17427    #[test]
17428    fn expander_with_limits_composes_with_struct_update_override() {
17429        // The `Copy` posture on [`ResourceLimits`] lets a caller compose
17430        // "the shipped posture with ONE ceiling overridden" as ONE
17431        // struct-update literal AND thread that composition through
17432        // [`Expander::with_limits`] as ONE typed call — the ergonomic
17433        // shape that six independent setter invocations cannot express
17434        // in ONE expression. Overridden ceiling carries the fresh value,
17435        // every other ceiling carries its shipped default. A future
17436        // refactor that removes `Copy` from [`ResourceLimits`] fails to
17437        // compile at THIS site rather than at every downstream consumer.
17438        let e = Expander::with_limits(ResourceLimits {
17439            max_macro_arity: 4,
17440            ..DEFAULT_RESOURCE_LIMITS
17441        });
17442        assert_eq!(e.max_macro_arity(), 4);
17443        assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17444        assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17445        assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17446        assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17447        assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17448    }
17449
17450    #[test]
17451    fn expander_with_limits_carries_the_new_execution_strategy() {
17452        // The (compile_templates, cache_enabled) execution-strategy pair
17453        // MUST carry the [`Expander::new`] default posture — bytecode +
17454        // cache both on — regardless of which resource posture the
17455        // caller passes in. Cache-count observation is the least-
17456        // invasive check available on the public surface: a fresh
17457        // `with_limits` expander that expands a repeat-call site must
17458        // populate at least one cache entry (bytecode + cache both on),
17459        // and a substitute-only sibling would leave `cache_size()` at
17460        // zero even after every call site expanded. A regression that
17461        // wired the wrong execution strategy into `with_limits`
17462        // (compile_templates: false, cache_enabled: false) fires here.
17463        let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
17464        let forms = read(
17465            "(defmacro id (x) `,x)
17466             (id one)
17467             (id one)",
17468        )
17469        .unwrap();
17470        e.expand_program(forms).unwrap();
17471        assert!(
17472            e.cache_size() >= 1,
17473            "with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
17474            e.cache_size()
17475        );
17476    }
17477
17478    #[test]
17479    fn expander_with_limits_fires_the_arity_gate_from_construction() {
17480        // Behavioral pin — the at-construction bundle assignment MUST
17481        // reach the SAME check sites the individual + bulk POST-
17482        // construction setters do. Construct an expander with
17483        // `max_macro_arity: 1` directly and confirm the arity gate
17484        // fires on a two-arg macro registration. Sibling of
17485        // `resource_limits_bulk_setter_keeps_the_arity_gate_in_effect`
17486        // one CONSTRUCTOR-STAGE axis over — that pin covers the POST-
17487        // construction setter reaching the gate; this pin covers the
17488        // AT-construction assignment reaching the SAME gate through
17489        // the SAME `self.limits.max_macro_arity` load. A regression
17490        // that wired `with_limits` to a shadow field rather than
17491        // `self.limits` fails here.
17492        let mut e = Expander::with_limits(ResourceLimits {
17493            max_macro_arity: 1,
17494            ..DEFAULT_RESOURCE_LIMITS
17495        });
17496        let forms = read("(defmacro two (a b) `,a)").unwrap();
17497        let err = e.expand_program(forms).unwrap_err();
17498        assert!(
17499            matches!(
17500                err,
17501                LispError::MacroArityExceeded {
17502                    arity: 2,
17503                    limit: 1,
17504                    ..
17505                }
17506            ),
17507            "at-construction arity ceiling must reach the register-time gate; got: {err:?}"
17508        );
17509    }
17510
17511    // ── UNBOUNDED_RESOURCE_LIMITS — ceiling-lifted preset posture ──────
17512
17513    #[test]
17514    fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
17515        // Field-level pin — the ceiling-lifted preset carries
17516        // [`usize::MAX`] on every axis of the six-field surface. Pinned
17517        // as six independent field asserts (rather than one struct
17518        // equality) so a drift on ONE field carries a distinct-name
17519        // failure that names the drifting axis. A future extension
17520        // that adds a SEVENTH ceiling requires an additional field
17521        // assert here in lockstep with the const literal — rustc's
17522        // field-exhaustiveness on the const forces the new field to
17523        // appear, and this pin exercises it.
17524        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
17525        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
17526        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
17527        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
17528        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
17529        assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
17530    }
17531
17532    #[test]
17533    fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
17534        // Structural-distinctness pin — the two constants
17535        // [`DEFAULT_RESOURCE_LIMITS`] and [`UNBOUNDED_RESOURCE_LIMITS`]
17536        // sit at DISTINCT points on the posture axis of the (posture ×
17537        // aggregation) grid, and the disagreement is EXHAUSTIVE on the
17538        // six-field surface — every shipped `DEFAULT_MAX_*` module
17539        // constant is a concrete positive value strictly less than
17540        // [`usize::MAX`]. Pins that a future re-tuning which cranked one
17541        // shipped default up to [`usize::MAX`] (collapsing the two
17542        // presets on that field) fires here loudly rather than
17543        // silently letting the two presets become indistinguishable
17544        // on that axis. A regression which flipped ONE unbounded-const
17545        // field back to its default value also fires here — either
17546        // half of the disagreement breaks the theorem.
17547        assert_ne!(
17548            UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
17549            DEFAULT_RESOURCE_LIMITS.max_expansion_depth
17550        );
17551        assert_ne!(
17552            UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
17553            DEFAULT_RESOURCE_LIMITS.max_cache_entries
17554        );
17555        assert_ne!(
17556            UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
17557            DEFAULT_RESOURCE_LIMITS.max_expansion_size
17558        );
17559        assert_ne!(
17560            UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
17561            DEFAULT_RESOURCE_LIMITS.max_macro_body_size
17562        );
17563        assert_ne!(
17564            UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
17565            DEFAULT_RESOURCE_LIMITS.max_registered_macros
17566        );
17567        assert_ne!(
17568            UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
17569            DEFAULT_RESOURCE_LIMITS.max_macro_arity
17570        );
17571    }
17572
17573    #[test]
17574    fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
17575        // Round-trip pin at CONSTRUCTION time — an expander built at
17576        // the ceiling-lifted preset MUST project back through
17577        // [`Expander::resource_limits`] to the SAME constant verbatim.
17578        // Peer of `expander_with_limits_seeds_the_provided_resource_posture`
17579        // one PRESET-POSTURE axis over — that pin exercises the
17580        // constructor's carry via a distinct-low-value bundle; this
17581        // pin exercises it via the ceiling-lifted preset constant.
17582        // Cross-pin the six-field snapshot equals the ceiling-lifted
17583        // constant so a regression that broke either the constant OR
17584        // the constructor's carry fires here.
17585        assert_eq!(
17586            Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
17587            UNBOUNDED_RESOURCE_LIMITS
17588        );
17589    }
17590
17591    #[test]
17592    fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
17593        // Behavioral pin — the ceiling-lifted preset MUST reach the
17594        // register-time body-size gate as an actually-lifted ceiling.
17595        // Register a macro whose body node count strictly exceeds
17596        // [`DEFAULT_MAX_MACRO_BODY_SIZE`] (a body which under the
17597        // shipped default would fail [`register_macro_def`] with a
17598        // [`LispError::MacroBodySizeExceeded`]) and confirm the
17599        // registration succeeds under the ceiling-lifted preset. Only
17600        // the body-size ceiling being lifted lets a body of that
17601        // magnitude land; the arity ceiling is left at 1 param via
17602        // struct-update override to prove the preset's other five
17603        // ceilings STILL project cleanly at [`usize::MAX`] alongside
17604        // the ceiling under test. A regression that failed to lift
17605        // ONE ceiling across the preset (a stale `DEFAULT_MAX_*` field
17606        // in the const literal) fires here on the axis it dropped.
17607        let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17608        // Build a huge quasi-quoted list body: `(k k k k k … k) whose
17609        // node_count() eclipses DEFAULT_MAX_MACRO_BODY_SIZE = 16384.
17610        let mut body = String::from("(defmacro huge (k) `(");
17611        for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
17612            body.push_str(",k ");
17613        }
17614        body.push_str("))");
17615        let forms = read(&body).unwrap();
17616        e.expand_program(forms)
17617            .expect("unbounded body-size ceiling must admit a body over the default limit");
17618        assert!(e.has("huge"));
17619    }
17620
17621    #[test]
17622    fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
17623        // Behavioral pin sibling of the body-size pin above one
17624        // RESOURCE-DIMENSION axis over on the REGISTER-time surface —
17625        // the ceiling-lifted preset MUST also reach the arity gate as
17626        // an actually-lifted ceiling. Register a macro whose param
17627        // list strictly exceeds [`DEFAULT_MAX_MACRO_ARITY`] (a
17628        // 129-slot lambda list which under the shipped default fails
17629        // with [`LispError::MacroArityExceeded`]) and confirm the
17630        // registration succeeds under the ceiling-lifted preset.
17631        use std::fmt::Write as _;
17632        let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17633        let mut src = String::from("(defmacro many-arity (");
17634        for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
17635            write!(src, "a-{i} ").unwrap();
17636        }
17637        src.push_str(") `,a-0)");
17638        let forms = read(&src).unwrap();
17639        e.expand_program(forms)
17640            .expect("unbounded arity ceiling must admit a param list over the default limit");
17641        assert!(e.has("many-arity"));
17642    }
17643
17644    #[test]
17645    fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
17646        // Ergonomic pin — the `Copy` posture on [`ResourceLimits`] lets
17647        // a caller build "every ceiling lifted EXCEPT this ONE" as ONE
17648        // struct-update literal on the ceiling-lifted preset AND thread
17649        // that composition through [`Expander::with_limits`] as ONE
17650        // typed call. This is the "only THIS ceiling gates" fixture
17651        // shape a chain of six independent setters cannot express in
17652        // ONE expression, and the shape the last commit named as a
17653        // future benefit of the (getter, setter, at-construction)
17654        // three-corner face closing at ONE bundle.
17655        //
17656        // Set only `max_macro_arity: 1`; confirm the arity gate fires
17657        // on a two-arg registration while the five other lifted
17658        // ceilings project through the getter as [`usize::MAX`]. A
17659        // regression that broke the ceiling-lifted preset would EITHER
17660        // fail to project [`usize::MAX`] on one of the five other
17661        // fields OR fail to gate the isolated arity ceiling at 1.
17662        let mut e = Expander::with_limits(ResourceLimits {
17663            max_macro_arity: 1,
17664            ..UNBOUNDED_RESOURCE_LIMITS
17665        });
17666        assert_eq!(e.max_macro_arity(), 1);
17667        assert_eq!(e.max_expansion_depth(), usize::MAX);
17668        assert_eq!(e.max_cache_entries(), usize::MAX);
17669        assert_eq!(e.max_expansion_size(), usize::MAX);
17670        assert_eq!(e.max_macro_body_size(), usize::MAX);
17671        assert_eq!(e.max_registered_macros(), usize::MAX);
17672        let forms = read("(defmacro two (a b) `,a)").unwrap();
17673        let err = e.expand_program(forms).unwrap_err();
17674        assert!(
17675            matches!(
17676                err,
17677                LispError::MacroArityExceeded {
17678                    arity: 2,
17679                    limit: 1,
17680                    ..
17681                }
17682            ),
17683            "struct-update override on the ceiling-lifted preset must \
17684             leave the isolated arity ceiling gating; got: {err:?}"
17685        );
17686    }
17687
17688    #[test]
17689    fn unbounded_resource_limits_carries_through_the_bulk_setter() {
17690        // Coherence pin between the AT-construction constructor and
17691        // the POST-construction bulk setter — both paths that reach the
17692        // ceiling-lifted preset must land at the SAME six values on
17693        // the expander state. Pre-lift a caller wanting the unbounded
17694        // posture composed six individual setter invocations at the
17695        // call site; post-lift both `Expander::with_limits(UNBOUNDED_
17696        // RESOURCE_LIMITS)` and `e.set_resource_limits(UNBOUNDED_
17697        // RESOURCE_LIMITS)` reach the SAME snapshot through the SAME
17698        // constant. A regression that wired the bulk setter to a
17699        // shadow field, or the ceiling-lifted preset to a stale field
17700        // literal, fires here on the constructor-vs-setter identity.
17701        let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17702        let mut b = Expander::new();
17703        b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
17704        assert_eq!(a.resource_limits(), b.resource_limits());
17705        assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
17706    }
17707
17708    // ── ResourceLimits::strictest / ::most_permissive — meet/join ──────
17709
17710    /// A hand-authored asymmetric posture used across the lattice-law
17711    /// tests. Every field is a distinct positive value strictly less
17712    /// than [`usize::MAX`] and distinct from every corresponding
17713    /// [`DEFAULT_MAX_*`] module constant, so composition witnesses
17714    /// (meet, join, absorption, distributivity) discriminate across
17715    /// all three postures unambiguously.
17716    const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
17717        max_expansion_depth: 7,
17718        max_cache_entries: 11,
17719        max_expansion_size: 13,
17720        max_macro_body_size: 17,
17721        max_registered_macros: 19,
17722        max_macro_arity: 23,
17723    };
17724
17725    /// A second asymmetric posture whose per-axis values are on both
17726    /// sides of `HAND_AUTHORED_MID_POSTURE`'s values — three axes
17727    /// smaller (so the meet picks THIS posture on those axes and the
17728    /// join picks `MID` on those axes) and three axes larger (mirror).
17729    /// Together the two postures exercise every ordering combination
17730    /// on the six-axis pointwise `min`/`max` cascade.
17731    const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
17732        max_expansion_depth: 3,   // smaller than MID's 7
17733        max_cache_entries: 29,    // larger  than MID's 11
17734        max_expansion_size: 5,    // smaller than MID's 13
17735        max_macro_body_size: 31,  // larger  than MID's 17
17736        max_registered_macros: 2, // smaller than MID's 19
17737        max_macro_arity: 41,      // larger  than MID's 23
17738    };
17739
17740    #[test]
17741    fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
17742        // Concrete-preset pin — the meet of the (shipped default,
17743        // ceiling-lifted unbounded) preset pair is the DEFAULT preset
17744        // structurally: every `DEFAULT_MAX_*` module constant is a
17745        // concrete positive value strictly less than [`usize::MAX`],
17746        // so on every axis the pointwise-`min` picks the DEFAULT side.
17747        // Peer of `most_permissive_of_default_and_unbounded_projects_the_unbounded`
17748        // one COMBINATOR axis over on the same shipped-preset-pair
17749        // surface.
17750        assert_eq!(
17751            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17752            DEFAULT_RESOURCE_LIMITS,
17753        );
17754        assert_eq!(
17755            UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17756            DEFAULT_RESOURCE_LIMITS,
17757        );
17758    }
17759
17760    #[test]
17761    fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
17762        // Concrete-preset pin — the join of the (shipped default,
17763        // ceiling-lifted unbounded) preset pair is the UNBOUNDED
17764        // preset structurally: every axis's pointwise-`max` picks the
17765        // [`usize::MAX`] side over any concrete default. Peer of
17766        // `strictest_of_default_and_unbounded_projects_the_default`.
17767        assert_eq!(
17768            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17769            UNBOUNDED_RESOURCE_LIMITS,
17770        );
17771        assert_eq!(
17772            UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17773            UNBOUNDED_RESOURCE_LIMITS,
17774        );
17775    }
17776
17777    #[test]
17778    fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
17779        // Field-level pin — the meet operation projects the pointwise
17780        // `min` across the six-field surface. Pinned as six
17781        // independent field asserts (rather than one struct equality)
17782        // so a drift on ONE axis carries a distinct-name failure that
17783        // names the drifting axis; a future SEVENTH ceiling extension
17784        // requires an additional field assert here in lockstep.
17785        let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
17786        assert_eq!(m.max_expansion_depth, 3);
17787        assert_eq!(m.max_cache_entries, 11);
17788        assert_eq!(m.max_expansion_size, 5);
17789        assert_eq!(m.max_macro_body_size, 17);
17790        assert_eq!(m.max_registered_macros, 2);
17791        assert_eq!(m.max_macro_arity, 23);
17792    }
17793
17794    #[test]
17795    fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
17796        // Field-level pin — the join operation projects the pointwise
17797        // `max` across the six-field surface. Pinned as six
17798        // independent field asserts so a drift on ONE axis carries a
17799        // distinct-name failure that names the drifting axis.
17800        let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
17801        assert_eq!(j.max_expansion_depth, 7);
17802        assert_eq!(j.max_cache_entries, 29);
17803        assert_eq!(j.max_expansion_size, 13);
17804        assert_eq!(j.max_macro_body_size, 31);
17805        assert_eq!(j.max_registered_macros, 19);
17806        assert_eq!(j.max_macro_arity, 41);
17807    }
17808
17809    #[test]
17810    fn resource_limits_strictest_is_idempotent() {
17811        // Lattice law — `a ∧ a = a`. Every posture is a fixed point of
17812        // its own meet with itself; a regression that changed the
17813        // per-axis primitive to something other than pointwise `min`
17814        // (a `min - 1` off-by-one, or an arbitrary tie-breaker on
17815        // equal values) fires here.
17816        assert_eq!(
17817            DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17818            DEFAULT_RESOURCE_LIMITS,
17819        );
17820        assert_eq!(
17821            UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17822            UNBOUNDED_RESOURCE_LIMITS,
17823        );
17824        assert_eq!(
17825            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17826            HAND_AUTHORED_MID_POSTURE,
17827        );
17828    }
17829
17830    #[test]
17831    fn resource_limits_most_permissive_is_idempotent() {
17832        // Lattice law — `a ∨ a = a`. Sibling of the strictest
17833        // idempotence pin one COMBINATOR axis over.
17834        assert_eq!(
17835            DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17836            DEFAULT_RESOURCE_LIMITS,
17837        );
17838        assert_eq!(
17839            UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17840            UNBOUNDED_RESOURCE_LIMITS,
17841        );
17842        assert_eq!(
17843            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17844            HAND_AUTHORED_MID_POSTURE,
17845        );
17846    }
17847
17848    #[test]
17849    fn resource_limits_strictest_is_commutative() {
17850        // Lattice law — `a ∧ b = b ∧ a`. Pointwise `min` is symmetric
17851        // on every axis, so the composition inherits commutativity
17852        // structurally. A regression that broke ordering (e.g. a
17853        // per-axis "prefer left on tie") fires here.
17854        assert_eq!(
17855            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
17856            HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17857        );
17858        assert_eq!(
17859            DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
17860            HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
17861        );
17862    }
17863
17864    #[test]
17865    fn resource_limits_most_permissive_is_commutative() {
17866        // Lattice law — `a ∨ b = b ∨ a`. Sibling of the strictest
17867        // commutativity pin one COMBINATOR axis over.
17868        assert_eq!(
17869            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
17870            HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17871        );
17872        assert_eq!(
17873            DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
17874            HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
17875        );
17876    }
17877
17878    #[test]
17879    fn resource_limits_strictest_is_associative() {
17880        // Lattice law — `(a ∧ b) ∧ c = a ∧ (b ∧ c)`. Pointwise `min`
17881        // is associative on every axis, so the composition inherits
17882        // associativity structurally. A regression that broke
17883        // grouping (e.g. an accidental accumulator that skewed the
17884        // order of `min` invocations) fires here.
17885        let a = DEFAULT_RESOURCE_LIMITS;
17886        let b = HAND_AUTHORED_MID_POSTURE;
17887        let c = HAND_AUTHORED_OTHER_POSTURE;
17888        assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
17889    }
17890
17891    #[test]
17892    fn resource_limits_most_permissive_is_associative() {
17893        // Lattice law — `(a ∨ b) ∨ c = a ∨ (b ∨ c)`. Sibling of the
17894        // strictest associativity pin one COMBINATOR axis over.
17895        let a = DEFAULT_RESOURCE_LIMITS;
17896        let b = HAND_AUTHORED_MID_POSTURE;
17897        let c = HAND_AUTHORED_OTHER_POSTURE;
17898        assert_eq!(
17899            a.most_permissive(b).most_permissive(c),
17900            a.most_permissive(b.most_permissive(c)),
17901        );
17902    }
17903
17904    #[test]
17905    fn resource_limits_meet_and_join_satisfy_absorption() {
17906        // Lattice absorption identities — `a ∧ (a ∨ b) = a` AND
17907        // `a ∨ (a ∧ b) = a`. Together with commutativity, idempotence,
17908        // and associativity they define the lattice axioms; the two
17909        // absorption laws are the operator PAIR's interlock (each
17910        // undoes the other's contribution on the shared operand).
17911        // Any per-axis primitive pair for which `min(a, max(a, b)) = a`
17912        // AND `max(a, min(a, b)) = a` inherits this at the pointwise
17913        // composition; pointwise-`min`/`max` on [`usize`] satisfy both.
17914        let a = HAND_AUTHORED_MID_POSTURE;
17915        let b = HAND_AUTHORED_OTHER_POSTURE;
17916        assert_eq!(a.strictest(a.most_permissive(b)), a);
17917        assert_eq!(a.most_permissive(a.strictest(b)), a);
17918    }
17919
17920    #[test]
17921    fn resource_limits_meet_distributes_over_join() {
17922        // Distributive-lattice pin — `a ∧ (b ∨ c) = (a ∧ b) ∨ (a ∧ c)`.
17923        // Pointwise-`min` distributes over pointwise-`max` on [`usize`]
17924        // on every axis, so the composition inherits distributivity
17925        // structurally. A regression that broke the pointwise
17926        // structure (e.g. a per-axis operation that used SUBTRACTION
17927        // rather than MIN/MAX) fires here — subtraction does NOT
17928        // distribute over addition and would break the law on the
17929        // asymmetric-posture witness pair.
17930        let a = DEFAULT_RESOURCE_LIMITS;
17931        let b = HAND_AUTHORED_MID_POSTURE;
17932        let c = HAND_AUTHORED_OTHER_POSTURE;
17933        assert_eq!(
17934            a.strictest(b.most_permissive(c)),
17935            a.strictest(b).most_permissive(a.strictest(c)),
17936        );
17937    }
17938
17939    #[test]
17940    fn resource_limits_join_distributes_over_meet() {
17941        // Distributive-lattice pin, sibling of the meet-over-join
17942        // distributivity pin one COMBINATOR axis over — `a ∨ (b ∧ c) =
17943        // (a ∨ b) ∧ (a ∨ c)`. In a distributive lattice BOTH
17944        // distributivity identities hold; pin both so a regression
17945        // that broke ONE without the other cannot slip through.
17946        let a = DEFAULT_RESOURCE_LIMITS;
17947        let b = HAND_AUTHORED_MID_POSTURE;
17948        let c = HAND_AUTHORED_OTHER_POSTURE;
17949        assert_eq!(
17950            a.most_permissive(b.strictest(c)),
17951            a.most_permissive(b).strictest(a.most_permissive(c)),
17952        );
17953    }
17954
17955    #[test]
17956    fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
17957        // Meet-lower-bound pin — the meet lies at or below BOTH
17958        // operands on every axis. This is the structural GLB property
17959        // of the meet in the pointwise partial order (tighter-first):
17960        // any admissible input under the meet is admissible under
17961        // both operands, so the meet's ceiling must not exceed either
17962        // operand's ceiling on any axis.
17963        let a = HAND_AUTHORED_MID_POSTURE;
17964        let b = HAND_AUTHORED_OTHER_POSTURE;
17965        let m = a.strictest(b);
17966        assert!(m.max_expansion_depth <= a.max_expansion_depth);
17967        assert!(m.max_expansion_depth <= b.max_expansion_depth);
17968        assert!(m.max_cache_entries <= a.max_cache_entries);
17969        assert!(m.max_cache_entries <= b.max_cache_entries);
17970        assert!(m.max_expansion_size <= a.max_expansion_size);
17971        assert!(m.max_expansion_size <= b.max_expansion_size);
17972        assert!(m.max_macro_body_size <= a.max_macro_body_size);
17973        assert!(m.max_macro_body_size <= b.max_macro_body_size);
17974        assert!(m.max_registered_macros <= a.max_registered_macros);
17975        assert!(m.max_registered_macros <= b.max_registered_macros);
17976        assert!(m.max_macro_arity <= a.max_macro_arity);
17977        assert!(m.max_macro_arity <= b.max_macro_arity);
17978    }
17979
17980    #[test]
17981    fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
17982        // Join-upper-bound pin — the join lies at or above BOTH
17983        // operands on every axis. Sibling of the strictest-lower-
17984        // bound pin one COMBINATOR axis over; the structural LUB
17985        // property of the join in the pointwise partial order.
17986        let a = HAND_AUTHORED_MID_POSTURE;
17987        let b = HAND_AUTHORED_OTHER_POSTURE;
17988        let j = a.most_permissive(b);
17989        assert!(j.max_expansion_depth >= a.max_expansion_depth);
17990        assert!(j.max_expansion_depth >= b.max_expansion_depth);
17991        assert!(j.max_cache_entries >= a.max_cache_entries);
17992        assert!(j.max_cache_entries >= b.max_cache_entries);
17993        assert!(j.max_expansion_size >= a.max_expansion_size);
17994        assert!(j.max_expansion_size >= b.max_expansion_size);
17995        assert!(j.max_macro_body_size >= a.max_macro_body_size);
17996        assert!(j.max_macro_body_size >= b.max_macro_body_size);
17997        assert!(j.max_registered_macros >= a.max_registered_macros);
17998        assert!(j.max_registered_macros >= b.max_registered_macros);
17999        assert!(j.max_macro_arity >= a.max_macro_arity);
18000        assert!(j.max_macro_arity >= b.max_macro_arity);
18001    }
18002
18003    #[test]
18004    fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
18005        // Const-fn pin — the meet is evaluable in const context, so a
18006        // caller can stitch a new `pub const` preset from two shipped
18007        // presets without deferring the composition to runtime. Pinned
18008        // as a `const` binding at the item level so the compiler
18009        // exercises the const-fn path structurally; the runtime
18010        // assertion is redundant for a `const` value that already
18011        // evaluated at compile time, but pins the identity as a typed
18012        // theorem alongside every OTHER `strictest` pin above.
18013        const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
18014            DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
18015        assert_eq!(
18016            DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
18017            min_usize(
18018                DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18019                HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
18020            ),
18021        );
18022        // Extra check: the composition MUST project the MID posture's
18023        // small value on every axis where MID is smaller than DEFAULT
18024        // — MID's per-axis values (7, 11, 13, 17, 19, 23) are all
18025        // strictly less than every `DEFAULT_MAX_*` constant, so the
18026        // meet projects MID verbatim.
18027        assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
18028    }
18029
18030    #[test]
18031    fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
18032        // Const-fn pin sibling of the strictest const-composition pin
18033        // one COMBINATOR axis over — the join is also `const fn`, so
18034        // a caller can stitch a new `pub const` preset via the LUB
18035        // combinator at compile time.
18036        const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
18037            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
18038        assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
18039    }
18040
18041    // ── ResourceLimits::leq — pointwise partial order ─────────────────
18042
18043    #[test]
18044    fn resource_limits_leq_is_pointwise_field_conjunction() {
18045        // Field-level pin — the leq relation projects the pointwise
18046        // `<=` conjunction across the six-field surface. `LOOSER` is
18047        // `MID + 1` on every axis, so `MID.leq(LOOSER)` holds; a
18048        // per-axis tighten on ANY field alone flips the relation to
18049        // false. Pinned as six independent tighten-per-axis asserts so
18050        // a drift on ONE axis carries a distinct-name failure that
18051        // names the drifting axis; a future SEVENTH ceiling extension
18052        // requires an additional per-axis tighten assert here in
18053        // lockstep.
18054        const LOOSER: ResourceLimits = ResourceLimits {
18055            max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18056            max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18057            max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18058            max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18059            max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18060            max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18061        };
18062        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18063        assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
18064
18065        // Per-axis exceed pins: raise MID's field ONE axis above LOOSER
18066        // on that axis; the leq must flip to false. Every other axis
18067        // stays at MID, so LOOSER still dominates on those axes —
18068        // isolating the single-axis violation.
18069        let exceed_depth = ResourceLimits {
18070            max_expansion_depth: LOOSER.max_expansion_depth + 1,
18071            ..HAND_AUTHORED_MID_POSTURE
18072        };
18073        assert!(!exceed_depth.leq(LOOSER));
18074        let exceed_cache = ResourceLimits {
18075            max_cache_entries: LOOSER.max_cache_entries + 1,
18076            ..HAND_AUTHORED_MID_POSTURE
18077        };
18078        assert!(!exceed_cache.leq(LOOSER));
18079        let exceed_expansion = ResourceLimits {
18080            max_expansion_size: LOOSER.max_expansion_size + 1,
18081            ..HAND_AUTHORED_MID_POSTURE
18082        };
18083        assert!(!exceed_expansion.leq(LOOSER));
18084        let exceed_body = ResourceLimits {
18085            max_macro_body_size: LOOSER.max_macro_body_size + 1,
18086            ..HAND_AUTHORED_MID_POSTURE
18087        };
18088        assert!(!exceed_body.leq(LOOSER));
18089        let exceed_registered = ResourceLimits {
18090            max_registered_macros: LOOSER.max_registered_macros + 1,
18091            ..HAND_AUTHORED_MID_POSTURE
18092        };
18093        assert!(!exceed_registered.leq(LOOSER));
18094        let exceed_arity = ResourceLimits {
18095            max_macro_arity: LOOSER.max_macro_arity + 1,
18096            ..HAND_AUTHORED_MID_POSTURE
18097        };
18098        assert!(!exceed_arity.leq(LOOSER));
18099    }
18100
18101    #[test]
18102    fn resource_limits_leq_is_reflexive() {
18103        // Partial-order law — `a ≤ a` for every posture. Reflexivity
18104        // is the identity axiom on any lattice's underlying partial
18105        // order; a regression that changed the per-axis primitive to
18106        // strict `<` (or that added an accidental "prefer other on
18107        // tie" branch) fires here on every shipped posture.
18108        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18109        assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18110        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18111        assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18112    }
18113
18114    #[test]
18115    fn resource_limits_leq_is_antisymmetric() {
18116        // Partial-order law — `a ≤ b ∧ b ≤ a ⇒ a = b`. The mutual-
18117        // dominance witness on a partial order pins the two operands
18118        // to structural equality. On the pointwise `<=` composition
18119        // this holds because `a.max_X <= b.max_X && b.max_X <= a.max_X
18120        // ⇒ a.max_X == b.max_X` on every axis, and struct equality
18121        // is field-conjunction. Sibling to reflexivity one AXIOM axis
18122        // over on the partial-order axiom surface.
18123        //
18124        // Two distinct constructions of the SAME six-field posture
18125        // (one via struct literal, one via `..DEFAULT_RESOURCE_LIMITS`
18126        // spread of the shipped preset) — mutual leq holds; struct
18127        // equality holds; antisymmetry pin closes.
18128        let a = DEFAULT_RESOURCE_LIMITS;
18129        let b = ResourceLimits {
18130            max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18131            max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
18132            max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
18133            max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
18134            max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
18135            max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
18136        };
18137        assert!(a.leq(b));
18138        assert!(b.leq(a));
18139        assert_eq!(a, b);
18140    }
18141
18142    #[test]
18143    fn resource_limits_leq_is_transitive() {
18144        // Partial-order law — `a ≤ b ∧ b ≤ c ⇒ a ≤ c`. Transitivity
18145        // is the composition axiom on any lattice's partial order.
18146        // Pointwise `<=` on [`usize`] is transitive per axis, so the
18147        // six-field conjunction inherits it structurally.
18148        //
18149        // Chain witness: MID's every-axis `+1` shift is LOOSER, and
18150        // LOOSER's every-axis `+1` shift is LOOSEST; the chain MID
18151        // ≤ LOOSER ≤ LOOSEST implies MID ≤ LOOSEST.
18152        const LOOSER: ResourceLimits = ResourceLimits {
18153            max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18154            max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18155            max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18156            max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18157            max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18158            max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18159        };
18160        const LOOSEST: ResourceLimits = ResourceLimits {
18161            max_expansion_depth: LOOSER.max_expansion_depth + 1,
18162            max_cache_entries: LOOSER.max_cache_entries + 1,
18163            max_expansion_size: LOOSER.max_expansion_size + 1,
18164            max_macro_body_size: LOOSER.max_macro_body_size + 1,
18165            max_registered_macros: LOOSER.max_registered_macros + 1,
18166            max_macro_arity: LOOSER.max_macro_arity + 1,
18167        };
18168        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18169        assert!(LOOSER.leq(LOOSEST));
18170        assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
18171    }
18172
18173    #[test]
18174    fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
18175        // Concrete-preset pin — the leq relation on the shipped
18176        // preset pair is STRICT: DEFAULT ≤ UNBOUNDED holds (every
18177        // `DEFAULT_MAX_*` module constant is a concrete positive
18178        // value at most [`usize::MAX`]) AND UNBOUNDED ≤ DEFAULT
18179        // fails (every `DEFAULT_MAX_*` is strictly less than
18180        // [`usize::MAX`], so on every axis [`usize::MAX`] exceeds
18181        // the matching default). Peer of
18182        // `strictest_of_default_and_unbounded_projects_the_default`
18183        // one PRIMITIVE-KIND axis over on the same shipped-preset-
18184        // pair surface: where the meet composition projects DEFAULT,
18185        // this relation asserts DEFAULT is the tighter of the two.
18186        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18187        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18188    }
18189
18190    #[test]
18191    fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
18192        // Partial-order NON-total pin — the two hand-authored asymmetric
18193        // postures have MID smaller on three axes (depth, expansion,
18194        // registered) and OTHER smaller on the other three (cache,
18195        // body, arity); neither dominates the other, so BOTH
18196        // directions of the leq relation fail. The pin discriminates
18197        // a regression that promoted `leq` from partial to total
18198        // (e.g. an accidental `||` swap in the conjunction, which
18199        // would make every pair comparable).
18200        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18201        assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18202    }
18203
18204    #[test]
18205    fn resource_limits_leq_agrees_with_meet() {
18206        // Lattice cross-check axiom — `a ≤ b ⇔ a ⊓ b = a`. Every
18207        // classification lattice one crate over
18208        // (`tatara-lattice/src/lib.rs`'s preamble) declares this
18209        // agreement axiom binding the partial order to the meet
18210        // combinator, and the default-impl body on
18211        // `tatara_lattice::Lattice::leq` derives `leq` FROM `meet`
18212        // through it. This pin discharges the same axiom on the
18213        // ResourceLimits algebra as a two-direction structural
18214        // equality across the strict-order preset pair AND the
18215        // reflexivity witness.
18216        //
18217        // Forward (a ≤ b ⇒ a ⊓ b = a):
18218        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18219        assert_eq!(
18220            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18221            DEFAULT_RESOURCE_LIMITS,
18222        );
18223        // Reverse (a ⊓ b = a ⇒ a ≤ b), reflexivity witness:
18224        assert_eq!(
18225            DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18226            DEFAULT_RESOURCE_LIMITS,
18227        );
18228        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18229
18230        // Non-agreement witness — the incomparable asymmetric postures
18231        // have meet distinct from EITHER operand, and leq false in
18232        // both directions; agreement holds on the negative side too.
18233        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18234        assert_ne!(
18235            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18236            HAND_AUTHORED_MID_POSTURE,
18237        );
18238    }
18239
18240    #[test]
18241    fn resource_limits_leq_agrees_with_join() {
18242        // Lattice cross-check axiom, sibling of the meet-agreement
18243        // pin one COMBINATOR axis over — `a ≤ b ⇔ a ⊔ b = b`.
18244        //
18245        // Forward (a ≤ b ⇒ a ⊔ b = b):
18246        assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18247        assert_eq!(
18248            DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18249            UNBOUNDED_RESOURCE_LIMITS,
18250        );
18251        // Reverse (a ⊔ b = b ⇒ a ≤ b), reflexivity witness:
18252        assert_eq!(
18253            DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18254            DEFAULT_RESOURCE_LIMITS,
18255        );
18256        assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18257
18258        // Non-agreement witness — the incomparable asymmetric postures
18259        // have join distinct from EITHER operand.
18260        assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18261        assert_ne!(
18262            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18263            HAND_AUTHORED_OTHER_POSTURE,
18264        );
18265    }
18266
18267    #[test]
18268    fn resource_limits_strictest_is_leq_both_operands() {
18269        // Meet-lower-bound pin, typed lattice-relation companion to
18270        // `resource_limits_strictest_is_dominated_by_both_operands_pointwise`
18271        // one PRIMITIVE-KIND axis over — where the per-axis pin
18272        // discharged the six independent `<=` asserts inline, this pin
18273        // routes through the typed `leq` primitive, so the
18274        // greatest-lower-bound property is stated in the LATTICE
18275        // vocabulary (meet-is-leq-both-operands) rather than the
18276        // six-fold field-conjunction vocabulary.
18277        let a = HAND_AUTHORED_MID_POSTURE;
18278        let b = HAND_AUTHORED_OTHER_POSTURE;
18279        let m = a.strictest(b);
18280        assert!(m.leq(a));
18281        assert!(m.leq(b));
18282    }
18283
18284    #[test]
18285    fn resource_limits_most_permissive_is_geq_both_operands() {
18286        // Join-upper-bound pin, typed lattice-relation companion to
18287        // `resource_limits_most_permissive_dominates_both_operands_pointwise`
18288        // one PRIMITIVE-KIND axis over. Stated in the LATTICE
18289        // vocabulary as `a ≤ a ⊔ b ∧ b ≤ a ⊔ b` — sibling of the
18290        // meet-lower-bound pin one COMBINATOR axis over.
18291        let a = HAND_AUTHORED_MID_POSTURE;
18292        let b = HAND_AUTHORED_OTHER_POSTURE;
18293        let j = a.most_permissive(b);
18294        assert!(a.leq(j));
18295        assert!(b.leq(j));
18296    }
18297
18298    #[test]
18299    fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
18300        // Const-fn pin — the leq relation is evaluable in const
18301        // context, so a caller can pin a preset-relation identity at
18302        // compile time. Sibling of the const-fn composition pins on
18303        // `strictest` / `most_permissive` one PRIMITIVE-KIND axis
18304        // over: those two operations compose two presets into a
18305        // const-time third, and this operation decides a const-time
18306        // yes/no on two presets. `const _: ()` at item scope is a
18307        // compile-time proof — the compiler rejects the module if
18308        // either identity does not hold; a runtime `assert!` over the
18309        // same expression would fire at test time on the same value
18310        // clippy's `assertions_on_constants` correctly names as a
18311        // redundant late check on a compile-time constant.
18312        //
18313        // Strict-order preset-pair pin — DEFAULT ≤ UNBOUNDED (every
18314        // `DEFAULT_MAX_*` module constant is at most [`usize::MAX`]):
18315        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18316        // Reverse direction fails — [`usize::MAX`] exceeds every
18317        // concrete `DEFAULT_MAX_*` constant, so the partial order is
18318        // STRICT on the shipped preset pair:
18319        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18320    }
18321
18322    // ── EMPTY_RESOURCE_LIMITS — bounded-lattice bottom preset ─────────
18323
18324    #[test]
18325    fn empty_resource_limits_binds_every_ceiling_to_zero() {
18326        // Field-level pin — the zero preset carries `0` on every axis
18327        // of the six-field surface. Pinned as six independent field
18328        // asserts (rather than one struct equality) so a drift on ONE
18329        // field carries a distinct-name failure that names the drifting
18330        // axis. A future extension that adds a SEVENTH ceiling requires
18331        // an additional field assert here in lockstep with the const
18332        // literal — rustc's field-exhaustiveness on the const forces
18333        // the new field to appear, and this pin exercises the seed
18334        // value. Structural peer of
18335        // `unbounded_resource_limits_binds_every_ceiling_to_usize_max`
18336        // one LATTICE-POLE axis over on the (bottom, top)
18337        // bounded-lattice preset-pair surface.
18338        assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
18339        assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
18340        assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
18341        assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
18342        assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
18343        assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
18344    }
18345
18346    #[test]
18347    fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
18348        // Structural-distinctness pin — the two constants
18349        // [`DEFAULT_RESOURCE_LIMITS`] and [`EMPTY_RESOURCE_LIMITS`] sit
18350        // at DISTINCT points on the preset-posture axis, and the
18351        // disagreement is EXHAUSTIVE on the six-field surface — every
18352        // shipped `DEFAULT_MAX_*` module constant is a concrete
18353        // strictly-positive value, so every axis distinguishes DEFAULT
18354        // from the all-zero bottom. Peer of
18355        // `unbounded_resource_limits_disagrees_with_default_on_every_ceiling`
18356        // one LATTICE-POLE axis over.
18357        assert_ne!(
18358            EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18359            DEFAULT_RESOURCE_LIMITS.max_expansion_depth
18360        );
18361        assert_ne!(
18362            EMPTY_RESOURCE_LIMITS.max_cache_entries,
18363            DEFAULT_RESOURCE_LIMITS.max_cache_entries
18364        );
18365        assert_ne!(
18366            EMPTY_RESOURCE_LIMITS.max_expansion_size,
18367            DEFAULT_RESOURCE_LIMITS.max_expansion_size
18368        );
18369        assert_ne!(
18370            EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18371            DEFAULT_RESOURCE_LIMITS.max_macro_body_size
18372        );
18373        assert_ne!(
18374            EMPTY_RESOURCE_LIMITS.max_registered_macros,
18375            DEFAULT_RESOURCE_LIMITS.max_registered_macros
18376        );
18377        assert_ne!(
18378            EMPTY_RESOURCE_LIMITS.max_macro_arity,
18379            DEFAULT_RESOURCE_LIMITS.max_macro_arity
18380        );
18381    }
18382
18383    #[test]
18384    fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
18385        // Diagonal-distinctness pin — the (BOTTOM, TOP) preset pair
18386        // disagrees on every axis (`0 != usize::MAX` on every field).
18387        // Structural companion to the DEFAULT-vs-EMPTY and
18388        // UNBOUNDED-vs-DEFAULT disagreement pins one PRESET-PAIR axis
18389        // over — together the three pin the six-field surface at the
18390        // full diagonal of the (EMPTY, DEFAULT, UNBOUNDED) triple.
18391        assert_ne!(
18392            EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18393            UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
18394        );
18395        assert_ne!(
18396            EMPTY_RESOURCE_LIMITS.max_cache_entries,
18397            UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
18398        );
18399        assert_ne!(
18400            EMPTY_RESOURCE_LIMITS.max_expansion_size,
18401            UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
18402        );
18403        assert_ne!(
18404            EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18405            UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
18406        );
18407        assert_ne!(
18408            EMPTY_RESOURCE_LIMITS.max_registered_macros,
18409            UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
18410        );
18411        assert_ne!(
18412            EMPTY_RESOURCE_LIMITS.max_macro_arity,
18413            UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
18414        );
18415    }
18416
18417    #[test]
18418    fn empty_resource_limits_is_the_join_identity() {
18419        // Lattice-identity law — `a ⊔ ⊥ = a`. Every posture's
18420        // pointwise `max` against 0 returns the posture verbatim, so
18421        // [`EMPTY_RESOURCE_LIMITS`] acts as the identity element of
18422        // the join monoid. Peer of
18423        // `empty_resource_limits_is_the_meet_annihilator` one
18424        // COMBINATOR axis over on the (meet, join) surface.
18425        //
18426        // Cross-preset exhaustion — verify the identity against every
18427        // named preset (DEFAULT, UNBOUNDED, both hand-authored
18428        // asymmetric postures) so a regression that broke ONE axis of
18429        // the algebra fires here on the operand whose field crosses
18430        // that axis's boundary.
18431        assert_eq!(
18432            DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18433            DEFAULT_RESOURCE_LIMITS,
18434        );
18435        assert_eq!(
18436            EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18437            DEFAULT_RESOURCE_LIMITS,
18438        );
18439        assert_eq!(
18440            UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18441            UNBOUNDED_RESOURCE_LIMITS,
18442        );
18443        assert_eq!(
18444            EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18445            UNBOUNDED_RESOURCE_LIMITS,
18446        );
18447        assert_eq!(
18448            HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18449            HAND_AUTHORED_MID_POSTURE,
18450        );
18451        assert_eq!(
18452            HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18453            HAND_AUTHORED_OTHER_POSTURE,
18454        );
18455        // Idempotence at the identity itself — sibling witness of the
18456        // `most_permissive_is_idempotent` general pin one PRESET axis
18457        // over.
18458        assert_eq!(
18459            EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18460            EMPTY_RESOURCE_LIMITS,
18461        );
18462    }
18463
18464    #[test]
18465    fn empty_resource_limits_is_the_meet_annihilator() {
18466        // Lattice-annihilator law — `a ⊓ ⊥ = ⊥`. Every posture's
18467        // pointwise `min` against 0 returns 0 on every axis, so
18468        // [`EMPTY_RESOURCE_LIMITS`] acts as the annihilator (absorbing
18469        // element) of the meet operation. Peer of
18470        // `empty_resource_limits_is_the_join_identity` one COMBINATOR
18471        // axis over — the identity on one side of the bounded lattice
18472        // is the annihilator on the other side.
18473        assert_eq!(
18474            DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18475            EMPTY_RESOURCE_LIMITS,
18476        );
18477        assert_eq!(
18478            EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18479            EMPTY_RESOURCE_LIMITS,
18480        );
18481        assert_eq!(
18482            UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18483            EMPTY_RESOURCE_LIMITS,
18484        );
18485        assert_eq!(
18486            EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18487            EMPTY_RESOURCE_LIMITS,
18488        );
18489        assert_eq!(
18490            HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18491            EMPTY_RESOURCE_LIMITS,
18492        );
18493        assert_eq!(
18494            HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18495            EMPTY_RESOURCE_LIMITS,
18496        );
18497    }
18498
18499    #[test]
18500    fn empty_resource_limits_is_the_lattice_minimum() {
18501        // Partial-order minimum law — `⊥ ≤ a` for every `a`. Every
18502        // posture's field is a `usize` and `0 <= x` holds for every
18503        // `usize x`, so [`EMPTY_RESOURCE_LIMITS`] sits at or below
18504        // every posture on every axis of the pointwise `<=` conjunction.
18505        // Peer of the UNBOUNDED-is-maximum property on the
18506        // partial-order face one LATTICE-POLE axis over — those two
18507        // pin the (min, max) bounds of the bounded lattice.
18508        assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18509        assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18510        assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18511        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
18512        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
18513        // Reverse direction FAILS on every non-EMPTY preset (each has
18514        // at least one field strictly greater than 0). The partial
18515        // order is STRICT between EMPTY and every other named preset,
18516        // structurally exhausting the diagonal of the
18517        // (EMPTY, DEFAULT, UNBOUNDED, MID, OTHER) 5-preset ordering
18518        // face at the BOTTOM row.
18519        assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18520        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18521        assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18522        assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18523    }
18524
18525    #[test]
18526    fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
18527        // Const-fn pin — the bounded-lattice identity + annihilator +
18528        // minimum laws hold at COMPILE time. Sibling of
18529        // `resource_limits_leq_evaluates_at_compile_time_via_const_fn`
18530        // one LATTICE-STRUCTURE axis over — that pin fixed the strict
18531        // order of the (DEFAULT, UNBOUNDED) preset pair at compile
18532        // time; this pin fixes the bounded-lattice pole role of the
18533        // (EMPTY, UNBOUNDED) preset pair. `const _: ()` at item scope
18534        // is a compile-time proof — a regression that broke either
18535        // identity element or the bottom-is-minimum invariant fires at
18536        // rustc time, not test-run time.
18537        //
18538        // Bottom-is-minimum: EMPTY ≤ DEFAULT ≤ UNBOUNDED (the
18539        // three-preset ordering chain on the shipped preset diagonal).
18540        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18541        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18542        // Reverse strictness — neither DEFAULT nor UNBOUNDED sits
18543        // below EMPTY (each has a strictly-positive field).
18544        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18545        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18546        // Join-identity + meet-annihilator laws cross-pinned through
18547        // the `leq` primitive at const time — the round-trip identity
18548        // (`a ⊔ ⊥ = a ⇔ ⊥ ≤ a`, `a ⊓ ⊥ = ⊥ ⇔ ⊥ ≤ a`) means the two
18549        // leq asserts above discharge both algebraic laws by the
18550        // `leq_agrees_with_meet` + `leq_agrees_with_join` lattice
18551        // cross-check axiom already pinned in this cohort.
18552    }
18553
18554    #[test]
18555    fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
18556        // Fold-identity behavioral pin — a slice of postures folded
18557        // through [`ResourceLimits::most_permissive`] with EMPTY as
18558        // the seed produces the pointwise least upper bound across
18559        // the slice on every axis. The join-identity property
18560        // (`a.most_permissive(EMPTY) == a`) guarantees the seed
18561        // contributes nothing to the fold's result — every field's
18562        // pointwise `max` against the seed's `0` returns the field's
18563        // value verbatim, so the fold reduces to the joined-across-
18564        // -operands upper bound. This is the destination usage the
18565        // constant's docstring names as its fold-identity role.
18566        //
18567        // A regression that seeded from ANY non-empty posture (e.g.
18568        // `DEFAULT_RESOURCE_LIMITS`) would inflate the fold's result
18569        // on every axis where the seed's field exceeds the pointwise
18570        // max of the slice — fires as a distinct-axis inequality here.
18571        let postures: [ResourceLimits; 3] = [
18572            HAND_AUTHORED_MID_POSTURE,
18573            HAND_AUTHORED_OTHER_POSTURE,
18574            DEFAULT_RESOURCE_LIMITS,
18575        ];
18576        let joined = postures
18577            .iter()
18578            .copied()
18579            .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
18580        // Every operand is `leq` the joined result — the least
18581        // upper bound dominates each contributor on every axis.
18582        assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
18583        assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
18584        assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
18585        // Field-level pin — each axis is the pointwise `max` of the
18586        // three sources' values. Six independent field asserts so a
18587        // drift on ONE axis carries a distinct-name failure.
18588        assert_eq!(
18589            joined.max_expansion_depth,
18590            HAND_AUTHORED_MID_POSTURE
18591                .max_expansion_depth
18592                .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18593                .max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18594        );
18595        assert_eq!(
18596            joined.max_cache_entries,
18597            HAND_AUTHORED_MID_POSTURE
18598                .max_cache_entries
18599                .max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18600                .max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18601        );
18602        assert_eq!(
18603            joined.max_expansion_size,
18604            HAND_AUTHORED_MID_POSTURE
18605                .max_expansion_size
18606                .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18607                .max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18608        );
18609        assert_eq!(
18610            joined.max_macro_body_size,
18611            HAND_AUTHORED_MID_POSTURE
18612                .max_macro_body_size
18613                .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18614                .max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18615        );
18616        assert_eq!(
18617            joined.max_registered_macros,
18618            HAND_AUTHORED_MID_POSTURE
18619                .max_registered_macros
18620                .max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18621                .max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18622        );
18623        assert_eq!(
18624            joined.max_macro_arity,
18625            HAND_AUTHORED_MID_POSTURE
18626                .max_macro_arity
18627                .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18628                .max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18629        );
18630        // Empty-slice edge case — folding over an EMPTY slice returns
18631        // the seed verbatim (the identity element as the null aggregate).
18632        // A wrapper-type approach (`Option<ResourceLimits>` for the
18633        // aggregate) would need a distinct absent branch here; the
18634        // typed identity-element approach resolves it structurally.
18635        let empty_slice: [ResourceLimits; 0] = [];
18636        assert_eq!(
18637            empty_slice
18638                .iter()
18639                .copied()
18640                .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18641            EMPTY_RESOURCE_LIMITS,
18642        );
18643    }
18644
18645    #[test]
18646    fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
18647        // Dual fold-identity pin — a slice of postures folded through
18648        // [`ResourceLimits::strictest`] with UNBOUNDED as the seed
18649        // produces the pointwise greatest lower bound across the slice
18650        // on every axis. Peer of the EMPTY-seeds-most_permissive-fold
18651        // pin one COMBINATOR axis over — the identity element for
18652        // meet is the join's ANNIHILATOR (`UNBOUNDED`), and the
18653        // identity element for join is the meet's annihilator
18654        // (`EMPTY`); the two together bind the bounded-lattice's
18655        // aggregation surface at BOTH poles.
18656        let postures: [ResourceLimits; 3] = [
18657            HAND_AUTHORED_MID_POSTURE,
18658            HAND_AUTHORED_OTHER_POSTURE,
18659            DEFAULT_RESOURCE_LIMITS,
18660        ];
18661        let met = postures
18662            .iter()
18663            .copied()
18664            .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
18665        // The greatest lower bound sits `leq` every operand.
18666        assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
18667        assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
18668        assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
18669        // Field-level pin — each axis is the pointwise `min` of the
18670        // three sources' values (the UNBOUNDED seed's `usize::MAX`
18671        // never wins the min against a concrete positive value).
18672        assert_eq!(
18673            met.max_expansion_depth,
18674            HAND_AUTHORED_MID_POSTURE
18675                .max_expansion_depth
18676                .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18677                .min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18678        );
18679        assert_eq!(
18680            met.max_cache_entries,
18681            HAND_AUTHORED_MID_POSTURE
18682                .max_cache_entries
18683                .min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18684                .min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18685        );
18686        assert_eq!(
18687            met.max_expansion_size,
18688            HAND_AUTHORED_MID_POSTURE
18689                .max_expansion_size
18690                .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18691                .min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18692        );
18693        assert_eq!(
18694            met.max_macro_body_size,
18695            HAND_AUTHORED_MID_POSTURE
18696                .max_macro_body_size
18697                .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18698                .min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18699        );
18700        assert_eq!(
18701            met.max_registered_macros,
18702            HAND_AUTHORED_MID_POSTURE
18703                .max_registered_macros
18704                .min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18705                .min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18706        );
18707        assert_eq!(
18708            met.max_macro_arity,
18709            HAND_AUTHORED_MID_POSTURE
18710                .max_macro_arity
18711                .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18712                .min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18713        );
18714        // Empty-slice edge — folding over an empty slice returns the
18715        // UNBOUNDED seed verbatim (the meet-identity as the null
18716        // aggregate). Symmetric to the EMPTY-seed empty-slice pin.
18717        let empty_slice: [ResourceLimits; 0] = [];
18718        assert_eq!(
18719            empty_slice
18720                .iter()
18721                .copied()
18722                .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18723            UNBOUNDED_RESOURCE_LIMITS,
18724        );
18725    }
18726
18727    #[test]
18728    fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
18729        // Null-aggregate pin — the N-ary meet over an empty slice is
18730        // the meet-identity element [`UNBOUNDED_RESOURCE_LIMITS`]. The
18731        // typed identity-element approach resolves the empty-input case
18732        // at the algebra layer; a wrapper-type approach
18733        // (`Option<ResourceLimits>` for the aggregate) would need a
18734        // distinct absent branch here, and a panic-on-empty seed-from-
18735        // -first-element approach would abort the caller.
18736        assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
18737    }
18738
18739    #[test]
18740    fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
18741        // Null-aggregate dual pin — the N-ary join over an empty slice
18742        // is the join-identity element [`EMPTY_RESOURCE_LIMITS`]. Peer
18743        // to `strictest_of_empty_slice` one COMBINATOR axis over on the
18744        // (N-ary meet, N-ary join) surface — the identity element for
18745        // join is the meet's ANNIHILATOR and vice versa; the two
18746        // together bind both bounded-lattice poles as null aggregates.
18747        assert_eq!(
18748            ResourceLimits::most_permissive_of(&[]),
18749            EMPTY_RESOURCE_LIMITS,
18750        );
18751    }
18752
18753    #[test]
18754    fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
18755        // Meet-identity absorption pin at N=1 — the N-ary meet of ONE
18756        // input is the input verbatim, since the meet-identity seed
18757        // (`UNBOUNDED_RESOURCE_LIMITS`) satisfies
18758        // `UNBOUNDED.strictest(a) == a` by the pointwise `min` behavior
18759        // against [`usize::MAX`]. This is the identity-law-at-N=1
18760        // pin: the fold's seed does not distort the 1-input case.
18761        assert_eq!(
18762            ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
18763            HAND_AUTHORED_MID_POSTURE,
18764        );
18765        assert_eq!(
18766            ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18767            HAND_AUTHORED_OTHER_POSTURE,
18768        );
18769        assert_eq!(
18770            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
18771            DEFAULT_RESOURCE_LIMITS,
18772        );
18773    }
18774
18775    #[test]
18776    fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
18777        // Join-identity absorption pin at N=1 — dual of
18778        // `strictest_of_single_element`, using the join-identity seed
18779        // (`EMPTY_RESOURCE_LIMITS`) whose per-axis `0` satisfies
18780        // `EMPTY.most_permissive(a) == a` against any non-zero value.
18781        assert_eq!(
18782            ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
18783            HAND_AUTHORED_MID_POSTURE,
18784        );
18785        assert_eq!(
18786            ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18787            HAND_AUTHORED_OTHER_POSTURE,
18788        );
18789        assert_eq!(
18790            ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
18791            DEFAULT_RESOURCE_LIMITS,
18792        );
18793    }
18794
18795    #[test]
18796    fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
18797        // N=2 reduction pin — the N-ary meet on a 2-element slice
18798        // reduces to the pairwise combinator, since
18799        // `UNBOUNDED.strictest(a).strictest(b) == a.strictest(b)`. The
18800        // strictest_of(&[a, b]) call MUST agree with a.strictest(b) on
18801        // every input pair the algebra distinguishes.
18802        assert_eq!(
18803            ResourceLimits::strictest_of(
18804                &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
18805            ),
18806            HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18807        );
18808        assert_eq!(
18809            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
18810            DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18811        );
18812    }
18813
18814    #[test]
18815    fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
18816        // N=2 reduction dual pin — the N-ary join on a 2-element slice
18817        // reduces to the pairwise combinator.
18818        assert_eq!(
18819            ResourceLimits::most_permissive_of(&[
18820                HAND_AUTHORED_MID_POSTURE,
18821                HAND_AUTHORED_OTHER_POSTURE,
18822            ]),
18823            HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18824        );
18825        assert_eq!(
18826            ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
18827            DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18828        );
18829    }
18830
18831    #[test]
18832    fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
18833        // Method-agrees-with-fold pin — the sole behavioral claim the
18834        // N-ary aggregator makes is that it IS the inline
18835        // `.iter().copied().fold(UNBOUNDED, strictest)` cascade the
18836        // pre-lift caller composed at every consumption site. A drift
18837        // between the method's implementation and the inline fold
18838        // would break this equality on every non-empty slice.
18839        let postures: [ResourceLimits; 3] = [
18840            HAND_AUTHORED_MID_POSTURE,
18841            HAND_AUTHORED_OTHER_POSTURE,
18842            DEFAULT_RESOURCE_LIMITS,
18843        ];
18844        assert_eq!(
18845            ResourceLimits::strictest_of(&postures),
18846            postures
18847                .iter()
18848                .copied()
18849                .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18850        );
18851    }
18852
18853    #[test]
18854    fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
18855        // Method-agrees-with-fold dual pin.
18856        let postures: [ResourceLimits; 3] = [
18857            HAND_AUTHORED_MID_POSTURE,
18858            HAND_AUTHORED_OTHER_POSTURE,
18859            DEFAULT_RESOURCE_LIMITS,
18860        ];
18861        assert_eq!(
18862            ResourceLimits::most_permissive_of(&postures),
18863            postures
18864                .iter()
18865                .copied()
18866                .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18867        );
18868    }
18869
18870    #[test]
18871    fn resource_limits_strictest_of_is_order_independent() {
18872        // Order-independence pin — the fold inherits commutativity AND
18873        // associativity of the pairwise `strictest`, so any permutation
18874        // of the input slice yields the same aggregate. Reversal is
18875        // the maximal permutation for a 3-element input (indices
18876        // 0,1,2 → 2,1,0); an aggregator that leaked fold order would
18877        // discriminate here.
18878        let forward = ResourceLimits::strictest_of(&[
18879            HAND_AUTHORED_MID_POSTURE,
18880            HAND_AUTHORED_OTHER_POSTURE,
18881            DEFAULT_RESOURCE_LIMITS,
18882        ]);
18883        let reversed = ResourceLimits::strictest_of(&[
18884            DEFAULT_RESOURCE_LIMITS,
18885            HAND_AUTHORED_OTHER_POSTURE,
18886            HAND_AUTHORED_MID_POSTURE,
18887        ]);
18888        assert_eq!(forward, reversed);
18889    }
18890
18891    #[test]
18892    fn resource_limits_most_permissive_of_is_order_independent() {
18893        let forward = ResourceLimits::most_permissive_of(&[
18894            HAND_AUTHORED_MID_POSTURE,
18895            HAND_AUTHORED_OTHER_POSTURE,
18896            DEFAULT_RESOURCE_LIMITS,
18897        ]);
18898        let reversed = ResourceLimits::most_permissive_of(&[
18899            DEFAULT_RESOURCE_LIMITS,
18900            HAND_AUTHORED_OTHER_POSTURE,
18901            HAND_AUTHORED_MID_POSTURE,
18902        ]);
18903        assert_eq!(forward, reversed);
18904    }
18905
18906    #[test]
18907    fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
18908        // Compile-time-composition pin — `strictest_of` is `const fn`,
18909        // so a caller binds an N-ary aggregate to a `pub const` and the
18910        // fold evaluates at compile time. Every `DEFAULT_MAX_*` is
18911        // strictly less than `usize::MAX`, so the meet of `[DEFAULT,
18912        // UNBOUNDED]` picks DEFAULT on every axis — a preset-pair
18913        // identity already pinned pairwise, verified here through the
18914        // N-ary method's const-context evaluation. The N-ary const-fn
18915        // peer of the pairwise const-fn pins on
18916        // [`ResourceLimits::strictest`]; a regression that turned
18917        // `strictest_of` into a runtime `fn` would break the `const`
18918        // binding below at compile time.
18919        const AGGREGATED: ResourceLimits =
18920            ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
18921        assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
18922    }
18923
18924    #[test]
18925    fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
18926        // Compile-time-composition dual pin — every `DEFAULT_MAX_*` is
18927        // strictly greater than `0`, so the join of `[EMPTY, DEFAULT]`
18928        // picks DEFAULT on every axis.
18929        const AGGREGATED: ResourceLimits =
18930            ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
18931        assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
18932    }
18933
18934    #[test]
18935    fn resource_limits_strictest_of_result_is_leq_every_operand() {
18936        // Lattice-relation pin — the N-ary meet sits `leq` every
18937        // operand. This is the N-ary extension of the pairwise
18938        // `strictest_is_leq_both_operands` pin; the meet aggregate
18939        // dominates from below (the greatest lower bound of the slice
18940        // sits at-or-below each input on every axis).
18941        let postures: [ResourceLimits; 3] = [
18942            HAND_AUTHORED_MID_POSTURE,
18943            HAND_AUTHORED_OTHER_POSTURE,
18944            DEFAULT_RESOURCE_LIMITS,
18945        ];
18946        let met = ResourceLimits::strictest_of(&postures);
18947        for p in postures {
18948            assert!(met.leq(p));
18949        }
18950    }
18951
18952    #[test]
18953    fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
18954        // Lattice-relation dual pin — every operand sits `leq` the
18955        // N-ary join. The join aggregate is the least upper bound of
18956        // the slice.
18957        let postures: [ResourceLimits; 3] = [
18958            HAND_AUTHORED_MID_POSTURE,
18959            HAND_AUTHORED_OTHER_POSTURE,
18960            DEFAULT_RESOURCE_LIMITS,
18961        ];
18962        let joined = ResourceLimits::most_permissive_of(&postures);
18963        for p in postures {
18964            assert!(p.leq(joined));
18965        }
18966    }
18967
18968    #[test]
18969    fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
18970        // Behavioral pin — an expander constructed from the N-ary meet
18971        // of a slice of postures MUST gate at the tightest ceiling
18972        // across the slice on every axis. Peer of
18973        // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
18974        // one AGGREGATION-ARITY axis over: where the pairwise pin
18975        // composes exactly two postures, this pin composes THREE
18976        // through the N-ary aggregator, confirming the arity extension
18977        // preserves the "meet gates at tightest" behavioral contract.
18978        //
18979        // The slice tightens macro_arity to 1 (the tightest across the
18980        // three postures) via the middle operand; every other axis
18981        // stays at usize::MAX (the outer two operands supply
18982        // usize::MAX; the middle operand's struct-update literal
18983        // preserves usize::MAX everywhere except macro_arity).
18984        let tightened = ResourceLimits::strictest_of(&[
18985            UNBOUNDED_RESOURCE_LIMITS,
18986            ResourceLimits {
18987                max_macro_arity: 1,
18988                ..UNBOUNDED_RESOURCE_LIMITS
18989            },
18990            UNBOUNDED_RESOURCE_LIMITS,
18991        ]);
18992        assert_eq!(tightened.max_macro_arity, 1);
18993        assert_eq!(tightened.max_expansion_depth, usize::MAX);
18994        assert_eq!(tightened.max_cache_entries, usize::MAX);
18995        assert_eq!(tightened.max_expansion_size, usize::MAX);
18996        assert_eq!(tightened.max_macro_body_size, usize::MAX);
18997        assert_eq!(tightened.max_registered_macros, usize::MAX);
18998        let mut e = Expander::with_limits(tightened);
18999        let forms = read("(defmacro two (a b) `,a)").unwrap();
19000        let err = e.expand_program(forms).unwrap_err();
19001        assert!(
19002            matches!(
19003                err,
19004                LispError::MacroArityExceeded {
19005                    arity: 2,
19006                    limit: 1,
19007                    ..
19008                }
19009            ),
19010            "expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
19011        );
19012    }
19013
19014    #[test]
19015    fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
19016        // Behavioral pin — an expander constructed from the meet of
19017        // two postures MUST gate at the tighter of the two ceilings
19018        // on every axis. Build the meet of the ceiling-lifted preset
19019        // and a hand-authored posture whose `max_macro_arity` sits at
19020        // 1; the meet inherits the 1-slot arity ceiling (tighter than
19021        // [`usize::MAX`]) AND the [`usize::MAX`] ceiling on every
19022        // other axis (tighter than the hand-authored posture's small
19023        // values — no, wait: the OTHER posture's small values ARE
19024        // tighter. So the meet inherits 1 on arity and the OTHER
19025        // small values elsewhere.) Confirm the constructed expander
19026        // rejects a 2-arg macro registration with the arity gate.
19027        let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
19028            max_macro_arity: 1,
19029            ..UNBOUNDED_RESOURCE_LIMITS
19030        });
19031        assert_eq!(tightened.max_macro_arity, 1);
19032        // Other five axes remain at `usize::MAX` (both operands carry
19033        // `usize::MAX` on those, so the pointwise `min` is `usize::MAX`).
19034        assert_eq!(tightened.max_expansion_depth, usize::MAX);
19035        assert_eq!(tightened.max_cache_entries, usize::MAX);
19036        assert_eq!(tightened.max_expansion_size, usize::MAX);
19037        assert_eq!(tightened.max_macro_body_size, usize::MAX);
19038        assert_eq!(tightened.max_registered_macros, usize::MAX);
19039        let mut e = Expander::with_limits(tightened);
19040        let forms = read("(defmacro two (a b) `,a)").unwrap();
19041        let err = e.expand_program(forms).unwrap_err();
19042        assert!(
19043            matches!(
19044                err,
19045                LispError::MacroArityExceeded {
19046                    arity: 2,
19047                    limit: 1,
19048                    ..
19049                }
19050            ),
19051            "the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
19052        );
19053    }
19054
19055    // ── ResourceLimits::clamp — bounded-lattice bracket combinator ────
19056
19057    /// A tighter-than-MID lower-bracket witness — every axis strictly
19058    /// smaller than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the
19059    /// pointwise `max(mid, floor) == mid` on every axis (`floor.leq(mid)`
19060    /// holds structurally). Peer of `HAND_AUTHORED_CLAMP_CEILING` one
19061    /// BRACKET-BOUND axis over on the (floor, ceiling) bracket-witness
19062    /// pair.
19063    const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
19064        max_expansion_depth: 2,
19065        max_cache_entries: 3,
19066        max_expansion_size: 5,
19067        max_macro_body_size: 7,
19068        max_registered_macros: 11,
19069        max_macro_arity: 13,
19070    };
19071
19072    /// A looser-than-MID upper-bracket witness — every axis strictly larger
19073    /// than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the pointwise
19074    /// `min(mid, ceiling) == mid` on every axis (`mid.leq(ceiling)` holds
19075    /// structurally). Peer of `HAND_AUTHORED_CLAMP_FLOOR` one BRACKET-
19076    /// BOUND axis over. Together with `HAND_AUTHORED_CLAMP_FLOOR` the two
19077    /// bracket `HAND_AUTHORED_MID_POSTURE` strictly on every axis:
19078    /// `FLOOR.leq(MID) && MID.leq(CEILING) && FLOOR != MID && MID !=
19079    /// CEILING`.
19080    const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
19081        max_expansion_depth: 47,
19082        max_cache_entries: 53,
19083        max_expansion_size: 59,
19084        max_macro_body_size: 61,
19085        max_registered_macros: 67,
19086        max_macro_arity: 71,
19087    };
19088
19089    #[test]
19090    fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
19091        // In-range identity — when the input already sits within the
19092        // `[lower, upper]` bracket (`lower.leq(a) && a.leq(upper)` per-
19093        // axis), the clamp is the identity function: the pointwise
19094        // `max(a, lower) == a` (since `lower ≤ a` per-axis picks `a`),
19095        // then the pointwise `min(a, upper) == a` (since `a ≤ upper`
19096        // per-axis picks `a`). The primary invariant on the "input
19097        // already satisfies the bracket" arm of the three-arm bracket-
19098        // membership surface.
19099        //
19100        // Prerequisite pins — the two hand-authored bounds bracket
19101        // `MID` strictly on every axis (structural check that discharges
19102        // the "already in range" premise). A regression that inflated
19103        // `FLOOR` or deflated `CEILING` past `MID` would fail these
19104        // asserts before the clamp identity was tested, making the
19105        // premise's failure name itself rather than showing up as a
19106        // downstream misleading clamp mismatch.
19107        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19108        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19109        let clamped =
19110            HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19111        assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
19112    }
19113
19114    #[test]
19115    fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
19116        // Below-lower floor — when the input is tighter than the
19117        // bracket floor on every axis (`a.leq(lower)` per-axis), the
19118        // clamp lifts the input up to `lower`: the pointwise `max(a,
19119        // lower) == lower` (since `a ≤ lower` per-axis picks `lower`),
19120        // then the pointwise `min(lower, upper) == lower` (since
19121        // `lower ≤ upper` per-axis picks `lower`). Peer of
19122        // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19123        // one BRACKET-POSITION axis over on the (in-range, below-lower,
19124        // above-upper) three-arm bracket-membership surface.
19125        //
19126        // `EMPTY_RESOURCE_LIMITS` sits `leq` every posture (bounded-
19127        // lattice bottom), so `EMPTY.leq(FLOOR)` holds structurally
19128        // for any FLOOR; a clamp of `EMPTY` into `[FLOOR, CEILING]`
19129        // MUST return `FLOOR`.
19130        assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
19131        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19132        let clamped =
19133            EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19134        assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
19135    }
19136
19137    #[test]
19138    fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
19139        // Above-upper ceiling — when the input is looser than the
19140        // bracket ceiling on every axis (`upper.leq(a)` per-axis), the
19141        // clamp lowers the input down to `upper`: the pointwise
19142        // `max(a, lower) == a` (since `lower ≤ upper ≤ a` per-axis picks
19143        // `a`), then the pointwise `min(a, upper) == upper` (since
19144        // `upper ≤ a` per-axis picks `upper`). Peer of the
19145        // below-lower arm one BRACKET-POSITION axis over on the three-
19146        // arm bracket-membership surface.
19147        //
19148        // `UNBOUNDED_RESOURCE_LIMITS` sits `geq` every posture (bounded-
19149        // lattice top), so `CEILING.leq(UNBOUNDED)` holds structurally
19150        // for any CEILING; a clamp of `UNBOUNDED` into `[FLOOR, CEILING]`
19151        // MUST return `CEILING`.
19152        assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
19153        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19154        let clamped =
19155            UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19156        assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
19157    }
19158
19159    #[test]
19160    fn resource_limits_clamp_result_sits_within_the_bracket() {
19161        // Bracket-membership contract — the DEFINING invariant on the
19162        // clamp primitive: for every well-formed bracket
19163        // (`lower.leq(upper)`) and every input `a`, the result sits
19164        // within the bracket `lower.leq(a.clamp(lower, upper)) &&
19165        // a.clamp(lower, upper).leq(upper)`. Exercised on ALL FOUR arm
19166        // positions of the bracket-membership surface via the four
19167        // known-preset inputs (below-lower → in-range → above-upper
19168        // → asymmetric-crossing) so a regression that inflated the
19169        // ceiling arm or deflated the floor arm exhausts all four
19170        // input positions and names the failing arm.
19171        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19172        let inputs = [
19173            EMPTY_RESOURCE_LIMITS,       // below-lower
19174            HAND_AUTHORED_MID_POSTURE,   // in-range
19175            UNBOUNDED_RESOURCE_LIMITS,   // above-upper
19176            HAND_AUTHORED_OTHER_POSTURE, // asymmetric (crosses the bracket)
19177        ];
19178        for a in inputs {
19179            let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19180            assert!(
19181                HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
19182                "clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
19183            );
19184            assert!(
19185                clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
19186                "clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
19187            );
19188        }
19189    }
19190
19191    #[test]
19192    fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
19193        // Extrema-bracket identity — clamping against the bounded-
19194        // lattice extrema (`EMPTY_RESOURCE_LIMITS` as the leq-minimum,
19195        // `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum) is the
19196        // identity function on every posture: every posture already
19197        // sits within the widest possible bracket. Cross-checks the
19198        // clamp primitive against the bounded-lattice identity elements
19199        // previously lifted; a regression that swapped the two extrema
19200        // in the impl would collapse every input to
19201        // `UNBOUNDED.strictest(EMPTY) == EMPTY` and fail this pin on
19202        // every non-EMPTY input.
19203        for a in [
19204            DEFAULT_RESOURCE_LIMITS,
19205            HAND_AUTHORED_MID_POSTURE,
19206            HAND_AUTHORED_OTHER_POSTURE,
19207            EMPTY_RESOURCE_LIMITS,
19208            UNBOUNDED_RESOURCE_LIMITS,
19209        ] {
19210            assert_eq!(
19211                a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19212                a,
19213                "clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
19214            );
19215        }
19216    }
19217
19218    #[test]
19219    fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
19220        // Degenerate-bracket collapse — a zero-width bracket
19221        // (`lower == upper`) collapses every input to the single point.
19222        // The pointwise `max(a, x)` lifts every axis to at least `x`,
19223        // then the pointwise `min(., x)` clamps every axis to exactly
19224        // `x` regardless of `a`'s per-axis position (since `x ≤ max(a,
19225        // x) ≤ max(x, x) == x` when `a.leq(x)` and `min(max(a, x), x)
19226        // == x` when `x.leq(a)`).
19227        for x in [
19228            HAND_AUTHORED_MID_POSTURE,
19229            HAND_AUTHORED_OTHER_POSTURE,
19230            DEFAULT_RESOURCE_LIMITS,
19231        ] {
19232            for a in [
19233                EMPTY_RESOURCE_LIMITS,
19234                HAND_AUTHORED_MID_POSTURE,
19235                HAND_AUTHORED_OTHER_POSTURE,
19236                UNBOUNDED_RESOURCE_LIMITS,
19237            ] {
19238                assert_eq!(
19239                    a.clamp(x, x),
19240                    x,
19241                    "zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
19242                );
19243            }
19244        }
19245    }
19246
19247    #[test]
19248    fn resource_limits_clamp_is_idempotent() {
19249        // Idempotence — re-applying the same bracket to an already-
19250        // clamped posture returns the same result. The clamp primitive
19251        // is idempotent because its output already sits within the
19252        // `[lower, upper]` bracket (pinned by
19253        // `resource_limits_clamp_result_sits_within_the_bracket`), so
19254        // the second application hits the in-range identity arm. Peer
19255        // of the idempotence pins on `strictest` and `most_permissive`
19256        // one ARITY axis over (2-input primitive → 3-input bracket) on
19257        // the lattice-algebra idempotence-law surface.
19258        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19259        for a in [
19260            EMPTY_RESOURCE_LIMITS,
19261            HAND_AUTHORED_MID_POSTURE,
19262            HAND_AUTHORED_OTHER_POSTURE,
19263            UNBOUNDED_RESOURCE_LIMITS,
19264        ] {
19265            let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19266            let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19267            assert_eq!(
19268                once, twice,
19269                "clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
19270            );
19271        }
19272    }
19273
19274    #[test]
19275    fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
19276        // Const-fn pin — the bracket combinator is evaluable in const
19277        // context, so a caller can pre-clamp a shipped preset against
19278        // an operator-supplied policy bracket at compile time. Sibling
19279        // of the const-fn composition pins on `strictest` /
19280        // `most_permissive` / `strictest_of` / `most_permissive_of`
19281        // one PRIMITIVE-KIND axis over: those four combinators compose
19282        // presets at const time, and this bracket combinator projects
19283        // a preset into a const-time bracket.
19284        //
19285        // A regression to a runtime `fn` here would fail the `pub
19286        // const` binding below at compile time — a stronger guarantee
19287        // than a runtime `assert!` because the const binding is
19288        // evaluated once at compile time, not per test invocation.
19289        const CLAMPED_DEFAULT: ResourceLimits =
19290            DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
19291        assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
19292
19293        // Compile-time bracket-membership pin — const `assert!` on the
19294        // lattice-relation contract discharges the "clamp result sits
19295        // within the bracket" invariant at compile time on the
19296        // (DEFAULT, EMPTY, UNBOUNDED) preset triple.
19297        const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
19298        const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
19299    }
19300
19301    #[test]
19302    fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
19303        // Structural-equivalence pin — the sole behavioral claim the
19304        // clamp primitive makes is that it IS the
19305        // `self.most_permissive(lower).strictest(upper)` two-step
19306        // cascade the pre-lift caller composed. A drift between the
19307        // method and the inline cascade would break this equality on
19308        // every input; the pin exhausts the four bracket-position arms
19309        // of the input surface.
19310        for a in [
19311            EMPTY_RESOURCE_LIMITS,
19312            HAND_AUTHORED_MID_POSTURE,
19313            HAND_AUTHORED_OTHER_POSTURE,
19314            UNBOUNDED_RESOURCE_LIMITS,
19315        ] {
19316            let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19317            let via_cascade = a
19318                .most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
19319                .strictest(HAND_AUTHORED_CLAMP_CEILING);
19320            assert_eq!(via_method, via_cascade);
19321        }
19322    }
19323
19324    #[test]
19325    fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
19326        // Behavioral pin — an expander constructed from a preset
19327        // clamped into `[EMPTY, TIGHT_BRACKET]` MUST gate at the
19328        // bracket ceiling's arity axis. Confirms the clamp primitive
19329        // reaches through to the runtime gates that consume
19330        // `ResourceLimits`, so a future consumer that pre-clamps a
19331        // preset against a policy bracket inherits the bracket's
19332        // ceilings at every guard. Peer of
19333        // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19334        // one COMBINATOR-ARITY axis over on the (pairwise-meet,
19335        // N-ary-meet, bracket) combinator-arity face.
19336        //
19337        // The bracket ceiling tightens `max_macro_arity` to 1; every
19338        // other axis stays at `usize::MAX` (the CEILING preset carries
19339        // `usize::MAX` on those, and the input UNBOUNDED preset also
19340        // carries `usize::MAX` on every axis — the clamp's `strictest`
19341        // arm picks `usize::MAX` from either side).
19342        let bracket_ceiling = ResourceLimits {
19343            max_macro_arity: 1,
19344            ..UNBOUNDED_RESOURCE_LIMITS
19345        };
19346        let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19347        assert_eq!(clamped.max_macro_arity, 1);
19348        assert_eq!(clamped.max_expansion_depth, usize::MAX);
19349        assert_eq!(clamped.max_cache_entries, usize::MAX);
19350        assert_eq!(clamped.max_expansion_size, usize::MAX);
19351        assert_eq!(clamped.max_macro_body_size, usize::MAX);
19352        assert_eq!(clamped.max_registered_macros, usize::MAX);
19353        let mut e = Expander::with_limits(clamped);
19354        let forms = read("(defmacro two (a b) `,a)").unwrap();
19355        let err = e.expand_program(forms).unwrap_err();
19356        assert!(
19357            matches!(
19358                err,
19359                LispError::MacroArityExceeded {
19360                    arity: 2,
19361                    limit: 1,
19362                    ..
19363                }
19364            ),
19365            "the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
19366        );
19367    }
19368
19369    // ── ResourceLimits::within — bracket-membership predicate ─────────
19370
19371    #[test]
19372    fn resource_limits_within_of_posture_in_range_is_true() {
19373        // In-range identity — when the input already sits within the
19374        // `[lower, upper]` bracket on every axis (`lower.leq(a) &&
19375        // a.leq(upper)`), the predicate returns `true`. Peer of
19376        // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19377        // one PRIMITIVE-KIND axis over on the (combinator, predicate)
19378        // face of the bracket-primitive surface.
19379        //
19380        // Prerequisite pins — the two hand-authored bounds bracket
19381        // `MID` strictly on every axis, structurally discharging the
19382        // "already in range" premise.
19383        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19384        assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19385        assert!(HAND_AUTHORED_MID_POSTURE
19386            .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19387    }
19388
19389    #[test]
19390    fn resource_limits_within_of_posture_below_lower_is_false() {
19391        // Below-lower rejection — when the input is strictly tighter
19392        // than the bracket floor on some axis (`!lower.leq(a)`), the
19393        // predicate returns `false`. `EMPTY_RESOURCE_LIMITS` has every
19394        // axis at `0`, strictly below `HAND_AUTHORED_CLAMP_FLOOR`'s
19395        // per-axis ceilings; so `FLOOR.leq(EMPTY)` fails on every axis,
19396        // making the conjunction reject regardless of `EMPTY`'s relation
19397        // to `CEILING`. Peer of
19398        // `resource_limits_clamp_of_posture_below_lower_returns_lower`
19399        // one PRIMITIVE-KIND axis over.
19400        assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
19401        assert!(
19402            !EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
19403        );
19404    }
19405
19406    #[test]
19407    fn resource_limits_within_of_posture_above_upper_is_false() {
19408        // Above-upper rejection — when the input is strictly looser
19409        // than the bracket ceiling on some axis (`!a.leq(upper)`), the
19410        // predicate returns `false`. `UNBOUNDED_RESOURCE_LIMITS` has
19411        // every axis at `usize::MAX`, strictly above
19412        // `HAND_AUTHORED_CLAMP_CEILING`'s per-axis ceilings; so
19413        // `UNBOUNDED.leq(CEILING)` fails on every axis. Peer of
19414        // `resource_limits_clamp_of_posture_above_upper_returns_upper`
19415        // one PRIMITIVE-KIND axis over.
19416        assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
19417        assert!(!UNBOUNDED_RESOURCE_LIMITS
19418            .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19419    }
19420
19421    #[test]
19422    fn resource_limits_within_with_lattice_extrema_is_true() {
19423        // Extrema-bracket identity — every posture sits within the
19424        // widest possible bracket `[EMPTY_RESOURCE_LIMITS,
19425        // UNBOUNDED_RESOURCE_LIMITS]`. Cross-checks the predicate
19426        // against the bounded-lattice identity elements (`EMPTY` is
19427        // the leq-minimum, `UNBOUNDED` is the leq-maximum). Peer of
19428        // `resource_limits_clamp_with_lattice_extrema_returns_the_input`
19429        // one PRIMITIVE-KIND axis over — the predicate view of the
19430        // "clamp is the identity" property.
19431        for a in [
19432            DEFAULT_RESOURCE_LIMITS,
19433            HAND_AUTHORED_MID_POSTURE,
19434            HAND_AUTHORED_OTHER_POSTURE,
19435            EMPTY_RESOURCE_LIMITS,
19436            UNBOUNDED_RESOURCE_LIMITS,
19437        ] {
19438            assert!(
19439                a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19440                "every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
19441            );
19442        }
19443    }
19444
19445    #[test]
19446    fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
19447        // Degenerate-bracket collapse — a zero-width bracket admits
19448        // only the single point at the bounds. `a.within(x, x)` holds
19449        // iff `x.leq(a) && a.leq(x)` iff `a == x` by antisymmetry of
19450        // `leq`. Peer of `resource_limits_clamp_with_equal_bounds_returns_the_bound`
19451        // one PRIMITIVE-KIND axis over: the clamp collapses to `x`
19452        // unconditionally, and the predicate distinguishes the "was
19453        // already equal" case from the "would have been clamped" case.
19454        let bounds = [
19455            HAND_AUTHORED_MID_POSTURE,
19456            HAND_AUTHORED_OTHER_POSTURE,
19457            DEFAULT_RESOURCE_LIMITS,
19458            EMPTY_RESOURCE_LIMITS,
19459            UNBOUNDED_RESOURCE_LIMITS,
19460        ];
19461        for x in bounds {
19462            for a in bounds {
19463                assert_eq!(
19464                    a.within(x, x),
19465                    a == x,
19466                    "a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
19467                );
19468            }
19469        }
19470    }
19471
19472    #[test]
19473    fn resource_limits_within_of_self_is_reflexive() {
19474        // Reflexive-bracket identity — every posture sits within the
19475        // zero-width bracket at itself, since `a.leq(a)` holds by
19476        // reflexivity of `leq`. The reflexive-bracket peer of the
19477        // reflexive-leq pin (`resource_limits_leq_is_reflexive`) one
19478        // ARITY axis over (pairwise → three-input).
19479        for a in [
19480            EMPTY_RESOURCE_LIMITS,
19481            DEFAULT_RESOURCE_LIMITS,
19482            HAND_AUTHORED_MID_POSTURE,
19483            HAND_AUTHORED_OTHER_POSTURE,
19484            UNBOUNDED_RESOURCE_LIMITS,
19485        ] {
19486            assert!(
19487                a.within(a, a),
19488                "a.within(a, a) must hold by reflexivity of leq; a={a:?}",
19489            );
19490        }
19491    }
19492
19493    #[test]
19494    fn resource_limits_within_agrees_with_clamp_fixed_point() {
19495        // Clamp fixed-point theorem — `a.within(lower, upper) ⇔
19496        // a.clamp(lower, upper) == a`. The CANONICAL cross-check axiom
19497        // binding the `within` predicate to the `clamp` combinator,
19498        // analogous to the meet-agreement (`a.leq(b) ⇔ a.strictest(b)
19499        // == a`, pinned by `resource_limits_leq_agrees_with_meet`) and
19500        // join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`,
19501        // pinned by `resource_limits_leq_agrees_with_join`) axioms
19502        // binding the `leq` relation to its combinator peers.
19503        //
19504        // Exercised on all four bracket-position arms of the input
19505        // surface (below-lower / in-range / above-upper / asymmetric-
19506        // crossing) so a regression that broke either direction of the
19507        // fixed-point equivalence exhausts every position and names the
19508        // failing arm.
19509        assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19510        for a in [
19511            EMPTY_RESOURCE_LIMITS,       // below-lower — clamp != a, within false
19512            HAND_AUTHORED_MID_POSTURE,   // in-range   — clamp == a, within true
19513            UNBOUNDED_RESOURCE_LIMITS,   // above-upper — clamp != a, within false
19514            HAND_AUTHORED_OTHER_POSTURE, // asymmetric — clamp != a, within false
19515        ] {
19516            let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19517            let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19518            let clamp_fixed = clamped == a;
19519            assert_eq!(
19520                within_holds, clamp_fixed,
19521                "within(lower, upper) must agree with clamp fixed-point; \
19522                 a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
19523            );
19524        }
19525    }
19526
19527    #[test]
19528    fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
19529        // Structural-equivalence pin — the sole behavioral claim the
19530        // predicate makes is that it IS the `lower.leq(a) && a.leq(upper)`
19531        // two-primitive conjunction the pre-lift caller composed. A drift
19532        // between the method and the inline conjunction would break this
19533        // equality on some input; the pin exhausts the four bracket-
19534        // position arms of the input surface. Peer of
19535        // `resource_limits_clamp_agrees_with_direct_two_step_cascade`
19536        // one PRIMITIVE-KIND axis over (combinator ↔ predicate).
19537        for a in [
19538            EMPTY_RESOURCE_LIMITS,
19539            HAND_AUTHORED_MID_POSTURE,
19540            HAND_AUTHORED_OTHER_POSTURE,
19541            UNBOUNDED_RESOURCE_LIMITS,
19542        ] {
19543            let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19544            let via_conjunction =
19545                HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
19546            assert_eq!(via_method, via_conjunction, "a={a:?}");
19547        }
19548    }
19549
19550    #[test]
19551    fn resource_limits_within_composes_at_compile_time_via_const_fn() {
19552        // Const-fn pin — the bracket predicate is evaluable in const
19553        // context, so a caller can pin a preset-bracket-membership
19554        // identity at compile time. Sibling of the const-fn evaluability
19555        // pins on `leq` and `clamp` one PRIMITIVE-KIND axis over: the
19556        // relation and combinator both evaluate at const time, and the
19557        // predicate closing the (combinator, predicate) row does too.
19558        //
19559        // A regression to a runtime `fn` here would fail the `const _:
19560        // () = assert!(...)` bindings below at compile time.
19561        const _: () = assert!(
19562            DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
19563        );
19564        const _: () = assert!(
19565            DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
19566        );
19567        const _: () = assert!(
19568            !UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
19569        );
19570    }
19571
19572    #[test]
19573    fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
19574        // Behavioral pin — a caller that CHECKS bracket-membership
19575        // before constructing an expander can safely reach through to
19576        // the runtime gates: if `within` reports true, `clamp` is a
19577        // no-op (by the fixed-point theorem), so the expander's
19578        // ceilings are the input posture's own. Peer of
19579        // `expander_built_at_clamped_preset_gates_at_the_bracket_ceiling`
19580        // one PRIMITIVE-KIND axis over — the predicate view of the
19581        // "clamp reaches through to the runtime gates" property.
19582        //
19583        // The candidate sits at the bracket ceiling (arity=1, other
19584        // axes at usize::MAX), so it MUST sit within the [EMPTY,
19585        // bracket_ceiling] bracket — the maximal within-membership arm
19586        // of the fixed-point theorem. Every other axis at usize::MAX
19587        // clears the register-time gates, so the arity gate at 1 is
19588        // the one that trips on a 2-parameter macro registration.
19589        let bracket_ceiling = ResourceLimits {
19590            max_macro_arity: 1,
19591            ..UNBOUNDED_RESOURCE_LIMITS
19592        };
19593        let candidate = ResourceLimits {
19594            max_macro_arity: 1,
19595            ..UNBOUNDED_RESOURCE_LIMITS
19596        };
19597        assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
19598        // Since candidate sits within the bracket, clamp is the
19599        // identity; the expander inherits candidate's ceilings verbatim.
19600        let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19601        assert_eq!(clamped, candidate);
19602        let mut e = Expander::with_limits(clamped);
19603        let forms = read("(defmacro two (a b) `,a)").unwrap();
19604        let err = e.expand_program(forms).unwrap_err();
19605        assert!(
19606            matches!(
19607                err,
19608                LispError::MacroArityExceeded {
19609                    arity: 2,
19610                    limit: 1,
19611                    ..
19612                }
19613            ),
19614            "the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
19615        );
19616    }
19617
19618    // ── ResourceLimits::is_lower_bound_of / is_upper_bound_of ─────────
19619    //   N-ary boolean-conjunction peers of the pairwise `leq` relation,
19620    //   closing the (combinator, predicate) row on the N-ary-aggregation
19621    //   face of the lattice-algebra surface. See the docstring on
19622    //   `ResourceLimits::is_lower_bound_of` for the (pairwise, N-ary,
19623    //   bracket) × (combinator, predicate) placement.
19624
19625    #[test]
19626    fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
19627        // Empty-conjunction identity — every posture is trivially a
19628        // lower bound of the empty set. Peer of `strictest_of(&[]) ==
19629        // UNBOUNDED_RESOURCE_LIMITS`'s empty-slice identity one
19630        // PRIMITIVE-KIND axis over: the empty slice aggregates to the
19631        // identity element of the underlying operation (`true` for
19632        // boolean conjunction, `UNBOUNDED` for pointwise `min`).
19633        assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
19634        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19635        assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19636        assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19637    }
19638
19639    #[test]
19640    fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
19641        // Empty-conjunction dual — every posture is trivially an upper
19642        // bound of the empty set. Peer of
19643        // `is_lower_bound_of_empty_slice_is_vacuously_true` one
19644        // COMBINATOR-DIRECTION axis over, AND peer of
19645        // `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
19646        // slice identity one PRIMITIVE-KIND axis over.
19647        assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
19648        assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19649        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19650        assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19651    }
19652
19653    #[test]
19654    fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
19655        // 1-input identity — the singleton predicate reduces to the
19656        // pairwise partial-order relation. The N-ary predicate on a
19657        // 1-element slice is the pairwise `leq` verbatim, mirroring
19658        // `strictest_of(&[a]) == a`'s reduction on the combinator side.
19659        let cases: [(ResourceLimits, ResourceLimits); 6] = [
19660            (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19661            (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19662            (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19663            (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19664            (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19665            (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19666        ];
19667        for (a, b) in cases {
19668            assert_eq!(
19669                a.is_lower_bound_of(&[b]),
19670                a.leq(b),
19671                "1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
19672            );
19673        }
19674    }
19675
19676    #[test]
19677    fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
19678        // 1-input dual identity — the singleton upper-bound predicate
19679        // is the pairwise `leq` with the direction flipped
19680        // (`b.leq(a)`), since `self` sits ABOVE the operand rather than
19681        // BELOW it.
19682        let cases: [(ResourceLimits, ResourceLimits); 6] = [
19683            (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19684            (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19685            (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19686            (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19687            (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19688            (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19689        ];
19690        for (a, b) in cases {
19691            assert_eq!(
19692                a.is_upper_bound_of(&[b]),
19693                b.leq(a),
19694                "1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
19695            );
19696        }
19697    }
19698
19699    #[test]
19700    fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
19701        // Definitional bridge — the N-ary meet is always a lower bound
19702        // of the slice it aggregates. Pins the link between the N-ary
19703        // COMBINATOR (`strictest_of`) and the N-ary PREDICATE
19704        // (`is_lower_bound_of`): the meet is an ELEMENT of the lower-
19705        // bound set the predicate CHARACTERIZES.
19706        let postures: [ResourceLimits; 3] = [
19707            HAND_AUTHORED_MID_POSTURE,
19708            HAND_AUTHORED_OTHER_POSTURE,
19709            DEFAULT_RESOURCE_LIMITS,
19710        ];
19711        let met = ResourceLimits::strictest_of(&postures);
19712        assert!(met.is_lower_bound_of(&postures));
19713    }
19714
19715    #[test]
19716    fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
19717        // Dual definitional bridge — the N-ary join is always an upper
19718        // bound of the slice it aggregates.
19719        let postures: [ResourceLimits; 3] = [
19720            HAND_AUTHORED_MID_POSTURE,
19721            HAND_AUTHORED_OTHER_POSTURE,
19722            DEFAULT_RESOURCE_LIMITS,
19723        ];
19724        let joined = ResourceLimits::most_permissive_of(&postures);
19725        assert!(joined.is_upper_bound_of(&postures));
19726    }
19727
19728    #[test]
19729    fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
19730        // Universal-bottom witness — the lattice bottom is a common
19731        // lower bound of every slice, since `EMPTY.leq(a) == true` for
19732        // every posture by the lattice-bottom axiom (`0 ≤ x` per-axis).
19733        // The bounded-lattice specialization of the general "the meet-
19734        // identity is a lower bound of every set" theorem.
19735        let postures: [ResourceLimits; 4] = [
19736            HAND_AUTHORED_MID_POSTURE,
19737            HAND_AUTHORED_OTHER_POSTURE,
19738            DEFAULT_RESOURCE_LIMITS,
19739            UNBOUNDED_RESOURCE_LIMITS,
19740        ];
19741        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
19742        // The three preset-carried postures each individually — the
19743        // universal-bottom witness holds on the singleton slice too.
19744        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19745        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19746        assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19747    }
19748
19749    #[test]
19750    fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
19751        // Universal-top witness — the lattice top is a common upper
19752        // bound of every slice, since `a.leq(UNBOUNDED) == true` for
19753        // every posture by the lattice-top axiom (`x ≤ usize::MAX`
19754        // per-axis).
19755        let postures: [ResourceLimits; 4] = [
19756            HAND_AUTHORED_MID_POSTURE,
19757            HAND_AUTHORED_OTHER_POSTURE,
19758            DEFAULT_RESOURCE_LIMITS,
19759            EMPTY_RESOURCE_LIMITS,
19760        ];
19761        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
19762        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19763        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19764        assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19765    }
19766
19767    #[test]
19768    fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
19769        // Any-operand short-circuit — if `self` fails `leq` against ANY
19770        // operand in the slice, the N-ary conjunction rejects. The dual
19771        // of the "meet is a lower bound" witness on the false side:
19772        // moving `self` STRICTLY ABOVE an operand on any axis costs the
19773        // lower-bound property.
19774        //
19775        // MID sits above the join (element-wise) on some axes and below
19776        // on others, so MID is NEITHER a lower bound of {MID, OTHER}
19777        // (fails against OTHER's smaller-axis values) NOR is OTHER a
19778        // lower bound of {MID, OTHER} (fails against MID's smaller-axis
19779        // values).
19780        assert!(!HAND_AUTHORED_MID_POSTURE
19781            .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19782        assert!(!HAND_AUTHORED_OTHER_POSTURE
19783            .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19784        // A UNIVERSAL rejection — the lattice top is a lower bound
19785        // ONLY of the empty set OR of slices whose every element equals
19786        // UNBOUNDED. Given a strictly-smaller preset in the slice, the
19787        // top's per-axis usize::MAX loses `leq` on every axis whose
19788        // preset value is strictly smaller.
19789        assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19790        assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19791    }
19792
19793    #[test]
19794    fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
19795        // Dual any-operand short-circuit — if ANY operand fails `leq`
19796        // against `self`, the N-ary conjunction rejects.
19797        assert!(!HAND_AUTHORED_MID_POSTURE
19798            .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19799        assert!(!HAND_AUTHORED_OTHER_POSTURE
19800            .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19801        // The bottom cannot bound anything from above except the empty
19802        // set OR a slice of EMPTY-only postures.
19803        assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19804        assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19805    }
19806
19807    #[test]
19808    fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
19809        // Method-vs-scaffold cross-check — the lifted N-ary predicate
19810        // agrees with the pre-lift two-primitive
19811        // `postures.iter().all(|p| self.leq(*p))` scaffolding on every
19812        // preset-carried posture across the shipped bounded-lattice
19813        // extrema AND the two hand-authored asymmetric witnesses. Pins
19814        // the lift is a semantic no-op — post-lift the consumer routes
19815        // through ONE name instead of composing the conjunction inline,
19816        // but the two paths compute the same boolean.
19817        let candidates: [ResourceLimits; 5] = [
19818            EMPTY_RESOURCE_LIMITS,
19819            DEFAULT_RESOURCE_LIMITS,
19820            UNBOUNDED_RESOURCE_LIMITS,
19821            HAND_AUTHORED_MID_POSTURE,
19822            HAND_AUTHORED_OTHER_POSTURE,
19823        ];
19824        let slice: [ResourceLimits; 3] = [
19825            HAND_AUTHORED_MID_POSTURE,
19826            HAND_AUTHORED_OTHER_POSTURE,
19827            DEFAULT_RESOURCE_LIMITS,
19828        ];
19829        for c in candidates {
19830            let direct = slice.iter().all(|p| c.leq(*p));
19831            assert_eq!(
19832                c.is_lower_bound_of(&slice),
19833                direct,
19834                "is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
19835            );
19836        }
19837    }
19838
19839    #[test]
19840    fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
19841        // Dual method-vs-scaffold cross-check.
19842        let candidates: [ResourceLimits; 5] = [
19843            EMPTY_RESOURCE_LIMITS,
19844            DEFAULT_RESOURCE_LIMITS,
19845            UNBOUNDED_RESOURCE_LIMITS,
19846            HAND_AUTHORED_MID_POSTURE,
19847            HAND_AUTHORED_OTHER_POSTURE,
19848        ];
19849        let slice: [ResourceLimits; 3] = [
19850            HAND_AUTHORED_MID_POSTURE,
19851            HAND_AUTHORED_OTHER_POSTURE,
19852            DEFAULT_RESOURCE_LIMITS,
19853        ];
19854        for c in candidates {
19855            let direct = slice.iter().all(|p| p.leq(c));
19856            assert_eq!(
19857                c.is_upper_bound_of(&slice),
19858                direct,
19859                "is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
19860            );
19861        }
19862    }
19863
19864    #[test]
19865    fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
19866        // Const-fn pin — the N-ary lower-bound predicate is evaluable
19867        // in const context, so a caller can pin an N-ary-bound-
19868        // membership identity at compile time. Sibling of the const-fn
19869        // evaluability pins on `leq` and `within` one PRIMITIVE-KIND
19870        // axis over, AND sibling of `strictest_of`'s const-fn pin one
19871        // COMBINATOR-KIND axis over.
19872        //
19873        // A regression to a runtime `fn` here would fail the `const _:
19874        // () = assert!(...)` bindings below at compile time.
19875        const _: () = assert!(EMPTY_RESOURCE_LIMITS
19876            .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
19877        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19878        const _: () =
19879            assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19880    }
19881
19882    #[test]
19883    fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
19884        // Const-fn dual pin.
19885        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
19886            .is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
19887        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19888        const _: () =
19889            assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19890    }
19891
19892    /// The full canonical preset roster the strict-order pins sweep
19893    /// through — the (bottom, middle, top) bounded-lattice preset
19894    /// triple plus the two hand-authored incomparable asymmetric
19895    /// postures. Every strict-order law below quantifies over this
19896    /// slice, so a regression that breaks strict `<` on any preset
19897    /// pair discriminates at ONE named fixture rather than at a
19898    /// per-test ad-hoc literal.
19899    const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
19900        EMPTY_RESOURCE_LIMITS,
19901        DEFAULT_RESOURCE_LIMITS,
19902        UNBOUNDED_RESOURCE_LIMITS,
19903        HAND_AUTHORED_MID_POSTURE,
19904        HAND_AUTHORED_OTHER_POSTURE,
19905    ];
19906
19907    #[test]
19908    fn resource_limits_lt_is_irreflexive() {
19909        // Strict partial-order law — `a < a == false` for every
19910        // posture. Irreflexivity is the strict-relation axiom peer of
19911        // reflexivity on `leq` one STRICTNESS axis over on the
19912        // partial-order axiom surface; a regression that dropped the
19913        // antisymmetric leg (`!other.leq(self)`) — leaving the body at
19914        // `self.leq(other)` — would fire here on every preset since
19915        // `leq` is reflexive.
19916        for &a in STRICT_ORDER_ROSTER {
19917            assert!(!a.lt(a), "irreflexivity failed on {a:?}");
19918        }
19919    }
19920
19921    #[test]
19922    fn resource_limits_lt_is_asymmetric() {
19923        // Strict partial-order law — `a < b ⇒ ¬(b < a)` for every
19924        // pair. The two directions of the strict relation cannot both
19925        // hold, since `a.lt(b)` implies `!b.leq(a)` and `b.lt(a)`
19926        // requires `b.leq(a)`. Sweeps every ordered pair in the
19927        // canonical preset roster; discriminates a regression that
19928        // dropped the antisymmetric leg in either direction.
19929        for &a in STRICT_ORDER_ROSTER {
19930            for &b in STRICT_ORDER_ROSTER {
19931                if a.lt(b) {
19932                    assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
19933                }
19934            }
19935        }
19936    }
19937
19938    #[test]
19939    fn resource_limits_lt_is_transitive() {
19940        // Strict partial-order law — `a < b ∧ b < c ⇒ a < c`. Sweeps
19941        // every ordered triple in the canonical preset roster; each
19942        // consequence is checked whenever both antecedents hold. On
19943        // the (bottom, middle, top) diagonal the canonical witness
19944        // `EMPTY < DEFAULT < UNBOUNDED` composes to
19945        // `EMPTY < UNBOUNDED`, exercising the transitivity chain on
19946        // the bounded-lattice diagonal explicitly; the sweep pins the
19947        // same law on every other transitively-related triple in the
19948        // roster.
19949        for &a in STRICT_ORDER_ROSTER {
19950            for &b in STRICT_ORDER_ROSTER {
19951                for &c in STRICT_ORDER_ROSTER {
19952                    if a.lt(b) && b.lt(c) {
19953                        assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
19954                    }
19955                }
19956            }
19957        }
19958    }
19959
19960    #[test]
19961    fn resource_limits_lt_refines_leq() {
19962        // Refinement axiom — `a < b ⇒ a ≤ b`. The strict relation is
19963        // a refinement of the non-strict one; a witness of strict `<`
19964        // is also a witness of `≤`. Sweeps every ordered pair in the
19965        // canonical preset roster.
19966        for &a in STRICT_ORDER_ROSTER {
19967            for &b in STRICT_ORDER_ROSTER {
19968                if a.lt(b) {
19969                    assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
19970                }
19971            }
19972        }
19973    }
19974
19975    #[test]
19976    fn resource_limits_lt_agrees_with_leq_minus_equality() {
19977        // Antisymmetry consequence — on ANY antisymmetric partial
19978        // order (which the pointwise `≤` on `ResourceLimits` is),
19979        // `a < b ⇔ a ≤ b ∧ a ≠ b`. Cross-checks the shipped
19980        // antisymmetric-leq encoding against the leq-and-not-equal
19981        // encoding across the full 5×5 preset matrix; a regression to
19982        // a non-antisymmetric preorder encoding would fire here.
19983        for &a in STRICT_ORDER_ROSTER {
19984            for &b in STRICT_ORDER_ROSTER {
19985                let shipped = a.lt(b);
19986                let alt = a.leq(b) && a != b;
19987                assert_eq!(
19988                    shipped, alt,
19989                    "encoding cross-check failed on ({a:?}, {b:?})",
19990                );
19991            }
19992        }
19993    }
19994
19995    #[test]
19996    fn resource_limits_lt_of_bottom_diagonal_pinned() {
19997        // Concrete-preset pin — the strict chain across the (bottom,
19998        // middle, top) preset triple on the bounded-lattice diagonal.
19999        // Every `DEFAULT_MAX_*` module constant is a concrete positive
20000        // value strictly less than [`usize::MAX`] and strictly greater
20001        // than `0`, so on every axis the three endpoints are strictly
20002        // ordered; the six-axis pointwise conjunction lifts the
20003        // per-axis strict chain to the posture-level strict chain.
20004        //
20005        // Peer of `resource_limits_leq_of_default_and_unbounded_is_a_
20006        // strict_order` one STRICTNESS axis over — where the non-
20007        // strict pin asserted "leq holds one way and not the other"
20008        // via two `leq` calls, this pin asserts the strict-chain
20009        // conclusion via one `lt` call per link and pinpoints the
20010        // three-preset chain on the bounded-lattice diagonal.
20011        assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20012        assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20013        assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20014    }
20015
20016    #[test]
20017    fn resource_limits_lt_rejects_incomparable_postures() {
20018        // Partial-order non-total pin — the two hand-authored
20019        // asymmetric postures have MID smaller on three axes and OTHER
20020        // smaller on the other three, so neither `leq` direction
20021        // holds; strict `<` therefore fails in BOTH directions. The
20022        // pin discriminates a regression that promoted strict `<`
20023        // from partial to total (e.g. treating incomparable pairs as
20024        // strictly `<` in some canonical direction), and pairs with
20025        // `resource_limits_leq_is_not_total_on_asymmetric_postures`
20026        // one STRICTNESS axis over.
20027        assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
20028        assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
20029    }
20030
20031    #[test]
20032    fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
20033        // Direct-encoding cross-check — `a.lt(b) == (a.leq(b) &&
20034        // !b.leq(a))` at every pair by definition. Sibling of the
20035        // `leq_minus_equality` pin one ENCODING axis over: that pin
20036        // routes through the equality-based encoding (an
20037        // antisymmetry consequence), this pin routes through the
20038        // definitional antisymmetric-leq encoding. A regression that
20039        // shipped a different (e.g. axis-wise strict) encoding of
20040        // `lt` diverges from this cross-check on the incomparable
20041        // asymmetric-preset pair.
20042        for &a in STRICT_ORDER_ROSTER {
20043            for &b in STRICT_ORDER_ROSTER {
20044                let shipped = a.lt(b);
20045                let direct = a.leq(b) && !b.leq(a);
20046                assert_eq!(
20047                    shipped, direct,
20048                    "direct-encoding cross-check failed on ({a:?}, {b:?})",
20049                );
20050            }
20051        }
20052    }
20053
20054    #[test]
20055    fn resource_limits_gt_is_dual_of_lt() {
20056        // Direction-flip pin — `a.gt(b) == b.lt(a)` at every pair. The
20057        // shipped `gt` body delegates to `lt` verbatim so this
20058        // agreement holds definitionally, but the pin discriminates a
20059        // regression that re-implemented `gt` independently and
20060        // dropped the direction flip. Sweeps the full 5×5 preset
20061        // matrix so incomparable + strictly-ordered + equal pairs
20062        // are all exercised.
20063        for &a in STRICT_ORDER_ROSTER {
20064            for &b in STRICT_ORDER_ROSTER {
20065                assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
20066            }
20067        }
20068    }
20069
20070    #[test]
20071    fn resource_limits_gt_of_top_diagonal_pinned() {
20072        // Concrete-preset pin — the strict chain across the (top,
20073        // middle, bottom) preset triple on the bounded-lattice
20074        // diagonal, walked in the "above" direction. Peer of
20075        // `resource_limits_lt_of_bottom_diagonal_pinned` one
20076        // DIRECTION axis over.
20077        assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20078        assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20079        assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20080    }
20081
20082    #[test]
20083    fn resource_limits_gt_is_irreflexive() {
20084        // Strict partial-order law dual — `a > a == false` for every
20085        // posture. Inherits from the irreflexivity of `lt` via the
20086        // shipped `gt` body's delegation, but pinned explicitly so a
20087        // future non-delegating re-implementation of `gt` still has
20088        // its irreflexivity gated at the type-level test cohort.
20089        for &a in STRICT_ORDER_ROSTER {
20090            assert!(!a.gt(a), "irreflexivity failed on {a:?}");
20091        }
20092    }
20093
20094    #[test]
20095    fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
20096        // Const-fn pin — the strict `<` relation is evaluable in
20097        // const context, so a caller can pin a preset-strict-order
20098        // identity at compile time. Sibling of the const-fn
20099        // evaluability pin on `leq` one STRICTNESS axis over.
20100        //
20101        // A regression to a runtime `fn` here would fail the `const
20102        // _: () = assert!(...)` bindings below at compile time.
20103        const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20104        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20105        const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20106        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20107        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
20108    }
20109
20110    #[test]
20111    fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
20112        // Const-fn dual pin — sibling of `lt`'s const-fn pin one
20113        // DIRECTION axis over.
20114        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20115        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20116        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20117        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20118        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
20119    }
20120
20121    #[test]
20122    fn resource_limits_geq_is_dual_of_leq() {
20123        // Direction-flip pin — `a.geq(b) == b.leq(a)` at every pair.
20124        // The shipped `geq` body delegates to `leq` verbatim so this
20125        // agreement holds definitionally, but the pin discriminates a
20126        // regression that re-implemented `geq` independently and
20127        // dropped the direction flip. Sweeps the full 5×5 preset
20128        // matrix so incomparable + strictly-ordered + equal pairs are
20129        // all exercised. Peer of `resource_limits_gt_is_dual_of_lt`
20130        // one STRICTNESS axis over on the (below, above) × (non-strict,
20131        // strict) 2×2 pairwise partial-order face.
20132        for &a in STRICT_ORDER_ROSTER {
20133            for &b in STRICT_ORDER_ROSTER {
20134                assert_eq!(
20135                    a.geq(b),
20136                    b.leq(a),
20137                    "geq/leq duality failed on ({a:?}, {b:?})",
20138                );
20139            }
20140        }
20141    }
20142
20143    #[test]
20144    fn resource_limits_geq_is_reflexive() {
20145        // Non-strict partial-order law — `a ≥ a == true` for every
20146        // posture. Reflexivity is the non-strict-relation axiom peer of
20147        // irreflexivity on `gt` one STRICTNESS axis over on the
20148        // partial-order axiom surface; a regression that hardened the
20149        // delegation into a strict `!other.leq(self)` would fire here
20150        // on every preset since the reflexive pair collapses.
20151        for &a in STRICT_ORDER_ROSTER {
20152            assert!(a.geq(a), "reflexivity failed on {a:?}");
20153        }
20154    }
20155
20156    #[test]
20157    fn resource_limits_geq_is_antisymmetric() {
20158        // Non-strict partial-order law — `a ≥ b ∧ b ≥ a ⇒ a == b`.
20159        // Antisymmetry is inherited from `leq` via the shipped `geq`
20160        // body's `other.leq(self)` delegation (which combined with
20161        // `b.geq(a)` — i.e. `a.leq(b)` — gives `leq` antisymmetry on
20162        // the pair). Sweeps the full 5×5 preset matrix; the pin fires
20163        // on any regression that let two DISTINCT postures satisfy
20164        // `geq` in both directions.
20165        for &a in STRICT_ORDER_ROSTER {
20166            for &b in STRICT_ORDER_ROSTER {
20167                if a.geq(b) && b.geq(a) {
20168                    assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
20169                }
20170            }
20171        }
20172    }
20173
20174    #[test]
20175    fn resource_limits_geq_is_transitive() {
20176        // Non-strict partial-order law — `a ≥ b ∧ b ≥ c ⇒ a ≥ c`.
20177        // Inherits from the transitivity of `leq` via the shipped
20178        // delegation; sweeps every ordered triple in the canonical
20179        // preset roster. Peer of `resource_limits_lt_is_transitive`
20180        // one STRICTNESS axis over and `resource_limits_leq_is_
20181        // transitive` one DIRECTION axis over.
20182        for &a in STRICT_ORDER_ROSTER {
20183            for &b in STRICT_ORDER_ROSTER {
20184                for &c in STRICT_ORDER_ROSTER {
20185                    if a.geq(b) && b.geq(c) {
20186                        assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20187                    }
20188                }
20189            }
20190        }
20191    }
20192
20193    #[test]
20194    fn resource_limits_geq_refines_gt() {
20195        // Refinement axiom peer — `a > b ⇒ a ≥ b`. The strict
20196        // relation is a refinement of the non-strict one in the
20197        // "above" direction, exactly as `lt` refines `leq` in the
20198        // "below" direction (pinned by
20199        // `resource_limits_lt_refines_leq` one DIRECTION axis over).
20200        // A witness of strict `>` is also a witness of `≥`.
20201        for &a in STRICT_ORDER_ROSTER {
20202            for &b in STRICT_ORDER_ROSTER {
20203                if a.gt(b) {
20204                    assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
20205                }
20206            }
20207        }
20208    }
20209
20210    #[test]
20211    fn resource_limits_geq_agrees_with_gt_plus_equality() {
20212        // Antisymmetry consequence peer — on ANY antisymmetric partial
20213        // order, `a ≥ b ⇔ a > b ∨ a == b`. Cross-checks the shipped
20214        // delegation against the strict-or-equal encoding across the
20215        // full 5×5 preset matrix; a regression that shipped a non-
20216        // antisymmetric preorder encoding for `geq` fires here. Peer
20217        // of `resource_limits_lt_agrees_with_leq_minus_equality` one
20218        // DIRECTION-AND-STRICTNESS axis over.
20219        for &a in STRICT_ORDER_ROSTER {
20220            for &b in STRICT_ORDER_ROSTER {
20221                let shipped = a.geq(b);
20222                let alt = a.gt(b) || a == b;
20223                assert_eq!(
20224                    shipped, alt,
20225                    "encoding cross-check failed on ({a:?}, {b:?})",
20226                );
20227            }
20228        }
20229    }
20230
20231    #[test]
20232    fn resource_limits_geq_of_top_diagonal_pinned() {
20233        // Concrete-preset pin — the non-strict chain across the (top,
20234        // middle, bottom) preset triple on the bounded-lattice
20235        // diagonal, walked in the "above" direction. Peer of
20236        // `resource_limits_gt_of_top_diagonal_pinned` one STRICTNESS
20237        // axis over: the non-strict pin also holds at the reflexive
20238        // diagonal, so every posture sits above ITSELF via `geq` in
20239        // addition to sitting above the strictly-tighter presets.
20240        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20241        assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20242        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20243        assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20244        assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20245        assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20246    }
20247
20248    #[test]
20249    fn resource_limits_geq_rejects_incomparable_postures() {
20250        // Partial-order non-total pin — the two hand-authored
20251        // asymmetric postures have MID smaller on three axes and OTHER
20252        // smaller on the other three, so neither `leq` direction holds
20253        // and therefore neither `geq` direction holds. The pin
20254        // discriminates a regression that promoted `geq` from partial
20255        // to total, and pairs with
20256        // `resource_limits_lt_rejects_incomparable_postures` one
20257        // STRICTNESS-AND-DIRECTION axis over.
20258        assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
20259        assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
20260    }
20261
20262    #[test]
20263    fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
20264        // Arity-reduction pin — `a.geq(b) == a.is_upper_bound_of(&[b])`
20265        // at every pair. The pairwise "above" relation is exactly the
20266        // 1-input specialization of the N-ary upper-bound predicate;
20267        // the pin cross-checks that the two named entries agree on
20268        // the single-element case, which discriminates a regression
20269        // that drifted one from the other. Peer of the single-element
20270        // identity docstring on `is_upper_bound_of` one ARITY axis
20271        // over.
20272        for &a in STRICT_ORDER_ROSTER {
20273            for &b in STRICT_ORDER_ROSTER {
20274                assert_eq!(
20275                    a.geq(b),
20276                    a.is_upper_bound_of(&[b]),
20277                    "arity-reduction failed on ({a:?}, {b:?})",
20278                );
20279            }
20280        }
20281    }
20282
20283    #[test]
20284    fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
20285        // Const-fn pin — the non-strict `≥` relation is evaluable in
20286        // const context, so a caller can pin a preset-non-strict-order
20287        // identity at compile time. Sibling of the const-fn evaluability
20288        // pins on `leq` one DIRECTION axis over AND on `gt` one
20289        // STRICTNESS axis over.
20290        //
20291        // A regression to a runtime `fn` here would fail the `const _:
20292        // () = assert!(...)` bindings below at compile time.
20293        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20294        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20295        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20296        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20297        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20298    }
20299
20300    #[test]
20301    fn resource_limits_is_incomparable_is_irreflexive() {
20302        // Antichain-corner reflexivity pin — `a.is_incomparable(a) ==
20303        // false` on every posture. Every posture is COMPARABLE to
20304        // itself via `leq`'s reflexivity (`a.leq(a) == true`), so the
20305        // conjunction's `!self.leq(other)` leg forces the verdict to
20306        // `false` at every diagonal input. Discriminates a regression
20307        // that dropped the `!` on `self.leq(other)` (leaving the body
20308        // at `self.leq(other) && !self.geq(other)`) — that would fold
20309        // the diagonal to `false` on the reflexive `leq` arm but
20310        // silently flip the antichain corner on non-diagonal cells.
20311        for &a in STRICT_ORDER_ROSTER {
20312            assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
20313        }
20314    }
20315
20316    #[test]
20317    fn resource_limits_is_incomparable_is_symmetric() {
20318        // Antichain-corner symmetry pin — `a.is_incomparable(b) ==
20319        // b.is_incomparable(a)` on every pair. The composition
20320        // `!self.leq(other) && !self.geq(other)` folds through the
20321        // `self.leq(other) == other.geq(self)` dual identity on `geq`
20322        // and the conjunction's commutativity into an order-
20323        // independent projection on its two inputs. Sweeps every
20324        // ordered pair in the canonical preset roster;
20325        // discriminates a regression that broke either leg's dual
20326        // identity — one direction would fold to `false` on cells the
20327        // other still folded to `true`.
20328        for &a in STRICT_ORDER_ROSTER {
20329            for &b in STRICT_ORDER_ROSTER {
20330                assert_eq!(
20331                    a.is_incomparable(b),
20332                    b.is_incomparable(a),
20333                    "symmetry failed on ({a:?}, {b:?})",
20334                );
20335            }
20336        }
20337    }
20338
20339    #[test]
20340    fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
20341        // De Morgan dual pin — `a.is_incomparable(b) == !(a.leq(b) ||
20342        // a.geq(b))` on every pair. The antichain corner is the SET-
20343        // COMPLEMENT of the union of the two pointwise-domination
20344        // directions on the (comparable, incomparable) two-cell
20345        // partition of the ordered pair × verdict surface. Sweeps
20346        // every ordered pair in the canonical preset roster;
20347        // discriminates a regression that drifted the antichain
20348        // characterization off the substrate's `!leq && !geq`
20349        // composition (e.g., a shortcut through `!self.leq(other) ||
20350        // !self.geq(other)` — the OR-form falsely reports
20351        // incomparability on non-equal comparable pairs).
20352        for &a in STRICT_ORDER_ROSTER {
20353            for &b in STRICT_ORDER_ROSTER {
20354                let via_named = a.is_incomparable(b);
20355                let via_composition = !(a.leq(b) || a.geq(b));
20356                assert_eq!(
20357                    via_named, via_composition,
20358                    "De Morgan dual failed on ({a:?}, {b:?})",
20359                );
20360            }
20361        }
20362    }
20363
20364    #[test]
20365    fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
20366        // Two-cell partition pin — on every ordered pair EITHER
20367        // (a.leq(b) || a.geq(b)) OR a.is_incomparable(b), and NEVER
20368        // both. The (comparable, incomparable) partition of the
20369        // ordered pair × verdict surface is exhaustive-and-disjoint —
20370        // a substrate-level EXCLUSIVITY THEOREM the projection pins
20371        // once. Sweeps every ordered pair in the canonical preset
20372        // roster; discriminates a regression that let the two cells
20373        // overlap (e.g., an implementation that reported the antichain
20374        // corner as `true` on the comparable diagonal).
20375        for &a in STRICT_ORDER_ROSTER {
20376            for &b in STRICT_ORDER_ROSTER {
20377                let comparable = a.leq(b) || a.geq(b);
20378                let incomparable = a.is_incomparable(b);
20379                assert!(
20380                    comparable != incomparable,
20381                    "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20382                );
20383            }
20384        }
20385    }
20386
20387    #[test]
20388    fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
20389        // Bottom-pole absorption pin — `a.is_incomparable(
20390        // EMPTY_RESOURCE_LIMITS) == false` on every posture `a`.
20391        // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20392        // `EMPTY.leq(a)` holds on every axis (usize zero is the
20393        // minimum), which gives `a.geq(EMPTY)` through the `geq`
20394        // dual identity, which falsifies the `!self.geq(other)` leg.
20395        // Sweeps the canonical preset roster; pairs with the top-
20396        // pole absorption sibling one POLE axis over.
20397        for &a in STRICT_ORDER_ROSTER {
20398            assert!(
20399                !a.is_incomparable(EMPTY_RESOURCE_LIMITS),
20400                "bottom-pole absorption failed on {a:?}",
20401            );
20402            assert!(
20403                !EMPTY_RESOURCE_LIMITS.is_incomparable(a),
20404                "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20405            );
20406        }
20407    }
20408
20409    #[test]
20410    fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
20411        // Top-pole absorption pin — `a.is_incomparable(
20412        // UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a`.
20413        // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20414        // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20415        // usize::MAX` unconditionally), which falsifies the
20416        // `!self.leq(other)` leg. Sweeps the canonical preset
20417        // roster; pairs with the bottom-pole absorption sibling one
20418        // POLE axis over.
20419        for &a in STRICT_ORDER_ROSTER {
20420            assert!(
20421                !a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
20422                "top-pole absorption failed on {a:?}",
20423            );
20424            assert!(
20425                !UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
20426                "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20427            );
20428        }
20429    }
20430
20431    #[test]
20432    fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
20433        // Antichain load-bearing arm — the two hand-authored
20434        // asymmetric postures sit on distinct branches (each is
20435        // smaller on three axes and larger on the other three), so
20436        // neither pointwise-domination direction closes and the
20437        // antichain verdict HOLDS in both orderings. Pairs with
20438        // `resource_limits_geq_rejects_incomparable_postures` +
20439        // `resource_limits_lt_rejects_incomparable_postures` one COVER
20440        // axis over: those pin the two comparable-direction predicates
20441        // FALSIFY on this pair; THIS pin confirms the antichain
20442        // verdict HOLDS on the SAME pair — the two are the negation-
20443        // paired sides of the (comparable, incomparable) two-cell
20444        // partition.
20445        assert!(
20446            HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
20447            "antichain arm failed on (MID, OTHER)",
20448        );
20449        assert!(
20450            HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
20451            "antichain arm failed on flipped (OTHER, MID)",
20452        );
20453    }
20454
20455    #[test]
20456    fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
20457        // Shipped preset comparability pin — every ordered pair drawn
20458        // from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset triangle
20459        // is COMPARABLE (in both directions the pairwise partial-order
20460        // relation gives a verdict). The three shipped presets form a
20461        // TOTALLY ORDERED chain on the pointwise partial-order (EMPTY
20462        // <= DEFAULT <= UNBOUNDED on every field), so the antichain
20463        // corner MUST fold to `false` on every ordered pair drawn from
20464        // them. Discriminates a regression that promoted a shipped
20465        // preset off the total-order chain by drifting ONE field
20466        // silently.
20467        const TRIANGLE: &[ResourceLimits] = &[
20468            EMPTY_RESOURCE_LIMITS,
20469            DEFAULT_RESOURCE_LIMITS,
20470            UNBOUNDED_RESOURCE_LIMITS,
20471        ];
20472        for &a in TRIANGLE {
20473            for &b in TRIANGLE {
20474                assert!(
20475                    !a.is_incomparable(b),
20476                    "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20477                );
20478            }
20479        }
20480    }
20481
20482    #[test]
20483    fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
20484        // Const-fn pin — the antichain characterization is evaluable
20485        // in const context, so a caller can pin an antichain-
20486        // disagreement identity at compile time. Sibling of the
20487        // const-fn evaluability pins on `leq` / `geq` / `lt` / `gt`
20488        // one COVER axis over on the pairwise-relation surface.
20489        //
20490        // A regression to a runtime `fn` here would fail the `const _:
20491        // () = assert!(...)` bindings below at compile time.
20492        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20493        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20494        const _: () =
20495            assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20496        const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20497        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20498        const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20499        const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20500    }
20501
20502    // ── ResourceLimits::is_comparable ─────────────────────────────────
20503    //
20504    // The COMPARABLE CORNER on the pairwise-relation surface — the
20505    // DIRECT POSITIVE DUAL of `is_incomparable` one COVER-COMPLEMENT
20506    // axis over on the (comparable, incomparable) two-cell partition
20507    // of the ordered pair × verdict surface. See `ResourceLimits::is_comparable`
20508    // for the algebra; the pins below fix each documented property
20509    // exactly once so a regression on any leg fires on a NAMED test
20510    // rather than an ad-hoc downstream break.
20511
20512    #[test]
20513    fn resource_limits_is_comparable_is_reflexive() {
20514        // Comparable-corner reflexivity pin — `a.is_comparable(a) ==
20515        // true` on every posture. Every posture is COMPARABLE to
20516        // itself via `leq`'s reflexivity (`a.leq(a) == true`), so
20517        // the disjunction's `self.leq(other)` leg holds at every
20518        // diagonal input. Sibling of `is_incomparable`'s
20519        // irreflexivity one CELL axis over on the (comparable,
20520        // incomparable) partition — the diagonal MUST fold to `true`
20521        // here iff it folds to `false` there.
20522        for &a in STRICT_ORDER_ROSTER {
20523            assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
20524        }
20525    }
20526
20527    #[test]
20528    fn resource_limits_is_comparable_is_symmetric() {
20529        // Comparable-corner symmetry pin — `a.is_comparable(b) ==
20530        // b.is_comparable(a)` on every pair. The composition
20531        // `self.leq(other) || self.geq(other)` folds through the
20532        // `self.leq(other) == other.geq(self)` dual identity on
20533        // `geq` and the disjunction's commutativity into an order-
20534        // independent projection on its two inputs. Both cells of an
20535        // EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface
20536        // must themselves be symmetric — the two verdicts flip in
20537        // lockstep on argument swap.
20538        for &a in STRICT_ORDER_ROSTER {
20539            for &b in STRICT_ORDER_ROSTER {
20540                assert_eq!(
20541                    a.is_comparable(b),
20542                    b.is_comparable(a),
20543                    "symmetry failed on ({a:?}, {b:?})",
20544                );
20545            }
20546        }
20547    }
20548
20549    #[test]
20550    fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
20551        // De Morgan dual pin — `a.is_comparable(b) ==
20552        // !a.is_incomparable(b)` on every pair. The comparable corner
20553        // is the SET-COMPLEMENT of the antichain corner on the
20554        // (comparable, incomparable) two-cell partition of the
20555        // ordered pair × verdict surface. The DIRECT dual of
20556        // `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
20557        // one CELL axis over: that test pins the antichain corner as
20558        // the negation of the `leq || geq` disjunction; THIS test
20559        // pins the comparability corner as the negation of the
20560        // antichain projection. Together the two pin BOTH SIDES of
20561        // the partition, closing it as a substrate-level
20562        // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM.
20563        for &a in STRICT_ORDER_ROSTER {
20564            for &b in STRICT_ORDER_ROSTER {
20565                let via_named = a.is_comparable(b);
20566                let via_negated_dual = !a.is_incomparable(b);
20567                assert_eq!(
20568                    via_named, via_negated_dual,
20569                    "De Morgan dual failed on ({a:?}, {b:?})",
20570                );
20571            }
20572        }
20573    }
20574
20575    #[test]
20576    fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
20577        // Direct composition pin — `a.is_comparable(b) == a.leq(b) ||
20578        // a.geq(b)` on every pair. Discriminates a regression that
20579        // drifted the comparability body off the substrate's `leq ||
20580        // geq` composition (e.g., a shortcut through `a.leq(b) &&
20581        // a.geq(b)` — the AND-form falsely reports comparability
20582        // only on the equality diagonal, dropping the strict-order
20583        // cells). Anchors the two-primitive delegation as a
20584        // substrate-level identity.
20585        for &a in STRICT_ORDER_ROSTER {
20586            for &b in STRICT_ORDER_ROSTER {
20587                let via_named = a.is_comparable(b);
20588                let via_composition = a.leq(b) || a.geq(b);
20589                assert_eq!(
20590                    via_named, via_composition,
20591                    "leq||geq composition failed on ({a:?}, {b:?})",
20592                );
20593            }
20594        }
20595    }
20596
20597    #[test]
20598    fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
20599        // Bottom-pole absorption pin — `a.is_comparable(
20600        // EMPTY_RESOURCE_LIMITS) == true` on every posture `a`.
20601        // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20602        // `EMPTY.leq(a)` holds on every axis (`usize` zero is the
20603        // minimum), which gives `a.geq(EMPTY)` through the `geq`
20604        // dual identity, which satisfies the `self.geq(other)` leg.
20605        // The DIRECT COMPLEMENT of
20606        // `resource_limits_is_incomparable_folds_false_at_the_bottom_pole`
20607        // one CELL axis over on the SAME preset roster — where the
20608        // antichain verdict FOLDS FALSE at the bottom pole, the
20609        // comparability verdict FOLDS TRUE.
20610        for &a in STRICT_ORDER_ROSTER {
20611            assert!(
20612                a.is_comparable(EMPTY_RESOURCE_LIMITS),
20613                "bottom-pole absorption failed on {a:?}",
20614            );
20615            assert!(
20616                EMPTY_RESOURCE_LIMITS.is_comparable(a),
20617                "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20618            );
20619        }
20620    }
20621
20622    #[test]
20623    fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
20624        // Top-pole absorption pin — `a.is_comparable(
20625        // UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a`.
20626        // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20627        // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20628        // usize::MAX` unconditionally), which satisfies the
20629        // `self.leq(other)` leg. The DIRECT COMPLEMENT of
20630        // `resource_limits_is_incomparable_folds_false_at_the_top_pole`
20631        // one CELL axis over on the SAME preset roster.
20632        for &a in STRICT_ORDER_ROSTER {
20633            assert!(
20634                a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
20635                "top-pole absorption failed on {a:?}",
20636            );
20637            assert!(
20638                UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
20639                "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20640            );
20641        }
20642    }
20643
20644    #[test]
20645    fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
20646        // Antichain load-bearing arm — the two hand-authored
20647        // asymmetric postures sit on distinct branches (each is
20648        // smaller on three axes and larger on the other three), so
20649        // NEITHER pointwise-domination direction closes and the
20650        // comparability verdict FALSIFIES in both orderings. The
20651        // DIRECT NEGATION of
20652        // `resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair`
20653        // one CELL axis over on the SAME hand-authored pair — the
20654        // two are the negation-paired sides of the (comparable,
20655        // incomparable) two-cell partition, and pinning BOTH cells'
20656        // verdicts on the SAME load-bearing antichain pair closes
20657        // the partition as a substrate-level EXCLUSIVITY THEOREM.
20658        assert!(
20659            !HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
20660            "antichain arm failed on (MID, OTHER)",
20661        );
20662        assert!(
20663            !HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
20664            "antichain arm failed on flipped (OTHER, MID)",
20665        );
20666    }
20667
20668    #[test]
20669    fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
20670        // Shipped preset comparability pin — every ordered pair
20671        // drawn from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset
20672        // triangle is COMPARABLE (in at least one direction the
20673        // pairwise partial-order relation gives a verdict). The
20674        // three shipped presets form a TOTALLY ORDERED chain on the
20675        // pointwise partial-order (EMPTY <= DEFAULT <= UNBOUNDED on
20676        // every field), so the comparability corner MUST fold to
20677        // `true` on every ordered pair drawn from them. The DIRECT
20678        // COMPLEMENT of
20679        // `resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle`
20680        // one CELL axis over on the SAME triangle. Discriminates a
20681        // regression that promoted a shipped preset off the total-
20682        // order chain by drifting ONE field silently.
20683        const TRIANGLE: &[ResourceLimits] = &[
20684            EMPTY_RESOURCE_LIMITS,
20685            DEFAULT_RESOURCE_LIMITS,
20686            UNBOUNDED_RESOURCE_LIMITS,
20687        ];
20688        for &a in TRIANGLE {
20689            for &b in TRIANGLE {
20690                assert!(
20691                    a.is_comparable(b),
20692                    "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20693                );
20694            }
20695        }
20696    }
20697
20698    #[test]
20699    fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
20700        // Exhaustive-and-disjoint partition pin — on every ordered
20701        // pair EXACTLY ONE of `a.is_comparable(b)` OR
20702        // `a.is_incomparable(b)` holds. The (comparable,
20703        // incomparable) partition of the ordered pair × verdict
20704        // surface is exhaustive-and-disjoint — a substrate-level
20705        // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM the two projections
20706        // together pin. Complements
20707        // `resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable`
20708        // one CELL axis over: that test pins the partition against
20709        // the raw `a.leq(b) || a.geq(b)` composition; THIS test pins
20710        // the partition against the NAMED comparability projection —
20711        // a regression that drifted `is_comparable` off the
20712        // composition without breaking the composition-based test
20713        // would still fire here.
20714        for &a in STRICT_ORDER_ROSTER {
20715            for &b in STRICT_ORDER_ROSTER {
20716                let comparable = a.is_comparable(b);
20717                let incomparable = a.is_incomparable(b);
20718                assert!(
20719                    comparable != incomparable,
20720                    "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20721                );
20722            }
20723        }
20724    }
20725
20726    #[test]
20727    fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
20728        // Const-fn pin — the comparability characterization is
20729        // evaluable in const context, so a caller can pin a
20730        // comparability-agreement identity at compile time. Sibling
20731        // of the const-fn evaluability pins on `leq` / `geq` / `lt`
20732        // / `gt` / `is_incomparable` one COVER axis over on the
20733        // pairwise-relation surface.
20734        //
20735        // A regression to a runtime `fn` here would fail the `const _:
20736        // () = assert!(...)` bindings below at compile time.
20737        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20738        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20739        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20740        const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20741        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20742        const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20743        const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20744    }
20745
20746    // ── ResourceLimits::partial_cmp ───────────────────────────────────
20747    //
20748    // The `Option<Ordering>` FULL-VERDICT corner on the pairwise-
20749    // relation surface — the JOINT LIFT of the (comparable, incomparable)
20750    // two-cell partition together with the ordering-direction refinement
20751    // on the comparable arm. See `ResourceLimits::partial_cmp` for the
20752    // algebra; the pins below fix each documented property exactly once
20753    // so a regression on any leg fires on a NAMED test rather than an
20754    // ad-hoc downstream break.
20755
20756    #[test]
20757    fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
20758        // Diagonal-equality pin — `a.partial_cmp(a) ==
20759        // Some(Ordering::Equal)` on every posture. The `leq`
20760        // reflexivity closes both arms of the equality-guarding
20761        // conjunction, so the diagonal cell of the (Less, Equal,
20762        // Greater, None) 4-tile partition pins to the Equal tile.
20763        // Sibling of `is_comparable_is_reflexive` one PROJECTION
20764        // axis over — where the projection folds TRUE at the
20765        // diagonal, THIS method refines the verdict to `Equal`.
20766        for &a in STRICT_ORDER_ROSTER {
20767            assert_eq!(
20768                a.partial_cmp(a),
20769                Some(Ordering::Equal),
20770                "diagonal-equality failed on {a:?}",
20771            );
20772        }
20773    }
20774
20775    #[test]
20776    fn resource_limits_partial_cmp_reverses_under_argument_swap() {
20777        // Antisymmetric-swap pin — `a.partial_cmp(b) ==
20778        // b.partial_cmp(a).map(Ordering::reverse)` on every pair.
20779        // Swapping the argument pair FLIPS the ordering-direction
20780        // verdict (`Less <-> Greater`), fixes the diagonal-equality
20781        // arm, and fixes the antichain arm. Discriminates
20782        // `is_incomparable` / `is_comparable` (both SYMMETRIC on
20783        // argument swap) one POSTURE axis over — the FULL verdict
20784        // carries directional content that inverts under swap where
20785        // the projections do not.
20786        for &a in STRICT_ORDER_ROSTER {
20787            for &b in STRICT_ORDER_ROSTER {
20788                let forward = a.partial_cmp(b);
20789                let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
20790                assert_eq!(
20791                    forward, swapped_reversed,
20792                    "swap-reverse failed on ({a:?}, {b:?})",
20793                );
20794            }
20795        }
20796    }
20797
20798    #[test]
20799    fn resource_limits_partial_cmp_none_iff_incomparable() {
20800        // Antichain-arm identity pin — `a.partial_cmp(b).is_none()
20801        // == a.is_incomparable(b)` on every pair. The `None` arm of
20802        // the return coincides EXACTLY with the antichain-corner
20803        // projection. Anchors the joint-lift claim: THIS method
20804        // subsumes `is_incomparable` at its `is_none()` projection
20805        // without drifting the antichain characterization.
20806        for &a in STRICT_ORDER_ROSTER {
20807            for &b in STRICT_ORDER_ROSTER {
20808                let via_partial_cmp = a.partial_cmp(b).is_none();
20809                let via_is_incomparable = a.is_incomparable(b);
20810                assert_eq!(
20811                    via_partial_cmp, via_is_incomparable,
20812                    "none-iff-incomparable failed on ({a:?}, {b:?})",
20813                );
20814            }
20815        }
20816    }
20817
20818    #[test]
20819    fn resource_limits_partial_cmp_some_iff_comparable() {
20820        // Comparability-arm identity pin — `a.partial_cmp(b).is_some()
20821        // == a.is_comparable(b)` on every pair. The `Some(_)` arm of
20822        // the return coincides EXACTLY with the comparability-corner
20823        // projection. The DUAL of the antichain-arm pin one CELL axis
20824        // over on the (comparable, incomparable) partition — together
20825        // the two pins close the JOINT LIFT of both cells at THIS
20826        // method.
20827        for &a in STRICT_ORDER_ROSTER {
20828            for &b in STRICT_ORDER_ROSTER {
20829                let via_partial_cmp = a.partial_cmp(b).is_some();
20830                let via_is_comparable = a.is_comparable(b);
20831                assert_eq!(
20832                    via_partial_cmp, via_is_comparable,
20833                    "some-iff-comparable failed on ({a:?}, {b:?})",
20834                );
20835            }
20836        }
20837    }
20838
20839    #[test]
20840    fn resource_limits_partial_cmp_less_iff_lt() {
20841        // Strict-less agreement pin — `a.partial_cmp(b) ==
20842        // Some(Ordering::Less)` iff `a.lt(b)`. The strict-less arm of
20843        // the (Less, Equal, Greater, None) 4-tile partition binds to
20844        // the named `lt` companion primitive at exactly one dispatch
20845        // site. A regression that drifted the arm off `lt` (e.g., to
20846        // `leq` — including the equality diagonal) would fire here.
20847        for &a in STRICT_ORDER_ROSTER {
20848            for &b in STRICT_ORDER_ROSTER {
20849                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
20850                let via_lt = a.lt(b);
20851                assert_eq!(
20852                    via_partial_cmp, via_lt,
20853                    "less-iff-lt failed on ({a:?}, {b:?})",
20854                );
20855            }
20856        }
20857    }
20858
20859    #[test]
20860    fn resource_limits_partial_cmp_greater_iff_gt() {
20861        // Strict-greater agreement pin — `a.partial_cmp(b) ==
20862        // Some(Ordering::Greater)` iff `a.gt(b)`. Dual of the strict-
20863        // less agreement pin one DIRECTION axis over on the (Less,
20864        // Greater) strict-order pair — a regression that dropped the
20865        // asymmetry between the two arms (e.g., collapsed both to
20866        // `Less`) would fire here.
20867        for &a in STRICT_ORDER_ROSTER {
20868            for &b in STRICT_ORDER_ROSTER {
20869                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
20870                let via_gt = a.gt(b);
20871                assert_eq!(
20872                    via_partial_cmp, via_gt,
20873                    "greater-iff-gt failed on ({a:?}, {b:?})",
20874                );
20875            }
20876        }
20877    }
20878
20879    #[test]
20880    fn resource_limits_partial_cmp_equal_iff_eq() {
20881        // Equality agreement pin — `a.partial_cmp(b) ==
20882        // Some(Ordering::Equal)` iff `a == b`. The equality arm of the
20883        // 4-tile partition binds through the pointwise antisymmetric-
20884        // equality characterization (`a.leq(b) && b.leq(a) → a == b`,
20885        // pinned by `resource_limits_leq_is_antisymmetric`) to the
20886        // `PartialEq::eq` companion. Discriminates a regression that
20887        // drifted the Equal arm off the antisymmetric conjunction.
20888        for &a in STRICT_ORDER_ROSTER {
20889            for &b in STRICT_ORDER_ROSTER {
20890                let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
20891                let via_eq = a == b;
20892                assert_eq!(
20893                    via_partial_cmp, via_eq,
20894                    "equal-iff-eq failed on ({a:?}, {b:?})",
20895                );
20896            }
20897        }
20898    }
20899
20900    #[test]
20901    fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
20902        // Bottom-to-top-pole ordering pin — every strictly-ascending
20903        // pair on the shipped preset chain (EMPTY <= DEFAULT <=
20904        // UNBOUNDED) folds to `Some(Ordering::Less)`. The three
20905        // shipped presets form a TOTALLY ORDERED chain on the
20906        // pointwise partial-order (every DEFAULT_MAX_* is a concrete
20907        // positive value strictly less than usize::MAX), so the
20908        // strict-less arm fires on every ordered pair drawn in
20909        // ascending direction.
20910        assert_eq!(
20911            EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20912            Some(Ordering::Less),
20913        );
20914        assert_eq!(
20915            DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20916            Some(Ordering::Less),
20917        );
20918        assert_eq!(
20919            EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20920            Some(Ordering::Less),
20921        );
20922    }
20923
20924    #[test]
20925    fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
20926        // Top-to-bottom-pole ordering pin — every strictly-descending
20927        // pair on the shipped preset chain folds to
20928        // `Some(Ordering::Greater)`. The DUAL of the ascending pin
20929        // one DIRECTION axis over — together the two pins close the
20930        // (ascending, descending) direction pair on the shipped
20931        // preset chain at the ordering-direction refinement.
20932        assert_eq!(
20933            DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20934            Some(Ordering::Greater),
20935        );
20936        assert_eq!(
20937            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20938            Some(Ordering::Greater),
20939        );
20940        assert_eq!(
20941            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20942            Some(Ordering::Greater),
20943        );
20944    }
20945
20946    #[test]
20947    fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
20948        // Antichain load-bearing arm — the two hand-authored
20949        // asymmetric postures sit on distinct branches (each is
20950        // smaller on three axes and larger on the other three), so
20951        // NEITHER pointwise-domination direction closes and the
20952        // fall-through `None` arm fires in both orderings. The DIRECT
20953        // LIFT of `is_incomparable_holds_on_the_hand_authored_antichain_pair`
20954        // one PROJECTION axis over on the SAME hand-authored pair —
20955        // where the projection folds TRUE, THIS method folds to
20956        // `None`. The two are the negation-paired sides of the
20957        // (Some(_), None) two-cell partition of `Option<Ordering>`.
20958        assert_eq!(
20959            HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
20960            None,
20961        );
20962        assert_eq!(
20963            HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
20964            None,
20965        );
20966    }
20967
20968    #[test]
20969    fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
20970        // Const-fn pin — the full-verdict projection is evaluable in
20971        // const context, so a caller can pin an ordering-direction
20972        // identity at compile time. Sibling of the const-fn
20973        // evaluability pins on `leq` / `geq` / `lt` / `gt` /
20974        // `is_incomparable` / `is_comparable` one COVER axis over on
20975        // the pairwise-relation surface.
20976        //
20977        // `matches!` in const context requires the pattern's variants
20978        // to be const-constructible — `Ordering::Less` / `Equal` /
20979        // `Greater` are plain enum constructors, so the assertions
20980        // below stay inside stable const-fn scope. A regression to a
20981        // runtime `fn` here would fail the `const _: () = assert!(
20982        // matches!(...))` bindings below at compile time.
20983        const _: () = assert!(matches!(
20984            EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20985            Some(Ordering::Equal),
20986        ));
20987        const _: () = assert!(matches!(
20988            EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20989            Some(Ordering::Less),
20990        ));
20991        const _: () = assert!(matches!(
20992            UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
20993            Some(Ordering::Greater),
20994        ));
20995        const _: () = assert!(matches!(
20996            DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20997            Some(Ordering::Less),
20998        ));
20999        const _: () = assert!(matches!(
21000            EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21001            Some(Ordering::Less),
21002        ));
21003    }
21004
21005    #[test]
21006    fn resource_limits_is_chain_empty_slice_is_vacuously_true() {
21007        // Empty-slice vacuous truth — the empty conjunction is vacuously
21008        // true; the empty slice contains no distinct pairs to reject.
21009        // Peer of `is_lower_bound_of(&[]) == true`'s empty-slice identity
21010        // one PRIMITIVE-KIND axis over on the (leq, comparable) ×
21011        // (pairwise, N-ary) primitive surface.
21012        assert!(ResourceLimits::is_chain(&[]));
21013    }
21014
21015    #[test]
21016    fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
21017        // Empty-slice vacuous truth — the empty conjunction is vacuously
21018        // true; the empty slice contains no distinct pairs to reject.
21019        // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21020        // COVER-COMPLEMENT axis over: both cells of the (chain, antichain)
21021        // set-level pair AGREE at the empty slice on the vacuous-truth
21022        // verdict — the two cells coincide at the empty face of the
21023        // set-level verdict surface.
21024        assert!(ResourceLimits::is_antichain(&[]));
21025    }
21026
21027    #[test]
21028    fn resource_limits_is_chain_singleton_is_vacuously_true() {
21029        // Singleton vacuous truth — a one-element slice contains no
21030        // distinct pairs, so the outer-and-inner-index conjunction never
21031        // enters the inner loop and returns `true` vacuously. Pinned on
21032        // every canonical preset AND on both hand-authored asymmetric
21033        // postures so the vacuous-truth verdict holds regardless of the
21034        // singleton element's lattice position.
21035        assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21036        assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
21037        assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
21038        assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
21039        assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
21040    }
21041
21042    #[test]
21043    fn resource_limits_is_antichain_singleton_is_vacuously_true() {
21044        // Singleton vacuous truth — a one-element slice contains no
21045        // distinct pairs, so the outer-and-inner-index conjunction never
21046        // enters the inner loop and returns `true` vacuously. Peer of
21047        // `is_chain`'s singleton-vacuous-truth identity: both cells AGREE
21048        // at singleton slices — the (chain, antichain) verdict surface
21049        // coincides at every slice with strictly fewer than two distinct
21050        // pairs.
21051        assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21052        assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
21053        assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
21054        assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
21055        assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
21056    }
21057
21058    #[test]
21059    fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
21060        // Diagonal-duplicate identity — `is_comparable` is REFLEXIVE
21061        // (`a.is_comparable(a) == true`), so the single distinct index
21062        // pair `(0, 1)` binds `a.is_comparable(a) == true` and the
21063        // conjunction holds at every diagonal-duplicate slice.
21064        // Distinguishes it from `is_antichain` one COVER-COMPLEMENT axis
21065        // over, whose diagonal-duplicate verdict is `false` via
21066        // `is_incomparable`'s IRREFLEXIVITY.
21067        assert!(ResourceLimits::is_chain(&[
21068            DEFAULT_RESOURCE_LIMITS,
21069            DEFAULT_RESOURCE_LIMITS,
21070        ]));
21071        assert!(ResourceLimits::is_chain(&[
21072            HAND_AUTHORED_MID_POSTURE,
21073            HAND_AUTHORED_MID_POSTURE,
21074        ]));
21075    }
21076
21077    #[test]
21078    fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
21079        // Diagonal-duplicate rejection — `is_incomparable` is
21080        // IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
21081        // distinct index pair `(0, 1)` binds `a.is_incomparable(a) ==
21082        // false` and the conjunction rejects at every diagonal-duplicate
21083        // slice. The reflexivity/irreflexivity divergence between the
21084        // two pair-level projections PROPAGATES to the set-level
21085        // projections at any slice with a duplicated element — the two
21086        // set-level cells DIVERGE at every diagonal-duplicate slice, the
21087        // mirror of their AGREEMENT at every empty-or-singleton slice
21088        // one CARDINALITY axis over.
21089        assert!(!ResourceLimits::is_antichain(&[
21090            DEFAULT_RESOURCE_LIMITS,
21091            DEFAULT_RESOURCE_LIMITS,
21092        ]));
21093        assert!(!ResourceLimits::is_antichain(&[
21094            HAND_AUTHORED_MID_POSTURE,
21095            HAND_AUTHORED_MID_POSTURE,
21096        ]));
21097    }
21098
21099    #[test]
21100    fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
21101        // Ordered-chain closure — the shipped-preset triple on the
21102        // bounded-lattice diagonal is an ascending chain (`EMPTY <=
21103        // DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among
21104        // the three is comparable. Pinned in every permutation of the
21105        // triple so the doubly-indexed all-pairs walk closes on every
21106        // enumeration of the same chain.
21107        assert!(ResourceLimits::is_chain(&[
21108            EMPTY_RESOURCE_LIMITS,
21109            DEFAULT_RESOURCE_LIMITS,
21110            UNBOUNDED_RESOURCE_LIMITS,
21111        ]));
21112        assert!(ResourceLimits::is_chain(&[
21113            UNBOUNDED_RESOURCE_LIMITS,
21114            DEFAULT_RESOURCE_LIMITS,
21115            EMPTY_RESOURCE_LIMITS,
21116        ]));
21117        assert!(ResourceLimits::is_chain(&[
21118            DEFAULT_RESOURCE_LIMITS,
21119            EMPTY_RESOURCE_LIMITS,
21120            UNBOUNDED_RESOURCE_LIMITS,
21121        ]));
21122    }
21123
21124    #[test]
21125    fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
21126        // Chain rejection — the shipped-preset pair on the bounded-
21127        // lattice diagonal is a strict chain (`EMPTY.lt(DEFAULT)`), so
21128        // the single distinct pair is COMPARABLE and the antichain
21129        // conjunction rejects. The DIRECT FALSIFICATION of
21130        // `is_chain_holds_on_the_shipped_preset_triple` one CELL axis
21131        // over on the SAME lattice diagonal — the two set-level cells
21132        // carry the negation-paired verdict at every strictly-chained
21133        // slice, the mirror of their AGREEMENT at every empty-or-
21134        // singleton slice one CARDINALITY axis over.
21135        assert!(!ResourceLimits::is_antichain(&[
21136            EMPTY_RESOURCE_LIMITS,
21137            DEFAULT_RESOURCE_LIMITS,
21138        ]));
21139        assert!(!ResourceLimits::is_antichain(&[
21140            DEFAULT_RESOURCE_LIMITS,
21141            UNBOUNDED_RESOURCE_LIMITS,
21142        ]));
21143        assert!(!ResourceLimits::is_antichain(&[
21144            EMPTY_RESOURCE_LIMITS,
21145            DEFAULT_RESOURCE_LIMITS,
21146            UNBOUNDED_RESOURCE_LIMITS,
21147        ]));
21148    }
21149
21150    #[test]
21151    fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
21152        // Antichain rejection — the two hand-authored asymmetric
21153        // postures sit on distinct branches (each is smaller on three
21154        // axes and larger on the other three), so neither pointwise-
21155        // domination direction closes and the pair is INCOMPARABLE. The
21156        // chain conjunction rejects at the first incomparable pair.
21157        // Pinned in both orderings so the doubly-indexed pair walk
21158        // rejects regardless of index order (the diagonal-adjacent
21159        // pair-level rejection is symmetric on argument swap).
21160        assert!(!ResourceLimits::is_chain(&[
21161            HAND_AUTHORED_MID_POSTURE,
21162            HAND_AUTHORED_OTHER_POSTURE,
21163        ]));
21164        assert!(!ResourceLimits::is_chain(&[
21165            HAND_AUTHORED_OTHER_POSTURE,
21166            HAND_AUTHORED_MID_POSTURE,
21167        ]));
21168    }
21169
21170    #[test]
21171    fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
21172        // Hand-authored antichain closure — the two hand-authored
21173        // asymmetric postures sit on distinct branches (each is smaller
21174        // on three axes and larger on the other three), so neither
21175        // pointwise-domination direction closes and the pair is
21176        // INCOMPARABLE. The antichain conjunction accepts every distinct
21177        // pair of the slice. DIRECT LIFT of `is_chain`'s antichain
21178        // rejection one CELL axis over on the SAME hand-authored pair —
21179        // pinning BOTH cells' verdicts on the SAME load-bearing
21180        // antichain pair closes the (chain, antichain) two-cell face on
21181        // arity-2 slices as a substrate-level EXCLUSIVITY THEOREM.
21182        assert!(ResourceLimits::is_antichain(&[
21183            HAND_AUTHORED_MID_POSTURE,
21184            HAND_AUTHORED_OTHER_POSTURE,
21185        ]));
21186        assert!(ResourceLimits::is_antichain(&[
21187            HAND_AUTHORED_OTHER_POSTURE,
21188            HAND_AUTHORED_MID_POSTURE,
21189        ]));
21190    }
21191
21192    #[test]
21193    fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
21194        // Mixed-set rejection — both `(DEFAULT, MID)` and `(DEFAULT,
21195        // OTHER)` are comparable pairs (DEFAULT's shipped `DEFAULT_MAX_*`
21196        // per-axis values are all much larger than either hand-authored
21197        // posture's, so DEFAULT dominates both pointwise), but the
21198        // `(MID, OTHER)` pair is the hand-authored INCOMPARABLE pair.
21199        // The set-level chain verdict is stricter than the union of its
21200        // pair-level verdicts — one antichain pair anywhere in the
21201        // slice falsifies the whole projection.
21202        assert!(!ResourceLimits::is_chain(&[
21203            DEFAULT_RESOURCE_LIMITS,
21204            HAND_AUTHORED_MID_POSTURE,
21205            HAND_AUTHORED_OTHER_POSTURE,
21206        ]));
21207    }
21208
21209    #[test]
21210    fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
21211        // Mixed-set rejection — even though the second-and-third pair
21212        // is incomparable (the hand-authored antichain pair), the first
21213        // pair (`DEFAULT`-and-`MID`) is comparable, so the whole-set
21214        // conjunction rejects. Pins the three-cell (chain, antichain,
21215        // mixed) set-level face at the mixed-set cell: the same slice
21216        // both `is_chain` AND `is_antichain` reject at, and the third
21217        // cell of the three-cell face the two set-level projections
21218        // carry the ends of.
21219        assert!(!ResourceLimits::is_antichain(&[
21220            DEFAULT_RESOURCE_LIMITS,
21221            HAND_AUTHORED_MID_POSTURE,
21222            HAND_AUTHORED_OTHER_POSTURE,
21223        ]));
21224    }
21225
21226    #[test]
21227    fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
21228        // De Morgan mirror at arity 2 — on any two-element slice `[a,
21229        // b]` with `a != b`, the set-level (chain, antichain) two-cell
21230        // face reduces to the pair-level (comparable, incomparable)
21231        // two-cell partition and the two cells become MUTUALLY
21232        // EXCLUSIVE (the mixed cell requires ≥3 elements to open). The
21233        // set-level De Morgan mirror of the pair-level identity
21234        // `a.is_comparable(b) == !a.is_incomparable(b)` `is_comparable`
21235        // pins one CARDINALITY axis over.
21236        let presets: [ResourceLimits; 5] = [
21237            EMPTY_RESOURCE_LIMITS,
21238            DEFAULT_RESOURCE_LIMITS,
21239            UNBOUNDED_RESOURCE_LIMITS,
21240            HAND_AUTHORED_MID_POSTURE,
21241            HAND_AUTHORED_OTHER_POSTURE,
21242        ];
21243        for a in presets {
21244            for b in presets {
21245                if a == b {
21246                    continue;
21247                }
21248                let pair = [a, b];
21249                let chain = ResourceLimits::is_chain(&pair);
21250                let antichain = ResourceLimits::is_antichain(&pair);
21251                assert_ne!(
21252                    chain, antichain,
21253                    "distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
21254                );
21255            }
21256        }
21257    }
21258
21259    #[test]
21260    fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
21261        // Empty-and-singleton agreement — the two set-level projections
21262        // carry the SAME vacuous-truth verdict at every slice with
21263        // strictly fewer than two distinct pairs (empty AND singleton).
21264        // Pinned as a substrate-level identity so a future rewrite of
21265        // either projection cannot silently drift from the shared
21266        // vacuous-truth verdict.
21267        assert_eq!(
21268            ResourceLimits::is_chain(&[]),
21269            ResourceLimits::is_antichain(&[]),
21270        );
21271        let presets: [ResourceLimits; 5] = [
21272            EMPTY_RESOURCE_LIMITS,
21273            DEFAULT_RESOURCE_LIMITS,
21274            UNBOUNDED_RESOURCE_LIMITS,
21275            HAND_AUTHORED_MID_POSTURE,
21276            HAND_AUTHORED_OTHER_POSTURE,
21277        ];
21278        for a in presets {
21279            let single = [a];
21280            assert_eq!(
21281                ResourceLimits::is_chain(&single),
21282                ResourceLimits::is_antichain(&single),
21283                "singleton agreement failed on {a:?}",
21284            );
21285        }
21286    }
21287
21288    #[test]
21289    fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
21290        // Const-fn pin — the set-level chain projection is evaluable in
21291        // const context, so a caller can pin a chain-membership
21292        // identity at compile time. Sibling of the const-fn
21293        // evaluability pins on `is_comparable` one CARDINALITY axis
21294        // over and on `is_lower_bound_of` / `is_upper_bound_of` one
21295        // PRIMITIVE-KIND axis over on the N-ary aggregation surface.
21296        const _: () = assert!(ResourceLimits::is_chain(&[]));
21297        const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21298        const _: () = assert!(ResourceLimits::is_chain(&[
21299            EMPTY_RESOURCE_LIMITS,
21300            DEFAULT_RESOURCE_LIMITS,
21301            UNBOUNDED_RESOURCE_LIMITS,
21302        ]));
21303    }
21304
21305    #[test]
21306    fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
21307        // Const-fn pin — the set-level antichain projection is
21308        // evaluable in const context, so a caller can pin an antichain-
21309        // membership identity at compile time. Sibling of the const-fn
21310        // evaluability pins on `is_incomparable` one CARDINALITY axis
21311        // over and on `is_chain` one COVER-COMPLEMENT axis over.
21312        const _: () = assert!(ResourceLimits::is_antichain(&[]));
21313        const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21314        const _: () = assert!(ResourceLimits::is_antichain(&[
21315            HAND_AUTHORED_MID_POSTURE,
21316            HAND_AUTHORED_OTHER_POSTURE,
21317        ]));
21318    }
21319
21320    #[test]
21321    fn resource_limits_is_mixed_empty_slice_is_false() {
21322        // Empty-slice contract — is_chain and is_antichain BOTH return
21323        // `true` vacuously, so `!true && !true == false`. The empty
21324        // slice is NOT mixed. Sibling posture to the vacuous-truth
21325        // verdicts of is_chain + is_antichain one CELL axis over on the
21326        // (chain, antichain, mixed) three-cell face.
21327        assert!(!ResourceLimits::is_mixed(&[]));
21328    }
21329
21330    #[test]
21331    fn resource_limits_is_mixed_singleton_is_false() {
21332        // Singleton contract — a one-element slice contains no distinct
21333        // pairs; is_chain + is_antichain BOTH return `true` vacuously.
21334        // Pinned on every canonical preset AND on both hand-authored
21335        // asymmetric postures so the false-at-singleton verdict holds
21336        // regardless of the singleton element's lattice position.
21337        assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21338        assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
21339        assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
21340        assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
21341        assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
21342    }
21343
21344    #[test]
21345    fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
21346        // Diagonal-duplicate contract — is_chain returns `true` via
21347        // is_comparable's REFLEXIVITY; is_antichain returns `false` via
21348        // is_incomparable's IRREFLEXIVITY; the conjunction
21349        // `!true && !false == false`. The diagonal-duplicate slice is a
21350        // CHAIN (per the reflexivity arm), not mixed.
21351        assert!(!ResourceLimits::is_mixed(&[
21352            DEFAULT_RESOURCE_LIMITS,
21353            DEFAULT_RESOURCE_LIMITS,
21354        ]));
21355        assert!(!ResourceLimits::is_mixed(&[
21356            HAND_AUTHORED_MID_POSTURE,
21357            HAND_AUTHORED_MID_POSTURE,
21358        ]));
21359    }
21360
21361    #[test]
21362    fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
21363        // Chain rejection — the shipped-preset triple on the bounded-
21364        // lattice diagonal is an ascending chain, is_chain returns
21365        // `true`, and `!true && …` short-circuits to `false`. A PURE
21366        // chain is not mixed. Pinned in every permutation of the triple
21367        // so the projection factors through the ordering-agnostic
21368        // sibling is_chain verdict on the same chain.
21369        assert!(!ResourceLimits::is_mixed(&[
21370            EMPTY_RESOURCE_LIMITS,
21371            DEFAULT_RESOURCE_LIMITS,
21372            UNBOUNDED_RESOURCE_LIMITS,
21373        ]));
21374        assert!(!ResourceLimits::is_mixed(&[
21375            UNBOUNDED_RESOURCE_LIMITS,
21376            DEFAULT_RESOURCE_LIMITS,
21377            EMPTY_RESOURCE_LIMITS,
21378        ]));
21379    }
21380
21381    #[test]
21382    fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
21383        // Antichain rejection — the hand-authored antichain pair binds
21384        // is_antichain to `true`, and `!… && !true == false`. A PURE
21385        // antichain is not mixed. Pinned in both orderings so the
21386        // projection factors through the argument-swap-symmetric
21387        // sibling is_antichain verdict on the same antichain.
21388        assert!(!ResourceLimits::is_mixed(&[
21389            HAND_AUTHORED_MID_POSTURE,
21390            HAND_AUTHORED_OTHER_POSTURE,
21391        ]));
21392        assert!(!ResourceLimits::is_mixed(&[
21393            HAND_AUTHORED_OTHER_POSTURE,
21394            HAND_AUTHORED_MID_POSTURE,
21395        ]));
21396    }
21397
21398    #[test]
21399    fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
21400    ) {
21401        // Mixed-set closure — LOAD-BEARING `true`-arm catch. The
21402        // (DEFAULT, MID) pair is comparable (falsifying is_antichain)
21403        // while the (MID, OTHER) pair is incomparable (falsifying
21404        // is_chain); the conjunction `!false && !false == true`. This
21405        // is the SAME fixture the sibling is_chain + is_antichain BOTH
21406        // reject at — the DISCRIMINATING positive-arm catch for the
21407        // (mixed) cell of the three-cell face the two set-level
21408        // projections carry the ends of.
21409        assert!(ResourceLimits::is_mixed(&[
21410            DEFAULT_RESOURCE_LIMITS,
21411            HAND_AUTHORED_MID_POSTURE,
21412            HAND_AUTHORED_OTHER_POSTURE,
21413        ]));
21414    }
21415
21416    #[test]
21417    fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
21418    ) {
21419        // Trichotomy exhaustiveness at arity ≥2 with pairwise-distinct
21420        // elements — EXACTLY ONE of is_chain, is_antichain, is_mixed
21421        // returns `true` on every pair OR triple drawn from the shipped
21422        // presets whose elements are pairwise distinct. The three cells
21423        // partition the set-level verdict surface at every non-
21424        // degenerate slice. Substrate-level identity: pinning it here
21425        // catches a future rewrite of any of the three projections that
21426        // silently drifts from the partition contract.
21427        let presets: [ResourceLimits; 5] = [
21428            EMPTY_RESOURCE_LIMITS,
21429            DEFAULT_RESOURCE_LIMITS,
21430            UNBOUNDED_RESOURCE_LIMITS,
21431            HAND_AUTHORED_MID_POSTURE,
21432            HAND_AUTHORED_OTHER_POSTURE,
21433        ];
21434        // Arity-2 sweep — the mixed cell CANNOT open at arity 2 (only
21435        // one distinct pair), so is_mixed is always false and the
21436        // partition reduces to the (chain, antichain) two-cell face
21437        // that is_chain + is_antichain already pinned MUTUALLY
21438        // EXCLUSIVE. Cross-check that the three-cell projection
21439        // AGREES with the two-cell projection at arity 2.
21440        for a in presets {
21441            for b in presets {
21442                if a == b {
21443                    continue;
21444                }
21445                let pair = [a, b];
21446                let chain = ResourceLimits::is_chain(&pair);
21447                let antichain = ResourceLimits::is_antichain(&pair);
21448                let mixed = ResourceLimits::is_mixed(&pair);
21449                let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21450                assert_eq!(
21451                    true_count, 1,
21452                    "trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21453                );
21454                assert!(
21455                    !mixed,
21456                    "is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
21457                );
21458            }
21459        }
21460        // Arity-3 sweep on distinct triples — the mixed cell opens
21461        // whenever the triple carries both a comparable and an
21462        // incomparable pair (e.g. DEFAULT + MID + OTHER above).
21463        for a in presets {
21464            for b in presets {
21465                for c in presets {
21466                    if a == b || b == c || a == c {
21467                        continue;
21468                    }
21469                    let triple = [a, b, c];
21470                    let chain = ResourceLimits::is_chain(&triple);
21471                    let antichain = ResourceLimits::is_antichain(&triple);
21472                    let mixed = ResourceLimits::is_mixed(&triple);
21473                    let true_count =
21474                        usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21475                    assert_eq!(
21476                        true_count, 1,
21477                        "trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21478                    );
21479                }
21480            }
21481        }
21482    }
21483
21484    #[test]
21485    fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
21486        // Const-fn pin — the set-level mixed projection is evaluable
21487        // in const context, so a caller can pin a mixed-set identity
21488        // at compile time. Sibling of the const-fn evaluability pins
21489        // on is_chain + is_antichain one CELL axis over.
21490        const _: () = assert!(!ResourceLimits::is_mixed(&[]));
21491        const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21492        const _: () = assert!(ResourceLimits::is_mixed(&[
21493            DEFAULT_RESOURCE_LIMITS,
21494            HAND_AUTHORED_MID_POSTURE,
21495            HAND_AUTHORED_OTHER_POSTURE,
21496        ]));
21497    }
21498
21499    // ── ResourceLimits::is_ascending / ::is_descending — sequence-level ──
21500
21501    #[test]
21502    fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
21503        // Empty-slice vacuous truth — the empty conjunction is vacuously
21504        // true; the empty slice contains no consecutive pairs to reject.
21505        // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21506        // CONSECUTIVE-VS-ALL-PAIRS axis over: both cells of the (set-
21507        // level, sequence-level) projection pair AGREE at the empty slice
21508        // on the vacuous-truth verdict.
21509        assert!(ResourceLimits::is_ascending(&[]));
21510    }
21511
21512    #[test]
21513    fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
21514        // Peer of is_ascending's empty-slice identity one PAIR-LEVEL-
21515        // PRIMITIVE axis over: both cells of the (ascending, descending)
21516        // sequence-level pair AGREE at the empty slice.
21517        assert!(ResourceLimits::is_descending(&[]));
21518    }
21519
21520    #[test]
21521    fn resource_limits_is_ascending_singleton_is_vacuously_true() {
21522        // Singleton vacuous truth — a one-element slice contains no
21523        // consecutive pairs, so the walk never enters the loop and
21524        // returns `true` vacuously. Pinned on every canonical preset AND
21525        // on both hand-authored asymmetric postures.
21526        assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21527        assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
21528        assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
21529        assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
21530        assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
21531    }
21532
21533    #[test]
21534    fn resource_limits_is_descending_singleton_is_vacuously_true() {
21535        assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21536        assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
21537        assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
21538        assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
21539        assert!(ResourceLimits::is_descending(&[
21540            HAND_AUTHORED_OTHER_POSTURE
21541        ]));
21542    }
21543
21544    #[test]
21545    fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
21546        // Diagonal-duplicate identity — `leq` is REFLEXIVE
21547        // (`a.leq(a) == true` via each axis's `<=` reflexivity), so the
21548        // single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
21549        // the conjunction holds at every diagonal-duplicate slice.
21550        // AGREES with is_chain's diagonal-duplicate verdict — the two
21551        // projections diverge only where the ORDER matters, and
21552        // duplicated elements carry no ordering information.
21553        assert!(ResourceLimits::is_ascending(&[
21554            DEFAULT_RESOURCE_LIMITS,
21555            DEFAULT_RESOURCE_LIMITS,
21556        ]));
21557        assert!(ResourceLimits::is_ascending(&[
21558            HAND_AUTHORED_MID_POSTURE,
21559            HAND_AUTHORED_MID_POSTURE,
21560        ]));
21561    }
21562
21563    #[test]
21564    fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
21565        // Diagonal-duplicate identity — `geq` is REFLEXIVE, so the
21566        // single consecutive pair `(0, 1)` binds `a.geq(a) == true`.
21567        // AGREES with is_ascending's diagonal-duplicate verdict — both
21568        // leq and geq carry reflexivity, so the sequence-level (T, T)
21569        // corner opens at every diagonal-duplicate slice, distinct from
21570        // the set-level (T, F) verdict is_chain / is_antichain carry on
21571        // the same slice.
21572        assert!(ResourceLimits::is_descending(&[
21573            DEFAULT_RESOURCE_LIMITS,
21574            DEFAULT_RESOURCE_LIMITS,
21575        ]));
21576        assert!(ResourceLimits::is_descending(&[
21577            HAND_AUTHORED_MID_POSTURE,
21578            HAND_AUTHORED_MID_POSTURE,
21579        ]));
21580    }
21581
21582    #[test]
21583    fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
21584        // Ordered ascending closure — the shipped-preset triple on the
21585        // bounded-lattice diagonal enumerated leq-ascending has
21586        // `EMPTY.leq(DEFAULT) == true` and `DEFAULT.leq(UNBOUNDED) ==
21587        // true`, so every consecutive pair passes the conjunction.
21588        // Pinned on the arity-2 prefix AND on the full arity-3 chain so
21589        // the walk closes at every non-degenerate ascending enumeration
21590        // length.
21591        assert!(ResourceLimits::is_ascending(&[
21592            EMPTY_RESOURCE_LIMITS,
21593            DEFAULT_RESOURCE_LIMITS,
21594        ]));
21595        assert!(ResourceLimits::is_ascending(&[
21596            DEFAULT_RESOURCE_LIMITS,
21597            UNBOUNDED_RESOURCE_LIMITS,
21598        ]));
21599        assert!(ResourceLimits::is_ascending(&[
21600            EMPTY_RESOURCE_LIMITS,
21601            DEFAULT_RESOURCE_LIMITS,
21602            UNBOUNDED_RESOURCE_LIMITS,
21603        ]));
21604    }
21605
21606    #[test]
21607    fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
21608        // Ordered descending closure — the reversed enumeration is a
21609        // geq-monotone descending sequence. The DIRECT MIRROR of
21610        // is_ascending's ascending closure one PERMUTATION axis over.
21611        assert!(ResourceLimits::is_descending(&[
21612            DEFAULT_RESOURCE_LIMITS,
21613            EMPTY_RESOURCE_LIMITS,
21614        ]));
21615        assert!(ResourceLimits::is_descending(&[
21616            UNBOUNDED_RESOURCE_LIMITS,
21617            DEFAULT_RESOURCE_LIMITS,
21618        ]));
21619        assert!(ResourceLimits::is_descending(&[
21620            UNBOUNDED_RESOURCE_LIMITS,
21621            DEFAULT_RESOURCE_LIMITS,
21622            EMPTY_RESOURCE_LIMITS,
21623        ]));
21624    }
21625
21626    #[test]
21627    fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
21628        // Order-sensitivity — the reversed enumeration of the same
21629        // shipped-preset chain has `UNBOUNDED.leq(DEFAULT) == false` at
21630        // the first consecutive pair. is_chain accepts BOTH orderings
21631        // (order-INSENSITIVE); this projection accepts only the leq-
21632        // sorted one — the DISCRIMINATING arm between the sequence-level
21633        // and set-level projections.
21634        assert!(!ResourceLimits::is_ascending(&[
21635            UNBOUNDED_RESOURCE_LIMITS,
21636            DEFAULT_RESOURCE_LIMITS,
21637            EMPTY_RESOURCE_LIMITS,
21638        ]));
21639    }
21640
21641    #[test]
21642    fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
21643        // The direct falsification of is_ascending's ascending closure
21644        // one CELL axis over on the SAME slice.
21645        assert!(!ResourceLimits::is_descending(&[
21646            EMPTY_RESOURCE_LIMITS,
21647            DEFAULT_RESOURCE_LIMITS,
21648            UNBOUNDED_RESOURCE_LIMITS,
21649        ]));
21650    }
21651
21652    #[test]
21653    fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
21654        // Non-monotone chain permutation — [DEFAULT, EMPTY, UNBOUNDED]
21655        // is a permutation of the shipped-preset chain, so it IS a chain
21656        // (is_chain accepts it), but the first consecutive pair binds
21657        // `DEFAULT.leq(EMPTY) == false`, so the sequence-level ascending
21658        // walk rejects. Also is_descending rejects — the third
21659        // consecutive pair binds `EMPTY.geq(UNBOUNDED) == false`. Pins
21660        // the DISCRIMINATING arm between the sequence-level and set-
21661        // level projections at a slice that is a chain but neither
21662        // ascending nor descending.
21663        let slice = [
21664            DEFAULT_RESOURCE_LIMITS,
21665            EMPTY_RESOURCE_LIMITS,
21666            UNBOUNDED_RESOURCE_LIMITS,
21667        ];
21668        assert!(ResourceLimits::is_chain(&slice));
21669        assert!(!ResourceLimits::is_ascending(&slice));
21670        assert!(!ResourceLimits::is_descending(&slice));
21671    }
21672
21673    #[test]
21674    fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
21675        // The two hand-authored asymmetric postures are incomparable
21676        // (neither MID.leq(OTHER) nor OTHER.leq(MID)), so the single
21677        // consecutive pair rejects. Pinned in both orderings — the pair-
21678        // level leq verdict rejects on the antichain pair regardless of
21679        // which side is `self`.
21680        assert!(!ResourceLimits::is_ascending(&[
21681            HAND_AUTHORED_MID_POSTURE,
21682            HAND_AUTHORED_OTHER_POSTURE,
21683        ]));
21684        assert!(!ResourceLimits::is_ascending(&[
21685            HAND_AUTHORED_OTHER_POSTURE,
21686            HAND_AUTHORED_MID_POSTURE,
21687        ]));
21688    }
21689
21690    #[test]
21691    fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
21692        assert!(!ResourceLimits::is_descending(&[
21693            HAND_AUTHORED_MID_POSTURE,
21694            HAND_AUTHORED_OTHER_POSTURE,
21695        ]));
21696        assert!(!ResourceLimits::is_descending(&[
21697            HAND_AUTHORED_OTHER_POSTURE,
21698            HAND_AUTHORED_MID_POSTURE,
21699        ]));
21700    }
21701
21702    #[test]
21703    fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
21704        // Transitivity theorem — for every slice `postures`,
21705        // `is_ascending(postures) == true` implies `is_chain(postures)
21706        // == true`. The pointwise partial order is TRANSITIVE, so a
21707        // consecutive-pair leq-chain closes under transitive composition
21708        // into the all-pairs leq-chain. Substrate-level THEOREM: the
21709        // sequence-level ascending verdict is strictly STRONGER than the
21710        // set-level chain verdict. Same identity for is_descending —
21711        // both sequence-level projections imply the set-level chain
21712        // projection.
21713        let presets: [ResourceLimits; 5] = [
21714            EMPTY_RESOURCE_LIMITS,
21715            DEFAULT_RESOURCE_LIMITS,
21716            UNBOUNDED_RESOURCE_LIMITS,
21717            HAND_AUTHORED_MID_POSTURE,
21718            HAND_AUTHORED_OTHER_POSTURE,
21719        ];
21720        // Every arity-2 slice from the 5×5 preset matrix witnesses the
21721        // implication in both directions of the (ascending, descending)
21722        // pair.
21723        for a in presets {
21724            for b in presets {
21725                let slice = [a, b];
21726                if ResourceLimits::is_ascending(&slice) {
21727                    assert!(
21728                        ResourceLimits::is_chain(&slice),
21729                        "is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
21730                    );
21731                }
21732                if ResourceLimits::is_descending(&slice) {
21733                    assert!(
21734                        ResourceLimits::is_chain(&slice),
21735                        "is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
21736                    );
21737                }
21738            }
21739        }
21740        // Load-bearing arity-3 witness — the shipped ascending chain
21741        // triple satisfies is_ascending AND is_chain; the descending
21742        // reversal satisfies is_descending AND is_chain.
21743        let asc = [
21744            EMPTY_RESOURCE_LIMITS,
21745            DEFAULT_RESOURCE_LIMITS,
21746            UNBOUNDED_RESOURCE_LIMITS,
21747        ];
21748        assert!(ResourceLimits::is_ascending(&asc));
21749        assert!(ResourceLimits::is_chain(&asc));
21750        let desc = [
21751            UNBOUNDED_RESOURCE_LIMITS,
21752            DEFAULT_RESOURCE_LIMITS,
21753            EMPTY_RESOURCE_LIMITS,
21754        ];
21755        assert!(ResourceLimits::is_descending(&desc));
21756        assert!(ResourceLimits::is_chain(&desc));
21757    }
21758
21759    #[test]
21760    fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
21761        // Empty-and-singleton agreement — the two sequence-level
21762        // projections carry the SAME vacuous-truth verdict at every
21763        // slice with fewer than two consecutive pairs. Pinned as a
21764        // substrate-level identity so a future rewrite of either
21765        // projection cannot silently drift from the shared vacuous-
21766        // truth verdict.
21767        assert_eq!(
21768            ResourceLimits::is_ascending(&[]),
21769            ResourceLimits::is_descending(&[]),
21770        );
21771        let presets: [ResourceLimits; 5] = [
21772            EMPTY_RESOURCE_LIMITS,
21773            DEFAULT_RESOURCE_LIMITS,
21774            UNBOUNDED_RESOURCE_LIMITS,
21775            HAND_AUTHORED_MID_POSTURE,
21776            HAND_AUTHORED_OTHER_POSTURE,
21777        ];
21778        for a in presets {
21779            let single = [a];
21780            assert_eq!(
21781                ResourceLimits::is_ascending(&single),
21782                ResourceLimits::is_descending(&single),
21783                "empty-and-singleton agreement failed on singleton {a:?}",
21784            );
21785            let dup = [a, a];
21786            assert_eq!(
21787                ResourceLimits::is_ascending(&dup),
21788                ResourceLimits::is_descending(&dup),
21789                "diagonal-duplicate agreement failed on {a:?}",
21790            );
21791        }
21792    }
21793
21794    #[test]
21795    fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
21796        // Const-fn pin — the sequence-level ascending projection is
21797        // evaluable in const context, so a caller can pin an ascending-
21798        // sequence identity at compile time.
21799        const _: () = assert!(ResourceLimits::is_ascending(&[]));
21800        const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21801        const _: () = assert!(ResourceLimits::is_ascending(&[
21802            EMPTY_RESOURCE_LIMITS,
21803            DEFAULT_RESOURCE_LIMITS,
21804            UNBOUNDED_RESOURCE_LIMITS,
21805        ]));
21806        const _: () = assert!(!ResourceLimits::is_ascending(&[
21807            UNBOUNDED_RESOURCE_LIMITS,
21808            EMPTY_RESOURCE_LIMITS,
21809        ]));
21810    }
21811
21812    #[test]
21813    fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
21814        // Const-fn pin — sibling of is_ascending one PAIR-LEVEL-
21815        // PRIMITIVE axis over.
21816        const _: () = assert!(ResourceLimits::is_descending(&[]));
21817        const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21818        const _: () = assert!(ResourceLimits::is_descending(&[
21819            UNBOUNDED_RESOURCE_LIMITS,
21820            DEFAULT_RESOURCE_LIMITS,
21821            EMPTY_RESOURCE_LIMITS,
21822        ]));
21823        const _: () = assert!(!ResourceLimits::is_descending(&[
21824            EMPTY_RESOURCE_LIMITS,
21825            UNBOUNDED_RESOURCE_LIMITS,
21826        ]));
21827    }
21828}