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(¶m.resolved_default(), site),
3671 };
3672 out.push(arg);
3673 }
3674 if let Some(_rest_name) = self.rest.as_ref() {
3675 // The `&rest` slot collects args[fixed_arity()..] (the empty
3676 // slice when the call is exactly saturated); the boundary is
3677 // the typed `fixed_arity()` primitive both branches share.
3678 let rest = args.get(self.fixed_arity()..).unwrap_or(&[]).to_vec();
3679 out.push(V::collect_rest(rest, site));
3680 } else {
3681 // No `&rest` slot — the param list has a FIXED maximum arity
3682 // of `fixed_arity()`. Surplus args have nowhere to bind;
3683 // reject rather than silently truncate. Closes the call-site
3684 // mirror of `RestParamTrailingTokens` (the definition-site
3685 // rejection lifted by the prior-run typed-promotion lineage),
3686 // so the typed-entry macro-call-gate is structurally complete
3687 // in both directions: too-few (`MissingMacroArg`) AND too-many
3688 // (`TooManyMacroArgs`).
3689 let expected = self.fixed_arity();
3690 if args.len() > expected {
3691 return Err(too_many_macro_args(macro_name, expected, args.len()));
3692 }
3693 }
3694 Ok(out)
3695 }
3696}
3697
3698/// Macro environment. Collects `defmacro` forms and rewrites callers.
3699///
3700/// Expansion strategy is tunable per-expander:
3701/// - **Compiled (default)** — every registered macro's template is walked once
3702/// and flattened into a linear `CompiledTemplate` (a tiny bytecode: Literal,
3703/// Subst(index), Splice(index), BeginList, EndList). Expansion of a call
3704/// is then a linear pass with no HashMap lookups and no recursion through
3705/// the template Sexp. Purely-literal subtrees compile to a single
3706/// `Literal(Sexp)` op — huge win for macros where most of the body is fixed.
3707/// - **Substitute-only** — runs the name-keyed `substitute` walker. Slower
3708/// but proves equivalence; used in the benchmark test to measure the
3709/// compiled-vs-substituted speedup.
3710#[derive(Clone, Default)]
3711pub struct Expander {
3712 macros: HashMap<String, MacroDef>,
3713 /// Pre-compiled template bytecodes, populated when `compile_templates`.
3714 templates: HashMap<String, CompiledTemplate>,
3715 /// When true, register a CompiledTemplate alongside each macro and dispatch
3716 /// expansion through the bytecode interpreter.
3717 compile_templates: bool,
3718 /// Memoization of `apply(macro, args)` — repeated calls with identical
3719 /// args skip expansion entirely. Shared across clones so realizations of
3720 /// the same `CompilerSpec` benefit across .compile() invocations.
3721 cache: Arc<Mutex<HashMap<CacheKey, Sexp>>>,
3722 /// Toggle caching. Default on — caching is the actual performance win
3723 /// the bytecode layer enables.
3724 cache_enabled: bool,
3725 /// The six resource ceilings, held as ONE typed [`ResourceLimits`] bundle
3726 /// rather than six sibling `max_*: usize` fields. Peer of the six
3727 /// getter/setter pairs one AGGREGATION axis over — where each pair
3728 /// projects ONE ceiling, this field carries the SIX-fold cross-product
3729 /// so [`Self::resource_limits`] returns `self.limits` verbatim and
3730 /// [`Self::set_resource_limits`] assigns through the bundle in ONE move.
3731 ///
3732 /// Adding a SEVENTH ceiling touches [`ResourceLimits`] + the matching
3733 /// `DEFAULT_MAX_*` module constant + the paired individual getter/setter
3734 /// — the [`Expander`] struct, its constructors, AND
3735 /// [`Self::resource_limits`] / [`Self::set_resource_limits`] stay
3736 /// exactly as they are. That's the compounding win of the
3737 /// consolidation: the (Expander, constructors, bulk getter, bulk
3738 /// setter) four-corner face is field-shape-agnostic — it composes with
3739 /// the bundle by value, not by field-by-field literal.
3740 ///
3741 /// Consumers on the read path (`register_macro_def`'s three REGISTER-time
3742 /// gates, `expand_with_depth`'s depth + output-size gates, `apply`'s
3743 /// cache-entries gate) dereference through `self.limits.max_*`; the
3744 /// projection is a `Copy usize` load — same shape as reading a bare
3745 /// field, no `Deref`, no `Arc`, no lock.
3746 limits: ResourceLimits,
3747}
3748
3749impl Expander {
3750 /// Default-posture expander with an operator-supplied resource-ceiling
3751 /// bundle — the AGGREGATION-axis peer of
3752 /// [`Self::set_resource_limits`] one CONSTRUCTOR-STAGE axis over.
3753 /// Where [`Self::set_resource_limits`] applies the bundled six ceilings
3754 /// POST-construction on an already-built expander, this constructor
3755 /// binds them AT construction so a call-site that wants a fresh
3756 /// expander with a non-default posture composes ONE typed call
3757 /// (`Expander::with_limits(limits)`) rather than the two-step
3758 /// (`let mut e = Expander::new(); e.set_resource_limits(limits);`)
3759 /// dance the post-construction sibling still admits.
3760 ///
3761 /// The (compile_templates, cache_enabled) execution-strategy pair
3762 /// carries the [`Self::new`] default posture (`true, true`) — the
3763 /// bytecode-with-cache strategy — since callers that want the
3764 /// substitute-only strategy still route through
3765 /// [`Self::new_substitute_only`]. Post-lift both convenience
3766 /// constructors delegate their `macros` / `templates` / `cache` field
3767 /// literals through THIS primitive at the resource-posture axis: the
3768 /// four-out-of-six field literals (`HashMap::new()` × 2 +
3769 /// `Arc::new(Mutex::new(HashMap::new()))` + `limits`) lived
3770 /// byte-identical across [`Self::new`] and [`Self::new_substitute_only`]
3771 /// pre-lift; the two divergent flags (`compile_templates`,
3772 /// `cache_enabled`) still differ between the two constructors, and the
3773 /// substitute-only variant flips them AFTER threading through
3774 /// `with_limits` — same shape as [`Self::new_bytecode_no_cache`] does
3775 /// on the `cache_enabled` flag.
3776 ///
3777 /// Round-trip identity: `Expander::with_limits(limits).resource_limits()
3778 /// == limits` for any `ResourceLimits` — pinned as a typed theorem in
3779 /// the test cohort. The at-construction assignment is a direct `Copy`
3780 /// of the bundled field (no per-field cascade), so a mid-construction
3781 /// panic window on the six knobs is structurally impossible. Peer of
3782 /// the `Expander::with_limits(DEFAULT_RESOURCE_LIMITS) ==
3783 /// Expander::new()` delegation identity — pinned on the resource-
3784 /// posture projection so a future refactor that drifts the two
3785 /// constructors on the ceiling axis fails loudly.
3786 ///
3787 /// Frontier inspiration: Cranelift's `settings::Flags::new(builder)` —
3788 /// codegen ceilings (opt level, allocator strategy, feature enables)
3789 /// bind at CONSTRUCTION-time on a typed `Flags` bundle rather than
3790 /// composed through post-construction setters on the `Isa`. Same
3791 /// posture as this lift: the (getter, setter, at-construction)
3792 /// three-corner face on the resource-posture surface now binds at ONE
3793 /// typed bundle across ALL three surfaces.
3794 ///
3795 /// Theory grounding: THEORY.md §II.1 invariants 1 + 2 — typed entry +
3796 /// free middle. Pre-lift the "expander with a chosen resource posture"
3797 /// concept was untyped: consumers composed [`Self::new`] +
3798 /// [`Self::set_resource_limits`] at their call site with no
3799 /// compile-time gate that BOTH steps land atomically. Post-lift the
3800 /// concept has ONE named entry point on the `Expander` surface, and a
3801 /// future consumer (`RealizedCompiler` builder that ships a
3802 /// CI-tightened preset, an LSP session that ships a REPL-permissive
3803 /// preset, a test-harness fixture that ships an `UNBOUNDED`-style
3804 /// preset) binds through ONE typed constructor.
3805 pub fn with_limits(limits: ResourceLimits) -> Self {
3806 Self {
3807 macros: HashMap::new(),
3808 templates: HashMap::new(),
3809 compile_templates: true,
3810 cache: Arc::new(Mutex::new(HashMap::new())),
3811 cache_enabled: true,
3812 limits,
3813 }
3814 }
3815
3816 /// Default expander — compiled bytecode + expansion cache enabled.
3817 /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3818 /// so the (macros, templates, cache, limits) field-shape agreement
3819 /// with every other constructor on this `Expander` surface lives at
3820 /// ONE literal.
3821 pub fn new() -> Self {
3822 Self::with_limits(DEFAULT_RESOURCE_LIMITS)
3823 }
3824
3825 /// Expander using the legacy substitute path (no template compilation,
3826 /// no cache). Kept for benchmarking + equivalence testing.
3827 /// Delegates to [`Self::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`]
3828 /// and flips the two divergent execution-strategy flags — same
3829 /// composition shape as [`Self::new_bytecode_no_cache`] one flag over.
3830 pub fn new_substitute_only() -> Self {
3831 let mut e = Self::with_limits(DEFAULT_RESOURCE_LIMITS);
3832 e.compile_templates = false;
3833 e.cache_enabled = false;
3834 e
3835 }
3836
3837 /// Expander with bytecode on but expansion cache off — isolates the cache
3838 /// contribution from the bytecode infrastructure. Benchmark baseline.
3839 pub fn new_bytecode_no_cache() -> Self {
3840 let mut e = Self::new();
3841 e.cache_enabled = false;
3842 e
3843 }
3844
3845 /// Toggle the expansion cache at runtime.
3846 pub fn set_cache_enabled(&mut self, enabled: bool) {
3847 self.cache_enabled = enabled;
3848 }
3849
3850 /// How many entries are currently cached.
3851 pub fn cache_size(&self) -> usize {
3852 self.cache.lock().unwrap().len()
3853 }
3854
3855 /// Clear the expansion cache (e.g., after redefining a macro).
3856 pub fn clear_cache(&self) {
3857 self.cache.lock().unwrap().clear();
3858 }
3859
3860 /// The configured ceiling on `expand`'s recursive re-entry into a
3861 /// macro-call form. Defaults to [`DEFAULT_MAX_EXPANSION_DEPTH`].
3862 #[must_use]
3863 pub fn max_expansion_depth(&self) -> usize {
3864 self.limits.max_expansion_depth
3865 }
3866
3867 /// Set the ceiling on `expand`'s recursive re-entry into a macro-call
3868 /// form. Tests bind low values (e.g. `4`) to pin the runaway-rejection
3869 /// shape without paying for a 256-round walk; consumers that compose
3870 /// deep proven macro cascades can raise it. Zero is admitted and
3871 /// makes every first macro-call return
3872 /// [`LispError::ExpansionDepthExceeded`] immediately — useful for
3873 /// contract-tests that assert "this reader path never expands a
3874 /// macro."
3875 pub fn set_max_expansion_depth(&mut self, depth: usize) {
3876 self.limits.max_expansion_depth = depth;
3877 }
3878
3879 /// The configured ceiling on the expansion cache's entry count.
3880 /// Defaults to [`DEFAULT_MAX_CACHE_ENTRIES`]. Peer to
3881 /// [`Self::max_expansion_depth`] one RESOURCE axis over.
3882 #[must_use]
3883 pub fn max_cache_entries(&self) -> usize {
3884 self.limits.max_cache_entries
3885 }
3886
3887 /// Set the ceiling on the expansion cache's entry count. Tests bind
3888 /// low values (e.g. `2`) to pin the bounded-cache contract without
3889 /// paying for an 8K-entry walk; consumers that expand many distinct
3890 /// argument shapes can raise it. Zero is admitted and effectively
3891 /// disables caching (every fresh `(name, args)` pair skips the
3892 /// insert path) — useful for contract-tests that assert "this
3893 /// expander realizes without ever caching." [`usize::MAX`] is the
3894 /// unbounded-cache mode for batch compilation runs where memory is
3895 /// not the concern.
3896 pub fn set_max_cache_entries(&mut self, cap: usize) {
3897 self.limits.max_cache_entries = cap;
3898 }
3899
3900 /// The configured ceiling on a single macro-`apply` output's
3901 /// [`crate::ast::Sexp::node_count`]. Defaults to
3902 /// [`DEFAULT_MAX_EXPANSION_SIZE`]. Peer to
3903 /// [`Self::max_expansion_depth`] and [`Self::max_cache_entries`]
3904 /// one RESOURCE-DIMENSION axis over.
3905 #[must_use]
3906 pub fn max_expansion_size(&self) -> usize {
3907 self.limits.max_expansion_size
3908 }
3909
3910 /// Set the ceiling on a single macro-`apply` output's node count.
3911 /// Tests bind low values (e.g. `8`) to pin the "expansion bomb"
3912 /// rejection shape without materializing a 64K-node blob;
3913 /// consumers that compose deep proven macro cascades can raise it.
3914 /// [`usize::MAX`] admits any lawful output (the ceiling is
3915 /// effectively lifted); operators that want strict "your macros
3916 /// must fit in N nodes" contract-tests set it exactly.
3917 pub fn set_max_expansion_size(&mut self, size: usize) {
3918 self.limits.max_expansion_size = size;
3919 }
3920
3921 /// The configured ceiling on a registered macro's BODY
3922 /// [`crate::ast::Sexp::node_count`]. Defaults to
3923 /// [`DEFAULT_MAX_MACRO_BODY_SIZE`]. Peer to
3924 /// [`Self::max_expansion_depth`], [`Self::max_cache_entries`],
3925 /// and [`Self::max_expansion_size`] one PIPELINE-STAGE axis over
3926 /// (the three prior guards fire at EXPAND time; this one fires
3927 /// at REGISTER time).
3928 #[must_use]
3929 pub fn max_macro_body_size(&self) -> usize {
3930 self.limits.max_macro_body_size
3931 }
3932
3933 /// Set the ceiling on a registered macro's BODY node count.
3934 /// Tests bind low values (e.g. `4`) to pin the authoring-bomb
3935 /// rejection shape without materializing a 16K-node blob;
3936 /// consumers that author deep proven macro-body templates can
3937 /// raise it. [`usize::MAX`] admits any lawful body (the ceiling
3938 /// is effectively lifted). Zero is admitted and rejects every
3939 /// non-empty body immediately (a `Nil` body is 1 node, so the
3940 /// smallest lawful body is still rejected under a zero
3941 /// ceiling) — useful for contract-tests that assert "this
3942 /// expander refuses every registration."
3943 pub fn set_max_macro_body_size(&mut self, size: usize) {
3944 self.limits.max_macro_body_size = size;
3945 }
3946
3947 /// The configured ceiling on the [`Self::macros`] table's entry
3948 /// count. Defaults to [`DEFAULT_MAX_REGISTERED_MACROS`]. Peer to
3949 /// [`Self::max_macro_body_size`] one RESOURCE-DIMENSION axis over
3950 /// on the REGISTER-time surface, and to [`Self::max_cache_entries`]
3951 /// one PIPELINE-STAGE axis over.
3952 #[must_use]
3953 pub fn max_registered_macros(&self) -> usize {
3954 self.limits.max_registered_macros
3955 }
3956
3957 /// Set the ceiling on the [`Self::macros`] table's entry count.
3958 /// Tests bind low values (e.g. `3`) to pin the registration-bomb
3959 /// rejection shape without materializing a 4K-entry macros table;
3960 /// consumers that compose deep proven typescapes can raise it.
3961 /// [`usize::MAX`] admits any lawful cumulative registration count
3962 /// (the ceiling is effectively lifted). Zero is admitted and
3963 /// rejects every FRESH registration immediately — useful for
3964 /// contract-tests that assert "this expander accepts no new
3965 /// macros" — while still admitting overwrites of any macro that
3966 /// was registered before the ceiling was set (a re-registration
3967 /// of an existing key never grows the table).
3968 pub fn set_max_registered_macros(&mut self, cap: usize) {
3969 self.limits.max_registered_macros = cap;
3970 }
3971
3972 /// The configured ceiling on a registered macro's lambda-list
3973 /// arity. Defaults to [`DEFAULT_MAX_MACRO_ARITY`]. Peer to
3974 /// [`Self::max_macro_body_size`] and [`Self::max_registered_macros`]
3975 /// one RESOURCE-DIMENSION axis over on the REGISTER-time surface.
3976 #[must_use]
3977 pub fn max_macro_arity(&self) -> usize {
3978 self.limits.max_macro_arity
3979 }
3980
3981 /// Set the ceiling on a registered macro's lambda-list arity —
3982 /// the [`MacroParams::total_arity`] projection of `def.params`.
3983 /// Tests bind low values (e.g. `2`) to pin the arity-bomb rejection
3984 /// shape without materializing a 128-slot authoring blob;
3985 /// consumers that author high-arity domain macros (a `defschema`
3986 /// with 40 typed slots) can raise it. [`usize::MAX`] admits any
3987 /// lawful arity (the ceiling is effectively lifted). Zero is
3988 /// admitted and rejects every non-nullary macro registration
3989 /// immediately — a `(defmacro nullary () BODY)` is still admitted
3990 /// (its `total_arity()` is `0`, and only strict overrun rejects) —
3991 /// useful for contract-tests that assert "this expander accepts
3992 /// only nullary macros."
3993 pub fn set_max_macro_arity(&mut self, cap: usize) {
3994 self.limits.max_macro_arity = cap;
3995 }
3996
3997 /// Snapshot the six configured resource ceilings as ONE typed
3998 /// [`ResourceLimits`] value — the bundled peer of the six
3999 /// [`Self::max_expansion_depth`] / [`Self::max_cache_entries`] /
4000 /// [`Self::max_expansion_size`] / [`Self::max_macro_body_size`] /
4001 /// [`Self::max_registered_macros`] / [`Self::max_macro_arity`]
4002 /// individual getters. Consumers that want to inspect the full
4003 /// resource posture (a test that compares a preset expander to a
4004 /// reference posture, a diagnostic that renders "the six ceilings"
4005 /// as one line, a caller that clones + selectively overrides one
4006 /// field via a struct-update literal) bind to this projection at
4007 /// ONE call site rather than composing the six individual getters.
4008 ///
4009 /// Post the [`Self::limits`]-field consolidation the projection is
4010 /// a direct `Copy` of the bundled field — a `usize`-sized fixed
4011 /// return with no per-field literal walk. Adding a SEVENTH ceiling
4012 /// extends [`ResourceLimits`] and the paired individual
4013 /// getter/setter; this bulk getter is field-shape-agnostic and
4014 /// stays exactly as it is.
4015 #[must_use]
4016 pub fn resource_limits(&self) -> ResourceLimits {
4017 self.limits
4018 }
4019
4020 /// Bulk-replace the six configured resource ceilings from ONE typed
4021 /// [`ResourceLimits`] value — the bundled peer of the six
4022 /// [`Self::set_max_expansion_depth`] /
4023 /// [`Self::set_max_cache_entries`] /
4024 /// [`Self::set_max_expansion_size`] /
4025 /// [`Self::set_max_macro_body_size`] /
4026 /// [`Self::set_max_registered_macros`] /
4027 /// [`Self::set_max_macro_arity`] individual setters. Presets
4028 /// (a hardened CI expander, a permissive REPL expander, a
4029 /// throttled test harness) compose the six ceilings ONCE at the
4030 /// preset call site and apply them ATOMICALLY here rather than
4031 /// threading six independent setter invocations whose ordering
4032 /// the type system does not gate.
4033 ///
4034 /// Round-trip identity: `let l = e.resource_limits(); e.set_resource_limits(l);`
4035 /// leaves `e` in an equivalent posture — pinned as a typed theorem
4036 /// in the [`ResourceLimits`] test cohort. Post the [`Self::limits`]-
4037 /// field consolidation the assignment is a direct `Copy` of the
4038 /// bundled field, structurally atomic — no per-field destructure +
4039 /// six-way write cascade in which a mid-cascade panic could leave
4040 /// the six knobs in a half-updated posture. Sibling of
4041 /// [`Self::resource_limits`]; the pair closes the (getter, setter)
4042 /// face on the aggregation axis.
4043 pub fn set_resource_limits(&mut self, limits: ResourceLimits) {
4044 self.limits = limits;
4045 }
4046
4047 pub fn with_macros<I: IntoIterator<Item = MacroDef>>(defs: I) -> Result<Self> {
4048 let mut e = Self::new();
4049 for d in defs {
4050 e.register_macro_def(d)?;
4051 }
4052 Ok(e)
4053 }
4054
4055 /// Register a parsed [`MacroDef`] into this expander's macro tables —
4056 /// the single named primitive on the `Expander` surface every
4057 /// macro-registration consumer routes through.
4058 ///
4059 /// The registration discipline is a two-step composition:
4060 /// 1. When [`Self::compile_templates`](Self::new) is on (the
4061 /// `Self::new` default; flipped off by [`Self::new_substitute_only`]),
4062 /// [`compile_template`] pre-compiles the macro body to a typed
4063 /// [`CompiledTemplate`] bytecode and inserts it into `self.templates`
4064 /// keyed by `def.name`.
4065 /// 2. The `MacroDef` is moved into `self.macros` keyed by `def.name` —
4066 /// always, regardless of `compile_templates`, because the substitute
4067 /// strategy reads `self.macros` exclusively while the bytecode
4068 /// strategy consults `self.templates` first and falls back to
4069 /// `self.macros` for the body and params.
4070 ///
4071 /// The order is structural: `compile_template` borrows `&def` while
4072 /// `self.macros.insert(def.name.clone(), def)` consumes `def` — the
4073 /// template pre-compile MUST precede the move into `self.macros`, and the
4074 /// `def.name.clone()` projection captures the key for the moved insert.
4075 /// `?`-routing through `compile_template` preserves the structural
4076 /// ordering of the rejection chain end-to-end: a template-compile error
4077 /// (`UnboundTemplateVar` for an unbound `,name`, `NonSymbolUnquoteTarget`
4078 /// for `,5` / `,(nested)`, et al.) short-circuits BEFORE `self.macros`
4079 /// is mutated, so a failed registration leaves both tables exactly as
4080 /// they were — no partial-write window in which `self.macros.has(name)`
4081 /// is true but `self.templates.has(name)` is missing (a regression that
4082 /// would silently coerce the bytecode strategy onto the substitute path
4083 /// for that one macro despite `compile_templates: true`).
4084 ///
4085 /// Before this lift the same two-step block —
4086 ///
4087 /// ```ignore
4088 /// if self.compile_templates {
4089 /// self.templates.insert(def.name.clone(), compile_template(&def)?);
4090 /// }
4091 /// self.macros.insert(def.name.clone(), def);
4092 /// ```
4093 ///
4094 /// — lived byte-identical (modulo `self`/`e` and `def`/`d` substitutions)
4095 /// at TWO sites: [`Self::with_macros`] (the constructor that
4096 /// bulk-registers an `IntoIterator<Item = MacroDef>`, e.g. a curated
4097 /// preloaded set the caller assembled out-of-band) and
4098 /// [`Self::expand_program`]'s `(defmacro …)`-head arm (the program-level
4099 /// walker that recognizes a `defmacro` / `defpoint-template` / `defcheck`
4100 /// head via [`macro_def_from`] and registers it as a side-effect of
4101 /// walking the program). After this lift the registration block lives in
4102 /// ONE method on the `Expander`; both consumers and any future
4103 /// macro-registration surface bind to ONE primitive instead of
4104 /// re-deriving the two-step discipline at every call site.
4105 ///
4106 /// `pub` so authoring surfaces (an LSP that incrementally registers a
4107 /// `(defmacro …)` head as the user finishes typing it without a full
4108 /// program re-expand, a REPL `:define-macro` command that registers a
4109 /// pre-parsed `MacroDef` directly, a future "library merge" operation
4110 /// that absorbs another expander's macro set MacroDef-by-MacroDef) can
4111 /// register a typed `MacroDef` without round-tripping through source
4112 /// serialization first. Sibling of [`Self::with_macros`] (the
4113 /// bulk-from-iterator constructor — itself the
4114 /// `defs.into_iter().try_fold((), |_, d| self.register_macro_def(d))`
4115 /// shape on a fresh expander) and [`Self::expand_program`] (the
4116 /// source-level walker that recognizes `(defmacro …)` heads via
4117 /// [`macro_def_from`] — itself the program-level fold-over-defmacro-heads
4118 /// of this method). All three end up at this primitive.
4119 ///
4120 /// Returns `Result<()>` so the consumer's rejection chain composes with
4121 /// `?`-routing — `with_macros` short-circuits its bulk loop on the first
4122 /// `compile_template` failure; `expand_program` short-circuits its
4123 /// program walk at the offending `(defmacro …)` form. Infallibility on
4124 /// the `compile_templates: false` path is preserved (`compile_template`
4125 /// is gated behind the conditional), so a substitute-only expander never
4126 /// emits the `compile_template`-side rejection chain.
4127 ///
4128 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4129 /// byte-identical inline copies of the registration block across
4130 /// `with_macros` and `expand_program` is past the ≥2 PRIME-DIRECTIVE
4131 /// trigger once the structural shape is named. THEORY.md §V.1 — knowable
4132 /// platform; the macro-registration discipline becomes a NAMED primitive
4133 /// on the substrate's `Expander` surface rather than a per-consumer
4134 /// inline duplication that future emitters (an LSP, a REPL, a library-
4135 /// merge operator) would have had to re-derive. THEORY.md §II.1
4136 /// invariant 2 — free middle; both consumers route through the SAME
4137 /// registration primitive, so a regression that drifts ONE consumer's
4138 /// discipline from the other (one path skips the template pre-compile,
4139 /// one path inserts the `self.macros` entry in a different order, a
4140 /// future third side-effect — logging, attestation, metrics — emitted
4141 /// at one site but not the other) cannot reach the substrate's runtime:
4142 /// there is exactly one implementation both consumers route through.
4143 ///
4144 /// Frontier inspiration: Racket's `(define-syntax name template)` at
4145 /// REPL is exactly this — register a typed macro into the live
4146 /// namespace, no source round-trip; the substrate's `register_macro_def`
4147 /// is the Rust-typed peer of that surface, lifted onto the `Expander`'s
4148 /// table-level algebra (`macros: HashMap<String, MacroDef>` +
4149 /// `templates: HashMap<String, CompiledTemplate>`). MLIR's
4150 /// `OpRegistry::registerOp<Op>()` — typed-op registration into a
4151 /// dialect's live table at construction-time AND at JIT-walk-time
4152 /// through ONE registration entry point; `register_macro_def` is the
4153 /// pleme-io peer of that registration entry point on the macro-table
4154 /// algebra.
4155 pub fn register_macro_def(&mut self, def: MacroDef) -> Result<()> {
4156 // Reject a registration-bomb (a code-generator emitting
4157 // unbounded fresh `(defmacro fresh-N (x) `(list ,x))` heads
4158 // whose bodies each sit comfortably under
4159 // `max_macro_body_size` yet collectively saturate process
4160 // memory) at the cheapest gate on the REGISTER-time surface
4161 // — a `HashMap::len` comparison, no body walk. Peer to the
4162 // body-size gate below one RESOURCE-DIMENSION axis over
4163 // (COUNT vs. SIZE) and to the expand-time cache-entries gate
4164 // in `apply` one PIPELINE-STAGE axis over (REGISTER-time
4165 // COUNT vs. EXPAND-time COUNT). Fires BEFORE the body-size
4166 // gate walks `def.body.node_count()` or any insert lands, so
4167 // a failed registration leaves BOTH tables exactly as they
4168 // were. Overwrites (a re-registration of an already-
4169 // registered key) are ADMITTED at any table size — a
4170 // re-`defmacro` never grows the table, so the ceiling cannot
4171 // gate operator redefinitions. Keyed on `>=` (a table
4172 // already at ceiling rejects a fresh key) so
4173 // `max_registered_macros` names the LARGEST admissible
4174 // `self.macros.len()` — same posture the expand-time
4175 // cache-entries gate holds on the memoization cache.
4176 let is_overwrite = self.macros.contains_key(&def.name);
4177 if !is_overwrite && self.macros.len() >= self.limits.max_registered_macros {
4178 return Err(LispError::RegisteredMacrosExceeded {
4179 macro_name: def.name.clone(),
4180 count: self.macros.len(),
4181 limit: self.limits.max_registered_macros,
4182 });
4183 }
4184 // Reject an arity-bomb `(defmacro huge (a-1 a-2 … a-N-million)
4185 // `,a-1)` — a code-generator whose macro body sits at ONE node
4186 // (well under `max_macro_body_size`) yet whose param list
4187 // carries millions of fresh names the per-index binder walks on
4188 // every call. Peer to the body-size gate below one RESOURCE-
4189 // DIMENSION axis over on the REGISTER-time surface (per-body
4190 // ARITY vs. per-body SIZE), AND peer to the table-count gate
4191 // above one RESOURCE-DIMENSION axis over on the REGISTER-time
4192 // surface (per-registration WIDTH vs. cumulative COUNT). Fires
4193 // BEFORE the body-size gate walks `def.body.node_count()` or
4194 // any insert lands, so a failed registration leaves BOTH tables
4195 // exactly as they were. Cheaper than the body-size gate — a
4196 // three-arm addition on `def.params` fields, no AST walk —
4197 // hence positioned BEFORE it on the O(1)-first ordering of the
4198 // REGISTER-time chain. Keyed on `>` (equality is admitted, only
4199 // strict overrun rejects) so `max_macro_arity` names the
4200 // LARGEST admissible `def.params.total_arity()` — same posture
4201 // the body-size gate holds on the body node count.
4202 let arity = def.params.total_arity();
4203 if arity > self.limits.max_macro_arity {
4204 return Err(LispError::MacroArityExceeded {
4205 macro_name: def.name.clone(),
4206 arity,
4207 limit: self.limits.max_macro_arity,
4208 });
4209 }
4210 // Reject an authoring-bomb `(defmacro huge (x) `<template-of-
4211 // N-million-nodes>)` at the REGISTRATION boundary — the
4212 // PIPELINE-STAGE peer of the EXPAND-time output-size gate in
4213 // `expand_with_depth`. Fires BEFORE `compile_template` walks
4214 // the body or `self.macros.insert` lands the entry, so a
4215 // failed registration leaves BOTH tables exactly as they
4216 // were (no partial-write window in which the body-size
4217 // rejection happened but the template pre-compile already
4218 // consumed cycles or the macros table already carries the
4219 // over-ceiling entry). Keyed on `>` rather than `>=` so
4220 // `max_macro_body_size` names the LARGEST admissible body —
4221 // equality is admitted, only strict overrun rejects — same
4222 // posture the expand-time output-size gate holds.
4223 let body_size = def.body.node_count();
4224 if body_size > self.limits.max_macro_body_size {
4225 return Err(LispError::MacroBodySizeExceeded {
4226 macro_name: def.name.clone(),
4227 size: body_size,
4228 limit: self.limits.max_macro_body_size,
4229 });
4230 }
4231 if self.compile_templates {
4232 self.templates
4233 .insert(def.name.clone(), compile_template(&def)?);
4234 }
4235 self.macros.insert(def.name.clone(), def);
4236 Ok(())
4237 }
4238
4239 /// Expand a whole program. Returns the list of top-level forms after
4240 /// `defmacro` definitions are registered and all macro calls expanded.
4241 pub fn expand_program(&mut self, forms: Vec<Sexp>) -> Result<Vec<Sexp>> {
4242 let mut out = Vec::new();
4243 for form in forms {
4244 if let Some(def) = macro_def_from(&form)? {
4245 self.register_macro_def(def)?;
4246 continue;
4247 }
4248 out.push(self.expand(&form)?);
4249 }
4250 Ok(out)
4251 }
4252
4253 /// Read a source string into top-level forms via [`crate::reader::read`],
4254 /// then route the forms through [`expand_program`](Self::expand_program) —
4255 /// the from-source posture of the yield-all-forms-after-expansion primitive,
4256 /// in ONE method on the `Expander` surface.
4257 ///
4258 /// Before this lift the same two-step chain — `let forms = read(src)?;
4259 /// <expander>.expand_program(forms)` — lived inline at two sites in
4260 /// [`crate::compiler_spec`]: [`RealizedCompiler::compile`](crate::compiler_spec::RealizedCompiler::compile)
4261 /// (the public from-source untyped-expansion entry on a realized compiler,
4262 /// returning the expanded `Vec<Sexp>` for untyped consumers like
4263 /// `tatara-check`'s per-form dispatcher) AND [`realize_in_memory`](crate::compiler_spec::realize_in_memory)'s
4264 /// `:macros` library load loop (the per-spec-macro source absorption that
4265 /// builds the preloaded expander's macro library through `expand_program`'s
4266 /// `defmacro` recognition side-effect). After this lift the read-then-expand
4267 /// composition lives in ONE method on the `Expander`; each of the two
4268 /// consumers binds it with the per-site expander posture that fits its call
4269 /// boundary — `self.preloaded.clone()` for `RealizedCompiler::compile`'s
4270 /// per-call clone, `&mut preloaded` for `realize_in_memory`'s shared
4271 /// build-up.
4272 ///
4273 /// Sibling of [`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)
4274 /// — that method stacks the typed-keyword projection on top of the
4275 /// from-source pipeline (`read + expand_program + iter_calls_to(_,
4276 /// keyword) + map + collect`); this method is the bare yield-all-forms
4277 /// from-source primitive (`read + expand_program`) the typed dispatchers
4278 /// stack their keyword projection atop. The two together close the
4279 /// from-source posture of the program-level walk family on the
4280 /// `Expander` surface: bare (this method) vs. typed-keyword-projected
4281 /// (the sibling).
4282 ///
4283 /// Closes the 2×2 program-level walk family on the `Expander` surface:
4284 /// from-forms × yield-all ([`expand_program`](Self::expand_program)),
4285 /// from-forms × keyword-projected ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)),
4286 /// from-source × keyword-projected ([`expand_source_and_collect_calls_to`](Self::expand_source_and_collect_calls_to)),
4287 /// AND now from-source × yield-all (this method). The four together name
4288 /// the canonical surfaces a dispatcher composes with to extract an
4289 /// expanded program from either a pre-parsed `Vec<Sexp>` (from-forms
4290 /// posture, for callers composing with another `Sexp`-producing surface)
4291 /// or a raw `&str` (from-source posture, for callers consuming
4292 /// authoring-surface source text directly), with or without a
4293 /// typed-keyword filter on the result.
4294 ///
4295 /// `?`-routing through `read` preserves the structural ordering of the
4296 /// rejection chain end-to-end: a reader error (lexer / parser /
4297 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4298 /// `expand_program` runs; an `expand_program` error
4299 /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4300 /// `RestParamMissingName`, et al.) short-circuits at the offending form.
4301 /// Each consumer's rejection chain remains exactly what it was pre-lift,
4302 /// now sourced from ONE composition point rather than two.
4303 ///
4304 /// `defmacro`-registration side-effects fire on `&mut self` exactly as
4305 /// they do for the from-forms primitive — `realize_in_memory`'s per-spec
4306 /// build-up depends on every `defmacro` in every `:macros` source
4307 /// landing in `self.macros` (and, when `compile_templates` is on, in
4308 /// `self.templates`); `RealizedCompiler::compile`'s per-call clone
4309 /// posture isolates absorption to the cloned expander, so a
4310 /// `defmacro` in the user's source does NOT leak into the persistent
4311 /// realized compiler. Both postures' absorption semantics are
4312 /// preserved by routing through this primitive instead of inlining
4313 /// the two-step chain.
4314 ///
4315 /// Theory anchor: THEORY.md §VI.1 — generation over composition; two
4316 /// inline copies of the `let forms = read(src)?; <expander>.expand_program(forms)`
4317 /// two-step chain across `RealizedCompiler::compile` and
4318 /// `realize_in_memory` is past the ≥2 PRIME-DIRECTIVE trigger once the
4319 /// structural shape is named. THEORY.md §V.1 — knowable platform; the
4320 /// read-then-expand composition becomes a NAMED primitive on the
4321 /// substrate's `Expander` surface rather than a re-derived two-step
4322 /// inline pipeline at every consumer. THEORY.md §II.1 invariant 2 —
4323 /// free middle; both consumers route through the SAME composition
4324 /// primitive, so a regression that drifts ONE consumer's pipeline
4325 /// shape from the other cannot reach the substrate's runtime.
4326 ///
4327 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4328 /// namespace — the from-source-string entry to namespace-level
4329 /// program evaluation is the Racket idiom; the substrate's
4330 /// `expand_source_program` is the Rust-typed peer of that, sourced
4331 /// from `&str` and yielding the post-macroexpansion `Vec<Sexp>`
4332 /// without a typed-keyword filter — exactly the shape an untyped
4333 /// consumer (`tatara-check`'s per-form dispatcher, a REPL's
4334 /// "expand this buffer" command, an LSP's "show me the expanded
4335 /// program" handler) binds to.
4336 pub fn expand_source_program(&mut self, src: &str) -> Result<Vec<Sexp>> {
4337 let forms = crate::reader::read(src)?;
4338 self.expand_program(forms)
4339 }
4340
4341 /// Compose the expander's program-level expansion with the substrate's
4342 /// slice-side typed-keyword projection ([`iter_calls_to`]) and a
4343 /// caller-supplied per-form projection — `expand_program(forms)?` followed
4344 /// by `iter_calls_to(&expanded, keyword).map(project).collect()`, in ONE
4345 /// method on the `Expander` surface. Both [`compile_typed`](crate::compile::compile_typed)
4346 /// and [`compile_named_from_forms`](crate::compile::compile_named_from_forms)
4347 /// route through this primitive — they differ only in the per-form
4348 /// projection `F`: `T::compile_from_args` for the bare-kwargs form
4349 /// (`compile_typed`), and the NAME-then-`T::compile_from_args` split
4350 /// (`compile_named_from_forms`).
4351 ///
4352 /// Before this lift each dispatcher opened the same three-step pipeline
4353 /// inline — `let mut exp = Expander::new(); let expanded =
4354 /// exp.expand_program(forms)?; iter_calls_to(&expanded, T::KEYWORD)
4355 /// .map(<per-site>).collect()` — past the ≥2 PRIME-DIRECTIVE trigger
4356 /// once the structural shape is named. After this lift the pipeline
4357 /// lives in ONE method on the `Expander` surface and the two
4358 /// dispatchers thread their per-site projection through `F`; a
4359 /// regression that drifts ONE dispatcher's pipeline from the other (a
4360 /// future emitter that re-derives `expand_program + iter_calls_to`
4361 /// inline rather than composing through this primitive, a future
4362 /// preloaded-expander consumer that wants the same walk but on its
4363 /// own `Expander` rather than a fresh one) is no longer a silent
4364 /// two-site divergence: the method binds the composition once and
4365 /// every consumer threads the per-site projection through `F`.
4366 ///
4367 /// Method on `Expander` (not a free function) so the primitive
4368 /// composes with the broader expander surface: a preloaded expander
4369 /// (e.g., one that has already absorbed a set of `defmacro` forms
4370 /// via `expand_program` or `with_macros`) can call this method on
4371 /// its own state to walk a follow-up program — the
4372 /// `compiler_spec`'s realize path is the natural future consumer for
4373 /// that shape. `compile_typed` and `compile_named_from_forms`
4374 /// instantiate a fresh `Expander::new()` and dispatch through this
4375 /// method on it; a future preloaded consumer dispatches through it
4376 /// on the preloaded expander directly. ONE primitive, two postures
4377 /// (fresh vs. preloaded), no per-posture duplication of the
4378 /// `expand_program + iter_calls_to + map + collect` pipeline.
4379 ///
4380 /// `F` is `FnMut(&[Sexp]) -> Result<R>` — the per-form projection
4381 /// that fits the consumer's call site. The standard-library
4382 /// `Iterator::map` bound is `FnMut`, so a closure that captures
4383 /// mutable state (a future dispatcher that threads a running index
4384 /// or a borrowed accumulator into the projection) composes
4385 /// naturally. The `Result<R>` projection short-circuits on the
4386 /// first error via `Iterator::collect::<Result<Vec<R>, _>>()`, so
4387 /// the per-form rejection chain (`compile_named_from_forms`'s
4388 /// `NamedFormMissingName` for the missing NAME slot,
4389 /// `NamedFormNonSymbolName` for the non-symbol NAME slot,
4390 /// `T::compile_from_args`'s typed-entry kwargs gate, AND
4391 /// `compile_typed`'s bare-kwargs `T::compile_from_args` rejection)
4392 /// fires in the same order under the new shape.
4393 ///
4394 /// `R` is owned by construction — the iterator's `&[Sexp]` items
4395 /// borrow from the local `expanded: Vec<Sexp>` and that borrow
4396 /// ends when the `.collect()` consumes the iterator, so a `R`
4397 /// that borrowed from `expanded` would fail to compile here. The
4398 /// two production consumers (`Vec<T>`, `Vec<NamedDefinition<T>>`)
4399 /// are both owned-`R` shapes, matching the borrow's structural
4400 /// constraint.
4401 ///
4402 /// Sibling of [`expand_program`](Self::expand_program) — that
4403 /// method names the FIRST step of the pipeline (program-level
4404 /// macroexpansion, yielding `Vec<Sexp>`); this method composes it
4405 /// with the SECOND step (slice-side typed-keyword projection +
4406 /// per-form mapper). The split lets a consumer that wants the
4407 /// expanded forms WITHOUT the keyword filter (`tatara-check`'s
4408 /// per-form dispatcher walks every form, not just matched
4409 /// keywords) bind to `expand_program` directly; a consumer that
4410 /// wants both halves composed binds to this method.
4411 ///
4412 /// Closes the substrate's program-level walk family on the
4413 /// `Expander` surface: `expand_program` (yield-all-forms-after-
4414 /// expansion), `expand` (per-form recursive expansion), `apply`
4415 /// (per-call substitution), AND now `expand_and_collect_calls_to`
4416 /// (typed-keyword projection of the expanded forms with a per-form
4417 /// mapper). Together the four name the canonical surfaces a
4418 /// dispatcher composes with to extract a typed program from a
4419 /// post-expansion form set.
4420 ///
4421 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4422 /// two inline copies of the `Expander::new() + expand_program +
4423 /// iter_calls_to(_, T::KEYWORD) + map + collect` pipeline across
4424 /// `compile_typed` and `compile_named_from_forms` is past the ≥2
4425 /// PRIME-DIRECTIVE trigger once the structural shape is named.
4426 /// THEORY.md §V.1 — knowable platform; the program-level walk's
4427 /// typed-keyword-projection composition becomes a NAMED primitive
4428 /// on the substrate's `Expander` surface rather than a re-derived
4429 /// three-step inline pipeline at every consumer. Authoring tools
4430 /// (REPL, LSP, `tatara-check`) that want to walk a program by
4431 /// typed keyword bind to ONE method on the `Expander` instead of
4432 /// re-implementing the composition. THEORY.md §II.1 invariant 1 —
4433 /// typed entry; the typed-keyword filter over an expanded program
4434 /// IS the rust-level typed-entry-batch gate, and naming its
4435 /// single shape lifts the gate from two-site duplication to one
4436 /// rust method the substrate's diagnostic promotions hang off of.
4437 /// THEORY.md §II.1 invariant 2 — free middle; both consumers
4438 /// route through the SAME composition primitive, each binding the
4439 /// per-form projection that fits its call site — a regression
4440 /// that drifts ONE dispatcher's pipeline shape from the other
4441 /// cannot reach the substrate's runtime.
4442 ///
4443 /// Frontier inspiration: MLIR's `Region::walk<Op>(callback)` —
4444 /// every typed rewriter that wants "for every Op of kind K in this
4445 /// region, run callback" binds to ONE typed walker that composes
4446 /// the kind filter with the per-op visitor; the substrate's
4447 /// `expand_and_collect_calls_to` is the unstructured-projection
4448 /// peer of that walker, lifted onto the post-expansion `&[Sexp]`
4449 /// algebra with the per-form projection as the visitor. Racket's
4450 /// `syntax-parse` `~seq (keyword args ...) ...` ellipsis-form —
4451 /// the program-level typed-keyword filter with per-match handler
4452 /// is the closed-form sibling of `~seq`'s repeated-pattern
4453 /// matcher, translated through pleme-io primitives as ONE method
4454 /// on `Expander` composing `expand_program` with `iter_calls_to`
4455 /// and a per-form mapper. GHC Core's `everything :: forall b. (b
4456 /// -> b -> b) -> GenericQ b -> GenericQ b` — the typed-IR rewriter's
4457 /// program-level fold over a typed selector is named ONCE and
4458 /// every consumer threads the per-node projection; the substrate's
4459 /// `expand_and_collect_calls_to` is the keyword-projected sibling
4460 /// of that fold on the `&[Sexp]` algebra.
4461 pub fn expand_and_collect_calls_to<R, F>(
4462 &mut self,
4463 forms: Vec<Sexp>,
4464 keyword: &str,
4465 mut project: F,
4466 ) -> Result<Vec<R>>
4467 where
4468 F: FnMut(&[Sexp]) -> Result<R>,
4469 {
4470 // Routes through the typed-decoded classifier sibling
4471 // [`Self::expand_and_collect_calls_to_any`] with a constant-
4472 // classifier decoder — the same constant-classifier composition
4473 // [`crate::ast::iter_calls_to`] uses to route through
4474 // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
4475 // discarded `()` typed witness (`then_some(())`) is consumed by
4476 // the wrapper projection `|(), args| project(args)` so the
4477 // keyword consumer's per-form mapper sees only the args tail,
4478 // matching the pre-lift signature exactly. Both the keyword AND
4479 // classifier expander walks now share ONE pipeline implementation
4480 // (`expand_program + iter_calls_to_any + map + collect`); a
4481 // regression that drifts a future debug-mode logger, span-aware
4482 // borrow walker, or fused-iterator invariant from one expander
4483 // walk to the other becomes structurally impossible.
4484 self.expand_and_collect_calls_to_any(
4485 forms,
4486 |h| (h == keyword).then_some(()),
4487 move |(), args| project(args),
4488 )
4489 }
4490
4491 /// Compose the expander's program-level expansion with the substrate's
4492 /// slice-side typed-decoded classifier projection ([`iter_calls_to_any`])
4493 /// and a caller-supplied per-form projection — `expand_program(forms)?`
4494 /// followed by `iter_calls_to_any(&expanded, decode).map(|(decoded, args)|
4495 /// project(decoded, args)).collect()`, in ONE method on the `Expander`
4496 /// surface. The typed-decoded sibling of
4497 /// [`Self::expand_and_collect_calls_to`] — where that method filters by
4498 /// ONE constant keyword, this method filters AND TYPES by a caller-
4499 /// supplied classifier, yielding the typed witness alongside the per-form
4500 /// projection's args input.
4501 ///
4502 /// Closes the substrate's program-level walk family on the `Expander`
4503 /// surface at the typed-decoded corner the prior runs' slice-side
4504 /// `iter_calls_to_any` lift left open — the (keyword, classifier) 2×2
4505 /// of compose-on-iter projections:
4506 ///
4507 /// | | per-form algebra | slice algebra | expander surface |
4508 /// |----------------|-----------------------------|------------------------------|-------------------------------------|
4509 /// | keyword | [`Sexp::as_call_to`] | [`iter_calls_to`] | [`Self::expand_and_collect_calls_to`] |
4510 /// | classifier `F` | [`Sexp::as_call_to_any`] | [`iter_calls_to_any`] | `expand_and_collect_calls_to_any` (this) |
4511 ///
4512 /// The keyword corner now ROUTES through the classifier corner at every
4513 /// row: [`Sexp::as_call_to`] = constant-classifier projection of
4514 /// [`Sexp::as_call_to_any`]; [`iter_calls_to`] = constant-classifier
4515 /// projection of [`iter_calls_to_any`]; [`Self::expand_and_collect_calls_to`]
4516 /// = constant-classifier projection of THIS method. The substrate's
4517 /// soft-dispatch family is structurally complete at every algebra
4518 /// level — per-form, slice, AND expander — so a regression that drifts
4519 /// a future debug-mode logger, span-aware borrow walker, or
4520 /// fused-iterator invariant from one walk to its sibling at any algebra
4521 /// level becomes structurally impossible.
4522 ///
4523 /// Two plausible future consumer shapes the typed-decoded expander walk
4524 /// admits with no boilerplate:
4525 /// * **Closed-set classifier** — `expand_and_collect_calls_to_any(forms,
4526 /// MacroDefHead::from_keyword, |head, args| dispatch(head, args))`
4527 /// macroexpands a program and walks each `(defmacro …)` /
4528 /// `(defpoint-template …)` / `(defcheck …)` form decoded to the typed
4529 /// `MacroDefHead` enum with its args tail. Future `tatara-check`
4530 /// consumers that want "for every macro-definition form in this
4531 /// buffer, dispatch by typed kind" bind to ONE method on the
4532 /// `Expander` rather than the three-step `expand_program +
4533 /// iter_calls_to_any + map + collect` pipeline at each consumer site.
4534 /// * **Live-registry classifier** — `expand_and_collect_calls_to_any(forms,
4535 /// |h| registry.lookup(h), |handler, args| handler.handle(args))`
4536 /// macroexpands a program and walks each form whose head matches a
4537 /// runtime registry, decoded to a handler reference. Future
4538 /// `tatara-check`-shaped runtime dispatchers (the kind already named
4539 /// by [`iter_calls_to_any`]'s docstring as "the natural future
4540 /// consumer") bind to ONE method on the `Expander` rather than the
4541 /// three-step pipeline.
4542 ///
4543 /// The closed-form composition binding the keyword sibling to this typed-
4544 /// decoded primitive is the structural identity every consumer can pin
4545 /// against:
4546 ///
4547 /// ```ignore
4548 /// expand_and_collect_calls_to(forms, k, p) ==
4549 /// expand_and_collect_calls_to_any(forms,
4550 /// |h| (h == k).then_some(()),
4551 /// |(), args| p(args))
4552 /// ```
4553 ///
4554 /// `D` is `FnMut(&str) -> Option<T>` — the typed-decoded classifier the
4555 /// substrate's per-form algebra already shapes ([`Sexp::as_call_to_any`]
4556 /// uses `FnOnce`, the slice algebra and this method use `FnMut` for the
4557 /// same reason: the walk calls the decoder once per matching form, and a
4558 /// decoder that captures mutable state (a counter, a registry cache, a
4559 /// visited-set) maintains that state across the program-level walk).
4560 /// `F` is `FnMut(T, &[Sexp]) -> Result<R>` — the per-form projection that
4561 /// receives BOTH the typed witness AND the args tail; the keyword sibling
4562 /// discards the (unit) typed witness via `|(), args| project(args)`. The
4563 /// `Result<R>` projection short-circuits on the first error via
4564 /// `Iterator::collect::<Result<Vec<R>, _>>()` so the per-form rejection
4565 /// chain fires in source order at the first failing matched form.
4566 ///
4567 /// `R` is owned by construction — same constraint as the keyword
4568 /// sibling. `T` is owned because the underlying [`iter_calls_to_any`]
4569 /// requires `T: 'a` where `'a` is the post-expansion `Vec<Sexp>` borrow
4570 /// lifetime; consumers projecting to a typed `Copy` enum
4571 /// (`MacroDefHead`, a future `AuthoringDirective`) get the value
4572 /// directly per form, consumers projecting to a borrowed `&'static str`
4573 /// (a closed-set head) project to `&'static str` and inherit the static
4574 /// lifetime through the classifier, consumers projecting to a `&Handler`
4575 /// (a live-registry classifier) bind through the registry's owned
4576 /// borrow with `&self` outliving the walk.
4577 ///
4578 /// Theory anchor: THEORY.md §VI.1 — generation over composition; the
4579 /// keyword sibling [`Self::expand_and_collect_calls_to`] is now a
4580 /// CONSEQUENCE of this typed-decoded primitive + a constant-classifier
4581 /// decoder, parallel to how [`iter_calls_to`] is a consequence of
4582 /// [`iter_calls_to_any`] on the slice algebra. The substrate's
4583 /// program-level walk family is no longer two parallel implementations
4584 /// the type system fails to bind (a future regression that drifts the
4585 /// keyword pipeline's instrumentation from the classifier pipeline's
4586 /// instrumentation cannot reach the runtime — both compose through ONE
4587 /// `expand_program + iter_calls_to_any + map + collect` body, with the
4588 /// keyword filter expressed as a closed-form classifier). THEORY.md
4589 /// §V.1 — knowable platform; the typed-decoded expander walk becomes a
4590 /// NAMED primitive on the substrate's `Expander` surface rather than a
4591 /// future re-derived three-step inline pipeline per consumer.
4592 /// Authoring tools (REPL, LSP, `tatara-check`) that want to walk a
4593 /// program by typed-decoded classifier bind to ONE method on the
4594 /// `Expander` instead of re-implementing the composition. THEORY.md
4595 /// §II.1 invariant 2 — free middle; both expander-surface dispatchers
4596 /// (keyword + classifier) route through the SAME `expand_program +
4597 /// iter_calls_to_any + map + collect` composition primitive.
4598 ///
4599 /// Frontier inspiration: MLIR's `Region::walk<OpInterface,
4600 /// OpInterface2, …>([&](auto op) { … })` — the typed-IR walk over a
4601 /// region yielding ops decoded to their typed interface witness with
4602 /// per-op callback is the MLIR idiom; the substrate's
4603 /// `expand_and_collect_calls_to_any` is the unstructured-Rust peer on
4604 /// the post-expansion `&[Sexp]` algebra, with `decode: FnMut(&str) ->
4605 /// Option<T>` standing in for MLIR's typed-interface dyn-cast bag and
4606 /// `project: FnMut(T, &[Sexp]) -> Result<R>` standing in for the
4607 /// typed-op callback. Racket's `syntax-parse` `~or* (~datum defX)
4608 /// (~datum defY) (head args …)` over an ellipsis-form combined with a
4609 /// `#:with name:id` typed-witness binder — the program-level
4610 /// typed-decoded filter with per-match handler is the closed-form
4611 /// sibling of `syntax-parse`'s typed-choice repeater, translated
4612 /// through pleme-io primitives as ONE method on `Expander`. GHC Core's
4613 /// `everythingBut :: (a -> a -> a) -> GenericQ (a, Bool) -> GenericQ a`
4614 /// — the typed-IR rewriter's program-level fold over a typed
4615 /// kind-with-stop selector is named ONCE and every consumer threads
4616 /// the per-node typed projection; this method is the typed-decoded
4617 /// peer on the `Vec<Sexp>` algebra, with the classifier playing
4618 /// `everythingBut`'s typed-selector role.
4619 pub fn expand_and_collect_calls_to_any<R, F, D, T>(
4620 &mut self,
4621 forms: Vec<Sexp>,
4622 decode: D,
4623 mut project: F,
4624 ) -> Result<Vec<R>>
4625 where
4626 D: FnMut(&str) -> Option<T>,
4627 F: FnMut(T, &[Sexp]) -> Result<R>,
4628 {
4629 let expanded = self.expand_program(forms)?;
4630 crate::ast::iter_calls_to_any(&expanded, decode)
4631 .map(|(decoded, args)| project(decoded, args))
4632 .collect()
4633 }
4634
4635 /// Read a source string into top-level forms via [`crate::reader::read`],
4636 /// then route the forms through
4637 /// [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to) — the
4638 /// from-source posture of the program-level walk, in ONE method on the
4639 /// `Expander` surface.
4640 ///
4641 /// Before this lift the same two-step chain — `let forms = read(src)?;
4642 /// <expander>.expand_and_collect_calls_to(forms, keyword, project)` — lived
4643 /// inline at four sites: the two free-function typed dispatchers
4644 /// ([`compile_typed`](crate::compile::compile_typed) and
4645 /// [`compile_named`](crate::compile::compile_named) via
4646 /// [`compile_named_from_forms`](crate::compile::compile_named_from_forms))
4647 /// AND the two preloaded-expander methods on
4648 /// [`RealizedCompiler`](crate::compiler_spec::RealizedCompiler)
4649 /// (`compile_typed`, `compile_named`). After this lift the read-then-walk
4650 /// composition lives in ONE method on the `Expander`; each of the four
4651 /// dispatchers binds it with the per-site `(expander posture, projection)`
4652 /// pair that fits its call boundary — `Expander::new()` for the
4653 /// fresh-expander dispatchers, `self.preloaded.clone()` for the
4654 /// preloaded-expander dispatchers; `T::compile_from_args` for the
4655 /// bare-kwargs projection, `named_form_projection::<T>` for the
4656 /// NAME-then-kwargs projection.
4657 ///
4658 /// Sibling of [`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to)
4659 /// — that method takes a pre-parsed `Vec<Sexp>` (the from-forms posture,
4660 /// for callers that have already read or that compose with another
4661 /// `Sexp`-producing surface like a macro-expanded subform); this method
4662 /// takes a `&str` (the from-source posture, for callers consuming
4663 /// authoring-surface source text directly). Both compose with the same
4664 /// `expand_program + iter_calls_to + map + collect` pipeline through ONE
4665 /// from-forms primitive — the from-source posture stacks one read step on
4666 /// top, projecting `crate::reader::read`'s `Result<Vec<Sexp>>` into the
4667 /// from-forms primitive via `?`.
4668 ///
4669 /// `?`-routing through `read` preserves the structural ordering of the
4670 /// rejection chain end-to-end: a reader error (lexer / parser /
4671 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4672 /// `expand_program` runs; an `expand_program` error
4673 /// (`defmacro`-NAME-not-a-symbol, `OptionalParamMalformed`,
4674 /// `RestParamMissingName`, et al.) short-circuits BEFORE the keyword
4675 /// filter walks anything; a per-form `project` error
4676 /// (`NamedFormMissingName`, `NamedFormNonSymbolName`,
4677 /// `T::compile_from_args`'s typed-entry kwargs gate) short-circuits at the
4678 /// first failing matched form via `Iterator::collect::<Result<Vec<R>, _>>()`.
4679 /// Each dispatcher's rejection chain remains exactly what it was pre-lift,
4680 /// now sourced from ONE composition point rather than four.
4681 ///
4682 /// Closes the program-level walk family on the `Expander` surface across
4683 /// BOTH the from-forms posture
4684 /// ([`expand_and_collect_calls_to`](Self::expand_and_collect_calls_to))
4685 /// AND the from-source posture (this method) — together with
4686 /// `expand_program` (yield-all-forms-after-expansion), `expand` (per-form
4687 /// recursive expansion), and `apply` (per-call substitution), they name
4688 /// the canonical surfaces a dispatcher composes with to extract a typed
4689 /// program from a post-expansion form set. A future dispatcher that wants
4690 /// "read this source, then walk every call to keyword K and project each
4691 /// to R" — a debug-mode REPL command, an LSP "find all typed-domain
4692 /// definitions in this buffer" handler, a `tatara-check` command that
4693 /// dispatches each typed `(defX …)` form in `checks.lisp` to its
4694 /// registered domain — binds to ONE method on the `Expander` instead of
4695 /// re-deriving the two-step `read + expand_and_collect_calls_to` chain.
4696 ///
4697 /// Theory anchor: THEORY.md §VI.1 — generation over composition; four
4698 /// inline copies of the `read(src)? + <expander>.expand_and_collect_calls_to`
4699 /// chain across the two fresh-expander dispatchers and the two
4700 /// preloaded-expander dispatchers is past the ≥2 PRIME-DIRECTIVE trigger
4701 /// once the structural shape is named. THEORY.md §V.1 — knowable
4702 /// platform; the read-then-walk composition becomes a NAMED primitive on
4703 /// the substrate's `Expander` surface rather than a re-derived two-step
4704 /// inline pipeline at every dispatcher. THEORY.md §II.1 invariant 2 —
4705 /// free middle; all four dispatchers route through the SAME composition
4706 /// primitive, so a regression that drifts ONE dispatcher's read-then-walk
4707 /// pipeline from the others cannot reach the substrate's runtime — the
4708 /// type system binds all consumers to the from-source primitive's single
4709 /// emission shape.
4710 ///
4711 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4712 /// namespace populated with the preloaded compiler's `require`d macros —
4713 /// the from-source-string entry to typed-program evaluation inside a
4714 /// namespace that carries the macro library is the Racket idiom; the
4715 /// substrate's `expand_source_and_collect_calls_to` is the Rust-typed
4716 /// peer of that, with the typed-keyword projection composed in.
4717 ///
4718 /// Post-lift the body routes through the typed-decoded sibling
4719 /// [`Self::expand_source_and_collect_calls_to_any`] with a
4720 /// constant-classifier decoder — the same constant-classifier
4721 /// composition [`Self::expand_and_collect_calls_to`] uses to route
4722 /// through [`Self::expand_and_collect_calls_to_any`] on the
4723 /// from-forms axis, and that [`crate::ast::iter_calls_to`] uses to
4724 /// route through [`crate::ast::iter_calls_to_any`] on the slice
4725 /// algebra. The discarded `()` typed witness (`then_some(())`) is
4726 /// consumed by the wrapper projection `|(), args| project(args)`
4727 /// so the keyword consumer's per-form mapper sees only the args
4728 /// tail. Both the keyword AND classifier from-source walks now
4729 /// share ONE pipeline implementation (`read + expand_program +
4730 /// iter_calls_to_any + map + collect`) — a regression that drifts
4731 /// a future debug-mode logger, span-aware borrow walker, or
4732 /// fused-iterator invariant from one from-source walk to the
4733 /// other becomes structurally impossible.
4734 pub fn expand_source_and_collect_calls_to<R, F>(
4735 &mut self,
4736 src: &str,
4737 keyword: &str,
4738 mut project: F,
4739 ) -> Result<Vec<R>>
4740 where
4741 F: FnMut(&[Sexp]) -> Result<R>,
4742 {
4743 self.expand_source_and_collect_calls_to_any(
4744 src,
4745 |h| (h == keyword).then_some(()),
4746 move |(), args| project(args),
4747 )
4748 }
4749
4750 /// Read a source string into top-level forms via [`crate::reader::read`],
4751 /// then route the forms through
4752 /// [`expand_and_collect_calls_to_any`](Self::expand_and_collect_calls_to_any) —
4753 /// the from-source posture of the typed-decoded classifier walk, in
4754 /// ONE method on the `Expander` surface. The typed-decoded sibling of
4755 /// [`Self::expand_source_and_collect_calls_to`] — where that method
4756 /// filters by ONE constant keyword, this method filters AND TYPES by
4757 /// a caller-supplied classifier, yielding the typed witness alongside
4758 /// the per-form projection's args input. Sourced from `&str` rather
4759 /// than a `Vec<Sexp>`.
4760 ///
4761 /// Closes the substrate's program-level walk family on the `Expander`
4762 /// surface across BOTH input postures × BOTH projection forms — the
4763 /// (from-forms, from-source) × (keyword, classifier) 2×2 of
4764 /// compose-on-iter projections:
4765 ///
4766 /// | | from-forms (`Vec<Sexp>`) | from-source (`&str`) |
4767 /// |----------------|---------------------------------------------------------------|---------------------------------------------------------------------|
4768 /// | keyword | [`Self::expand_and_collect_calls_to`] | [`Self::expand_source_and_collect_calls_to`] |
4769 /// | classifier `F` | [`Self::expand_and_collect_calls_to_any`] | `expand_source_and_collect_calls_to_any` (this) |
4770 ///
4771 /// Each row's keyword corner now ROUTES through the classifier corner
4772 /// at every column: [`Self::expand_source_and_collect_calls_to`]'s
4773 /// body composes this typed-decoded primitive with a
4774 /// `|h| (h == keyword).then_some(())` decoder and the wrapper
4775 /// projection `|(), args| project(args)`, parallel to how
4776 /// [`Self::expand_and_collect_calls_to`] routes through
4777 /// [`Self::expand_and_collect_calls_to_any`] on the from-forms axis
4778 /// and [`crate::ast::iter_calls_to`] routes through
4779 /// [`crate::ast::iter_calls_to_any`] on the slice algebra. The 2×2
4780 /// reduces to ONE pipeline implementation
4781 /// (`read + expand_program + iter_calls_to_any + map + collect`)
4782 /// every consumer threads its `(decoder, project)` pair through.
4783 ///
4784 /// Future consumer shapes the typed-decoded from-source primitive
4785 /// admits:
4786 /// * **Closed-set classifier** — `expand_source_and_collect_calls_to_any(
4787 /// src, MacroDefHead::from_keyword, |head, args| { … })` walks a
4788 /// source buffer dispatching every `(defmacro …)` / `(defpoint-template …)` /
4789 /// `(defcheck …)` form to its typed `MacroDefHead` arm — exactly the
4790 /// shape a future LSP "find every macro-definition form in this
4791 /// buffer, decode each by typed kind" handler reaches for, sourced
4792 /// from authoring text directly rather than a pre-parsed
4793 /// `Vec<Sexp>`.
4794 /// * **Live-registry classifier** — `expand_source_and_collect_calls_to_any(
4795 /// src, |h| registry.get(h), |handler, args| handler.compile(args))`
4796 /// walks a source buffer dispatching every form whose head is
4797 /// registered to its typed handler — exactly the shape
4798 /// `tatara-check`'s "macroexpand checks.lisp and dispatch every
4799 /// `(defX …)` form through the registered domain dispatcher"
4800 /// walker reaches for, sourced from disk-loaded source text
4801 /// rather than the round-tripped `Vec<Sexp>` the typed
4802 /// dispatchers consume.
4803 ///
4804 /// `?`-routing through `read` preserves the structural ordering of
4805 /// the rejection chain end-to-end: a reader error (lexer / parser /
4806 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
4807 /// `expand_program` runs; an `expand_program` error short-circuits
4808 /// BEFORE the classifier filter walks anything; a per-form `project`
4809 /// error short-circuits at the first failing matched form via
4810 /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
4811 /// rejection chain inherits the from-forms typed-decoded primitive's
4812 /// shape verbatim, now sourced from `&str` via ONE composition point.
4813 ///
4814 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4815 /// the (from-forms, from-source) × (keyword, classifier) 2×2 closes
4816 /// at the from-source classifier corner this method establishes —
4817 /// the prior runs' from-forms classifier sibling
4818 /// (`expand_and_collect_calls_to_any`, run e47d043) AND from-source
4819 /// keyword sibling (`expand_source_and_collect_calls_to`, run
4820 /// b477d81) AND slice-side classifier sibling (`iter_calls_to_any`,
4821 /// run 38625e3) named three of the four corners; this lift names
4822 /// the fourth so the family is structurally complete at every
4823 /// algebra level. THEORY.md §V.1 — knowable platform; the
4824 /// read-then-classifier-walk composition becomes a NAMED primitive
4825 /// on the substrate's `Expander` surface rather than a future
4826 /// re-derived `read(src)? + expand_and_collect_calls_to_any(…)`
4827 /// two-step chain at every consumer that wants to walk source text
4828 /// by typed-decoded classifier. THEORY.md §II.1 invariant 2 — free
4829 /// middle; the from-source keyword filter
4830 /// (`expand_source_and_collect_calls_to`) now routes through the
4831 /// from-source classifier filter via the constant-classifier
4832 /// composition, so a regression that drifts the keyword filter's
4833 /// instrumentation from the classifier filter's instrumentation
4834 /// becomes structurally impossible. The pre-lift inline
4835 /// `read(src)? + expand_and_collect_calls_to(forms, keyword, project)`
4836 /// chain is now a TYPED CONSEQUENCE of the typed-decoded primitive
4837 /// composed with a constant-classifier decoder, not a parallel
4838 /// implementation the type system happens to not catch.
4839 ///
4840 /// Frontier inspiration: Racket's `(eval-string str ns)` against a
4841 /// namespace + typed-syntax-class dispatch — the from-source-string
4842 /// entry to typed-program evaluation inside a namespace that carries
4843 /// the macro library, dispatching by typed syntax-class is the
4844 /// Racket idiom; the substrate's
4845 /// `expand_source_and_collect_calls_to_any` is the Rust-typed peer
4846 /// of that, with the typed-decoded classifier composed in. MLIR's
4847 /// `parseSourceFile<Op>(srcFile, ctx)` then `mod.walk<OpInterface>(
4848 /// callback)` — the parse-then-typed-walk over a source buffer
4849 /// dispatching by typed-interface witness is the MLIR idiom; this
4850 /// method is the substrate's typed `&[Sexp]`-algebra peer.
4851 pub fn expand_source_and_collect_calls_to_any<R, F, D, T>(
4852 &mut self,
4853 src: &str,
4854 decode: D,
4855 project: F,
4856 ) -> Result<Vec<R>>
4857 where
4858 D: FnMut(&str) -> Option<T>,
4859 F: FnMut(T, &[Sexp]) -> Result<R>,
4860 {
4861 let forms = crate::reader::read(src)?;
4862 self.expand_and_collect_calls_to_any(forms, decode, project)
4863 }
4864
4865 /// Compose the expander's program-level expansion with the substrate's
4866 /// typed-decoded classifier walk AND the named-form NAME-shape gate
4867 /// ([`crate::compile::split_name_slot`]) — the from-forms posture of
4868 /// the (named NAME-then-kwargs × typed-decoded classifier) cell of
4869 /// the typed-dispatcher matrix on the `Expander` surface, sibling of
4870 /// [`Self::expand_to_named`] (the constant-`T::KEYWORD` × named cell)
4871 /// and of [`Self::expand_and_collect_calls_to_any`] (the from-forms
4872 /// classifier × bare-kwargs cell).
4873 ///
4874 /// Routes through [`Self::expand_and_collect_calls_to_any`] with a
4875 /// wrapper projection that composes [`crate::compile::split_name_slot`]
4876 /// (the named-form arity + NAME-shape gate on the substrate's
4877 /// `&[Sexp]` algebra) with the caller-supplied per-form projection.
4878 /// The decoder yields `Option<(T, &'static str)>` — the typed witness
4879 /// PAIRED with the canonical static keyword the named-form structural
4880 /// rejection variants ([`crate::error::LispError::NamedFormMissingName`],
4881 /// [`crate::error::LispError::NamedFormNonSymbolName`]) carry as
4882 /// `&'static str` slots. Threading the `&'static` constraint through
4883 /// the decoder's return type pins the same compile-time discipline
4884 /// [`crate::compile::split_name_slot`]'s `keyword: &'static str`
4885 /// parameter pins at the gate boundary — a typo in the canonical
4886 /// keyword can never drift into the diagnostic at runtime, same posture
4887 /// as the constant-`T::KEYWORD` cell where `T::KEYWORD` is
4888 /// `&'static str` by trait construction.
4889 ///
4890 /// Closes the (named × typed-decoded classifier) corner of the
4891 /// typed-dispatcher matrix on the `Expander` surface that the prior
4892 /// run's [`crate::compile::split_name_slot`] lift (dd50801) named as
4893 /// the future change the slice-side gate's named lift enables. The
4894 /// 2×2 of compose-on-iter projections on the named-form axis becomes
4895 /// structurally complete on the `Expander` surface:
4896 ///
4897 /// | | constant `T::KEYWORD` | typed-decoded classifier |
4898 /// |----------------|-------------------------------------|----------------------------------------------|
4899 /// | from-forms | [`Self::expand_to_named`] | `expand_and_collect_named_calls_to_any` (this) |
4900 /// | from-source | [`Self::expand_source_to_named`] | [`Self::expand_source_and_collect_named_calls_to_any`] |
4901 ///
4902 /// The constant-`T::KEYWORD` column is the typed CONSEQUENCE of the
4903 /// classifier column: an `expand_to_named::<T>(forms)` call composes
4904 ///
4905 /// ```ignore
4906 /// expand_and_collect_named_calls_to_any(forms,
4907 /// |h| (h == T::KEYWORD).then_some(((), T::KEYWORD)),
4908 /// |(), name, spec_args| {
4909 /// let spec = T::compile_from_args(spec_args)?;
4910 /// Ok(NamedDefinition { name: name.to_string(), spec })
4911 /// })
4912 /// ```
4913 ///
4914 /// Both columns route through ONE composition point on the
4915 /// `Expander` surface (the typed-decoded classifier walk inside
4916 /// [`Self::expand_and_collect_calls_to_any`]) and ONE gate body on
4917 /// the `&[Sexp]` algebra ([`crate::compile::split_name_slot`]). A
4918 /// regression that drifts ONE cell's NAME-slot rejection chain from
4919 /// the others is structurally impossible — every cell binds to the
4920 /// SAME `split_name_slot` body, the SAME `expand_program +
4921 /// iter_calls_to_any + map + collect` pipeline. The
4922 /// pre-lift `NamedFormMissingName` / `NamedFormNonSymbolName`
4923 /// rejection chain inside `compile::named_form_projection` (the
4924 /// `pub(crate)` typed-domain SPECIALIZATION that
4925 /// [`Self::expand_to_named`] routes through) fires identically
4926 /// through this classifier primitive — both consumers compose
4927 /// [`crate::compile::split_name_slot`] with their per-site typed
4928 /// continuation.
4929 ///
4930 /// Future consumer shapes the typed-decoded named-form primitive
4931 /// admits with no boilerplate:
4932 /// * **Closed-set classifier** — a `tatara-check` runner that
4933 /// dispatches every `(defmonitor NAME …)` / `(defnotify NAME …)`
4934 /// / `(defalertpolicy NAME …)` form in `checks.lisp` through ONE
4935 /// classifier (a closed-set enum `Domain::{Monitor, Notify,
4936 /// AlertPolicy}` with `Domain::from_keyword`) decoding each
4937 /// head to its typed kind PAIRED with its canonical static
4938 /// keyword, then dispatches in the per-form projection — binds
4939 /// to ONE method rather than two-step `expand_and_collect_calls_to_any
4940 /// + split_name_slot` composition at each consumer site.
4941 /// * **Live-registry classifier** — a future `tatara-check`-shaped
4942 /// runtime dispatcher: `expand_and_collect_named_calls_to_any(forms,
4943 /// |h| registry.get(h).map(|d| (d, d.keyword())), |dispatcher,
4944 /// name, args| dispatcher.compile(name, args))` walks a program
4945 /// dispatching every named form whose head is in a runtime
4946 /// registry, decoded to a handler reference AND its canonical
4947 /// keyword (sourced from the dispatcher itself, NOT from the
4948 /// matched head text). The `&'static str` discipline on the
4949 /// keyword slot is preserved through the dispatcher's own
4950 /// `keyword() -> &'static str` accessor.
4951 /// * **`compile_named_any` free function family** — the natural
4952 /// fresh-expander free-function + preloaded-`RealizedCompiler`
4953 /// consumer pair, sibling of the existing `compile_typed_any`
4954 /// / `RealizedCompiler::compile_typed_any` pair. A future run
4955 /// lands those as 3-line composes on top of this primitive.
4956 ///
4957 /// `D` is `FnMut(&str) -> Option<(T, &'static str)>` — the typed-
4958 /// decoded classifier yields a typed witness `T` AND its canonical
4959 /// static keyword for the gate. `F` is `FnMut(T, &str, &[Sexp]) ->
4960 /// Result<R>` — the per-form projection receives the typed witness
4961 /// `T` ALONGSIDE the NAME slot's BORROWED `&str` projection (sourced
4962 /// from [`Sexp::as_symbol_or_string`], which accepts BOTH symbol and
4963 /// string NAME-author surfaces) AND the spec args tail. Consumers
4964 /// that need owned ownership of the NAME (`NamedDefinition.name:
4965 /// String`, JSON-serialized payloads) `.to_string()` themselves —
4966 /// pushing the clone to the consumer boundary keeps the primitive
4967 /// allocation-free. The `Result<R>` projection short-circuits on
4968 /// the first error via `Iterator::collect::<Result<Vec<R>, _>>()`
4969 /// inside [`Self::expand_and_collect_calls_to_any`].
4970 ///
4971 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
4972 /// the (named × typed-decoded classifier) cell becomes a NAMED
4973 /// primitive on the `Expander` surface composed FROM TWO substrate
4974 /// primitives ([`Self::expand_and_collect_calls_to_any`] +
4975 /// [`crate::compile::split_name_slot`]) rather than re-derived
4976 /// inline at every named-classifier consumer's call site.
4977 /// THEORY.md §II.1 invariant 1 — typed entry; the typed-decoded
4978 /// classifier-filtered + NAME-shape-gated + caller-projected walk
4979 /// IS the typed-entry-batch gate for named-form dispatch at the
4980 /// `Expander` surface, with the `&'static str` keyword discipline
4981 /// preserved through the decoder's return type. THEORY.md §II.1
4982 /// invariant 2 — free middle; ALL four cells of the named-form
4983 /// dispatcher matrix on the `Expander` surface route through ONE
4984 /// composition point — a regression that drifts ONE cell's
4985 /// instrumentation from the others is structurally impossible.
4986 /// THEORY.md §V.1 — knowable platform; the named-form classifier
4987 /// walk becomes a discoverable primitive that LSP / REPL /
4988 /// `tatara-check` consumers bind to ONE method on the `Expander`
4989 /// instead of re-implementing the two-step `expand_and_collect_calls_to_any
4990 /// + split_name_slot` composition.
4991 ///
4992 /// Frontier inspiration: MLIR's `Region::walk<NamedOpInterface>(
4993 /// [&](auto op) { auto name = op.getName(); … })` — the typed-IR
4994 /// walk over a region yielding ops decoded to their typed interface
4995 /// witness with the named-symbol accessor pre-extracted is the
4996 /// MLIR idiom for named typed dispatch; this method is the
4997 /// substrate's unstructured-Rust peer with the typed-decoded
4998 /// classifier composed in and the NAME slot extracted via the
4999 /// substrate's `split_name_slot` gate. Racket's `syntax-parse`
5000 /// `~or* ((~datum defX) name:id arg ...) ((~datum defY) name:id
5001 /// arg ...) ((~datum defZ) name:id arg ...)` over an ellipsis-form
5002 /// — the typed-choice repeater with named-slot binder PAIRED with
5003 /// per-arm dispatch — is the Racket idiom; this primitive is the
5004 /// substrate's Rust-typed peer with the closed-set classifier
5005 /// playing the `~or*` typed-choice role and the NAME slot extracted
5006 /// by the substrate's slice-side gate.
5007 pub fn expand_and_collect_named_calls_to_any<R, F, D, T>(
5008 &mut self,
5009 forms: Vec<Sexp>,
5010 decode: D,
5011 mut project: F,
5012 ) -> Result<Vec<R>>
5013 where
5014 D: FnMut(&str) -> Option<(T, &'static str)>,
5015 F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5016 {
5017 // Routes through the slice-side typed-decoded named projection
5018 // [`crate::ast::iter_named_calls_to_any`] — the same
5019 // `expand_program + iter_*_to_any + map + collect` shape
5020 // [`Self::expand_and_collect_calls_to_any`] uses on the bare-kwargs
5021 // axis. Pre-lift this method routed through the bare expander
5022 // surface and welded [`crate::compile::split_name_slot`] inside
5023 // the projection closure; post-lift the named gate composition
5024 // lives at the slice level (`iter_named_calls_to_any`'s body)
5025 // and the Expander surface inherits it through delegation. Both
5026 // rows of the Expander surface (bare-kwargs, named) now share
5027 // ONE pipeline skeleton on the slice algebra — a regression that
5028 // drifts a future debug-mode logger, span-aware borrow walker,
5029 // or fused-iterator invariant from one row to the other becomes
5030 // structurally impossible at the slice boundary.
5031 let expanded = self.expand_program(forms)?;
5032 crate::ast::iter_named_calls_to_any(&expanded, decode)
5033 .map(|maybe_triple| {
5034 let (decoded, name, spec_args) = maybe_triple?;
5035 project(decoded, name, spec_args)
5036 })
5037 .collect()
5038 }
5039
5040 /// Read a source string into top-level forms via [`crate::reader::read`],
5041 /// then route the forms through
5042 /// [`Self::expand_and_collect_named_calls_to_any`] — the from-source
5043 /// posture of the (named × typed-decoded classifier) cell on the
5044 /// `Expander` surface, sibling of
5045 /// [`Self::expand_source_and_collect_calls_to_any`] (the from-source
5046 /// classifier × bare-kwargs cell).
5047 ///
5048 /// Closes the (from-forms, from-source) × (constant `T::KEYWORD`,
5049 /// typed-decoded classifier) 2×2 on the named-form axis of the
5050 /// `Expander` surface — together with [`Self::expand_to_named`],
5051 /// [`Self::expand_source_to_named`], and
5052 /// [`Self::expand_and_collect_named_calls_to_any`], every cell of the
5053 /// matrix binds to ONE composition point.
5054 ///
5055 /// `?`-routing through `read` preserves the structural ordering of
5056 /// the rejection chain end-to-end: a reader error (lexer / parser /
5057 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5058 /// `expand_program` runs; an `expand_program` error short-circuits
5059 /// BEFORE the classifier filter walks anything; the named-form gate
5060 /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5061 /// [`crate::compile::split_name_slot`] fires for the first matched
5062 /// form that violates either condition; a per-form `project` error
5063 /// short-circuits at the first failing matched form via
5064 /// `Iterator::collect::<Result<Vec<R>, _>>()`.
5065 ///
5066 /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to_any`].
5067 /// THEORY.md §VI.1 (generation over composition; the from-source
5068 /// posture inherits the named-form gate composition through
5069 /// delegation rather than re-deriving the `read(src)? + walk + gate`
5070 /// chain at every from-source named-classifier consumer's call
5071 /// site), THEORY.md §II.1 invariant 2 (free middle; the from-source
5072 /// posture and the from-forms posture route through the SAME named-
5073 /// form composition).
5074 pub fn expand_source_and_collect_named_calls_to_any<R, F, D, T>(
5075 &mut self,
5076 src: &str,
5077 decode: D,
5078 project: F,
5079 ) -> Result<Vec<R>>
5080 where
5081 D: FnMut(&str) -> Option<(T, &'static str)>,
5082 F: FnMut(T, &str, &[Sexp]) -> Result<R>,
5083 {
5084 let forms = crate::reader::read(src)?;
5085 self.expand_and_collect_named_calls_to_any(forms, decode, project)
5086 }
5087
5088 /// Macroexpand a pre-parsed program through `self` and project every
5089 /// `(keyword NAME :k v …)` form into `R` via a caller-supplied
5090 /// `(name, args) -> Result<R>` projection — the constant-keyword
5091 /// sibling of [`Self::expand_and_collect_named_calls_to_any`] on the
5092 /// named-form axis of the `Expander` surface, parallel to how
5093 /// [`Self::expand_and_collect_calls_to`] is the constant-keyword
5094 /// sibling of [`Self::expand_and_collect_calls_to_any`] on the
5095 /// bare-kwargs axis.
5096 ///
5097 /// Routes through the classifier sibling with a constant-classifier
5098 /// decoder that yields a discarded `()` typed witness paired with
5099 /// the `&'static str` keyword — the same `(T, &'static str)` decoder
5100 /// shape [`crate::compile::split_name_slot`] (composed inside the
5101 /// classifier primitive) pins at the slice-side gate boundary for
5102 /// the named-form structural rejection chain
5103 /// (`NamedFormMissingName.keyword`, `NamedFormNonSymbolName.keyword`).
5104 /// `&'static` is the same lifetime discipline the typed-decoded
5105 /// classifier signature enforces; the named gate's verbatim keyword
5106 /// threading inherits it through the constant-classifier composition.
5107 ///
5108 /// Projection signature `FnMut(&str, &[Sexp]) -> Result<R>` receives
5109 /// the BORROWED NAME slot and the BORROWED spec args tail. Consumers
5110 /// that need owned ownership (`NamedDefinition.name: String`) call
5111 /// `.to_string()` themselves — pushing the clone to the consumer
5112 /// boundary keeps the primitive allocation-free, matching the
5113 /// classifier sibling's projection signature on the NAME slot.
5114 ///
5115 /// Closes the (named NAME-then-kwargs × constant-keyword) cell of
5116 /// the `Expander` typed-walk family at the runtime-keyword × untyped
5117 /// `R` corner — pre-lift the cell was reachable ONLY through
5118 /// [`Self::expand_to_named`] with the `T: TataraDomain` type
5119 /// parameter baking `T::KEYWORD` AND the `T::compile_from_args`-
5120 /// based `crate::compile::named_form_projection<T>` projection
5121 /// into the dispatch through `expand_and_collect_calls_to(forms,
5122 /// T::KEYWORD, named_form_projection::<T>)`. Post-lift the cell
5123 /// surfaces as ONE method that takes the keyword and the
5124 /// `(name, args) -> R` projection as caller-supplied parameters,
5125 /// and [`Self::expand_to_named`] routes through it as the typed
5126 /// `T::KEYWORD`-constant specialization. The split lets a consumer
5127 /// that wants "walk every form whose head is a runtime keyword `kw`
5128 /// and project `(NAME, spec_args) -> R` for an arbitrary `R`" (a
5129 /// future REPL `:walk-named <kw>` command, an LSP "find-named-
5130 /// declarations-of-keyword `kw`" handler, a `tatara-check` runner
5131 /// dispatching on a single typed keyword whose projection isn't
5132 /// `T::compile_from_args`) bind to ONE method on the `Expander`
5133 /// surface rather than re-deriving the
5134 /// `expand_and_collect_named_calls_to_any(forms, |h| (h ==
5135 /// kw).then_some(((), kw)), |(), name, args| project(name, args))`
5136 /// two-line composition inline.
5137 ///
5138 /// Sibling of [`Self::expand_source_and_collect_named_calls_to`]
5139 /// — that method stacks a [`crate::reader::read`] step on top of
5140 /// this one (the from-source posture); this method takes a
5141 /// pre-parsed `Vec<Sexp>` (the from-forms posture). Together with
5142 /// [`Self::expand_and_collect_named_calls_to_any`] /
5143 /// [`Self::expand_source_and_collect_named_calls_to_any`], the four
5144 /// cells of the (named × {constant-keyword, classifier} ×
5145 /// {from-forms, from-source}) sub-matrix close on the `Expander`
5146 /// surface — each cell binds to ONE composition point, with the
5147 /// constant-keyword cells routing through the classifier cells via
5148 /// constant-classifier decoding.
5149 ///
5150 /// Theory anchor: THEORY.md §VI.1 — generation over composition;
5151 /// the `split_name_slot` composition lived at TWO sites pre-lift
5152 /// (`crate::compile::named_form_projection` body, which
5153 /// [`Self::expand_to_named`] routes through, AND
5154 /// [`Self::expand_and_collect_named_calls_to_any`] body, which the
5155 /// typed-decoded classifier consumers route through) — past the
5156 /// ≥2 PRIME-DIRECTIVE trigger once the named-form gate is named.
5157 /// THEORY.md §V.1 — knowable platform; the named constant-keyword
5158 /// walk becomes a NAMED primitive on the `Expander` surface
5159 /// composable by any future consumer (REPL, LSP, `tatara-check`)
5160 /// instead of a re-derived two-line `expand_and_collect_named_
5161 /// calls_to_any(forms, constant-decoder, wrapper-projection)`
5162 /// composition. THEORY.md §II.1 invariant 2 — free middle; both
5163 /// the typed (`expand_to_named<T>`) and untyped (this method)
5164 /// constant-keyword named cells route through the SAME named
5165 /// classifier primitive — a regression that drifts ONE cell's
5166 /// NAME-slot rejection chain (`NamedFormMissingName`,
5167 /// `NamedFormNonSymbolName`) from the other becomes structurally
5168 /// impossible.
5169 ///
5170 /// Frontier inspiration: Racket's `syntax-parse` `((~datum kw)
5171 /// name:id arg ...)` arm — the named NAME-slot binder under a
5172 /// constant `~datum` keyword paired with a per-match handler is
5173 /// the Racket idiom; this method is the substrate's Rust-typed
5174 /// peer with the constant-keyword filter composed at the
5175 /// classifier corner. MLIR's `Region::walk<NamedOpKind>([&](auto
5176 /// op) { auto name = op.getName(); … })` against a single
5177 /// operation kind — the typed-IR walk yielding ops decoded to
5178 /// their typed kind with the named-symbol accessor pre-extracted,
5179 /// specialized on a constant kind; this method is the substrate's
5180 /// unstructured-`R` peer with the constant-classifier composition
5181 /// pinned at the call boundary.
5182 pub fn expand_and_collect_named_calls_to<R, F>(
5183 &mut self,
5184 forms: Vec<Sexp>,
5185 keyword: &'static str,
5186 mut project: F,
5187 ) -> Result<Vec<R>>
5188 where
5189 F: FnMut(&str, &[Sexp]) -> Result<R>,
5190 {
5191 // Routes through the typed-decoded named-classifier sibling
5192 // [`Self::expand_and_collect_named_calls_to_any`] with a
5193 // constant-classifier decoder — the same constant-classifier
5194 // composition [`Self::expand_and_collect_calls_to`] uses to
5195 // route through [`Self::expand_and_collect_calls_to_any`] on
5196 // the bare-kwargs axis, and that
5197 // [`crate::ast::iter_calls_to`] uses to route through
5198 // [`crate::ast::iter_calls_to_any`] on the slice algebra. The
5199 // discarded `()` typed witness (`then_some(((), keyword))`)
5200 // is consumed by the wrapper projection
5201 // `|(), name, args| project(name, args)` so the keyword
5202 // consumer's per-form mapper sees only `(name, spec_args)`,
5203 // matching the bare projection signature.
5204 self.expand_and_collect_named_calls_to_any(
5205 forms,
5206 |h| (h == keyword).then_some(((), keyword)),
5207 move |(), name, args| project(name, args),
5208 )
5209 }
5210
5211 /// Read a source string into top-level forms via [`crate::reader::read`],
5212 /// then route the forms through
5213 /// [`Self::expand_and_collect_named_calls_to`] — the from-source
5214 /// posture of the (named × constant-keyword) cell on the
5215 /// `Expander` surface.
5216 ///
5217 /// Composes [`crate::reader::read`] with
5218 /// [`Self::expand_and_collect_named_calls_to`] — the
5219 /// `(keyword, project)` binding is bound in ONE place (the
5220 /// from-forms row) and this from-source sibling inherits the
5221 /// binding through delegation, mirroring how
5222 /// [`Self::expand_source_and_collect_calls_to`] composes
5223 /// [`crate::reader::read`] with [`Self::expand_and_collect_calls_to`]
5224 /// on the bare-kwargs axis.
5225 ///
5226 /// `?`-routing through `read` preserves the structural ordering of
5227 /// the rejection chain end-to-end: a reader error (lexer / parser /
5228 /// unbalanced-paren / unterminated-string) short-circuits BEFORE
5229 /// `expand_program` runs; an `expand_program` error short-circuits
5230 /// BEFORE the keyword filter walks anything; the named-form gate
5231 /// (`split_first()` arity → `as_symbol_or_string()` shape) inside
5232 /// [`crate::compile::split_name_slot`] fires for the first matched
5233 /// form that violates either condition; a per-form `project` error
5234 /// short-circuits at the first failing matched form via
5235 /// `Iterator::collect::<Result<Vec<R>, _>>()`. Each consumer's
5236 /// rejection chain inherits the constant-keyword named primitive's
5237 /// shape verbatim, now sourced from `&str` via ONE composition
5238 /// point.
5239 ///
5240 /// Sibling of [`Self::expand_and_collect_named_calls_to`] (the
5241 /// from-forms posture) and of [`Self::expand_source_and_collect_calls_to`]
5242 /// (the from-source × bare-kwargs cell on the constant-keyword
5243 /// row). The four cells of (named × {constant, classifier} ×
5244 /// {from-forms, from-source}) close on the `Expander` surface
5245 /// post-lift, each routing through ONE composition point.
5246 ///
5247 /// Theory anchor: same as [`Self::expand_and_collect_named_calls_to`].
5248 /// THEORY.md §VI.1 (generation over composition; the from-source
5249 /// posture inherits the constant-keyword named composition through
5250 /// delegation rather than re-deriving the
5251 /// `read(src)? + expand_and_collect_named_calls_to` chain at every
5252 /// from-source named-keyword consumer's call site),
5253 /// THEORY.md §II.1 invariant 2 (free middle; the from-source
5254 /// posture and the from-forms posture route through the SAME
5255 /// composition).
5256 pub fn expand_source_and_collect_named_calls_to<R, F>(
5257 &mut self,
5258 src: &str,
5259 keyword: &'static str,
5260 mut project: F,
5261 ) -> Result<Vec<R>>
5262 where
5263 F: FnMut(&str, &[Sexp]) -> Result<R>,
5264 {
5265 // From-source = from-forms × constant-classifier specialization
5266 // of the named classifier primitive's from-source sibling. The
5267 // discarded `()` typed witness and the `move |(), name, args|
5268 // project(name, args)` wrapper mirror the from-forms posture's
5269 // composition — both postures route through the SAME named
5270 // classifier primitive on the `Expander` surface, the
5271 // from-source row stacking one `read` step on top.
5272 self.expand_source_and_collect_named_calls_to_any(
5273 src,
5274 |h| (h == keyword).then_some(((), keyword)),
5275 move |(), name, args| project(name, args),
5276 )
5277 }
5278
5279 /// Expand a single form. Top-level macro calls are rewritten; recurses
5280 /// into list children.
5281 ///
5282 /// Routes the macro-call dispatch surface through the substrate's
5283 /// typed-decoded call decomposition: `as_call_to_any(|h|
5284 /// self.macros.get(h))` answers "is this form an invocation of any
5285 /// registered macro, decoded to `(&MacroDef, args)`?" in ONE
5286 /// structural query on the `Sexp` algebra. Pre-lift the same site
5287 /// opened the three-step chain `as_list() + as_call() + self.macros.
5288 /// get(head)` inline — `as_list()` for the children-walk fallthrough,
5289 /// `as_call()` for the (head, args) decomposition (which itself
5290 /// re-derives `as_list()` internally), and `self.macros.get(head)`
5291 /// for the registry lookup; post-lift the call-recognition runs as
5292 /// ONE `as_call_to_any` projection with the HashMap lookup as its
5293 /// classifier, and the `as_list()` fallthrough fires only on the
5294 /// not-a-macro-call path. Sibling consumer to `macro_def_from` — the
5295 /// typed-macro-definition dispatcher that routes through
5296 /// `as_call_to_any(MacroDefHead::from_keyword)` with the closed-set
5297 /// enum classifier. With both in place, BOTH dispatch sites in the
5298 /// macro expander (definition-recognition + call-recognition)
5299 /// project through the SAME family primitive on the `Sexp` algebra,
5300 /// each binding the classifier that fits its candidate set — closed
5301 /// enum for the static head-set, HashMap lookup for the live
5302 /// registry. A regression that drifts ONE site from the other (a
5303 /// future emitter that re-derives `as_list()` + `head.as_symbol()` +
5304 /// `self.macros.get(_)` inline rather than routing through the
5305 /// family) is no longer a silent two-site divergence.
5306 pub fn expand(&self, form: &Sexp) -> Result<Sexp> {
5307 self.expand_with_depth(form, 0)
5308 }
5309
5310 /// The depth-carrying peer of [`Self::expand`]. Each
5311 /// post-apply re-expansion increments `depth`; each tree-child
5312 /// descent passes `depth` through unchanged. The runaway
5313 /// `(defmacro loop (x) `(loop ,x))` accretes depth on the
5314 /// re-expansion path — one unit per round — and hits the ceiling
5315 /// in `max_expansion_depth` rounds; lawful nested macros
5316 /// (`(when1 (when1 x))`) accrete depth in the low single digits
5317 /// because tree traversal doesn't count.
5318 ///
5319 /// The `depth >= limit` check sits INSIDE the macro-call arm
5320 /// (`Some((def, args)) = form.as_call_to_any(…)`) rather than at
5321 /// the top of the function so `def.name.clone()` is available at
5322 /// the rejection site — the operator gets `macro_name` populated
5323 /// from the actual offending call rather than a `<unknown>`
5324 /// fallback. Tree-child descent is not gated on the ceiling
5325 /// because the tree-child arm does not accrete depth; a lawfully
5326 /// deep tree is bounded by the reader's own stack, not by this
5327 /// ceiling.
5328 ///
5329 /// The peer OUTPUT-SIZE gate (`expanded.node_count() >
5330 /// self.limits.max_expansion_size`) sits INSIDE the same macro-call arm,
5331 /// between the `apply` and the re-expansion recursion, so a
5332 /// single macro's `apply` output whose structural size crosses
5333 /// the ceiling is rejected AT the offending `apply` boundary
5334 /// (with `macro_name` populated from `def.name`) before the
5335 /// runaway tree feeds back into further expansion. The gate is
5336 /// keyed on `>` rather than `>=` so `max_expansion_size` names
5337 /// the LARGEST admissible output — the ceiling admits equality,
5338 /// only strict overrun rejects. Together with the depth ceiling
5339 /// (recursion length) and the cache ceiling (memoization width),
5340 /// this closes the expander's RESOURCE surface at three typed
5341 /// dimensions.
5342 fn expand_with_depth(&self, form: &Sexp, depth: usize) -> Result<Sexp> {
5343 if let Some((def, args)) = form.as_call_to_any(|h| self.macros.get(h)) {
5344 if depth >= self.limits.max_expansion_depth {
5345 return Err(LispError::ExpansionDepthExceeded {
5346 macro_name: def.name.clone(),
5347 limit: self.limits.max_expansion_depth,
5348 });
5349 }
5350 let expanded = self.apply(def, args)?;
5351 // Reject a single `apply` output whose structural node
5352 // count crosses the OUTPUT-SIZE ceiling — the RESOURCE
5353 // axis peer of the depth (recursion length) and
5354 // cache-entries (memoization width) ceilings. Catches
5355 // the canonical "expansion bomb" where a well-defined
5356 // macro produces a 2^N-node blob from a small input
5357 // while `depth` stays in the low single digits and the
5358 // cache entry count sits under any reasonable ceiling.
5359 let expanded_size = expanded.node_count();
5360 if expanded_size > self.limits.max_expansion_size {
5361 return Err(LispError::ExpansionSizeExceeded {
5362 macro_name: def.name.clone(),
5363 size: expanded_size,
5364 limit: self.limits.max_expansion_size,
5365 });
5366 }
5367 // Recurse — the expansion itself may contain more macro calls.
5368 return self.expand_with_depth(&expanded, depth + 1);
5369 }
5370 // Not a macro call — expand children if this is a list; otherwise
5371 // (atom / Nil / quote-family wrapper) return the form verbatim.
5372 let Some(list) = form.as_list() else {
5373 return Ok(form.clone());
5374 };
5375 let mut out = Vec::with_capacity(list.len());
5376 for item in list {
5377 out.push(self.expand_with_depth(item, depth)?);
5378 }
5379 Ok(Sexp::List(out))
5380 }
5381
5382 /// Apply a macro to its argument list.
5383 ///
5384 /// Three-layer fast path:
5385 /// 1. If `cache_enabled`, hash `(name, args)` and consult the memo table.
5386 /// 2. If a compiled template exists, run the bytecode interpreter.
5387 /// 3. Otherwise fall back to the name-keyed substitute walker.
5388 fn apply(&self, def: &MacroDef, args: &[Sexp]) -> Result<Sexp> {
5389 // Layer 1: expansion cache.
5390 let cache_key = if self.cache_enabled {
5391 args_cache_key(&def.name, args)
5392 } else {
5393 None
5394 };
5395 if let Some(ref key) = cache_key {
5396 if let Some(cached) = self.cache.lock().unwrap().get(key) {
5397 return Ok(cached.clone());
5398 }
5399 }
5400
5401 // Layer 2: compiled bytecode.
5402 let result = if let Some(tmpl) = self.templates.get(&def.name) {
5403 apply_compiled(&def.name, &def.params, tmpl, args)?
5404 } else {
5405 // Layer 3: substitute fallback. Walk the body's substitution
5406 // projection — the inner of the outer quasi-quote when present,
5407 // the body verbatim otherwise — through the shared
5408 // `MacroDef::template_body` primitive both strategies route on.
5409 let bindings = bind_args(&def.name, &def.params, args)?;
5410 substitute(def.template_body(), &bindings)?
5411 };
5412
5413 // Populate cache on miss — capped by `max_cache_entries` so the
5414 // memoization table cannot drift unboundedly across a long-
5415 // running host session. When the cap is reached the freshly-
5416 // computed result is returned verbatim; correctness is
5417 // unaffected because caching is a pure PERFORMANCE optimization
5418 // (the miss path always recomputes the same value the cache
5419 // would have returned).
5420 if let Some(key) = cache_key {
5421 let mut cache = self.cache.lock().unwrap();
5422 if cache.len() < self.limits.max_cache_entries {
5423 cache.insert(key, result.clone());
5424 }
5425 }
5426 Ok(result)
5427 }
5428
5429 pub fn has(&self, name: &str) -> bool {
5430 self.macros.contains_key(name)
5431 }
5432
5433 pub fn len(&self) -> usize {
5434 self.macros.len()
5435 }
5436
5437 pub fn is_empty(&self) -> bool {
5438 self.macros.is_empty()
5439 }
5440}
5441
5442// ── Compiled template bytecode ───────────────────────────────────────
5443
5444/// One op in the template bytecode. Emitted during compilation; consumed at
5445/// expansion to materialize a form without HashMap lookups or recursion.
5446#[derive(Clone, Debug, PartialEq)]
5447pub enum TemplateOp {
5448 /// Push a literal Sexp. Used for atoms and entirely-literal subtrees.
5449 Literal(Sexp),
5450 /// Push the bound arg at the given param index.
5451 Subst(usize),
5452 /// If the bound arg is a list, append its items to the current list; else
5453 /// push it as a single item.
5454 Splice(usize),
5455 /// Begin a new List — pushes a fresh builder onto the expansion stack.
5456 BeginList,
5457 /// End the current List — pops the builder, wraps as `Sexp::List`.
5458 EndList,
5459}
5460
5461/// Pre-compiled template. Built once per macro, interpreted many times.
5462#[derive(Clone, Debug, Default, PartialEq)]
5463pub struct CompiledTemplate {
5464 pub ops: Vec<TemplateOp>,
5465}
5466
5467/// Walk a macro definition's template body and emit linear bytecode.
5468/// Purely-literal subtrees compile to a single `Literal(clone)` op.
5469///
5470/// Compilation can fail if the template references a name that isn't a
5471/// declared parameter — same semantic as the substitute path.
5472///
5473/// Top-level `,@X` bodies (the splice is the entire body, not nested inside
5474/// a `(... ,@xs ...)` list) are rejected here at compile time so the
5475/// bytecode path agrees with the substitute path's emission-time rejection
5476/// (`splice_outside_list`). Without this gate the bytecode interpreter's
5477/// outermost stack frame silently absorbed the splice's items, and the same
5478/// macro emitted different output across paths — `compiled_template_matches
5479/// _substitute_path` only covered well-positioned splice bodies. After this
5480/// gate every `,@-outside-list` body is rejected at registration time on
5481/// both paths with ONE structural variant (`LispError::SpliceOutsideList`).
5482///
5483/// The gate routes through [`Sexp::as_unquote`] — the typed-marker
5484/// projection that pairs `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at
5485/// ONE structural query — matching `Some((UnquoteForm::Splice, inner))`
5486/// rather than the per-arm `Sexp::UnquoteSplice(inner)` literal that
5487/// recurred at three sites pre-lift (`compile_node` Subst/Splice arms,
5488/// `substitute` top-level + list-inner). Sibling shape to `substitute`'s
5489/// list-inner Splice arm — both use the same `Some((UnquoteForm::Splice,
5490/// inner))` shape; `substitute`'s top-level arm uses the wider
5491/// `Some((kind, inner))` and dispatches inside on `kind`. After this lift
5492/// every production-site recognizer of "is this an `,@X` form" routes
5493/// through ONE typed-marker projection rather than re-deriving the
5494/// (Sexp variant, UnquoteForm variant) pair inline.
5495pub fn compile_template(def: &MacroDef) -> Result<CompiledTemplate> {
5496 // Walk the body's substitution projection — the inner of the outer
5497 // quasi-quote when present, the body verbatim otherwise — through the
5498 // shared `MacroDef::template_body` primitive the substitute path also
5499 // routes on. Same projection, both strategies, by construction.
5500 let body = def.template_body();
5501 if let Some((UnquoteForm::Splice, inner)) = body.as_unquote() {
5502 return Err(splice_outside_list(inner));
5503 }
5504 let names = def.params.names();
5505 let mut ops = Vec::new();
5506 compile_node(body, &names, &mut ops)?;
5507 Ok(CompiledTemplate { ops })
5508}
5509
5510fn compile_node(node: &Sexp, params: &[&str], ops: &mut Vec<TemplateOp>) -> Result<()> {
5511 // Fast-path literal: if the subtree has no Unquote/UnquoteSplice, emit a
5512 // single Literal op. This is the big win for macros where most of the
5513 // template is fixed structure.
5514 if !contains_unquote(node) {
5515 ops.push(TemplateOp::Literal(node.clone()));
5516 return Ok(());
5517 }
5518 // Routes the `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice(inner)` arms
5519 // through [`Sexp::as_unquote`] — the typed-marker projection that
5520 // pairs `Sexp::Unquote ↔ UnquoteForm::Unquote` and
5521 // `Sexp::UnquoteSplice ↔ UnquoteForm::Splice` at ONE site. The per-form
5522 // `TemplateOp` emission (`Subst` vs `Splice`) keys on the same typed
5523 // `form` value the gate-1+gate-2 composition `resolve_unquote_in_params`
5524 // threads through. Pre-lift the (Sexp variant, UnquoteForm variant)
5525 // pairing was bound per-arm — a future emitter that matched
5526 // `Sexp::Unquote(_)` but threaded `UnquoteForm::Splice` into
5527 // `resolve_unquote_in_params` (or vice versa) would type-check but
5528 // render a misleading diagnostic at the gate-1 / gate-2 rejection.
5529 // Post-lift the pair is bound at ONE projection function and the
5530 // `match form` mechanically lowers it to the bytecode op.
5531 if let Some((form, inner)) = node.as_unquote() {
5532 let idx = resolve_unquote_in_params(inner, params, form)?;
5533 ops.push(match form {
5534 UnquoteForm::Unquote => TemplateOp::Subst(idx),
5535 UnquoteForm::Splice => TemplateOp::Splice(idx),
5536 });
5537 return Ok(());
5538 }
5539 match node {
5540 Sexp::List(items) => {
5541 ops.push(TemplateOp::BeginList);
5542 for item in items {
5543 compile_node(item, params, ops)?;
5544 }
5545 ops.push(TemplateOp::EndList);
5546 }
5547 _ => ops.push(TemplateOp::Literal(node.clone())),
5548 }
5549 Ok(())
5550}
5551
5552fn contains_unquote(node: &Sexp) -> bool {
5553 // Route every quote-family wrapper recognition — both the
5554 // unquote-only subset (Unquote/UnquoteSplice → short-circuit `true`)
5555 // AND the quote-only subset (Quote/Quasiquote → recurse into inner) —
5556 // through [`Sexp::as_quote_form`]'s typed-marker projection. Pre-lift
5557 // the recognizer split into two arms: `as_unquote().is_some()` (the
5558 // substitution subset gate) + the inline
5559 // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm (the remaining
5560 // quote-only subset). Post-lift both arms route through ONE
5561 // projection and the (Sexp variant, QuoteForm variant) pairing binds
5562 // at the closed-set algebra. The `form.as_unquote_form().is_some()`
5563 // gate is the SAME 2-of-4 subset projection [`Sexp::as_unquote`]
5564 // derives from, so recognizing "is this an unquote-family wrapper"
5565 // and "is this a quote-only wrapper" share ONE typed dispatch site;
5566 // rustc enforces that a future `Sexp` wrapper extension carry
5567 // through both `QuoteForm::ALL` AND `QuoteForm::as_unquote_form`'s
5568 // arm. Sibling posture to `Hash for Sexp`'s four-arm
5569 // `hash_discriminator` collapse, `Display for Sexp`'s `prefix`
5570 // collapse, `interop`'s `iac_forge_tag` collapse, and `domain`'s
5571 // `sexp_shape` collapse — every production-site quote-family
5572 // recognizer now routes through ONE projection on the algebra.
5573 if let Some((form, inner)) = node.as_quote_form() {
5574 return form.as_unquote_form().is_some() || contains_unquote(inner);
5575 }
5576 match node {
5577 Sexp::List(items) => items.iter().any(contains_unquote),
5578 _ => false,
5579 }
5580}
5581
5582/// Splice a resolved template value into an in-progress list builder —
5583/// the SHARED coercion both expansion strategies apply once `,@name`'s
5584/// gate-1 (must-be-a-symbol) and gate-2 (must-be-bound-in-scope) have
5585/// resolved the bound value. ONE named primitive the bytecode path
5586/// (`apply_compiled`'s `TemplateOp::Splice` arm) AND the substitute path
5587/// (`substitute`'s list-inner `Sexp::UnquoteSplice` arm) share. Before
5588/// this lift the three-arm coercion —
5589///
5590/// ```ignore
5591/// match value {
5592/// Sexp::List(items) => builder.extend(items.iter().cloned()),
5593/// Sexp::Nil => {}
5594/// other => builder.push(other.clone()),
5595/// }
5596/// ```
5597///
5598/// — was inlined at BOTH sites; the splice RESULT semantics (the last
5599/// inline-duplicated piece of the splice path after the prior runs lifted
5600/// gate-1, gate-2, and their composition) lived in two places that MUST
5601/// agree. After this lift the coercion lives in ONE function, so a
5602/// regression that drifts one strategy's splice posture from the other —
5603/// e.g. changing the `Sexp::Nil` arm to push an empty list at the
5604/// bytecode path but not the substitute path, or coercing a non-list
5605/// scalar differently across the two strategies — becomes structurally
5606/// impossible: there is exactly one implementation both strategies call.
5607///
5608/// The coercion's three arms ARE the no-evaluator template language's
5609/// splice contract: a bound LIST flattens its elements into the builder
5610/// (the canonical splice), a bound NIL contributes nothing (splicing the
5611/// empty list), and any other bound value splices as a single element (a
5612/// scalar `,@x` degrades to `,x` rather than erroring — invariant 2's
5613/// "free middle" lets the macro author rely on this without a
5614/// mid-rewrite type check; the typed-exit gate re-validates the
5615/// assembled form). Naming the contract once gives a future gate-3
5616/// (typed-shape enforcement on bound splice targets) ONE site to wrap
5617/// rather than two inline arms to keep in lockstep.
5618///
5619/// Theory anchor: THEORY.md §II.1 invariant 2 — free middle; the two
5620/// expansion strategies MUST produce identical output for the same
5621/// (macro, args) pair, and naming the splice coercion once makes that
5622/// per-strategy agreement structural rather than a two-site discipline
5623/// the `expansion_layers_agree_on_output_and_cache_wins` benchmark only
5624/// observes after the fact. THEORY.md §V.1 — knowable platform; the
5625/// splice RESULT semantics becomes a NAMED primitive authoring tools and
5626/// future runs bind to. THEORY.md §VI.1 — generation over composition;
5627/// the two-site coercion is lifted to ONE function, closing the last
5628/// inline-duplicated piece of the splice path the prior runs' gate lifts
5629/// (02173dc gate-1, 68da647 gate-2, b456f1f composition) left behind.
5630fn splice_value_into(builder: &mut Vec<Sexp>, value: &Sexp) {
5631 match value {
5632 Sexp::List(items) => builder.extend(items.iter().cloned()),
5633 Sexp::Nil => {}
5634 other => builder.push(other.clone()),
5635 }
5636}
5637
5638/// Promote the previously `LispError::Compile`-shaped helper into the
5639/// structural `LispError::TemplateInvariant { macro_name, kind }` variant.
5640/// The four reachable bytecode-runtime invariant violations in
5641/// `apply_compiled` — Subst-bad-index, Splice-bad-index, EndList-empty-
5642/// stack, final-no-value — funnel through ONE emission shape keyed on
5643/// the closed-set `TemplateInvariantKind` enum. The index payload of
5644/// the Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
5645/// / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
5646/// kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
5647/// is structurally unrepresentable — the type system encodes "this gate
5648/// has an index, that gate does not."
5649///
5650/// Display matches the legacy `Compile`-shaped diagnostic byte-for-byte
5651/// across all four kinds (`"compile error in {macro_name}: <invariant>"`)
5652/// via the closed-set `TemplateInvariantKind::message()` projection, so
5653/// authoring-tool substring greps (`tatara-check`, REPL) see no drift
5654/// across the lift.
5655///
5656/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
5657/// of bytecode-invariant failure modes becomes a TYPE rather than a
5658/// free-form `message: String` slot. THEORY.md §VI.1 — generation over
5659/// composition; the typed enum lands the structural-completeness floor
5660/// for the bytecode-runtime surface, parallel to how `CompilerSpecIoStage`
5661/// lands the structural-completeness floor for the disk-persistence
5662/// surface (`compiler_spec.rs`, the immediately prior claude-routine
5663/// lift on a sibling file). THEORY.md §II.1 invariant 5 (composition
5664/// preserves proofs): a well-formed bytecode invariant is the proof
5665/// that drives the interpreter; the structural variant makes the
5666/// proof's REJECTION shape first-class — authoring tools (REPL, LSP,
5667/// `tatara-check`) pattern-match on the `kind` slot and bind to the
5668/// gate identity directly instead of substring-parsing the rendered
5669/// diagnostic.
5670fn template_invariant_violation(macro_name: &str, kind: TemplateInvariantKind) -> LispError {
5671 LispError::TemplateInvariant {
5672 macro_name: macro_name.into(),
5673 kind,
5674 }
5675}
5676
5677/// Look up a bound-arg by its template-bytecode index, or raise the
5678/// structural `LispError::TemplateInvariant` rejection with the
5679/// caller-supplied `kind` constructor applied to the bad index. ONE
5680/// named primitive both bytecode-runtime arms that read a bound arg
5681/// by index — [`TemplateOp::Subst`] (single-value push) AND
5682/// [`TemplateOp::Splice`] (list-splicing) — route through.
5683///
5684/// Before this lift the same `args_by_index.get(*idx).ok_or_else(||
5685/// template_invariant_violation(macro_name, KIND(*idx)))?` projection
5686/// appeared at BOTH arms of [`apply_compiled`], differing only in the
5687/// kind constructor: [`TemplateInvariantKind::SubstBadIndex`] at the
5688/// `Subst` arm, [`TemplateInvariantKind::SpliceBadIndex`] at the
5689/// `Splice` arm. The arms also diverged on what they did with the
5690/// returned `&Sexp` — `Subst` cloned and pushed, `Splice` consumed
5691/// the borrow through [`splice_value_into`] — but the lookup-and-
5692/// reject prelude was byte-identical modulo the kind, well past the
5693/// ≥2 PRIME-DIRECTIVE trigger.
5694///
5695/// After this lift the lookup-and-reject shape lives in ONE function;
5696/// the two arms thread the per-call-site kind constructor through the
5697/// helper and apply their respective post-lookup verbs at the call
5698/// site. The `kind: FnOnce(usize) -> TemplateInvariantKind` parameter
5699/// encodes the closed-set bytecode-runtime "this gate has an index"
5700/// surface at the type level — only the two
5701/// [`TemplateInvariantKind`] variants whose payload IS the bad index
5702/// (`SubstBadIndex(usize)` and `SpliceBadIndex(usize)`) construct
5703/// directly through `FnOnce(usize) -> TemplateInvariantKind`; the
5704/// stack-gate variants ([`TemplateInvariantKind::EndListEmptyStack`]
5705/// and [`TemplateInvariantKind::FinalNoValue`]) carry no payload and
5706/// would not type-check at this boundary, so the invalid combination
5707/// "stack-gate kind reached from an op-index lookup" is structurally
5708/// unrepresentable at the helper's call boundary the same way
5709/// [`TemplateInvariantKind`]'s closed-set shape makes it
5710/// unrepresentable in the variant itself.
5711///
5712/// Sibling of [`template_invariant_violation`]: that helper builds the
5713/// typed [`LispError::TemplateInvariant`] variant from a fully-formed
5714/// `kind`; this helper composes the index-keyed lookup with the
5715/// variant-builder, so the kind constructor doesn't have to be evaluated
5716/// eagerly at the call site (lazy via `FnOnce`, only fires on the bad-
5717/// index path). A future fifth bytecode op that reads a bound arg by
5718/// index (a hypothetical [`TemplateOp::Conditional`] that branches on a
5719/// bound boolean, a [`TemplateOp::Project`] that extracts a sub-field
5720/// of a bound `Sexp::List`) extends the family in ONE call to
5721/// `resolve_bound_arg` with the new kind constructor (`KIND(usize) ->
5722/// TemplateInvariantKind`) — the bytecode-runtime's bound-arg-by-index
5723/// projection becomes ONE structural primitive consumers compose with.
5724///
5725/// The returned `&'a Sexp` borrows from `args_by_index` verbatim —
5726/// `Subst`'s arm consumes it through `.clone()` (the consumer pushes
5727/// an owned value into the builder); `Splice`'s arm consumes it
5728/// through [`splice_value_into`] (the consumer borrows for the
5729/// per-arm coercion). The borrow's lifetime `'a` is the unified
5730/// lifetime of `args_by_index`, matching the call site's borrow
5731/// posture.
5732///
5733/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5734/// inline copies of the index-lookup-and-reject prelude across the
5735/// `apply_compiled` body's `Subst` and `Splice` arms is past the ≥2
5736/// PRIME-DIRECTIVE trigger once the structural shape is named.
5737/// THEORY.md §V.1 — knowable platform / "make invalid states
5738/// unrepresentable"; the bytecode-runtime bound-arg-by-index lookup
5739/// becomes a NAMED primitive on the substrate's `&[Sexp]` algebra
5740/// rather than a re-derived `get + ok_or_else + template_invariant_
5741/// violation` chain at every op-arm that reads by index. A future
5742/// authoring tool (REPL, LSP, `tatara-check`) that wants to surface
5743/// "this bytecode op's bound-arg lookup misfired at idx N" binds to
5744/// ONE function. THEORY.md §II.1 invariant 2 — free middle; both
5745/// expansion strategies route through the SHARED `MacroParams::bind`,
5746/// AND the bytecode strategy's op-arms route through this SHARED
5747/// `resolve_bound_arg` lookup — the bytecode-runtime's
5748/// proof-of-well-formedness is now structurally uniform across the
5749/// two reachable index-lookup ops, so a regression that drifts ONE
5750/// arm's posture (e.g. accepts an out-of-range idx at one arm but
5751/// not the other, or swaps the kind constructor at a single arm) is
5752/// no longer a silent two-site divergence.
5753fn resolve_bound_arg<'a>(
5754 args_by_index: &'a [Sexp],
5755 idx: usize,
5756 macro_name: &str,
5757 kind: impl FnOnce(usize) -> TemplateInvariantKind,
5758) -> Result<&'a Sexp> {
5759 args_by_index
5760 .get(idx)
5761 .ok_or_else(|| template_invariant_violation(macro_name, kind(idx)))
5762}
5763
5764/// Project the bytecode-runtime stack to its in-progress builder frame —
5765/// the `&mut Vec<Sexp>` every value-emitting op writes into. ONE named
5766/// primitive both push-emitting arms (`TemplateOp::Literal` /
5767/// `TemplateOp::Subst` / post-`EndList` parent fold) AND the splice-
5768/// emitting arm (`TemplateOp::Splice`) route through.
5769///
5770/// Before this lift the same `stack.last_mut().unwrap()` projection
5771/// appeared at FOUR sites inside [`apply_compiled`]'s op-loop:
5772///
5773/// * `TemplateOp::Literal` — pushes the literal `Sexp` into the
5774/// current builder.
5775/// * `TemplateOp::Subst` — pushes the cloned bound-arg into the
5776/// current builder.
5777/// * `TemplateOp::Splice` — splices the bound-arg into the current
5778/// builder via [`splice_value_into`].
5779/// * `TemplateOp::EndList` — after popping the just-finished list
5780/// frame, pushes the folded `Sexp::List(items)` into the parent
5781/// builder (the new current frame).
5782///
5783/// Four byte-identical re-derivations of the same projection, well past
5784/// the ≥2 PRIME-DIRECTIVE trigger. After this lift the four sites
5785/// collapse to a single `current_builder_mut(&mut stack).{push|extend}`
5786/// call, and the bytecode-runtime invariant the projection rests on
5787/// — "the op-loop always sees at least one stack frame" — lives in ONE
5788/// expect message rather than four silent `.unwrap()` calls.
5789///
5790/// The expect rationale: [`apply_compiled`] seeds the stack with the
5791/// outermost frame at entry (`vec![Vec::with_capacity(1)]`); every
5792/// `TemplateOp::BeginList` pushes a NEW frame and every
5793/// `TemplateOp::EndList` pops it, so the count stays at OR ABOVE 1
5794/// throughout the op-loop. Stack-depleting failure modes are caught
5795/// upstream by their own structural variants:
5796/// [`TemplateInvariantKind::EndListEmptyStack`] fires inside
5797/// [`apply_compiled`]'s `EndList` arm via [`Vec::pop`]'s `Option`
5798/// gate, BEFORE the parent-fold push runs against
5799/// `current_builder_mut`; [`TemplateInvariantKind::FinalNoValue`]
5800/// fires AFTER the op-loop completes, on the outermost `stack.pop()`
5801/// that returns the assembled result. So a reachable
5802/// `current_builder_mut(&mut stack)` always observes a non-empty
5803/// stack, and the `expect` is a structural-invariant marker, not a
5804/// load-bearing rejection path.
5805///
5806/// Sibling of [`resolve_bound_arg`] (the bytecode-runtime bound-arg
5807/// lookup primitive lifted in the prior claude-routine run on this
5808/// module — 492a235) and [`template_invariant_violation`] (the
5809/// structural-variant error builder for the bytecode-runtime's
5810/// closed-set invariant-violation surface). Together the three primitives
5811/// name the bytecode-runtime's substrate-level operations: lookup-a-
5812/// bound-arg ([`resolve_bound_arg`]), build-the-invariant-rejection
5813/// ([`template_invariant_violation`]), and project-to-the-current-
5814/// builder (this lift). A future bytecode op that emits ONE OR MORE
5815/// values into the current builder — a hypothetical
5816/// `TemplateOp::SpliceMany(indices: Vec<usize>)` that splices a batch,
5817/// a `TemplateOp::PushQuoted(form: Sexp)` that wraps before push, a
5818/// span-annotated emit-with-position op — composes with ONE call to
5819/// [`current_builder_mut`] and the per-op post-projection verb
5820/// (`.push(…)`, `.extend(…)`, `splice_value_into(…, _)`); a future
5821/// instrumentation hook that wants to log every op's emit before
5822/// it lands in the builder wraps ONE call boundary, not four inline
5823/// `stack.last_mut().unwrap()` sites.
5824///
5825/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
5826/// inline copies of the top-of-stack projection in one function is
5827/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5828/// named. THEORY.md §V.1 — knowable platform; the bytecode-runtime's
5829/// current-builder projection becomes a NAMED primitive on the
5830/// substrate's `&mut [Vec<Sexp>]` slice algebra rather than a re-derived
5831/// `last_mut + unwrap` chain at every op-arm that emits into the
5832/// builder. The expect message names the invariant
5833/// ("bytecode-runtime invariant: at least one stack frame during
5834/// op-loop") so a regression that drifts the loop's frame management
5835/// surfaces a NAMED panic, not a silent `unwrap` over `None`.
5836/// THEORY.md §II.1 invariant 2 — free middle; both expansion
5837/// strategies route through the SHARED `MacroParams::bind` upstream
5838/// AND the bytecode strategy's op-arms now route through this SHARED
5839/// `current_builder_mut` projection downstream — the bytecode-runtime's
5840/// substrate-level surface (lookup + emit) is named in two
5841/// composable primitives the op-arms compose with.
5842fn current_builder_mut(stack: &mut [Vec<Sexp>]) -> &mut Vec<Sexp> {
5843 stack
5844 .last_mut()
5845 .expect("bytecode-runtime invariant: at least one stack frame during op-loop")
5846}
5847
5848/// Pop the top stack frame off the bytecode-runtime stack, or raise the
5849/// structural [`LispError::TemplateInvariant`] rejection with the
5850/// supplied [`TemplateInvariantKind`] when the stack is empty — ONE
5851/// named primitive both pop-emitting sites in [`apply_compiled`] route
5852/// through.
5853///
5854/// Before this lift two byte-identical
5855/// `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
5856/// kind))?` chains lived inline in [`apply_compiled`]:
5857///
5858/// * `TemplateOp::EndList` arm — pops the just-finished list frame
5859/// before the parent-fold push, with kind
5860/// [`TemplateInvariantKind::EndListEmptyStack`] guarding the
5861/// unreachable empty-stack failure mode.
5862/// * Post-loop final pop — consumes the outermost frame that
5863/// accumulated the template's result, with kind
5864/// [`TemplateInvariantKind::FinalNoValue`] guarding the
5865/// unreachable seed-frame-already-popped failure mode.
5866///
5867/// Two byte-identical re-derivations of the same projection inside one
5868/// function, past the ≥2 PRIME-DIRECTIVE trigger once the structural
5869/// shape is named. After this lift the two sites collapse to a single
5870/// `pop_builder_frame(&mut stack, macro_name, KIND)?` call, and the
5871/// bytecode-runtime invariant the projection rests on — "an empty-stack
5872/// pop is a structural-variant rejection, not a silent `Option::None`"
5873/// — lives in ONE composition point rather than two.
5874///
5875/// Sibling of [`current_builder_mut`] (the bytecode-runtime stack's
5876/// *project-to-top-frame* primitive — the borrow face, never resizes
5877/// the stack) and [`resolve_bound_arg`] (the bound-arg-by-index
5878/// lookup primitive with per-call-site `TemplateInvariantKind`
5879/// constructor). Where `current_builder_mut` borrows the in-progress
5880/// top frame for emission and never panics on a reachable input
5881/// (callers route past it only when the seed frame is present), this
5882/// primitive *consumes* the top frame off the stack and projects its
5883/// absence into a structural `LispError::TemplateInvariant` rejection
5884/// — the failure-on-empty face of the same `&mut Vec<Vec<Sexp>>`
5885/// algebra. The pair —
5886/// [`current_builder_mut`] + [`pop_builder_frame`] — close the
5887/// substrate's bytecode-runtime stack-frame projection algebra at the
5888/// borrow/consume boundary: borrow-the-top-frame for emission (no
5889/// rejection — the invariant rests on the seed-frame contract),
5890/// consume-the-top-frame for finalization (rejection-routed —
5891/// the absence projects through `TemplateInvariantKind` into a
5892/// structural variant of [`LispError`]). Together with
5893/// [`resolve_bound_arg`] (lookup args by index, with per-call-site
5894/// kind constructor) and [`template_invariant_violation`] (the typed
5895/// rejection emitter the three primitives share), the four
5896/// substrate-level operations name the bytecode-runtime's named
5897/// projection surface: lookup-a-bound-arg, project-to-the-current-
5898/// builder, consume-a-finished-builder-frame, build-the-invariant-
5899/// rejection.
5900///
5901/// `kind: TemplateInvariantKind` is the closed-set typed enum whose
5902/// four variants are EXACTLY the four reachable bytecode-runtime
5903/// invariant-violation modes (`SubstBadIndex(usize)` /
5904/// `SpliceBadIndex(usize)` from the index-lookup sibling primitive,
5905/// `EndListEmptyStack` from the `EndList` arm, `FinalNoValue` from
5906/// the post-loop final pop). The Subst / Splice indexed variants
5907/// thread their `usize` payload INSIDE the variant, so the invalid
5908/// combination "stack-gate kind with an op-index" (a hypothetical
5909/// `EndListEmptyStack(99)` carrying a Subst-style payload) is
5910/// structurally unrepresentable at this helper's boundary — the
5911/// caller cannot misroute an indexed kind through `pop_builder_frame`
5912/// at compile time because the kind's data shape is part of its
5913/// variant identity. Same closed-set guarantee
5914/// [`template_invariant_violation`] gives the four kinds; this
5915/// helper composes that guarantee with the `stack.pop()` projection
5916/// at the two pop-emitting sites.
5917///
5918/// The future-run extensions ride this floor: a future bytecode op
5919/// that consumes one or more finished frames — a hypothetical
5920/// `TemplateOp::EndMany(n: usize)` that pops `n` frames into a
5921/// flattened list, a span-aware `TemplateOp::EndListWithSpan(pos)`
5922/// that pops with a position-annotated rejection — composes with
5923/// ONE call (or a fold over N calls) to [`pop_builder_frame`]
5924/// without re-deriving the stack-pop-and-reject shape. A future
5925/// instrumentation hook (a debug-mode logger that records every
5926/// frame consumption, a span-aware pop that threads `Sexp` positions
5927/// through, a multi-frame fold that pops N frames in one step)
5928/// wraps ONE call boundary rather than keeping two inline chains in
5929/// lockstep at the production op-loop sites.
5930///
5931/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
5932/// inline copies of the stack-pop-and-reject projection across the
5933/// `apply_compiled` body's `EndList` arm and post-loop final-pop is
5934/// past the ≥2 PRIME-DIRECTIVE trigger once the structural shape is
5935/// named — the same threshold [`resolve_bound_arg`] (the index-lookup
5936/// sibling) crossed in the prior claude-routine run on this file
5937/// (492a235) and [`current_builder_mut`] (the top-frame-borrow
5938/// sibling) crossed two runs ago (c6a5a9d). THEORY.md §V.1 — knowable
5939/// platform / "make invalid states unrepresentable"; the bytecode-
5940/// runtime stack-frame consume operation becomes a NAMED primitive
5941/// on the substrate's `&mut Vec<Vec<Sexp>>` algebra rather than a
5942/// re-derived `pop + ok_or_else + template_invariant_violation`
5943/// chain at every op-arm that consumes a frame. THEORY.md §II.1
5944/// invariant 2 — free middle; both pop-emitting sites route through
5945/// the SHARED `pop_builder_frame` projection, so a regression that
5946/// drifts ONE site's posture (e.g. accepts an empty-stack pop at the
5947/// `EndList` arm but not the final pop, or swaps the kind constructor
5948/// at a single site) is no longer a silent two-site divergence — the
5949/// type system binds both sites to ONE composition point.
5950///
5951/// Frontier inspiration: MLIR's `Block::eraseFromParent()` against a
5952/// region's block list — the structured-IR's block-consumption
5953/// operation is a named typed primitive that yields a typed
5954/// `LogicalResult` rejection rather than a silent `nullptr` projection
5955/// when the parent region is empty; the substrate's
5956/// `pop_builder_frame` is the unstructured-projection peer on the
5957/// substrate's `&mut Vec<Vec<Sexp>>` stack-frame algebra, with
5958/// `TemplateInvariantKind` standing in for MLIR's `LogicalResult`'s
5959/// closed-set rejection identity. GHC Core's `popTickish` /
5960/// `stackPop` family — every Core-IR transform that consumes a stack
5961/// frame off the rewriter's working stack binds to one named pop
5962/// primitive that threads a typed `WantedFailure` rejection when the
5963/// stack is empty; translated through pleme-io primitives as ONE
5964/// `pop_builder_frame(stack, macro_name, kind)` call with
5965/// `TemplateInvariantKind` carrying the closed-set rejection
5966/// identity.
5967fn pop_builder_frame(
5968 stack: &mut Vec<Vec<Sexp>>,
5969 macro_name: &str,
5970 kind: TemplateInvariantKind,
5971) -> Result<Vec<Sexp>> {
5972 stack
5973 .pop()
5974 .ok_or_else(|| template_invariant_violation(macro_name, kind))
5975}
5976
5977/// Execute a pre-compiled template against the macro's argument list.
5978fn apply_compiled(
5979 macro_name: &str,
5980 params: &MacroParams,
5981 tmpl: &CompiledTemplate,
5982 args: &[Sexp],
5983) -> Result<Sexp> {
5984 // Resolve args by param index through the shared positional binder —
5985 // identical semantics to the `bind_args` (substitute) path by construction.
5986 let args_by_index = params.bind(macro_name, args)?;
5987
5988 // Run the bytecode against a stack of in-progress list builders. The
5989 // outermost frame accumulates the single result the template yields.
5990 // Each emit-into-builder arm routes through the shared
5991 // `current_builder_mut` projection — the bytecode-runtime invariant
5992 // "at least one stack frame during the op-loop" lives in ONE expect
5993 // message rather than four silent `.unwrap()` calls.
5994 let mut stack: Vec<Vec<Sexp>> = vec![Vec::with_capacity(1)];
5995 for op in &tmpl.ops {
5996 match op {
5997 TemplateOp::Literal(s) => current_builder_mut(&mut stack).push(s.clone()),
5998 TemplateOp::Subst(idx) => {
5999 // Bound-arg-by-index lookup routes through the shared
6000 // `resolve_bound_arg` projection with `SubstBadIndex` as
6001 // the per-call-site kind constructor; the post-lookup
6002 // verb (clone + push into the current builder) is the
6003 // Subst arm's per-op shape.
6004 let v = resolve_bound_arg(
6005 &args_by_index,
6006 *idx,
6007 macro_name,
6008 TemplateInvariantKind::SubstBadIndex,
6009 )?
6010 .clone();
6011 current_builder_mut(&mut stack).push(v);
6012 }
6013 TemplateOp::Splice(idx) => {
6014 // Sibling lookup through `resolve_bound_arg` with
6015 // `SpliceBadIndex` as the per-call-site kind constructor;
6016 // the post-lookup verb (`splice_value_into` against the
6017 // current builder) consumes the borrow directly without
6018 // an intermediate clone.
6019 let v = resolve_bound_arg(
6020 &args_by_index,
6021 *idx,
6022 macro_name,
6023 TemplateInvariantKind::SpliceBadIndex,
6024 )?;
6025 splice_value_into(current_builder_mut(&mut stack), v);
6026 }
6027 TemplateOp::BeginList => stack.push(Vec::new()),
6028 TemplateOp::EndList => {
6029 // Pop the just-finished list frame through the shared
6030 // `pop_builder_frame` projection with
6031 // `EndListEmptyStack` as the per-call-site kind
6032 // constructor; the post-pop verb (folded
6033 // `Sexp::List(items)` push into the parent frame via
6034 // `current_builder_mut`) is the EndList arm's per-op
6035 // shape — sibling of the Subst/Splice arms' index-
6036 // lookup-then-emit posture, with the index-lookup
6037 // primitive (`resolve_bound_arg`) and the frame-
6038 // consume primitive (`pop_builder_frame`) BOTH routing
6039 // through the same `TemplateInvariantKind` closed-set
6040 // rejection identity.
6041 let items = pop_builder_frame(
6042 &mut stack,
6043 macro_name,
6044 TemplateInvariantKind::EndListEmptyStack,
6045 )?;
6046 current_builder_mut(&mut stack).push(Sexp::List(items));
6047 }
6048 }
6049 }
6050 // Final pop consumes the outermost (seed) frame via the same
6051 // shared projection — `FinalNoValue` as the per-call-site kind
6052 // constructor pins this site's structural-variant identity, the
6053 // post-pop verb (the `top.len() == 1` arity gate below) is the
6054 // post-loop tail's per-site shape.
6055 let mut top = pop_builder_frame(&mut stack, macro_name, TemplateInvariantKind::FinalNoValue)?;
6056 if top.len() == 1 {
6057 Ok(top.remove(0))
6058 } else {
6059 Ok(Sexp::List(top))
6060 }
6061}
6062
6063/// Hash of `(macro_name, args)` for cache keying — hot path, kept lean.
6064/// Uses `DefaultHasher` (SipHash-2-4) — fast enough that the cache hit rate
6065/// needed to net a win is low even for cheap macros.
6066fn args_cache_key(macro_name: &str, args: &[Sexp]) -> Option<CacheKey> {
6067 let mut h = DefaultHasher::new();
6068 args.len().hash(&mut h);
6069 for a in args {
6070 a.hash(&mut h);
6071 }
6072 Some((macro_name.to_string(), h.finish()))
6073}
6074
6075/// `pub(crate)` so the span-preserving expander recognizes a macro
6076/// definition through THIS function rather than a second copy of the
6077/// `defmacro`-head / name / param-list / body decomposition. A `MacroDef`
6078/// retains no spans (macros are keyed by name), so the spanned path lowers
6079/// its form and lands here — one recognizer, one lambda-list parser, one
6080/// error taxonomy.
6081pub(crate) fn macro_def_from(form: &Sexp) -> Result<Option<MacroDef>> {
6082 // Route the typed-macro-definition dispatch surface through the
6083 // substrate's typed-decoded call decomposition: `as_call_to_any`
6084 // performs the `as_list + head_symbol + MacroDefHead::from_keyword`
6085 // three-step chain in ONE structural query on the `Sexp` algebra.
6086 // The legacy diagnostic anchors on `list.len()` (the FULL form arity
6087 // including the head) — preserved here as `args.len() + 1` so
6088 // `LispError::DefmacroArity.arity` carries the same value across the
6089 // lift.
6090 let Some((head, args)) = form.as_call_to_any(MacroDefHead::from_keyword) else {
6091 return Ok(None);
6092 };
6093 if args.len() < 3 {
6094 return Err(defmacro_arity(head, args.len() + 1));
6095 }
6096 let name = args[0]
6097 .as_symbol()
6098 .ok_or_else(|| defmacro_non_symbol_name(head, &args[0]))?
6099 .to_string();
6100 let param_list = args[1]
6101 .as_list()
6102 .ok_or_else(|| defmacro_non_list_params(head, &args[1]))?;
6103 let params = parse_params(param_list)?;
6104 let body = args[2].clone();
6105 Ok(Some(MacroDef { name, params, body }))
6106}
6107
6108fn parse_params(list: &[Sexp]) -> Result<MacroParams> {
6109 let mut required = Vec::new();
6110 let mut optional: Vec<OptionalParam> = Vec::new();
6111 let mut optional_marker: Option<usize> = None;
6112 let mut i = 0;
6113 while i < list.len() {
6114 // In the optional section a `(name default)` LIST form is a valid spec
6115 // alongside a bare-symbol spec. The list form is only meaningful here,
6116 // so the dispatch fires before the `as_symbol()` gate that would
6117 // otherwise reject it as `NonSymbolParam`.
6118 if optional_marker.is_some() {
6119 if let Sexp::List(items) = &list[i] {
6120 optional.push(parse_optional_list_spec(i, &list[i], items)?);
6121 i += 1;
6122 continue;
6123 }
6124 }
6125 let s = list[i]
6126 .as_symbol()
6127 .ok_or_else(|| non_symbol_param(i, &list[i]))?;
6128 if s == MacroParams::REST_MARKER {
6129 let Some(next) = list.get(i + 1) else {
6130 return Err(rest_param_missing_name(i, None));
6131 };
6132 let Some(name) = next.as_symbol() else {
6133 return Err(rest_param_missing_name(i, Some(next)));
6134 };
6135 let trailing = &list[i + 2..];
6136 if !trailing.is_empty() {
6137 return Err(rest_param_trailing_tokens(i, trailing));
6138 }
6139 return Ok(MacroParams {
6140 required,
6141 optional,
6142 rest: Some(name.to_string()),
6143 });
6144 }
6145 if s == MacroParams::OPTIONAL_MARKER {
6146 if let Some(first) = optional_marker {
6147 return Err(optional_marker_repeated(first, i));
6148 }
6149 optional_marker = Some(i);
6150 i += 1;
6151 continue;
6152 }
6153 if optional_marker.is_some() {
6154 optional.push(OptionalParam::bare(s));
6155 } else {
6156 required.push(s.to_string());
6157 }
6158 i += 1;
6159 }
6160 Ok(MacroParams {
6161 required,
6162 optional,
6163 rest: None,
6164 })
6165}
6166
6167/// Project a `Sexp::List` in the `&optional` section to a typed
6168/// [`OptionalParam`]. The only admissible shape is `(NAME DEFAULT)` — a
6169/// list of exactly TWO elements whose first element is a symbol. Every
6170/// other list shape is the structural rejection
6171/// [`LispError::OptionalParamMalformed`], with a typed `reason`
6172/// ([`OptionalParamMalformedReason`]) naming WHICH way the spec is
6173/// malformed — empty, missing-default, extra-elements, or non-symbol name.
6174///
6175/// `position` is the loop index inside `parse_params`, mirroring the
6176/// `position`/`rest_position`/`first_position` slots on the sibling
6177/// `parse_params` rejection variants. `list_form` is the offending
6178/// `Sexp::List` itself, projected through `crate::domain::sexp_witness` so
6179/// the variant carries BOTH `SexpShape::List` AND the rendered form (for
6180/// LSP / REPL / `tatara-check` consumption). `items` is the list body,
6181/// avoiding a re-`as_list()` at the call boundary.
6182fn parse_optional_list_spec(
6183 position: usize,
6184 list_form: &Sexp,
6185 items: &[Sexp],
6186) -> Result<OptionalParam> {
6187 use crate::error::OptionalParamMalformedReason as R;
6188 // Arity dispatch — threads through the typed classifier on the
6189 // closed-set algebra so the accept-arity binding (arity ==
6190 // `R::OPTIONAL_PARAM_SPEC_ARITY`) is derived from ONE typed const
6191 // rather than the bare literal `2`. Post-classification the arity
6192 // is exactly the accept target; the head-symbol gate is the only
6193 // remaining rejection axis.
6194 if let Some(reason) = R::classify_arity(items.len()) {
6195 return Err(optional_param_malformed(position, list_form, reason));
6196 }
6197 let Some(name) = items[0].as_symbol() else {
6198 return Err(optional_param_malformed(
6199 position,
6200 list_form,
6201 R::NonSymbolName,
6202 ));
6203 };
6204 Ok(OptionalParam::with_default(name, items[1].clone()))
6205}
6206
6207fn bind_args(
6208 macro_name: &str,
6209 params: &MacroParams,
6210 args: &[Sexp],
6211) -> Result<HashMap<String, Sexp>> {
6212 // Zip the shared positional binding (parallel to `names()`) into the
6213 // name-keyed map the `substitute` path looks substitutions up in.
6214 let vals = params.bind(macro_name, args)?;
6215 Ok(params
6216 .names()
6217 .into_iter()
6218 .map(String::from)
6219 .zip(vals)
6220 .collect())
6221}
6222
6223/// Substitute `,name` and `,@name` within a template.
6224/// `,@name` only makes sense inside a List — it splices the bound list into
6225/// the containing list.
6226///
6227/// Routes both unquote-family sites — the top-level `,X` / `,@X`
6228/// recognition AND the list-inner per-item splice recognition — through
6229/// the substrate's typed-marker projection [`Sexp::as_unquote`]. Pre-lift
6230/// each site opened its own `Sexp::Unquote(inner)` / `Sexp::UnquoteSplice
6231/// (inner)` arm paired with a `UnquoteForm::Unquote` / `UnquoteForm::
6232/// Splice` literal; post-lift the (Sexp variant, UnquoteForm variant)
6233/// pairing is bound at ONE projection function the type system threads
6234/// through `(UnquoteForm, &Sexp)`, eliminating the silent two-site
6235/// pairing drift the prior shape allowed.
6236fn substitute(form: &Sexp, bindings: &HashMap<String, Sexp>) -> Result<Sexp> {
6237 if let Some((kind, inner)) = form.as_unquote() {
6238 return match kind {
6239 UnquoteForm::Unquote => resolve_unquote_in_bindings(inner, bindings, kind).cloned(),
6240 UnquoteForm::Splice => Err(splice_outside_list(inner)),
6241 };
6242 }
6243 match form {
6244 Sexp::List(items) => {
6245 let mut out: Vec<Sexp> = Vec::with_capacity(items.len());
6246 for item in items {
6247 if let Some((UnquoteForm::Splice, inner)) = item.as_unquote() {
6248 let val = resolve_unquote_in_bindings(inner, bindings, UnquoteForm::Splice)?;
6249 splice_value_into(&mut out, val);
6250 } else {
6251 out.push(substitute(item, bindings)?);
6252 }
6253 }
6254 Ok(Sexp::List(out))
6255 }
6256 _ => Ok(form.clone()),
6257 }
6258}
6259
6260/// Lift the four inline `LispError::Compile { form: format!("{prefix}{name}"),
6261/// message: "unbound" }` triples (compile_node Unquote/UnquoteSplice +
6262/// substitute Unquote/UnquoteSplice) behind ONE named primitive. Pairs the
6263/// structural variant with `crate::domain::suggest`'s bounded edit-distance
6264/// scan over the candidate set so a typo in `,name` against a macro's params
6265/// (or against a substitution scope's live bindings) surfaces as
6266/// `"compile error in ,xs: unbound; did you mean ,x?"` instead of the bare
6267/// `"unbound"`. The candidate set is per-call — params during compile,
6268/// `bindings.keys()` during substitute — so the operator's hint is always
6269/// drawn from the in-scope name set, never a stale snapshot.
6270///
6271/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6272/// variants are EXACTLY the two reachable syntactic markers
6273/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6274/// boundary (rather than `&'static str`) lands the same compile-time
6275/// closed-set guarantee `defmacro_arity` / `defmacro_non_symbol_name` /
6276/// `defmacro_non_list_params` get from threading `MacroDefHead`: the
6277/// closed set is encoded in the type system, so a regression that drifts
6278/// the marker (e.g. a fourth `prefix: ",,"` call site) becomes a type
6279/// error at the call site, not a runtime substring drift. `name` is the
6280/// offender from source; the hint is `Option<String>` because the matched
6281/// candidate borrows from a transient `Vec<&str>` we built locally —
6282/// copying the matched name into the variant is the cheapest way to keep
6283/// `LispError` lifetime-free.
6284///
6285/// Theory anchor: THEORY.md §VI.1 — generation over composition; four inline
6286/// copies in one module is well past the three-times rule. THEORY.md §V.1 —
6287/// knowable platform; the structural variant exposes `prefix` / `name` /
6288/// `hint` as first-class fields so authoring tools (LSP, REPL,
6289/// `tatara-check`) bind to the data shape instead of substring-parsing the
6290/// rendered diagnostic.
6291fn unbound_template_var(prefix: UnquoteForm, name: &str, candidates: &[&str]) -> LispError {
6292 LispError::UnboundTemplateVar {
6293 prefix,
6294 name: name.to_string(),
6295 hint: crate::domain::suggest(name, candidates).map(str::to_string),
6296 }
6297}
6298
6299/// Lift the four inline `LispError::Compile { form: "unquote" /
6300/// "unquote-splice", message: "only bound symbols may appear after `,` /
6301/// `,@`" }` triples in this module (compile_node Unquote / UnquoteSplice +
6302/// substitute Unquote / UnquoteSplice-inside-list) behind ONE named
6303/// primitive. Sibling of `unbound_template_var`: that helper fires when the
6304/// slot IS a symbol but the symbol isn't bound; this helper fires when the
6305/// slot isn't a symbol at all. Together they close every distinct
6306/// typed-entry template-gate failure mode for the no-evaluator template
6307/// language: each is a structural variant of `LispError`, not a
6308/// `Compile`-shaped substring.
6309///
6310/// `prefix` is `UnquoteForm` — the closed-set typed enum whose two
6311/// variants are EXACTLY the two reachable syntactic markers
6312/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the helper
6313/// boundary (rather than `&'static str`) lands the same compile-time
6314/// closed-set guarantee `unbound_template_var` carries: the closed set is
6315/// encoded in the type system. The inner is the offending `Sexp` routed
6316/// through `crate::domain::sexp_witness` — the typed joint projection
6317/// pairing `SexpShape` (structural shape) with `Sexp::Display`
6318/// (renderable literal) at ONE call boundary. Authoring tools bind to
6319/// BOTH `got.shape` (e.g. `SexpShape::List`) AND `got.display` (e.g.
6320/// `"(list 1 2)"`) jointly — same posture as `splice_outside_list`
6321/// after its prior-run promotion to `SexpWitness`. The two template-
6322/// gate `,X/,@X` rejection variants now share ONE typed witness
6323/// identity at their `got` slot.
6324///
6325/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6326/// inline copies in one module is past the three-times rule. THEORY.md
6327/// §V.1 — knowable platform; the structural variant exposes `prefix` /
6328/// `got` as first-class fields so authoring tools (LSP, REPL,
6329/// `tatara-check`) bind to the data shape instead of substring-parsing
6330/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6331/// a non-symbol unquote target is exactly the failure mode the
6332/// typed-entry gate exists to reject.
6333fn non_symbol_unquote_target(prefix: UnquoteForm, got: &Sexp) -> LispError {
6334 LispError::NonSymbolUnquoteTarget {
6335 prefix,
6336 got: got.witness(),
6337 }
6338}
6339
6340/// Project the inner of a `,X` / `,@X` form to its bound symbol name, or
6341/// raise the structural `LispError::NonSymbolUnquoteTarget` rejection at
6342/// the typed-entry template-gate boundary. ONE named primitive every
6343/// `,X` / `,@X` resolution site in the substrate shares — the inline
6344/// `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(form,
6345/// inner))?` pattern appeared four times across `compile_node`
6346/// (bytecode-path Unquote / UnquoteSplice arms) AND `substitute`
6347/// (substitute-path Unquote / list-inner UnquoteSplice arms), well past
6348/// the three-times-rule trigger. After this lift the four sites collapse
6349/// to a single `unquote_target_symbol(inner, form)?` call, and the
6350/// substrate's understanding of "an unquote target's first gate is `must
6351/// be a symbol`" lives in ONE function — a regression that drifts the
6352/// gate's posture (e.g. accepts non-symbol targets at the bytecode path
6353/// but not the substitute path) becomes a type-level change at this
6354/// helper, not a silent four-site divergence.
6355///
6356/// Sibling of `non_symbol_unquote_target` (the error builder this gate
6357/// calls on failure) and `unbound_template_var` (the typed-entry
6358/// template-gate's SECOND gate — fires once `unquote_target_symbol`
6359/// projects the symbol successfully but the symbol isn't bound in the
6360/// in-scope name set). Together the three close the substrate's
6361/// understanding of the two-step typed-entry template-gate: gate-1 is
6362/// `must-be-a-symbol`, gate-2 is `must-be-bound-in-scope`. With this
6363/// lift, gate-1 lives at ONE call boundary across all four template-
6364/// gate sites — bytecode path AND substitute path AND both `,X` and
6365/// `,@X` forms.
6366///
6367/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6368/// variants are EXACTLY the two reachable syntactic markers
6369/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6370/// helper boundary (rather than `&'static str`) lands the same
6371/// compile-time closed-set guarantee `non_symbol_unquote_target` and
6372/// `unbound_template_var` get from their `UnquoteForm` slots — a
6373/// regression that drifts the marker (e.g. a third pseudo-marker call
6374/// site) becomes a type error at the call site, not a runtime
6375/// substring drift. The returned `&'a str` borrows from `inner` — the
6376/// caller feeds it directly into `params.iter().position(|p| *p ==
6377/// name)` (`compile_node`) or `bindings.get(name)` (`substitute`)
6378/// without an intermediate allocation.
6379///
6380/// Theory anchor: THEORY.md §VI.1 — generation over composition; four
6381/// inline copies of the gate-1 projection (`compile_node`
6382/// Unquote/UnquoteSplice + `substitute` Unquote + `substitute`
6383/// list-inner UnquoteSplice) is past the three-times rule. THEORY.md
6384/// §V.1 — knowable platform; the gate's identity becomes a NAMED
6385/// primitive consumer-binding rather than a four-times-inlined
6386/// match-and-reject snippet — authoring surfaces (REPL, LSP,
6387/// `tatara-check`) that want to surface "the typed-entry template-gate
6388/// rejected your form because the unquote target wasn't a symbol" bind
6389/// to ONE function. THEORY.md §II.1 invariant 1 — typed entry; an
6390/// unquote target that isn't a symbol is exactly the failure mode the
6391/// typed-entry template-gate exists to reject. THEORY.md §II.1
6392/// invariant 2 — free middle; both bytecode AND substitute expansion
6393/// paths now project through the SAME gate-1 primitive, so a macro
6394/// that compiles under one strategy compiles under the other (the
6395/// gate's posture is uniform across the two strategies, no
6396/// per-strategy drift can creep in).
6397fn unquote_target_symbol(inner: &Sexp, form: UnquoteForm) -> Result<&str> {
6398 inner
6399 .as_symbol()
6400 .ok_or_else(|| non_symbol_unquote_target(form, inner))
6401}
6402
6403/// Gate-2 for the bytecode-template compile path: resolve a template
6404/// variable name to its index inside the macro's static param list, or
6405/// raise the structural `LispError::UnboundTemplateVar` rejection. ONE
6406/// named primitive that the two `compile_node` sites — `Sexp::Unquote(_)`
6407/// and `Sexp::UnquoteSplice(_)` arms — share. Before this lift the same
6408/// `params.iter().position(|p| *p == name).ok_or_else(|| unbound_template_var(
6409/// FORM, name, params))?` projection was inlined twice in one match
6410/// block; after this lift the two sites collapse to a single
6411/// `resolve_param_index(name, params, form)?` call and the
6412/// `Subst(idx)` / `Splice(idx)` ops push from a uniform projection
6413/// boundary.
6414///
6415/// Sibling of `resolve_binding`: the same gate-2 contract on the
6416/// substitute path. Together the two close the typed-entry template
6417/// gate's gate-2 (must-be-bound-in-scope) primitive across BOTH
6418/// expansion strategies — gate-1 (`unquote_target_symbol`) projects the
6419/// inner to a symbol name; gate-2 looks the name up in the in-scope
6420/// candidate set. The two paths' candidate sets differ structurally
6421/// (compile path: `&[&str]` of macro params, returning `usize`;
6422/// substitute path: `&HashMap<String, Sexp>` of live bindings, returning
6423/// `&Sexp`), so the gate-2 primitive bifurcates by path — but the
6424/// rejection shape (`LispError::UnboundTemplateVar { prefix, name, hint }`
6425/// with `crate::domain::suggest`-driven hint) is identical across both
6426/// paths. A regression that drifts gate-2's posture (e.g., accepts an
6427/// unbound `,name` at the bytecode path but not the substitute path) is
6428/// now a type-level change at this helper, not a silent four-site
6429/// divergence.
6430///
6431/// `form` is `UnquoteForm` — the closed-set typed enum whose two
6432/// variants are EXACTLY the two reachable syntactic markers
6433/// (`Unquote` ⊎ `Splice`). Threading the typed marker through the
6434/// helper boundary (rather than `&'static str`) lands the same
6435/// compile-time closed-set guarantee `unquote_target_symbol`,
6436/// `unbound_template_var`, and `non_symbol_unquote_target` carry — a
6437/// regression that drifts the marker becomes a type error at the call
6438/// site, not a runtime substring drift.
6439///
6440/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6441/// inline copies of the gate-2 projection in one match block, paired
6442/// with the two substitute-path inline copies, is four copies in two
6443/// functions — past the three-times rule once the structural shape is
6444/// named. THEORY.md §V.1 — knowable platform; the gate's identity
6445/// becomes a NAMED primitive consumer-binding rather than a
6446/// twice-inlined position-and-reject snippet — authoring surfaces
6447/// (REPL, LSP, `tatara-check`) that want to surface "the typed-entry
6448/// template-gate rejected your form because the name isn't bound in
6449/// scope" bind to ONE function per path. THEORY.md §II.1 invariant 1 —
6450/// typed entry; an unbound template variable is exactly the failure
6451/// mode the typed-entry template-gate exists to reject. THEORY.md
6452/// §II.1 invariant 2 — free middle; both expansion strategies'
6453/// gate-2 emit the SAME structural variant, so a macro that compiles
6454/// under one strategy compiles under the other.
6455fn resolve_param_index(name: &str, params: &[&str], form: UnquoteForm) -> Result<usize> {
6456 params
6457 .iter()
6458 .position(|p| *p == name)
6459 .ok_or_else(|| unbound_template_var(form, name, params))
6460}
6461
6462/// Gate-2 for the substitute expansion path: resolve a template
6463/// variable name to its bound `Sexp` value inside the runtime bindings
6464/// map, or raise the structural `LispError::UnboundTemplateVar`
6465/// rejection. ONE named primitive that the two `substitute` sites —
6466/// the top-level `Sexp::Unquote(_)` arm and the list-inner
6467/// `Sexp::UnquoteSplice(_)` arm — share. Before this lift the same
6468/// `bindings.get(sym).<cloned>?.ok_or_else(|| unbound_template_var(
6469/// FORM, sym, &bound_names(bindings)))` projection was inlined twice
6470/// across the substitute walker; after this lift the two sites
6471/// collapse to a single `resolve_binding(bindings, sym, form)?` call
6472/// (with a trailing `.cloned()` at the top-level arm because that arm
6473/// returns an owned `Sexp` while the list-inner arm consumes the
6474/// `&Sexp` borrow directly).
6475///
6476/// Sibling of `resolve_param_index`: the same gate-2 contract on the
6477/// bytecode-template compile path. Together the two close the
6478/// typed-entry template gate's gate-2 (must-be-bound-in-scope)
6479/// primitive across BOTH expansion strategies. The candidate set on
6480/// the substitute path is the live bindings' keys (built fresh per
6481/// call via `bound_names`) — never a stale snapshot, so the
6482/// suggest-driven hint is always drawn from the actual in-scope name
6483/// set the operator sees.
6484///
6485/// The returned `&'a Sexp` borrows from `bindings` — the list-inner
6486/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6487/// splice-expansion match without an intermediate allocation. The
6488/// top-level caller's owned-Sexp obligation is satisfied by the
6489/// `.cloned()` projection at the call site, which is a single typed
6490/// `Sexp::clone` and not a redundant lookup.
6491///
6492/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6493/// `resolve_param_index` and `unquote_target_symbol`. A regression
6494/// that drifts the marker becomes a type error at the call site, not
6495/// a runtime substring drift.
6496///
6497/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6498/// inline copies of the gate-2 projection in the substitute walker,
6499/// paired with the two compile-path inline copies, is four copies in
6500/// two functions — past the three-times rule once the structural
6501/// shape is named. THEORY.md §V.1 — knowable platform; the gate's
6502/// identity becomes a NAMED primitive consumer-binding rather than a
6503/// twice-inlined lookup-and-reject snippet. THEORY.md §II.1
6504/// invariant 1 — typed entry; an unbound template variable is exactly
6505/// the failure mode the typed-entry template-gate exists to reject.
6506/// THEORY.md §II.1 invariant 2 — free middle; both expansion
6507/// strategies' gate-2 emit the SAME structural variant.
6508fn resolve_binding<'a>(
6509 bindings: &'a HashMap<String, Sexp>,
6510 name: &str,
6511 form: UnquoteForm,
6512) -> Result<&'a Sexp> {
6513 bindings
6514 .get(name)
6515 .ok_or_else(|| unbound_template_var(form, name, &bound_names(bindings)))
6516}
6517
6518/// Compose gate-1 + gate-2 for the bytecode-template compile path into ONE
6519/// named primitive: project the unquote `inner` to a symbol name
6520/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6521/// index inside the macro's static param list (gate-2, via
6522/// `resolve_param_index`). Sibling of `resolve_unquote_in_bindings`: the
6523/// same gate-1+gate-2 composition on the substitute expansion path.
6524///
6525/// Before this lift, the two `compile_node` arms (`Sexp::Unquote(_)` and
6526/// `Sexp::UnquoteSplice(_)`) threaded `form: UnquoteForm` through TWO
6527/// helper calls each — once into `unquote_target_symbol(inner, form)?`
6528/// (gate-1) AND once into `resolve_param_index(name, params, form)?`
6529/// (gate-2). The marker's typed identity was re-asserted at the call site
6530/// twice per arm — four `UnquoteForm::Unquote` / `UnquoteForm::Splice`
6531/// literal occurrences across the two arms, for what is structurally ONE
6532/// marker-identity per syntactic-marker arm. After this lift each arm
6533/// threads the marker ONCE through ONE call, and the gate-1-then-gate-2
6534/// sequencing lives in the helper body, not at the call site.
6535///
6536/// The composition is load-bearing: gate-1 (must-be-a-symbol) MUST fire
6537/// before gate-2 (must-be-bound-in-scope) — a non-symbol inner is
6538/// structurally a different failure mode (`LispError::NonSymbolUnquoteTarget`,
6539/// which carries the offending `SexpWitness`) than an unbound symbol
6540/// (`LispError::UnboundTemplateVar`, which carries a `name: String` plus
6541/// a `crate::domain::suggest`-driven hint over the candidate set). A
6542/// regression that reorders or skips gate-1 would emit
6543/// `LispError::UnboundTemplateVar { name: "(list 1 2)", ... }` for a
6544/// non-symbol inner (re-treating the rendered list literal as a bound-
6545/// name lookup key), which is exactly the diagnostic-confusion this
6546/// composition exists to rule out. Naming the composition as one
6547/// primitive makes the sequencing structural — the helper body IS the
6548/// proof that gate-1 ran before gate-2.
6549///
6550/// `form` is `UnquoteForm` — the closed-set typed enum threaded through
6551/// the composition once and passed onward to both gate-1 and gate-2's
6552/// rejection-builders. Same posture as `unquote_target_symbol`,
6553/// `resolve_param_index`, `resolve_binding`, `non_symbol_unquote_target`,
6554/// and `unbound_template_var` — a regression that drifts the marker
6555/// becomes a type error at the helper boundary, not a runtime substring
6556/// drift, AND the marker can no longer drift BETWEEN gate-1 and gate-2
6557/// at a single call site (which the prior pre-lift shape allowed:
6558/// `unquote_target_symbol(inner, UnquoteForm::Unquote)?` followed by
6559/// `resolve_param_index(name, params, UnquoteForm::Splice)?` would
6560/// type-check but render a misleading diagnostic).
6561///
6562/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6563/// gate-1+gate-2 SEQUENCE is itself a named primitive once both halves
6564/// have been named (two prior runs landed the halves; this run lands the
6565/// composition). THEORY.md §V.1 — knowable platform; the gate's
6566/// composition is now load-bearing in the type system — gate-1 cannot be
6567/// silently skipped, gate-2 cannot be silently reordered before gate-1,
6568/// and the marker cannot drift between the two halves. THEORY.md §II.1
6569/// invariant 1 — typed entry; the typed-entry template gate's full
6570/// rejection chain (non-symbol → unbound-symbol) is now ONE primitive.
6571/// THEORY.md §II.1 invariant 2 — free middle; both expansion strategies
6572/// expose the gate's identity as ONE primitive per path, so a macro that
6573/// passes the gate under one strategy passes under the other (no per-
6574/// strategy composition drift can creep in).
6575fn resolve_unquote_in_params(inner: &Sexp, params: &[&str], form: UnquoteForm) -> Result<usize> {
6576 let name = unquote_target_symbol(inner, form)?;
6577 resolve_param_index(name, params, form)
6578}
6579
6580/// Compose gate-1 + gate-2 for the substitute expansion path into ONE
6581/// named primitive: project the unquote `inner` to a symbol name
6582/// (gate-1, via `unquote_target_symbol`) THEN resolve the name to its
6583/// bound `Sexp` value inside the runtime bindings map (gate-2, via
6584/// `resolve_binding`). Sibling of `resolve_unquote_in_params`: the same
6585/// gate-1+gate-2 composition on the bytecode-template compile path.
6586///
6587/// Before this lift, the substitute walker's two unquote sites (the
6588/// top-level `Sexp::Unquote(_)` arm and the list-inner
6589/// `Sexp::UnquoteSplice(_)` arm) threaded `form: UnquoteForm` through
6590/// TWO helper calls each — once into `unquote_target_symbol(inner,
6591/// form)?` (gate-1) AND once into `resolve_binding(bindings, name,
6592/// form)?` (gate-2). After this lift each site threads the marker
6593/// ONCE through ONE call. Same composition contract as
6594/// `resolve_unquote_in_params` — gate-1 fires before gate-2 by the
6595/// helper body's `?`-then-call sequencing, NOT by call-site discipline.
6596///
6597/// The returned `&'a Sexp` borrows from `bindings` so the list-inner
6598/// caller feeds it straight into the `Sexp::List`/`Sexp::Nil`/other
6599/// splice-expansion match without an intermediate allocation; the
6600/// top-level caller's owned-Sexp obligation is satisfied by a
6601/// `.cloned()` projection at the call site (one typed `Sexp::clone`,
6602/// no redundant lookup).
6603///
6604/// `form` is `UnquoteForm` — same closed-set typed enum threading as
6605/// `resolve_unquote_in_params` and all the helpers it composes. After
6606/// this lift, the marker's identity flows through the substitute path's
6607/// typed-entry template gate via ONE explicit pass per call site, not
6608/// two; the gate's gate-1+gate-2 sequencing is structural across both
6609/// expansion strategies.
6610///
6611/// Theory anchor: same as `resolve_unquote_in_params`. THEORY.md §VI.1
6612/// (generation over composition; named composition of named gates),
6613/// THEORY.md §V.1 (knowable platform; gate composition is type-system
6614/// load-bearing), THEORY.md §II.1 invariant 1 (typed entry; the full
6615/// rejection chain is ONE primitive), THEORY.md §II.1 invariant 2
6616/// (free middle; both strategies share the same composition shape).
6617fn resolve_unquote_in_bindings<'a>(
6618 inner: &Sexp,
6619 bindings: &'a HashMap<String, Sexp>,
6620 form: UnquoteForm,
6621) -> Result<&'a Sexp> {
6622 let name = unquote_target_symbol(inner, form)?;
6623 resolve_binding(bindings, name, form)
6624}
6625
6626/// Lift the lone `LispError::Compile { form: "unquote-splice", message:
6627/// "`,@` may only appear inside a list" }` triple — the substitute path's
6628/// top-level `,@X` rejection — behind ONE named primitive. Sibling of
6629/// `non_symbol_unquote_target` and `unbound_template_var`: those helpers
6630/// fire when the slot inside a `,X` / `,@X` is malformed (non-symbol or
6631/// unbound symbol); this helper fires when the `,@X` form itself is
6632/// ill-positioned (no containing list to flatten into). Together the three
6633/// close every distinct typed-entry template-gate failure mode for the
6634/// no-evaluator template language: each is a structural variant of
6635/// `LispError`, not a `Compile`-shaped substring.
6636///
6637/// `inner` is the offending `Sexp` projected through `Display` so the
6638/// operator sees the literal value they wrote — `xs`, `(list 1 2)`, `5` —
6639/// instead of just the bare "may only appear inside a list" verdict. The
6640/// helper takes `&Sexp` (parallel to `non_symbol_unquote_target`) and
6641/// projects through `to_string()` at the variant boundary; the `prefix:
6642/// &'static str` slot is implicit (always `,@`) and absent from the variant
6643/// itself, parallel to how `OddKwargs { dangling }` names ONE failure mode
6644/// without a syntactic-marker slot.
6645///
6646/// Used by both the substitute path (top-level `,@X` body) AND the bytecode
6647/// path's `compile_template` gate (top-level `,@X` body — closing the prior
6648/// silent-divergence where the bytecode interpreter's outermost stack frame
6649/// absorbed the splice). After this lift `,@-outside-list` is rejected on
6650/// both paths with ONE structural variant — the typed-entry template gate
6651/// is fully structural across both expansion strategies.
6652///
6653/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6654/// emission sites (substitute + compile_template) for one failure mode is
6655/// past the three-times rule once the structural shape is named. THEORY.md
6656/// §V.1 — knowable platform; the structural variant exposes `got` as a
6657/// first-class field so authoring tools (LSP, REPL, `tatara-check`) bind to
6658/// the data shape instead of substring-parsing the rendered diagnostic.
6659/// THEORY.md §II.1 invariant 1 — typed entry; a `,@X` at a position with no
6660/// containing list is exactly the failure mode the typed-entry gate exists
6661/// to reject. THEORY.md §II.1 invariant 2 — free middle; both expansion
6662/// paths now reject the same set of templates, so a macro that registers
6663/// successfully has the same expansion behavior under either strategy.
6664fn splice_outside_list(inner: &Sexp) -> LispError {
6665 LispError::SpliceOutsideList {
6666 got: inner.witness(),
6667 }
6668}
6669
6670/// Lift the two inline `LispError::Compile { form: format!("call to
6671/// {macro_name}"), message: format!("missing required arg: {name}") }`
6672/// triples — `bind_args` (substitute path) AND `apply_compiled` (bytecode
6673/// path) — behind ONE named primitive. Sibling of the typed-entry kwargs
6674/// `MissingKwarg { key }` lift: that variant fires when a `(<head> :key
6675/// value …)` kwargs form omits a required keyword; this variant fires when
6676/// a `(<macroname> a b …)` call omits a required positional param. Together
6677/// they close every distinct typed-entry missing-required surface in the
6678/// substrate — kwargs-gate AND macro-call-gate now share a single
6679/// structural-variant idiom.
6680///
6681/// Same single emission shape across both expansion strategies — before
6682/// this lift the same failure mode emitted byte-identical
6683/// `LispError::Compile { … }` triples at TWO call sites; after this lift
6684/// both sites share ONE structural variant. Two strategies that picked
6685/// different code paths now emit the same structural variant for the same
6686/// failure mode (THEORY.md §II.1 invariant 2 — free middle: which strategy
6687/// you picked must not change which inputs you reject OR how the rejection
6688/// is shaped). Same posture as `splice_outside_list`'s path-uniform
6689/// rejection across substitute + compile_template.
6690///
6691/// `macro_name` and `name` are `&str` borrows from the call-site / param
6692/// list; the variant's owned `String`s are formed at the boundary so
6693/// `LispError` stays lifetime-free.
6694///
6695/// Theory anchor: THEORY.md §VI.1 — generation over composition; two
6696/// inline copies of one shape is past the three-times-rule trigger once
6697/// the structural variant is named (the test count gives this the
6698/// fail-before-pass-after edge). THEORY.md §V.1 — knowable platform; the
6699/// structural variant exposes `macro_name` / `param` as first-class
6700/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the data
6701/// shape instead of substring-parsing the rendered diagnostic. THEORY.md
6702/// §II.1 invariant 1 — typed entry; a macro call with too few args is
6703/// exactly the failure mode the typed-entry gate exists to reject.
6704fn missing_macro_arg(macro_name: &str, param: &str) -> LispError {
6705 LispError::MissingMacroArg {
6706 macro_name: macro_name.to_string(),
6707 param: param.to_string(),
6708 }
6709}
6710
6711/// Mirror at the call-site of `missing_macro_arg`: that helper fires when
6712/// the macro CALL supplies TOO FEW args for the required arity (a required
6713/// slot has no arg); this helper fires when the macro CALL supplies TOO
6714/// MANY args for a rest-less param list (the surplus has nowhere to bind).
6715/// Together they close the typed-entry macro-call-gate's positional-arity
6716/// surface in both directions; together with the definition-site
6717/// `RestParamTrailingTokens` (lifted by the prior-run typed-promotion
6718/// lineage at the parse_params boundary), every distinct way a macro
6719/// definition + call pair can MISCOUNT args is now a named structural
6720/// rejection.
6721///
6722/// `expected` is the rest-less binder's fixed maximum arity
6723/// (`required.len() + optional.len()`); `got` is the actual call-site arg
6724/// count. Both are surfaced at the variant boundary so authoring tools
6725/// (REPL, LSP, `tatara-check`) name the "you supplied {got} args but the
6726/// macro takes at most {expected}" quick-fix from one structural projection
6727/// rather than re-deriving either count from the source. `macro_name` is
6728/// `&str` borrowed from the call-site; the variant's owned `String` is
6729/// formed at the boundary so `LispError` stays lifetime-free — same posture
6730/// as `missing_macro_arg`.
6731///
6732/// Theory anchor: THEORY.md §VI.1 — generation over composition; the
6733/// rest-less surplus-args gate is a SINGLE-OWNER named rejection, not a
6734/// silent truncation re-asserted at every consumer that walks the bound
6735/// values. THEORY.md §V.1 — knowable platform; the structural variant
6736/// exposes `macro_name` / `expected` / `got` as first-class fields so
6737/// authoring tools bind to the data shape instead of substring-parsing
6738/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a
6739/// macro call with too many args (and no `&rest` slot to absorb them) is
6740/// exactly the failure mode the typed-entry gate exists to reject —
6741/// silently dropping `args[expected..]` is structurally indistinguishable
6742/// from honoring them, the asymmetry this gate closes. THEORY.md §II.1
6743/// invariant 2 — free middle; both expansion strategies route through the
6744/// SHARED `MacroParams::bind`, so the new rejection lands once and the
6745/// substitute + bytecode paths inherit it unable to drift.
6746fn too_many_macro_args(macro_name: &str, expected: usize, got: usize) -> LispError {
6747 LispError::TooManyMacroArgs {
6748 macro_name: macro_name.to_string(),
6749 expected,
6750 got,
6751 }
6752}
6753
6754/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6755/// "expected symbol" }` triple in `parse_params` behind ONE named
6756/// primitive. Sibling of `missing_macro_arg`: that helper fires when the
6757/// macro CALL is malformed (call-site missing a positional arg); this
6758/// helper fires when the macro DEFINITION is malformed (definition-site
6759/// has a non-symbol where a param name should be). Together they open
6760/// the defmacro-syntax-gate / macro-call-gate split — call-site
6761/// rejections vs. definition-site rejections — each as its own
6762/// structural-variant family on `LispError`.
6763///
6764/// `position` is the loop index inside `parse_params`, i.e. the 0-based
6765/// index of the offending element within the param list (`(defmacro f
6766/// (a 5 b) …)` — position 1 is the literal `5`); naming it lets an LSP
6767/// quick-fix point at the exact list element instead of the whole
6768/// param list. `got` is the offending `Sexp` projected through
6769/// `Display` so the operator sees the literal value they wrote
6770/// (`5`, `:foo`, `(nested)`) at the variant boundary; the helper takes
6771/// `&Sexp` (parallel to `non_symbol_unquote_target` and
6772/// `splice_outside_list`) and projects through `to_string()` so the
6773/// variant stays lifetime-free.
6774///
6775/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6776/// inline copy still earns a named primitive once the structural shape
6777/// is named (the test count gives this the fail-before-pass-after edge,
6778/// parallel to how `OddKwargs` was lifted from a single site for the
6779/// structural-completeness payoff). THEORY.md §V.1 — knowable platform;
6780/// the structural variant exposes `position` / `got` as first-class
6781/// fields so authoring tools (LSP, REPL, `tatara-check`) bind to the
6782/// data shape instead of substring-parsing the rendered diagnostic.
6783/// THEORY.md §II.1 invariant 1 — typed entry; a non-symbol element
6784/// inside a defmacro param list is exactly the failure mode the
6785/// typed-entry gate exists to reject — and it must reject DEFINITIONS
6786/// as readily as it rejects CALLS.
6787fn non_symbol_param(position: usize, got: &Sexp) -> LispError {
6788 LispError::NonSymbolParam {
6789 position,
6790 got: got.witness(),
6791 }
6792}
6793
6794/// Lift the lone `LispError::Compile { form: "defmacro params", message:
6795/// "&rest needs a name" }` triple in `parse_params` behind ONE named
6796/// primitive. Sibling of `non_symbol_param`: that helper fires when a
6797/// NON-`&rest` element at a param position isn't a symbol; this helper
6798/// fires specifically on the post-`&rest` follower slot, where the
6799/// failure mode bifurcates into "missing entirely" (`got = None`) vs.
6800/// "present but not a symbol" (`got = Some(...)`). Together, the two
6801/// helpers close the `parse_params` walker — every distinct failure
6802/// mode the walker can emit is now a structural variant of `LispError`,
6803/// not a `Compile`-shaped substring.
6804///
6805/// `rest_position` is the loop index inside `parse_params` at which
6806/// the `&rest` marker was matched, i.e. the 0-based index of `&rest`
6807/// within the param list (`(defmacro f (a &rest 5) …)` — rest_position
6808/// 1 is `&rest`, the offender follows at 2); naming the marker
6809/// position lets an LSP quick-fix point at the `&rest` form itself
6810/// rather than at the next list element. `got` is `Option<&Sexp>`
6811/// because the follower slot bifurcates: `None` when the marker was
6812/// the param list's last element (no follower at all), `Some(sexp)`
6813/// when a follower exists but isn't a symbol; the helper projects
6814/// through `to_string()` at the variant boundary so the variant stays
6815/// lifetime-free.
6816///
6817/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6818/// inline copy still earns a named primitive once the structural shape
6819/// is named (the test count gives this the fail-before-pass-after
6820/// edge, parallel to how `non_symbol_param` was lifted from a single
6821/// site for the structural-completeness payoff). THEORY.md §V.1 —
6822/// knowable platform; the structural variant exposes `rest_position` /
6823/// `got` as first-class fields so authoring tools (LSP, REPL,
6824/// `tatara-check`) bind to the data shape instead of substring-parsing
6825/// the rendered diagnostic. THEORY.md §II.1 invariant 1 — typed entry;
6826/// a `&rest` marker followed by no name (or by a non-symbol) is
6827/// exactly the failure mode the typed-entry gate exists to reject —
6828/// and the gate must reject DEFINITIONS as readily as it rejects
6829/// CALLS.
6830fn rest_param_missing_name(rest_position: usize, got: Option<&Sexp>) -> LispError {
6831 LispError::RestParamMissingName {
6832 rest_position,
6833 got: got.map(Sexp::witness),
6834 }
6835}
6836
6837/// The third and final `parse_params` definition-site rejection — a
6838/// `&rest <name>` followed by further tokens. Sibling of `non_symbol_param`
6839/// (a param slot that isn't a symbol) and `rest_param_missing_name` (the
6840/// post-`&rest` follower is missing or malformed): this helper fires once
6841/// the rest name is bound and the walker finds the param list does not end
6842/// there. The `&rest` name absorbs every remaining call arg, so it is
6843/// structurally the LAST param a list can name; trailing tokens are
6844/// unrepresentable in `MacroParams` and were previously dropped silently.
6845///
6846/// `rest_position` is the loop index of the `&rest` marker (parallel to
6847/// `rest_param_missing_name`); `trailing` is the non-empty token run after
6848/// the bound rest name — the helper records its length and the typed
6849/// witness of its first element. The caller guarantees `trailing` is
6850/// non-empty (it is only built when `list[i + 2..].first()` is `Some`), so
6851/// `trailing[0]` does not panic.
6852///
6853/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6854/// unrepresentable"; a param list with tokens after `&rest <name>` is
6855/// nonsense `MacroParams` cannot hold, so the gate must REJECT it rather
6856/// than truncate to the representable prefix. THEORY.md §II.1 invariant 1 —
6857/// typed entry; the gate rejects malformed DEFINITIONS as readily as
6858/// malformed calls. THEORY.md §VI.1 — generation over composition; this
6859/// closes the `parse_params` walker's last uncovered failure mode, making
6860/// the sibling docs' "every distinct failure mode is a structural variant"
6861/// claim finally true.
6862fn rest_param_trailing_tokens(rest_position: usize, trailing: &[Sexp]) -> LispError {
6863 LispError::RestParamTrailingTokens {
6864 rest_position,
6865 extra: trailing.len(),
6866 first: trailing[0].witness(),
6867 }
6868}
6869
6870/// A `&optional` marker appeared a SECOND time in one param list —
6871/// `(defmacro f (a &optional b &optional c) …)`. The lambda-list has exactly
6872/// ONE optional section (between the required run and the rest); a second
6873/// `&optional` is nonsense `MacroParams` cannot hold (its `optional` field is
6874/// one flat run, not a sequence of sections). Without this gate the parser
6875/// would otherwise treat the second `&optional` as an optional param literally
6876/// NAMED `&optional`, binding call args to a marker symbol — exactly the kind
6877/// of silent misalignment the typed shape exists to forbid.
6878///
6879/// Sibling of `rest_param_trailing_tokens` (the rest-section ordering gate):
6880/// both reject a param list whose marker structure the canonical lambda-list
6881/// ordering cannot represent. `first_position` is the loop index of the
6882/// first `&optional`, `second_position` the second — naming both lets an LSP
6883/// quick-fix point at the redundant marker to delete.
6884///
6885/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6886/// unrepresentable"; a param list with two `&optional` sections is nonsense
6887/// `MacroParams` cannot hold, so the gate must REJECT rather than bind args
6888/// to a marker symbol. THEORY.md §II.1 invariant 1 — typed entry; the gate
6889/// rejects malformed DEFINITIONS as readily as malformed calls.
6890fn optional_marker_repeated(first_position: usize, second_position: usize) -> LispError {
6891 LispError::OptionalMarkerRepeated {
6892 first_position,
6893 second_position,
6894 }
6895}
6896
6897/// An `&optional` section entry that's a `Sexp::List` did NOT match the only
6898/// admissible shape `(NAME DEFAULT)` — exactly two elements with a symbol
6899/// head. This helper builds the structural rejection from the loop position,
6900/// the offending list form (projected through `crate::domain::sexp_witness`
6901/// to carry both `SexpShape::List` and the literal display), and the typed
6902/// `OptionalParamMalformedReason` naming which of the four malformed shapes
6903/// fired (empty list / missing default / extra elements / non-symbol name).
6904///
6905/// Sibling of `optional_marker_repeated` (the `&optional`-section marker
6906/// gate) and `non_symbol_param` (the bare-symbol gate): the three together
6907/// close every distinct typed-entry rejection the optional section can
6908/// emit. The bare-symbol form `&optional x` is still routed through
6909/// `non_symbol_param`'s sibling acceptance path; the list form `&optional
6910/// (x default)` is admitted iff this gate accepts the spec.
6911///
6912/// Theory anchor: THEORY.md §V.1 — knowable platform / "make invalid states
6913/// unrepresentable"; an `&optional` list spec of any other shape is
6914/// nonsense `MacroParams` cannot hold, so the gate must REJECT rather than
6915/// bind args to a marker symbol or drop the extras silently. THEORY.md
6916/// §II.1 invariant 1 — typed entry; a malformed default-form spec is
6917/// exactly the failure mode the typed-entry gate exists to reject — and
6918/// the gate must reject DEFINITIONS as readily as it rejects CALLS.
6919fn optional_param_malformed(
6920 position: usize,
6921 got: &Sexp,
6922 reason: crate::error::OptionalParamMalformedReason,
6923) -> LispError {
6924 LispError::OptionalParamMalformed {
6925 position,
6926 got: got.witness(),
6927 reason,
6928 }
6929}
6930
6931/// Lift the lone `LispError::Compile { form: head.to_string(), message:
6932/// "(defmacro name (params) body) required" }` triple in
6933/// `macro_def_from` behind ONE named primitive. Sibling of
6934/// `non_symbol_param` and `rest_param_missing_name`: those helpers
6935/// fire INSIDE `parse_params`, AFTER the arity gate has passed; this
6936/// helper fires AT the arity gate itself, BEFORE name / params / body
6937/// validation can run. Together the three close `macro_def_from`'s
6938/// outermost rejection chain — every distinct failure mode the gate
6939/// can emit at the top level becomes a structural variant of
6940/// `LispError`, not a `Compile`-shaped substring.
6941///
6942/// `head` is `MacroDefHead` (the typed closed-set enum), having been
6943/// projected through `MacroDefHead::from_keyword` at the top of
6944/// `macro_def_from`. The helper threads `head` straight into the
6945/// variant's typed `head: MacroDefHead` slot — no `&'static str`
6946/// projection at the helper boundary; the projection through
6947/// `MacroDefHead::keyword()` happens at Display rendering time via
6948/// `MacroDefHead`'s Display impl inside the variant's `#[error(...)]`
6949/// annotation. Same posture as how
6950/// `compiler_spec.rs::compiler_spec_io_err` threads
6951/// `CompilerSpecIoStage` straight into
6952/// `LispError::CompilerSpecIo.stage`. `arity` is `usize` (the length
6953/// of the form including the head element).
6954///
6955/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
6956/// inline copy still earns a named primitive once the structural
6957/// shape is named (the test count gives this the fail-before/pass-
6958/// after edge, parallel to how `non_symbol_param` and
6959/// `rest_param_missing_name` were lifted from a single site for the
6960/// structural-completeness payoff). THEORY.md §V.1 — knowable
6961/// platform; the structural variant exposes `head` / `arity` as
6962/// first-class fields so authoring tools (LSP, REPL, `tatara-check`)
6963/// bind to the data shape instead of substring-parsing the rendered
6964/// diagnostic. THEORY.md §II.1 invariant 1 — typed entry; a defmacro
6965/// form with too few elements is exactly the failure mode the typed-
6966/// entry gate exists to reject — and the gate must reject
6967/// DEFINITIONS as readily as it rejects CALLS. THEORY.md §II.1
6968/// invariant 2 — free middle; the arity gate fires inside
6969/// `macro_def_from` BEFORE either expansion strategy runs, so both
6970/// `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
6971/// (substitute) reject the SAME malformed defmacro at the SAME gate.
6972fn defmacro_arity(head: MacroDefHead, arity: usize) -> LispError {
6973 LispError::DefmacroArity { head, arity }
6974}
6975
6976/// Lift the lone `LispError::Compile { form: head.to_string(), message:
6977/// "expected name symbol" }` triple in `macro_def_from` behind ONE
6978/// named primitive. Sibling of `defmacro_arity`, `non_symbol_param`,
6979/// and `rest_param_missing_name`: those helpers fire at the OUTERMOST
6980/// arity gate (`defmacro_arity`) or INSIDE `parse_params`
6981/// (`non_symbol_param`, `rest_param_missing_name`); this helper fires
6982/// AFTER the arity gate has passed but BEFORE `parse_params` runs —
6983/// at the second of three `macro_def_from` rejection points
6984/// (arity → name-symbol → param-list → parse_params).
6985///
6986/// Walking a malformed `(defmacro …)` from the outside in, the gate
6987/// fires:
6988/// 1. `defmacro_arity(head, arity)` if the form has fewer than 4
6989/// elements (`(defmacro)`, `(defmacro f)`).
6990/// 2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't a
6991/// symbol (`(defmacro 5 () body)`, `(defmacro :foo () body)`).
6992/// 3. The `expected param list` gate (NEXT LIFT) if list[2] isn't a
6993/// list (`(defmacro f x body)`).
6994/// 4. Inside `parse_params`: `non_symbol_param` and
6995/// `rest_param_missing_name`.
6996///
6997/// After this lift step 2 is structural; the only remaining
6998/// `Compile`-shaped site in `macro_def_from` is step 3 (`expected
6999/// param list`).
7000///
7001/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7002/// projected through `MacroDefHead::from_keyword` at the top of
7003/// `macro_def_from`. The helper threads `head` straight into the
7004/// variant's typed `head: MacroDefHead` slot — same posture as
7005/// `defmacro_arity` after the typed-slot promotion. `got` is `&Sexp`
7006/// at the call site (a borrow into the form's name slot); the helper
7007/// projects through `crate::domain::sexp_witness` — the typed joint
7008/// projection (`SexpShape` + `Sexp::Display`) — so the variant's
7009/// `got: SexpWitness` slot carries BOTH structural shape AND
7010/// renderable literal across the boundary, parallel to how
7011/// `non_symbol_param` and `non_symbol_unquote_target` project their
7012/// `&Sexp` arguments. The fourth consumer of the typed `SexpWitness`
7013/// primitive on the substrate's Sexp-display-source rejection
7014/// surface.
7015///
7016/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7017/// inline copy still earns a named primitive once the structural
7018/// shape is named (the test count gives this the fail-before/pass-
7019/// after edge, parallel to how `defmacro_arity`, `non_symbol_param`,
7020/// and `rest_param_missing_name` were lifted from a single site for
7021/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7022/// platform; the structural variant exposes `head` / `got` as
7023/// first-class fields so authoring tools (LSP, REPL,
7024/// `tatara-check`) bind to the data shape instead of substring-
7025/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7026/// typed entry; a defmacro form whose name slot isn't a symbol is
7027/// exactly the failure mode the typed-entry gate exists to reject —
7028/// and the gate must reject DEFINITIONS as readily as it rejects
7029/// CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7030/// name-symbol gate fires inside `macro_def_from` BEFORE either
7031/// expansion strategy runs, so both `Expander::new()` (bytecode) and
7032/// `Expander::new_substitute_only()` (substitute) reject the SAME
7033/// malformed defmacro at the SAME gate.
7034fn defmacro_non_symbol_name(head: MacroDefHead, got: &Sexp) -> LispError {
7035 LispError::DefmacroNonSymbolName {
7036 head,
7037 got: got.witness(),
7038 }
7039}
7040
7041/// Lift the lone `LispError::Compile { form: head.to_string(), message:
7042/// "expected param list" }` triple in `macro_def_from` behind ONE
7043/// named primitive. Sibling of `defmacro_arity`,
7044/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7045/// `rest_param_missing_name`: those helpers fire at the OUTERMOST
7046/// arity gate (`defmacro_arity`), at the second `macro_def_from`
7047/// rejection point (`defmacro_non_symbol_name`), or INSIDE
7048/// `parse_params` (`non_symbol_param`, `rest_param_missing_name`);
7049/// this helper fires AFTER both the arity gate AND the name-symbol
7050/// gate have passed but BEFORE `parse_params` runs — at the third
7051/// of three `macro_def_from` rejection points
7052/// (arity → name-symbol → param-list → parse_params).
7053///
7054/// Walking a malformed `(defmacro …)` from the outside in, the gate
7055/// fires:
7056/// 1. `defmacro_arity(head, arity)` if the form has fewer than 4
7057/// elements (`(defmacro)`, `(defmacro f)`).
7058/// 2. `defmacro_non_symbol_name(head, &list[1])` if list[1] isn't
7059/// a symbol (`(defmacro 5 () body)`).
7060/// 3. `defmacro_non_list_params(head, &list[2])` if list[2] isn't
7061/// a list (`(defmacro f x body)`, `(defmacro f 5 body)`).
7062/// 4. Inside `parse_params`: `non_symbol_param` and
7063/// `rest_param_missing_name`.
7064///
7065/// After this lift step 3 is structural; every inline
7066/// `LispError::Compile { … }` triple in `macro_def_from` has been
7067/// lifted to a structural variant — the entire `macro_def_from`
7068/// rejection chain is structurally typed for failure modes.
7069///
7070/// `head` is `MacroDefHead` (the typed closed-set enum), having been
7071/// projected through `MacroDefHead::from_keyword` at the top of
7072/// `macro_def_from`. The helper threads `head` straight into the
7073/// variant's typed `head: MacroDefHead` slot — same posture as
7074/// `defmacro_arity` and `defmacro_non_symbol_name` after the
7075/// typed-slot promotion. `got` is `&Sexp` at the call site (a
7076/// borrow into the form's param-list slot); the helper projects
7077/// through `crate::domain::sexp_witness(_)` — the typed joint
7078/// primitive that pairs the offending `Sexp`'s `SexpShape` with its
7079/// `Sexp::Display` projection in ONE owned `SexpWitness` value, so
7080/// authoring tools bind to both the structural shape AND the rendered
7081/// literal across the variant slot. Same posture as `non_symbol_param`,
7082/// `non_symbol_unquote_target`, `splice_outside_list`, and
7083/// `defmacro_non_symbol_name`'s helpers after the typed-witness
7084/// promotion of their `got` slots.
7085///
7086/// Theory anchor: THEORY.md §VI.1 — generation over composition; one
7087/// inline copy still earns a named primitive once the structural
7088/// shape is named (the test count gives this the fail-before/pass-
7089/// after edge, parallel to how `defmacro_arity`,
7090/// `defmacro_non_symbol_name`, `non_symbol_param`, and
7091/// `rest_param_missing_name` were lifted from a single site for
7092/// the structural-completeness payoff). THEORY.md §V.1 — knowable
7093/// platform; the structural variant exposes `head` / `got` as
7094/// first-class fields so authoring tools (LSP, REPL,
7095/// `tatara-check`) bind to the data shape instead of substring-
7096/// parsing the rendered diagnostic. THEORY.md §II.1 invariant 1 —
7097/// typed entry; a defmacro form whose param-list slot isn't a list
7098/// is exactly the failure mode the typed-entry gate exists to
7099/// reject — and the gate must reject DEFINITIONS as readily as it
7100/// rejects CALLS. THEORY.md §II.1 invariant 2 — free middle; the
7101/// param-list gate fires inside `macro_def_from` BEFORE either
7102/// expansion strategy runs, so both `Expander::new()` (bytecode)
7103/// and `Expander::new_substitute_only()` (substitute) reject the
7104/// SAME malformed defmacro at the SAME gate.
7105fn defmacro_non_list_params(head: MacroDefHead, got: &Sexp) -> LispError {
7106 LispError::DefmacroNonListParams {
7107 head,
7108 got: got.witness(),
7109 }
7110}
7111
7112/// Project a `bindings: &HashMap<String, Sexp>` into the `&[&str]` candidate
7113/// set `crate::domain::suggest` wants. Cold path — only allocated when an
7114/// `,name` / `,@name` substitution misses, i.e. when we're already on the
7115/// diagnostic side of the substitute walker.
7116fn bound_names(bindings: &HashMap<String, Sexp>) -> Vec<&str> {
7117 bindings.keys().map(String::as_str).collect()
7118}
7119
7120#[cfg(test)]
7121mod tests {
7122 use super::*;
7123 use crate::reader::read;
7124
7125 fn parse(src: &str) -> Sexp {
7126 read(src).unwrap().into_iter().next().unwrap()
7127 }
7128
7129 #[test]
7130 fn identity_macro() {
7131 let mut e = Expander::new();
7132 let forms = read("(defmacro id (x) `,x) (id 42)").unwrap();
7133 let out = e.expand_program(forms).unwrap();
7134 assert_eq!(out.len(), 1);
7135 assert_eq!(out[0], Sexp::int(42));
7136 }
7137
7138 #[test]
7139 fn wrap_macro_duplicates_arg() {
7140 let mut e = Expander::new();
7141 let forms = read("(defmacro wrap (x) `(list ,x ,x)) (wrap hello)").unwrap();
7142 let out = e.expand_program(forms).unwrap();
7143 assert_eq!(out[0], parse("(list hello hello)"));
7144 }
7145
7146 #[test]
7147 fn rest_param_splices_with_at() {
7148 let mut e = Expander::new();
7149 let forms = read("(defmacro call (f &rest args) `(,f ,@args)) (call foo a b c)").unwrap();
7150 let out = e.expand_program(forms).unwrap();
7151 assert_eq!(out[0], parse("(foo a b c)"));
7152 }
7153
7154 #[test]
7155 fn nested_macro_expansion() {
7156 let mut e = Expander::new();
7157 let forms = read(
7158 "(defmacro twice (x) `(list ,x ,x))
7159 (defmacro quad (x) `(twice ,x))
7160 (quad hey)",
7161 )
7162 .unwrap();
7163 let out = e.expand_program(forms).unwrap();
7164 assert_eq!(out[0], parse("(list hey hey)"));
7165 }
7166
7167 #[test]
7168 fn unbound_unquote_errors() {
7169 let mut e = Expander::new();
7170 let forms = read("(defmacro bad (x) `(list ,y)) (bad 1)").unwrap();
7171 assert!(e.expand_program(forms).is_err());
7172 }
7173
7174 #[test]
7175 fn missing_required_arg_errors() {
7176 let mut e = Expander::new();
7177 let forms = read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap();
7178 assert!(e.expand_program(forms).is_err());
7179 }
7180
7181 #[test]
7182 fn defpoint_template_treated_as_defmacro() {
7183 let mut e = Expander::new();
7184 let forms = read(
7185 "(defpoint-template obs (name) `(defpoint ,name :class (Gate Observability)))
7186 (obs grafana)",
7187 )
7188 .unwrap();
7189 let out = e.expand_program(forms).unwrap();
7190 assert_eq!(
7191 out[0],
7192 parse("(defpoint grafana :class (Gate Observability))")
7193 );
7194 }
7195
7196 #[test]
7197 fn defcheck_treated_as_defmacro() {
7198 let mut e = Expander::new();
7199 let forms = read(
7200 "(defcheck pair (a b) `(do (yaml-parses ,a) (yaml-parses ,b)))
7201 (pair \"x.yaml\" \"y.yaml\")",
7202 )
7203 .unwrap();
7204 let out = e.expand_program(forms).unwrap();
7205 assert_eq!(
7206 out[0],
7207 parse("(do (yaml-parses \"x.yaml\") (yaml-parses \"y.yaml\"))")
7208 );
7209 }
7210
7211 #[test]
7212 fn empty_rest_splices_nothing() {
7213 let mut e = Expander::new();
7214 let forms = read("(defmacro f (x &rest r) `(list ,x ,@r)) (f 1)").unwrap();
7215 let out = e.expand_program(forms).unwrap();
7216 assert_eq!(out[0], parse("(list 1)"));
7217 }
7218
7219 #[test]
7220 fn macro_expanded_inside_list() {
7221 // A macro call nested in a list position also expands.
7222 let mut e = Expander::new();
7223 let forms = read("(defmacro two () `(list 1 2)) (outer (two))").unwrap();
7224 let out = e.expand_program(forms).unwrap();
7225 assert_eq!(out[0], parse("(outer (list 1 2))"));
7226 }
7227
7228 // ── Compiled-template bytecode equivalence + speedup ──────────────
7229
7230 #[test]
7231 fn compiled_template_matches_substitute_path() {
7232 // Same program, two expanders with different strategies — outputs must agree.
7233 let src = "
7234 (defmacro wrap (x) `(list ,x ,x))
7235 (defmacro call (f &rest args) `(,f ,@args))
7236 (defmacro twice (x) `(list ,x ,x))
7237 (defmacro quad (x) `(twice ,x))
7238 (wrap hello)
7239 (call foo a b c)
7240 (quad hey)
7241 (outer (wrap deep))
7242 ";
7243 let forms = read(src).unwrap();
7244 let mut fast = Expander::new();
7245 let mut slow = Expander::new_substitute_only();
7246 let out_fast = fast.expand_program(forms.clone()).unwrap();
7247 let out_slow = slow.expand_program(forms).unwrap();
7248 assert_eq!(out_fast, out_slow);
7249 }
7250
7251 #[test]
7252 fn literal_subtree_compiles_to_single_literal_op() {
7253 // Macro body where only one leaf is a substitution — the rest of the
7254 // template is literal, so the compiler should prune large chunks to
7255 // a single Literal op.
7256 let def = MacroDef {
7257 name: "label".into(),
7258 params: MacroParams {
7259 required: vec!["x".into()],
7260 optional: Vec::new(),
7261 rest: None,
7262 },
7263 body: Sexp::Quasiquote(Box::new(parse(
7264 "(observed (at timestamp) (in region) (value ,x) (tags (one two three)))",
7265 ))),
7266 };
7267 let compiled = compile_template(&def).expect("compile");
7268 // The template is ONE list. After compile:
7269 // BeginList,
7270 // Literal((observed (at timestamp) (in region))), // wait — `observed` is a list too
7271 // ...
7272 // EndList
7273 // Point is: many subtrees should be single Literals. We simply count
7274 // that the op stream is SHORTER than the full Sexp size.
7275 let ops_count = compiled.ops.len();
7276 assert!(
7277 ops_count < 15,
7278 "expected pruned op stream, got {ops_count} ops: {:?}",
7279 compiled.ops
7280 );
7281 }
7282
7283 /// The cache-friendly workload the two tests below share: 10 macros, and
7284 /// calls that hit only 10 distinct (macro, args) pairs, so a memo should
7285 /// serve 99.9% of them.
7286 fn cache_friendly_workload(calls: usize) -> String {
7287 let macros = "
7288 (defmacro m1 (a b) `(list ,a ,b))
7289 (defmacro m2 (x) `(if ,x true false))
7290 (defmacro m3 (a b c) `(list ,a ,b ,c ,a ,b ,c))
7291 (defmacro m4 (f &rest args) `(,f ,@args))
7292 (defmacro m5 (x) `(and ,x (not (not ,x))))
7293 (defmacro m6 (a b) `(or ,a ,b (and ,a ,b)))
7294 (defmacro m7 (x) `(debug (at timestamp) (in region) (value ,x)))
7295 (defmacro m8 (x y) `(cond ((= ,x ,y) equal) (#t not-equal)))
7296 (defmacro m9 (x) `(loop (times 10) (eval ,x)))
7297 (defmacro m10 (f g &rest args) `(,f (,g ,@args)))
7298 ";
7299 let mut call_src = String::with_capacity(calls * 12);
7300 for i in 0..calls {
7301 match i % 10 {
7302 0 => call_src.push_str("(m1 a b)\n"),
7303 1 => call_src.push_str("(m2 true)\n"),
7304 2 => call_src.push_str("(m3 x y z)\n"),
7305 3 => call_src.push_str("(m4 f a b c d e)\n"),
7306 4 => call_src.push_str("(m5 y)\n"),
7307 5 => call_src.push_str("(m6 a b)\n"),
7308 6 => call_src.push_str("(m7 answer)\n"),
7309 7 => call_src.push_str("(m8 p q)\n"),
7310 8 => call_src.push_str("(m9 body)\n"),
7311 _ => call_src.push_str("(m10 f g a b c)\n"),
7312 }
7313 }
7314 let all_src = format!("{macros}\n{call_src}");
7315 all_src
7316 }
7317
7318 /// **All three expansion strategies produce identical output.**
7319 ///
7320 /// The load-bearing invariant — THEORY.md §II.1's free middle. Fully
7321 /// deterministic, so it belongs in the ordinary suite. It used to be
7322 /// welded to the timing assertion below, which meant a benchmark losing a
7323 /// core to a neighbouring test reported *this* as failing, and the
7324 /// agreement property is the one you actually want a red build for.
7325 #[test]
7326 fn expansion_layers_agree_on_output() {
7327 let forms = read(&cache_friendly_workload(10_000)).unwrap();
7328
7329 let out_subst = Expander::new_substitute_only()
7330 .expand_program(forms.clone())
7331 .unwrap();
7332 let out_byte = Expander::new_bytecode_no_cache()
7333 .expand_program(forms.clone())
7334 .unwrap();
7335 let mut byte_cache = Expander::new();
7336 let out_cached = byte_cache.expand_program(forms).unwrap();
7337
7338 assert_eq!(out_subst, out_byte);
7339 assert_eq!(out_subst, out_cached);
7340
7341 // Cache captured the 10 unique (macro, args) pairs (plus some inner
7342 // expansions — macros that expand into calls to other macros).
7343 let cache_size = byte_cache.cache_size();
7344 assert!(
7345 (10..=50).contains(&cache_size),
7346 "expected ~10 unique cache entries, got {cache_size}"
7347 );
7348 }
7349
7350 /// **The expansion strategies, measured.**
7351 ///
7352 /// A wall-clock measurement, and therefore fragile in a way the agreement
7353 /// test above is not. Two changes keep it honest rather than merely quiet:
7354 ///
7355 /// - **Best-of-N, not a single sample.** The minimum over several runs is
7356 /// the least contaminated statistic when 1900 sibling tests compete for
7357 /// the same cores; a single sample measures contention as much as code.
7358 /// - **Margins that match measurement.** The previous version compared
7359 /// with a bare `<` while its own comment claimed "a 1.5× threshold so
7360 /// the test is stable across hosts". The threshold was never in the
7361 /// code, and the bare comparison flaked in the full suite.
7362 ///
7363 /// # A finding, recorded so it is not re-derived
7364 ///
7365 /// Putting a real threshold on it surfaced something the old assertion hid.
7366 /// On a workload with a **99.9% cache hit rate**, the memo beats
7367 /// bytecode-no-cache by only **~1.17× in release and ~1.04× in debug** —
7368 /// not the "real, visible speedup" the original docstring asserted. The
7369 /// bare `<` had been reporting a 4% difference on one noisy sample as a
7370 /// win.
7371 ///
7372 /// The reading: expansion was never the expensive part of this path.
7373 /// Reading, walking and allocating the output tree dominate, so removing
7374 /// essentially *all* expansion work moves the total by a fifth. That is
7375 /// still worth having and it is not what the memo was sold as. The bytecode
7376 /// strategy is where the real gain is (~1.4× over substitution), and it is
7377 /// there with or without the cache.
7378 ///
7379 /// So the margins differ per axis, deliberately: a real one against
7380 /// substitution, and a non-regression bound against bytecode, because
7381 /// asserting 1.5× there would assert something untrue.
7382 #[test]
7383 fn the_expansion_cache_beats_both_uncached_strategies() {
7384 use std::time::{Duration, Instant};
7385
7386 /// Against substitution the gain is structural — bytecode plus a memo
7387 /// against a naive tree walk. Measured at 1.44–1.54×; gated well below
7388 /// that, since the purpose is to catch the fast path being *disabled*,
7389 /// not to track a ratio.
7390 const MARGIN_VS_SUBSTITUTE: f64 = 1.25;
7391 /// Against bytecode-no-cache the memo is a modest win, and "modest" is
7392 /// the problem: the two paths differ by a few ms on a 10k workload, so
7393 /// on shared CI the comparison measures the runner, not the cache.
7394 /// Measured 1.03× locally (debug) and 0.90× on a GitHub runner in the
7395 /// same commit — a 13-point spread with no code change between them.
7396 ///
7397 /// The bound is therefore stated as what this test can actually prove:
7398 /// the memo must not be *catastrophically* worse, which catches it
7399 /// being wired up wrong (a cache that never hits pays lookup + insert
7400 /// on every call and lands far below this). Holding a tighter ratio
7401 /// would need a benchmark harness with a quiet machine — nextest on a
7402 /// shared runner is not that, and a gate that fails on hardware noise
7403 /// teaches people to re-run rather than to read it.
7404 const MAX_OVERHEAD_VS_BYTECODE: f64 = 1.30;
7405 const SAMPLES: usize = 5;
7406
7407 let forms = read(&cache_friendly_workload(10_000)).unwrap();
7408
7409 let best = |mut make: Box<dyn FnMut() -> Expander>| -> Duration {
7410 (0..SAMPLES)
7411 .map(|_| {
7412 let mut e = make();
7413 let f = forms.clone();
7414 let t = Instant::now();
7415 let out = e.expand_program(f).unwrap();
7416 let d = t.elapsed();
7417 std::hint::black_box(out);
7418 d
7419 })
7420 .min()
7421 .expect("SAMPLES > 0")
7422 };
7423
7424 let t_subst = best(Box::new(Expander::new_substitute_only));
7425 let t_byte = best(Box::new(Expander::new_bytecode_no_cache));
7426 let t_cached = best(Box::new(Expander::new));
7427
7428 let vs_subst = t_subst.as_secs_f64() / t_cached.as_secs_f64();
7429 let vs_byte = t_byte.as_secs_f64() / t_cached.as_secs_f64();
7430 eprintln!(
7431 "\n=== macroexpand: 10k calls × 10 unique (macro, args) pairs, \
7432 best of {SAMPLES} ===\n\
7433 substitute only : {t_subst:?}\n\
7434 bytecode no cache : {t_byte:?}\n\
7435 bytecode + cache : {t_cached:?}\n\
7436 speedup : {vs_subst:.2}× vs substitute, {vs_byte:.2}× vs bytecode\n"
7437 );
7438
7439 assert!(
7440 vs_subst >= MARGIN_VS_SUBSTITUTE,
7441 "the bytecode+cache path should beat substitution by \
7442 {MARGIN_VS_SUBSTITUTE}×, got {vs_subst:.2}× \
7443 ({t_cached:?} vs {t_subst:?})"
7444 );
7445 assert!(
7446 vs_byte >= 1.0 / MAX_OVERHEAD_VS_BYTECODE,
7447 "the memo must not cost more than it saves: {vs_byte:.2}× vs \
7448 bytecode-no-cache ({t_cached:?} vs {t_byte:?}). A ratio below 1 \
7449 means maintaining the cache is now pure overhead on this workload."
7450 );
7451 }
7452
7453 #[test]
7454 fn cache_respects_arg_changes() {
7455 // Cache must not return stale results when args differ.
7456 let src = "
7457 (defmacro wrap (x) `(list ,x ,x))
7458 (wrap a)
7459 (wrap b)
7460 (wrap a) ;; same as first — cached hit
7461 ";
7462 let mut e = Expander::new();
7463 let out = e.expand_program(read(src).unwrap()).unwrap();
7464 assert_eq!(out.len(), 3);
7465 assert_eq!(out[0], parse("(list a a)"));
7466 assert_eq!(out[1], parse("(list b b)"));
7467 assert_eq!(out[2], parse("(list a a)"));
7468 // Two distinct args → 2 cache entries.
7469 assert_eq!(e.cache_size(), 2);
7470 }
7471
7472 #[test]
7473 fn clear_cache_empties_memo() {
7474 let mut e = Expander::new();
7475 let out = e
7476 .expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
7477 .unwrap();
7478 assert_eq!(out.len(), 2);
7479 assert_eq!(e.cache_size(), 2);
7480 e.clear_cache();
7481 assert_eq!(e.cache_size(), 0);
7482 }
7483
7484 // ── Unbound template-var: structural variant + did-you-mean hint ──
7485
7486 /// Helper for the unbound-template-var tests — pins the variant shape
7487 /// and carries any error context up to the assert site for legibility.
7488 fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
7489 match err {
7490 LispError::UnboundTemplateVar { prefix, name, hint } => {
7491 (*prefix, name.as_str(), hint.as_deref())
7492 }
7493 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7494 }
7495 }
7496
7497 #[test]
7498 fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
7499 // `,xs` against macro params `[x]` — distance 1, bound 1 — hints `,x`.
7500 // Path: compile_node Unquote (the bytecode-template compile, default
7501 // expander).
7502 let mut e = Expander::new();
7503 let err = e
7504 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7505 .expect_err("unbound template var must error");
7506 let (prefix, name, hint) = unbound_var(&err);
7507 assert_eq!(prefix, UnquoteForm::Unquote);
7508 assert_eq!(name, "xs");
7509 assert_eq!(hint, Some("x"));
7510 }
7511
7512 #[test]
7513 fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
7514 // `,@argz` against macro params `[args]` — distance 1, bound 2 —
7515 // hints `,@args`. Path: compile_node UnquoteSplice.
7516 let mut e = Expander::new();
7517 let err = e
7518 .expand_program(
7519 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7520 )
7521 .expect_err("unbound splice must error");
7522 let (prefix, name, hint) = unbound_var(&err);
7523 assert_eq!(prefix, UnquoteForm::Splice);
7524 assert_eq!(name, "argz");
7525 assert_eq!(hint, Some("args"));
7526 }
7527
7528 #[test]
7529 fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
7530 // Same shape but routed through the substitute-only expander — proves
7531 // the substitute path emits the same variant as the bytecode path.
7532 let mut e = Expander::new_substitute_only();
7533 let err = e
7534 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7535 .expect_err("substitute unbound must error");
7536 let (prefix, name, hint) = unbound_var(&err);
7537 assert_eq!(prefix, UnquoteForm::Unquote);
7538 assert_eq!(name, "xs");
7539 assert_eq!(hint, Some("x"));
7540 }
7541
7542 #[test]
7543 fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
7544 // The substitute path's UnquoteSplice branch fires for splices that
7545 // appear inside a list during the recursive walk. `,@argz` against
7546 // `[args]` hints `,@args`.
7547 let mut e = Expander::new_substitute_only();
7548 let err = e
7549 .expand_program(
7550 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7551 )
7552 .expect_err("substitute splice unbound must error");
7553 let (prefix, name, hint) = unbound_var(&err);
7554 assert_eq!(prefix, UnquoteForm::Splice);
7555 assert_eq!(name, "argz");
7556 assert_eq!(hint, Some("args"));
7557 }
7558
7559 #[test]
7560 fn unbound_template_var_omits_hint_when_no_close_match() {
7561 // `,wholly-unrelated` against `[x]` — far past the bound, so no
7562 // hint. Negative control: a wrong hint is worse than no hint, so
7563 // the slot must stay empty when the substrate isn't confident.
7564 let mut e = Expander::new();
7565 let err = e
7566 .expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
7567 .expect_err("unrelated unbound must error");
7568 let (prefix, name, hint) = unbound_var(&err);
7569 assert_eq!(prefix, UnquoteForm::Unquote);
7570 assert_eq!(name, "wholly-unrelated");
7571 assert_eq!(hint, None);
7572 }
7573
7574 #[test]
7575 fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
7576 // End-to-end through the Display impl — pins the rendered diagnostic
7577 // a downstream tool sees today (REPL, tatara-check). Hint stays
7578 // additive: the legacy `"unbound"` substring still appears, so any
7579 // assertion that pattern-matches on it keeps passing.
7580 let mut e = Expander::new();
7581 let err = e
7582 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7583 .expect_err("unbound must error");
7584 let msg = format!("{err}");
7585 assert!(
7586 msg.contains("did you mean ,x?"),
7587 "expected hint suffix in message, got: {msg}"
7588 );
7589 assert!(
7590 msg.contains("unbound"),
7591 "expected legacy `unbound` substring in message, got: {msg}"
7592 );
7593 assert!(
7594 msg.contains(",xs"),
7595 "expected the offending form in message, got: {msg}"
7596 );
7597 }
7598
7599 #[test]
7600 fn unbound_template_var_position_is_none_today() {
7601 // Negative control for the future-spans move: until `Sexp` carries
7602 // source positions, `position()` returns `None` for this variant.
7603 let mut e = Expander::new();
7604 let err = e
7605 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7606 .expect_err("unbound must error");
7607 assert_eq!(err.position(), None);
7608 }
7609
7610 // ── Non-symbol unquote target: structural variant ─────────────────
7611
7612 /// Helper for the non-symbol-unquote-target tests — pins the variant
7613 /// shape and carries any error context up to the assert site for
7614 /// legibility. Sibling of `unbound_var` and `splice_outside_list_got`;
7615 /// returns the `display` projection of the typed `SexpWitness` so the
7616 /// existing call sites stay byte-for-byte comparable to the legacy
7617 /// `got: String` shape.
7618 fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
7619 match err {
7620 LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
7621 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7622 }
7623 }
7624
7625 #[test]
7626 fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
7627 // `,(list 1 2)` — the inner is a list, not a symbol. Path:
7628 // compile_node Unquote (the bytecode-template compile, default
7629 // expander). Pins variant identity AND prefix AND the offending
7630 // literal so a regression that re-inlines the legacy
7631 // `LispError::Compile` shape fails-loudly here.
7632 let mut e = Expander::new();
7633 let err = e
7634 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7635 .expect_err("non-symbol unquote target must error");
7636 let (prefix, got) = non_symbol_target(&err);
7637 assert_eq!(prefix, UnquoteForm::Unquote);
7638 assert_eq!(got, "(list 1 2)");
7639 }
7640
7641 #[test]
7642 fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
7643 // `,@5` — the inner is an int atom, not a symbol. Path:
7644 // compile_node UnquoteSplice. The integer literal round-trips
7645 // through the variant's `got` slot via `Sexp::Display`.
7646 let mut e = Expander::new();
7647 let err = e
7648 .expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
7649 .expect_err("non-symbol splice target must error");
7650 let (prefix, got) = non_symbol_target(&err);
7651 assert_eq!(prefix, UnquoteForm::Splice);
7652 assert_eq!(got, "5");
7653 }
7654
7655 #[test]
7656 fn non_symbol_unquote_in_substitute_emits_structural_variant() {
7657 // Same shape as the bytecode path but routed through the
7658 // substitute-only expander — proves the substitute path emits the
7659 // same variant as the compile_node path. Pins that the lift is
7660 // path-uniform.
7661 let mut e = Expander::new_substitute_only();
7662 let err = e
7663 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7664 .expect_err("substitute non-symbol target must error");
7665 let (prefix, got) = non_symbol_target(&err);
7666 assert_eq!(prefix, UnquoteForm::Unquote);
7667 assert_eq!(got, "(list 1 2)");
7668 }
7669
7670 #[test]
7671 fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
7672 // The substitute path's UnquoteSplice-inside-list branch fires for
7673 // splices that appear inside a list during the recursive walk.
7674 // `,@(list 1 2)` inside the body — the inner is a literal list, not
7675 // a symbol — emits the same variant as the compile_node path.
7676 let mut e = Expander::new_substitute_only();
7677 let err = e
7678 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7679 .expect_err("substitute non-symbol splice must error");
7680 let (prefix, got) = non_symbol_target(&err);
7681 assert_eq!(prefix, UnquoteForm::Splice);
7682 assert_eq!(got, "(list 1 2)");
7683 }
7684
7685 #[test]
7686 fn non_symbol_unquote_target_position_is_none_today() {
7687 // Negative control for the future-spans move: until `Sexp` carries
7688 // source positions, `position()` returns `None` for this variant.
7689 // A future run that gives `Sexp` source spans adds `pos:
7690 // Option<usize>` to ONE place; this test gives that change a
7691 // deliberate fail-before/pass-after delta.
7692 let mut e = Expander::new();
7693 let err = e
7694 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7695 .expect_err("non-symbol target must error");
7696 assert_eq!(err.position(), None);
7697 }
7698
7699 // ── unquote_target_symbol: typed gate-1 primitive for ,X / ,@X ──────
7700 //
7701 // The `unquote_target_symbol(inner, form)?` primitive lifts the
7702 // inline `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(
7703 // form, inner))?` pattern that previously appeared at four call
7704 // sites (`compile_node` Unquote/UnquoteSplice + `substitute` Unquote
7705 // + `substitute` list-inner UnquoteSplice) behind ONE named
7706 // primitive. The tests below pin: (a) the Ok-arm borrows the
7707 // symbol name from `inner` for both UnquoteForm variants; (b) the
7708 // Err-arm routes through `non_symbol_unquote_target` and emits the
7709 // structural `LispError::NonSymbolUnquoteTarget` variant carrying
7710 // the typed `SexpWitness` (joint shape + display identity) for the
7711 // closed set of reachable non-symbol shapes (int / keyword / list /
7712 // nil); (c) the helper is path-uniform — the same Ok / Err
7713 // contracts hold regardless of which call site invokes it. A
7714 // regression that re-inlines the gate-1 projection at any of the
7715 // four call sites can no longer drift independent of the others —
7716 // the helper IS the gate.
7717
7718 #[test]
7719 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
7720 // Positive control for the Ok-arm: `inner = Sexp::Symbol("xs")`
7721 // under `UnquoteForm::Unquote` projects through `as_symbol()`
7722 // to the borrowed `&str`. The returned slice's lifetime is
7723 // tied to `inner` so the caller can feed it directly into
7724 // `params.iter().position(...)` (`compile_node`) or
7725 // `bindings.get(...)` (`substitute`) without an intermediate
7726 // allocation. Fail-before/pass-after: this assert is meaningless
7727 // pre-lift because the helper does not exist; post-lift it
7728 // pins the typed gate-1 contract at the named primitive.
7729 let inner = Sexp::symbol("xs");
7730 let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7731 .expect("symbol inner must project to Ok");
7732 assert_eq!(name, "xs");
7733 }
7734
7735 #[test]
7736 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
7737 // Sibling positive control: `UnquoteForm::Splice` shares the
7738 // gate-1 contract with `Unquote`. The helper is path-uniform
7739 // across both syntactic markers — a regression that bifurcates
7740 // the two arms (e.g., accepting non-symbols for `,@X` but not
7741 // `,X`) fails-loudly here. Pins that the closed-set
7742 // `UnquoteForm` enum's two variants share ONE projection
7743 // posture across the gate-1 boundary.
7744 let inner = Sexp::symbol("rest");
7745 let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
7746 .expect("symbol inner must project to Ok under Splice");
7747 assert_eq!(name, "rest");
7748 }
7749
7750 #[test]
7751 fn unquote_target_symbol_rejects_int_inner_under_unquote() {
7752 // Negative control for the Err-arm: `inner = Sexp::Int(5)` is
7753 // NOT a symbol — the gate-1 projection fires and routes through
7754 // `non_symbol_unquote_target` to the structural
7755 // `LispError::NonSymbolUnquoteTarget` variant. Pin the variant
7756 // identity AND the typed `SexpWitness` joint identity (shape +
7757 // display literal): a regression that drops the witness shape
7758 // or display fails-loudly here.
7759 let inner = Sexp::int(5);
7760 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7761 .expect_err("int inner must error at gate-1");
7762 match err {
7763 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7764 assert_eq!(prefix, UnquoteForm::Unquote);
7765 assert_eq!(got.shape, crate::error::SexpShape::Int);
7766 assert_eq!(got.display, "5");
7767 }
7768 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7769 }
7770 }
7771
7772 #[test]
7773 fn unquote_target_symbol_rejects_list_inner_under_splice() {
7774 // Sibling negative control: `inner = (list 1 2)` is a list, not
7775 // a symbol — the gate-1 projection fires AND routes through
7776 // `non_symbol_unquote_target(UnquoteForm::Splice, inner)`. Pins
7777 // both the variant identity AND the typed witness's joint
7778 // shape (`SexpShape::List`) + display (`"(list 1 2)"`) so a
7779 // future shape drift fails-loudly. Sibling of the Int / Unquote
7780 // pin: closes the gate-1 contract across the closed-set
7781 // product of {Int, List, Keyword, …} × {Unquote, Splice}.
7782 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
7783 let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
7784 .expect_err("list inner must error at gate-1");
7785 match err {
7786 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7787 assert_eq!(prefix, UnquoteForm::Splice);
7788 assert_eq!(got.shape, crate::error::SexpShape::List);
7789 assert_eq!(got.display, "(list 1 2)");
7790 }
7791 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7792 }
7793 }
7794
7795 #[test]
7796 fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
7797 // Pin a third reachable non-symbol shape: `Sexp::Keyword(":foo")`.
7798 // The gate-1 projection rejects keywords AS WELL as ints and
7799 // lists — closes the closed-set of "non-symbol shapes the gate
7800 // rejects" across one more reachable variant. The typed witness
7801 // carries `SexpShape::Keyword` + display `:foo` jointly so
7802 // authoring tools (REPL, LSP) bind on the structural shape
7803 // directly.
7804 let inner = Sexp::keyword("foo");
7805 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7806 .expect_err("keyword inner must error at gate-1");
7807 match err {
7808 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7809 assert_eq!(prefix, UnquoteForm::Unquote);
7810 assert_eq!(got.shape, crate::error::SexpShape::Keyword);
7811 assert_eq!(got.display, ":foo");
7812 }
7813 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7814 }
7815 }
7816
7817 #[test]
7818 fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
7819 // Path-uniformity pin: end-to-end through ALL FOUR call sites
7820 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7821 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7822 // every non-symbol unquote target now routes through the SAME
7823 // `unquote_target_symbol(inner, form)?` helper. The four
7824 // end-to-end expansions below all reject with the SAME variant
7825 // (`NonSymbolUnquoteTarget`) — pins that the lift preserves the
7826 // path-uniform rejection contract `non_symbol_unquote_target`'s
7827 // prior lift established (and that drove the bytecode-vs-
7828 // substitute reunification in 0e9c… and successors). A
7829 // regression that re-inlines the gate-1 projection at one of
7830 // the four sites can drift the four call sites independent of
7831 // each other — this test would catch that drift.
7832 let cases: &[(&str, UnquoteForm)] = &[
7833 // compile_node Unquote (bytecode-path)
7834 ("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
7835 // compile_node UnquoteSplice (bytecode-path)
7836 ("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
7837 ];
7838 for (src, expected_form) in cases {
7839 let mut e = Expander::new();
7840 let err = e
7841 .expand_program(read(src).unwrap())
7842 .expect_err("non-symbol unquote target must error end-to-end");
7843 match err {
7844 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
7845 assert_eq!(prefix, *expected_form, "for src: {src}");
7846 }
7847 other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
7848 }
7849 }
7850 // substitute Unquote (substitute-only path) — sibling pin to
7851 // `non_symbol_unquote_in_substitute_emits_structural_variant`.
7852 let mut e_subst = Expander::new_substitute_only();
7853 let err = e_subst
7854 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7855 .expect_err("substitute Unquote must error end-to-end");
7856 assert!(
7857 matches!(
7858 err,
7859 LispError::NonSymbolUnquoteTarget {
7860 prefix: UnquoteForm::Unquote,
7861 ..
7862 }
7863 ),
7864 "expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
7865 );
7866 // substitute list-inner UnquoteSplice (substitute-only path) —
7867 // sibling pin to
7868 // `non_symbol_unquote_splice_inside_list_in_substitute_emits_…`.
7869 let mut e_subst2 = Expander::new_substitute_only();
7870 let err = e_subst2
7871 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7872 .expect_err("substitute UnquoteSplice-in-list must error end-to-end");
7873 assert!(
7874 matches!(
7875 err,
7876 LispError::NonSymbolUnquoteTarget {
7877 prefix: UnquoteForm::Splice,
7878 ..
7879 }
7880 ),
7881 "expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
7882 );
7883 }
7884
7885 // ── Gate-2 (must-be-bound-in-scope) typed primitives ──────────────
7886 // Pins the contract of the two gate-2 helpers — `resolve_param_index`
7887 // (bytecode-template compile path) and `resolve_binding`
7888 // (substitute path) — that the four inline `<lookup>.ok_or_else(||
7889 // unbound_template_var(FORM, name, candidates))` projections at
7890 // `compile_node` Unquote/UnquoteSplice AND `substitute` Unquote/
7891 // UnquoteSplice-inside-list collapse behind. Tests pin: (a) Ok-arm
7892 // projection under both `UnquoteForm` variants — the helper returns
7893 // the resolved `usize` (compile path) or `&Sexp` (substitute path)
7894 // for in-scope names; (b) Err-arm projection routes through
7895 // `unbound_template_var` to the typed `LispError::UnboundTemplateVar`
7896 // variant with the correct `prefix` AND the suggest-driven `hint`;
7897 // (c) the helpers are path-uniform — both compile-path arms share
7898 // ONE `resolve_param_index`; both substitute-path arms share ONE
7899 // `resolve_binding`. A regression that re-inlines the gate-2
7900 // projection at any of the four call sites can no longer drift
7901 // independent of the others — the two helpers ARE the gate.
7902
7903 #[test]
7904 fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
7905 // Positive control for the Ok-arm: `name = "x"` against
7906 // `params = ["a", "x", "rest"]` projects through
7907 // `params.iter().position(|p| *p == name)` to `Some(1)`, which
7908 // the helper unwraps to `Ok(1)`. The returned index feeds
7909 // directly into `TemplateOp::Subst(idx)` at the compile site.
7910 let params = ["a", "x", "rest"];
7911 let idx = resolve_param_index("x", ¶ms, UnquoteForm::Unquote)
7912 .expect("bound name must project to Ok at gate-2");
7913 assert_eq!(idx, 1);
7914 }
7915
7916 #[test]
7917 fn resolve_param_index_returns_position_for_bound_name_under_splice() {
7918 // Sibling positive control: `UnquoteForm::Splice` shares the
7919 // gate-2 contract with `Unquote`. The helper is path-uniform
7920 // across both syntactic markers on the compile path — a
7921 // regression that bifurcates the two arms fails-loudly here.
7922 let params = ["a", "x", "rest"];
7923 let idx = resolve_param_index("rest", ¶ms, UnquoteForm::Splice)
7924 .expect("bound name must project to Ok at gate-2 under Splice");
7925 assert_eq!(idx, 2);
7926 }
7927
7928 #[test]
7929 fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
7930 // Negative control for the Err-arm: `name = "xs"` against
7931 // `params = ["x"]` — distance 1, bound 1 — routes through
7932 // `unbound_template_var` to the structural
7933 // `LispError::UnboundTemplateVar` variant with `hint = Some("x")`.
7934 // Pin the variant identity AND the prefix AND the suggest-driven
7935 // hint: a regression that drops the suggestion fails-loudly here.
7936 let params = ["x"];
7937 let err = resolve_param_index("xs", ¶ms, UnquoteForm::Unquote)
7938 .expect_err("unbound name must error at gate-2");
7939 match err {
7940 LispError::UnboundTemplateVar { prefix, name, hint } => {
7941 assert_eq!(prefix, UnquoteForm::Unquote);
7942 assert_eq!(name, "xs");
7943 assert_eq!(hint.as_deref(), Some("x"));
7944 }
7945 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7946 }
7947 }
7948
7949 #[test]
7950 fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
7951 // Sibling negative control: `name = "wholly-unrelated"` against
7952 // `params = ["x"]` — past the bounded edit distance, so no hint.
7953 // Pin that the suggest-driven hint stays empty under Splice when
7954 // the substrate isn't confident — a wrong hint is worse than no
7955 // hint. Closes the closed-set product of {hint, no-hint} ×
7956 // {Unquote, Splice} on the compile-path gate-2.
7957 let params = ["x"];
7958 let err = resolve_param_index("wholly-unrelated", ¶ms, UnquoteForm::Splice)
7959 .expect_err("unrelated unbound must error at gate-2");
7960 match err {
7961 LispError::UnboundTemplateVar { prefix, name, hint } => {
7962 assert_eq!(prefix, UnquoteForm::Splice);
7963 assert_eq!(name, "wholly-unrelated");
7964 assert_eq!(hint, None);
7965 }
7966 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7967 }
7968 }
7969
7970 #[test]
7971 fn resolve_binding_returns_value_for_bound_name_under_unquote() {
7972 // Positive control for the substitute-path Ok-arm: `name = "x"`
7973 // against a bindings map `{x: 42, y: "hi"}` projects through
7974 // `bindings.get(name)` to `Some(&Sexp::Int(42))`, which the
7975 // helper unwraps to `Ok(&Sexp::Int(42))`. The returned
7976 // `&Sexp` borrows from the bindings map — the top-level
7977 // `Sexp::Unquote(_)` substitute caller adds a single
7978 // `.cloned()` to satisfy its owned-`Sexp` return obligation.
7979 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7980 bindings.insert("x".to_string(), Sexp::int(42));
7981 bindings.insert("y".to_string(), Sexp::string("hi"));
7982 let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
7983 .expect("bound name must project to Ok at gate-2 (substitute)");
7984 assert_eq!(val, &Sexp::int(42));
7985 }
7986
7987 #[test]
7988 fn resolve_binding_returns_value_for_bound_name_under_splice() {
7989 // Sibling positive control: `UnquoteForm::Splice` shares the
7990 // gate-2 contract with `Unquote` on the substitute path too.
7991 // The bound value is a `Sexp::List` because the splice arm's
7992 // caller match expression expects `Sexp::List(items)` — but
7993 // the helper itself doesn't inspect the value's shape; it
7994 // just hands back the borrow. A regression that gate-checks
7995 // the value's shape inside `resolve_binding` (instead of at
7996 // the caller match arm) fails-loudly here.
7997 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7998 bindings.insert(
7999 "args".to_string(),
8000 Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
8001 );
8002 let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
8003 .expect("bound name must project to Ok at gate-2 under Splice");
8004 assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8005 }
8006
8007 #[test]
8008 fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
8009 // Negative control for the substitute-path Err-arm: `name =
8010 // "xs"` against bindings `{x: 1}` — distance 1, bound 1 —
8011 // routes through `unbound_template_var` to the structural
8012 // `LispError::UnboundTemplateVar` variant with `hint =
8013 // Some("x")`. The candidate set is drawn from
8014 // `bound_names(bindings)` — the live bindings' keys, never a
8015 // stale snapshot.
8016 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8017 bindings.insert("x".to_string(), Sexp::int(1));
8018 let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
8019 .expect_err("unbound name must error at gate-2 (substitute)");
8020 match err {
8021 LispError::UnboundTemplateVar { prefix, name, hint } => {
8022 assert_eq!(prefix, UnquoteForm::Unquote);
8023 assert_eq!(name, "xs");
8024 assert_eq!(hint.as_deref(), Some("x"));
8025 }
8026 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8027 }
8028 }
8029
8030 #[test]
8031 fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
8032 // Sibling negative control on the substitute path: past-bound
8033 // distance → no hint. Closes the closed-set product of
8034 // {hint, no-hint} × {Unquote, Splice} on the substitute-path
8035 // gate-2.
8036 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8037 bindings.insert("args".to_string(), Sexp::Nil);
8038 let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
8039 .expect_err("unrelated unbound must error at gate-2");
8040 match err {
8041 LispError::UnboundTemplateVar { prefix, name, hint } => {
8042 assert_eq!(prefix, UnquoteForm::Splice);
8043 assert_eq!(name, "wholly-unrelated");
8044 assert_eq!(hint, None);
8045 }
8046 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8047 }
8048 }
8049
8050 #[test]
8051 fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
8052 // Path-uniformity pin: end-to-end through ALL FOUR call sites
8053 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
8054 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
8055 // every unbound-template-var rejection now routes through one
8056 // of the TWO `resolve_param_index` / `resolve_binding` helpers
8057 // — `Expander::new()` runs the compile path, so its two arms
8058 // share `resolve_param_index`; `Expander::new_substitute_only()`
8059 // runs the substitute path, so its two arms share
8060 // `resolve_binding`. The four end-to-end expansions below all
8061 // reject with the SAME variant (`UnboundTemplateVar`) with the
8062 // expected `prefix` — pins that the lift preserves the
8063 // path-uniform rejection contract `unbound_template_var`'s
8064 // prior naming established. A regression that re-inlines the
8065 // gate-2 projection at one of the four sites can drift the
8066 // four call sites independent of each other — this test would
8067 // catch that drift.
8068 struct Case {
8069 src: &'static str,
8070 expander: fn() -> Expander,
8071 expected_form: UnquoteForm,
8072 }
8073 let cases: &[Case] = &[
8074 // compile_node Unquote (bytecode path) — uses resolve_param_index
8075 Case {
8076 src: "(defmacro w (x) `(list ,xs)) (w 1)",
8077 expander: Expander::new,
8078 expected_form: UnquoteForm::Unquote,
8079 },
8080 // compile_node UnquoteSplice (bytecode path) — uses resolve_param_index
8081 Case {
8082 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8083 expander: Expander::new,
8084 expected_form: UnquoteForm::Splice,
8085 },
8086 // substitute Unquote (substitute-only path) — uses resolve_binding
8087 Case {
8088 src: "(defmacro w (x) `(list ,xs)) (w 1)",
8089 expander: Expander::new_substitute_only,
8090 expected_form: UnquoteForm::Unquote,
8091 },
8092 // substitute UnquoteSplice-in-list (substitute-only path) — uses resolve_binding
8093 Case {
8094 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8095 expander: Expander::new_substitute_only,
8096 expected_form: UnquoteForm::Splice,
8097 },
8098 ];
8099 for case in cases {
8100 let mut e = (case.expander)();
8101 let err = e
8102 .expand_program(read(case.src).unwrap())
8103 .expect_err("unbound template var must error end-to-end");
8104 match err {
8105 LispError::UnboundTemplateVar { prefix, .. } => {
8106 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8107 }
8108 other => panic!(
8109 "expected UnboundTemplateVar for {}, got: {other:?}",
8110 case.src
8111 ),
8112 }
8113 }
8114 }
8115
8116 // ── resolve_unquote_in_params / _in_bindings: gate-1+gate-2 composition ─
8117
8118 #[test]
8119 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
8120 // Ok-arm composition under `UnquoteForm::Unquote`: gate-1 projects
8121 // the symbol-inner to "x"; gate-2 looks "x" up in `params` and
8122 // returns its index. The combined helper returns the gate-2
8123 // result directly — pins that gate-1's Ok-arm threads into
8124 // gate-2's input without intermediate state.
8125 let inner = Sexp::symbol("x");
8126 let params = ["x", "y"];
8127 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8128 .expect("symbol-inner bound at index 0 must resolve");
8129 assert_eq!(idx, 0);
8130 }
8131
8132 #[test]
8133 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
8134 // Sibling Ok-arm under `UnquoteForm::Splice`: pins that the
8135 // marker doesn't change the projection — only the rejection
8136 // path's `prefix` slot.
8137 let inner = Sexp::symbol("args");
8138 let params = ["f", "args"];
8139 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8140 .expect("symbol-inner bound at index 1 must resolve");
8141 assert_eq!(idx, 1);
8142 }
8143
8144 #[test]
8145 fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
8146 // Err-arm at gate-1 (must-be-a-symbol): the inner is a list, not
8147 // a symbol, so gate-1 rejects via `non_symbol_unquote_target`
8148 // BEFORE gate-2's param lookup runs. Pins that the composition's
8149 // sequencing is gate-1-then-gate-2: a regression that runs
8150 // gate-2 first would attempt to look up "(list 1 2)" as a param
8151 // name and emit `LispError::UnboundTemplateVar { name: "(list 1
8152 // 2)", ... }` — a confusing diagnostic that would substring-grep
8153 // "unbound" instead of "expected symbol". This test pins the
8154 // structural floor: a non-symbol inner is rejected as a non-
8155 // symbol, never re-treated as a bound-name lookup key.
8156 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
8157 let params = ["x"];
8158 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8159 .expect_err("non-symbol inner must reject at gate-1");
8160 match err {
8161 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8162 assert_eq!(prefix, UnquoteForm::Unquote);
8163 assert_eq!(got.display, "(list 1 2)");
8164 }
8165 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8166 }
8167 }
8168
8169 #[test]
8170 fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
8171 // Err-arm at gate-2 (must-be-bound-in-scope): the inner IS a
8172 // symbol (gate-1 passes) but the name isn't in `params`, so
8173 // gate-2 rejects via `unbound_template_var`. Pins that gate-1
8174 // forwards its Ok-arm `&str` borrow into gate-2's lookup, and
8175 // that the marker `prefix` is threaded into gate-2's rejection
8176 // unchanged (a regression that hard-codes `UnquoteForm::Unquote`
8177 // at the composition boundary would fail this Splice-marker
8178 // assertion).
8179 let inner = Sexp::symbol("missing");
8180 let params = ["x", "y"];
8181 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8182 .expect_err("unbound symbol must reject at gate-2");
8183 match err {
8184 LispError::UnboundTemplateVar { prefix, name, .. } => {
8185 assert_eq!(prefix, UnquoteForm::Splice);
8186 assert_eq!(name, "missing");
8187 }
8188 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8189 }
8190 }
8191
8192 #[test]
8193 fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
8194 // Substitute-path sibling of `resolve_unquote_in_params_returns_
8195 // index_for_symbol_inner_under_unquote`. The combined helper
8196 // composes gate-1 (project inner to symbol) THEN gate-2 (look
8197 // up name in bindings). The returned `&Sexp` borrows from
8198 // `bindings` so the list-inner caller threads it straight into
8199 // the splice-expansion match without an intermediate allocation.
8200 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8201 bindings.insert("v".to_string(), Sexp::int(42));
8202 let inner = Sexp::symbol("v");
8203 let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8204 .expect("symbol-inner bound to 42 must resolve");
8205 assert_eq!(val, &Sexp::int(42));
8206 }
8207
8208 #[test]
8209 fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
8210 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8211 // non_symbol_inner_at_gate_1`. Pins the gate-1-then-gate-2
8212 // sequencing on the substitute path: a non-symbol inner is
8213 // rejected as a non-symbol BEFORE the bindings map is consulted.
8214 let bindings: HashMap<String, Sexp> = HashMap::new();
8215 let inner = Sexp::int(5);
8216 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
8217 .expect_err("non-symbol inner must reject at gate-1");
8218 match err {
8219 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8220 assert_eq!(prefix, UnquoteForm::Splice);
8221 assert_eq!(got.display, "5");
8222 }
8223 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8224 }
8225 }
8226
8227 #[test]
8228 fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
8229 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8230 // unbound_symbol_at_gate_2`. Pins the gate-2 rejection on the
8231 // substitute path with the marker threaded into the rejection's
8232 // `prefix` slot.
8233 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8234 bindings.insert("known".to_string(), Sexp::Nil);
8235 let inner = Sexp::symbol("missing");
8236 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8237 .expect_err("unbound symbol must reject at gate-2");
8238 match err {
8239 LispError::UnboundTemplateVar { prefix, name, .. } => {
8240 assert_eq!(prefix, UnquoteForm::Unquote);
8241 assert_eq!(name, "missing");
8242 }
8243 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8244 }
8245 }
8246
8247 #[test]
8248 fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
8249 // End-to-end pin: all FOUR call sites of the gate-1+gate-2
8250 // composition (compile_node Unquote, compile_node UnquoteSplice,
8251 // substitute Unquote, substitute list-inner UnquoteSplice) now
8252 // share TWO composed primitives — `resolve_unquote_in_params`
8253 // on the bytecode path, `resolve_unquote_in_bindings` on the
8254 // substitute path — and ALL four reject gate-1 failures (non-
8255 // symbol inner) with the SAME `LispError::NonSymbolUnquoteTarget`
8256 // variant carrying the expected `prefix` slot. Before the lift,
8257 // each site threaded `form` twice through two helper calls; this
8258 // test pins that the lift preserves the gate's rejection-shape
8259 // identity across all four sites for a non-symbol inner — i.e.
8260 // gate-1 fires identically across both expansion strategies.
8261 struct Case {
8262 src: &'static str,
8263 expander: fn() -> Expander,
8264 expected_form: UnquoteForm,
8265 }
8266 let cases: &[Case] = &[
8267 Case {
8268 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8269 expander: Expander::new,
8270 expected_form: UnquoteForm::Unquote,
8271 },
8272 Case {
8273 src: "(defmacro w (x) `(outer ,@5)) (w 1)",
8274 expander: Expander::new,
8275 expected_form: UnquoteForm::Splice,
8276 },
8277 Case {
8278 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8279 expander: Expander::new_substitute_only,
8280 expected_form: UnquoteForm::Unquote,
8281 },
8282 Case {
8283 src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
8284 expander: Expander::new_substitute_only,
8285 expected_form: UnquoteForm::Splice,
8286 },
8287 ];
8288 for case in cases {
8289 let mut e = (case.expander)();
8290 let err = e
8291 .expand_program(read(case.src).unwrap())
8292 .expect_err("non-symbol inner must error end-to-end");
8293 match err {
8294 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
8295 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8296 }
8297 other => panic!(
8298 "expected NonSymbolUnquoteTarget for {}, got: {other:?}",
8299 case.src
8300 ),
8301 }
8302 }
8303 }
8304
8305 // ── Splice outside list: structural variant + path-uniform rejection ─
8306
8307 /// Helper for the splice-outside-list tests — pins the variant shape
8308 /// and carries the offending `got` field up to the assert site for
8309 /// legibility. Sibling of `unbound_var` and `non_symbol_target`.
8310 fn splice_outside_list_got(err: &LispError) -> &str {
8311 match err {
8312 LispError::SpliceOutsideList { got } => got.display.as_str(),
8313 other => panic!("expected SpliceOutsideList, got: {other:?}"),
8314 }
8315 }
8316
8317 #[test]
8318 fn splice_outside_list_in_substitute_emits_structural_variant() {
8319 // `,@xs` at the body's top level — there is no containing list to
8320 // splice into. Path: substitute (the `Expander::new_substitute_only`
8321 // path's top-level `Sexp::UnquoteSplice(_)` arm). Pins variant
8322 // identity AND the offending inner so a regression that re-inlines
8323 // the legacy `LispError::Compile` shape fails-loudly here.
8324 let mut e = Expander::new_substitute_only();
8325 let err = e
8326 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8327 .expect_err("splice outside list must error");
8328 assert_eq!(splice_outside_list_got(&err), "xs");
8329 }
8330
8331 #[test]
8332 fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
8333 // `,@(list 1 2)` at the body's top level — the inner is a literal
8334 // list, not a symbol. The structural variant carries the inner's
8335 // Sexp::Display projection so the operator sees the literal value
8336 // they wrote in the parenthetical.
8337 let mut e = Expander::new_substitute_only();
8338 let err = e
8339 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8340 .expect_err("splice outside list must error");
8341 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8342 }
8343
8344 #[test]
8345 fn splice_outside_list_in_compile_template_emits_structural_variant() {
8346 // The bytecode path's `compile_template` gate now rejects top-level
8347 // `,@X` bodies BEFORE walking — closing the prior silent-divergence
8348 // where the bytecode interpreter's outermost stack frame absorbed
8349 // the splice. Pins that the bytecode path emits the SAME structural
8350 // variant the substitute path emits — `,@-outside-list` is rejected
8351 // path-uniformly. Path: `Expander::new()` (compile_templates = true)
8352 // → `compile_template` gate.
8353 let mut e = Expander::new();
8354 let err = e
8355 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8356 .expect_err("compile-template splice outside list must error");
8357 assert_eq!(splice_outside_list_got(&err), "xs");
8358 }
8359
8360 #[test]
8361 fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
8362 // Same shape as the substitute test but routed through the bytecode
8363 // path's `compile_template` gate. Proves the gate fires on a
8364 // non-symbol inner too — the slot's contents are irrelevant; only
8365 // the syntactic position matters.
8366 let mut e = Expander::new();
8367 let err = e
8368 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8369 .expect_err("compile-template splice outside list must error");
8370 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8371 }
8372
8373 #[test]
8374 fn splice_outside_list_substitute_and_bytecode_paths_agree() {
8375 // Path-uniform rejection: the SAME source emits the SAME structural
8376 // variant (`SpliceOutsideList { got: "xs" }`) under both expansion
8377 // strategies. Before the `compile_template` gate, the bytecode path
8378 // silently produced a list while the substitute path errored —
8379 // expansion strategy was observable. After the gate, the gate is
8380 // strategy-uniform, so a macro that registers under one strategy
8381 // registers under the other.
8382 let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
8383 let mut subst = Expander::new_substitute_only();
8384 let mut bytecode = Expander::new();
8385 let err_subst = subst
8386 .expand_program(read(src).unwrap())
8387 .expect_err("substitute must error");
8388 let err_byte = bytecode
8389 .expand_program(read(src).unwrap())
8390 .expect_err("bytecode must error");
8391 assert_eq!(splice_outside_list_got(&err_subst), "xs");
8392 assert_eq!(splice_outside_list_got(&err_byte), "xs");
8393 }
8394
8395 #[test]
8396 fn splice_outside_list_position_is_none_today() {
8397 // Negative control for the future-spans move: until `Sexp` carries
8398 // source positions, `position()` returns `None` for this variant.
8399 // A future run that gives `Sexp` source spans adds `pos:
8400 // Option<usize>` to ONE place; this test gives that change a
8401 // deliberate fail-before/pass-after delta.
8402 let mut e = Expander::new_substitute_only();
8403 let err = e
8404 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8405 .expect_err("splice outside list must error");
8406 assert_eq!(err.position(), None);
8407 }
8408
8409 #[test]
8410 fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
8411 // End-to-end through the Display impl — pins the rendered diagnostic
8412 // a downstream tool sees today (REPL, tatara-check). The legacy
8413 // substring `"\`,@\` may only appear inside a list"` is preserved
8414 // verbatim AND the parenthetical `(got ,@xs)` names the offending
8415 // form; tools that pattern-match on the variant gain structural
8416 // binding to `got`.
8417 let mut e = Expander::new_substitute_only();
8418 let err = e
8419 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8420 .expect_err("splice outside list must error");
8421 let msg = format!("{err}");
8422 assert_eq!(
8423 msg,
8424 "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
8425 );
8426 }
8427
8428 #[test]
8429 fn splice_inside_list_still_succeeds_under_both_paths() {
8430 // Negative control: a well-positioned splice (`,@xs` INSIDE a list)
8431 // continues to succeed under both paths — the new gate only fires
8432 // when the splice is the entire body. Pins that the gate is scoped
8433 // to top-level only, not all `,@` occurrences. Uses a `&rest`-bound
8434 // list so `xs` is unambiguously a Sexp::List `(1 2)` rather than a
8435 // bare list-literal whose first symbol would also splice through.
8436 let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
8437 let mut subst = Expander::new_substitute_only();
8438 let mut bytecode = Expander::new();
8439 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8440 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8441 assert_eq!(out_subst, out_byte);
8442 assert_eq!(out_subst[0], parse("(outer 1 2)"));
8443 }
8444
8445 // ── splice_value_into: the shared splice-result coercion ──
8446
8447 #[test]
8448 fn splice_value_into_list_flattens_elements_into_builder() {
8449 // The canonical splice arm: a bound LIST contributes its elements
8450 // in order, preserving anything already in the builder.
8451 let mut builder = vec![Sexp::symbol("outer")];
8452 splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8453 assert_eq!(
8454 builder,
8455 vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
8456 );
8457 }
8458
8459 #[test]
8460 fn splice_value_into_nil_is_a_noop() {
8461 // Splicing the empty list (`Sexp::Nil`) contributes nothing —
8462 // the builder is unchanged.
8463 let mut builder = vec![Sexp::symbol("outer")];
8464 splice_value_into(&mut builder, &Sexp::Nil);
8465 assert_eq!(builder, vec![Sexp::symbol("outer")]);
8466 }
8467
8468 #[test]
8469 fn splice_value_into_scalar_pushes_single_element() {
8470 // A non-list, non-nil bound value degrades `,@x` to `,x`: it
8471 // splices as exactly one element. Pins the "free middle" coercion
8472 // every scalar shape (int, keyword, …) shares.
8473 let mut builder = vec![Sexp::symbol("outer")];
8474 splice_value_into(&mut builder, &Sexp::int(5));
8475 assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
8476 let mut other: Vec<Sexp> = vec![];
8477 splice_value_into(&mut other, &Sexp::keyword("k"));
8478 assert_eq!(other, vec![Sexp::keyword("k")]);
8479 }
8480
8481 #[test]
8482 fn splice_of_non_list_value_coerces_identically_under_both_paths() {
8483 // The point of the lift: the NON-list splice arms (scalar → single
8484 // element, nil → nothing) coerce identically under the substitute
8485 // AND bytecode strategies. Before the coercion was lifted to ONE
8486 // primitive these two arms lived inline at two sites; this test
8487 // pins that the two strategies cannot drift on the non-list arms.
8488 let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
8489 let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
8490 for src in [scalar, empty] {
8491 let mut subst = Expander::new_substitute_only();
8492 let mut bytecode = Expander::new();
8493 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8494 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8495 assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
8496 }
8497 let mut e = Expander::new();
8498 assert_eq!(
8499 e.expand_program(read(scalar).unwrap()).unwrap()[0],
8500 parse("(outer 5)")
8501 );
8502 let mut e2 = Expander::new();
8503 assert_eq!(
8504 e2.expand_program(read(empty).unwrap()).unwrap()[0],
8505 parse("(outer)")
8506 );
8507 }
8508
8509 // ── Missing macro arg: structural variant + path-uniform rejection ──
8510
8511 /// Helper for the missing-macro-arg tests — pins the variant shape
8512 /// and carries the failing macro's name + un-bound param up to the
8513 /// assert site for legibility. Sibling of `unbound_var`,
8514 /// `non_symbol_target`, and `splice_outside_list_got`.
8515 fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
8516 match err {
8517 LispError::MissingMacroArg { macro_name, param } => {
8518 (macro_name.as_str(), param.as_str())
8519 }
8520 other => panic!("expected MissingMacroArg, got: {other:?}"),
8521 }
8522 }
8523
8524 #[test]
8525 fn missing_macro_arg_in_compile_template_emits_structural_variant() {
8526 // `(need-two 1)` against `(need-two a b)` — `b` has no arg. Path:
8527 // `apply_compiled` (the bytecode-template path, default expander).
8528 // Pins variant identity AND macro_name AND the un-bound param so a
8529 // regression that re-inlines the legacy `LispError::Compile` shape
8530 // fails-loudly here.
8531 let mut e = Expander::new();
8532 let err = e
8533 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8534 .expect_err("missing required macro arg must error");
8535 let (macro_name, param) = missing_macro_arg_fields(&err);
8536 assert_eq!(macro_name, "need-two");
8537 assert_eq!(param, "b");
8538 }
8539
8540 #[test]
8541 fn missing_macro_arg_in_substitute_emits_structural_variant() {
8542 // Same shape as the bytecode test but routed through the
8543 // substitute-only expander → `bind_args` is the failing site.
8544 // Proves the substitute path emits the SAME structural variant the
8545 // bytecode path emits — `missing required arg` rejection is
8546 // path-uniform across both expansion strategies.
8547 let mut e = Expander::new_substitute_only();
8548 let err = e
8549 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8550 .expect_err("missing required macro arg must error");
8551 let (macro_name, param) = missing_macro_arg_fields(&err);
8552 assert_eq!(macro_name, "need-two");
8553 assert_eq!(param, "b");
8554 }
8555
8556 #[test]
8557 fn missing_macro_arg_first_position_is_named() {
8558 // `(f)` against `(f a b)` — `a` (the FIRST required param) has no
8559 // arg. The variant names `a`, not `b` — naming the LEFTMOST
8560 // un-bound param is the shape `bind_args` / `apply_compiled` both
8561 // emit (each iterates positionally and bails on the first missing
8562 // slot). Pins the leftmost-bail contract so a regression that
8563 // names the rightmost (or a surplus) param fails-loudly.
8564 let mut e = Expander::new();
8565 let err = e
8566 .expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
8567 .expect_err("missing first required arg must error");
8568 let (macro_name, param) = missing_macro_arg_fields(&err);
8569 assert_eq!(macro_name, "f");
8570 assert_eq!(param, "a");
8571 }
8572
8573 #[test]
8574 fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
8575 // Path-uniform rejection: the SAME source emits the SAME structural
8576 // variant under both expansion strategies. Negative control for
8577 // the divergence-closing posture: a future refactor that drifts
8578 // either path's rejection shape (or drops one path's rejection
8579 // entirely) fails-loudly here. Sibling of
8580 // `splice_outside_list_substitute_and_bytecode_paths_agree` —
8581 // both close `THEORY.md §II.1 invariant 2 — free middle` for one
8582 // failure mode each.
8583 let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
8584 let mut subst = Expander::new_substitute_only();
8585 let mut bytecode = Expander::new();
8586 let err_subst = subst
8587 .expand_program(read(src).unwrap())
8588 .expect_err("substitute must error");
8589 let err_byte = bytecode
8590 .expand_program(read(src).unwrap())
8591 .expect_err("bytecode must error");
8592 assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
8593 assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
8594 }
8595
8596 #[test]
8597 fn missing_macro_arg_position_is_none_today() {
8598 // Negative control for the future-spans move: until `Sexp` carries
8599 // source positions, `position()` returns `None` for this variant.
8600 // A future run that gives `Sexp` source spans adds `pos:
8601 // Option<usize>` to ONE place; this test gives that change a
8602 // deliberate fail-before/pass-after delta. Parallel to
8603 // `splice_outside_list_position_is_none_today`.
8604 let mut e = Expander::new();
8605 let err = e
8606 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8607 .expect_err("missing required macro arg must error");
8608 assert_eq!(err.position(), None);
8609 }
8610
8611 #[test]
8612 fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
8613 // End-to-end through the Display impl — pins the rendered diagnostic
8614 // a downstream tool sees today (REPL, tatara-check). The legacy
8615 // substring `"missing required arg: {param}"` is preserved verbatim
8616 // AND the head clause names the failing macro via `"call to
8617 // {macro_name}"`; tools that pattern-match on the variant gain
8618 // structural binding to `macro_name` / `param`.
8619 let mut e = Expander::new();
8620 let err = e
8621 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8622 .expect_err("missing required macro arg must error");
8623 assert_eq!(
8624 format!("{err}"),
8625 "compile error in call to need-two: missing required arg: b"
8626 );
8627 }
8628
8629 #[test]
8630 fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
8631 // Both `macro_name` (`wrap-twice`) and `param` (`notify-ref`)
8632 // round-trip through the variant unchanged. Pinning this contract
8633 // means a regression that camelCases or lowercases either side
8634 // fails-loudly here. Parallel to the
8635 // `unknown_kwarg_display_carries_kebab_case_keys_unchanged`
8636 // assertion for the kwarg-gate's symmetric surface.
8637 let mut e = Expander::new();
8638 let err = e
8639 .expand_program(
8640 read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
8641 .unwrap(),
8642 )
8643 .expect_err("missing required macro arg must error");
8644 let (macro_name, param) = missing_macro_arg_fields(&err);
8645 assert_eq!(macro_name, "wrap-twice");
8646 assert_eq!(param, "body");
8647 }
8648
8649 #[test]
8650 fn rest_param_only_macro_with_no_args_still_succeeds() {
8651 // Negative control: a macro whose only param is `&rest` must NOT
8652 // error when called with zero args — the rest-param binds to the
8653 // empty list. The new structural variant fires only on REQUIRED
8654 // params; the `Param::Rest` arm in both `bind_args` and
8655 // `apply_compiled` continues to bind the empty tail. Pins that the
8656 // helper is scoped to required-param failure, not all
8657 // arity-mismatch shapes.
8658 let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
8659 let mut subst = Expander::new_substitute_only();
8660 let mut bytecode = Expander::new();
8661 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8662 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8663 assert_eq!(out_subst, out_byte);
8664 assert_eq!(out_subst[0], parse("(list)"));
8665 }
8666
8667 // ── TooManyMacroArgs: call-site mirror of RestParamTrailingTokens ──
8668 //
8669 // A rest-less param list has a FIXED maximum arity equal to
8670 // `required.len() + optional.len()`. Surplus call args have nowhere to
8671 // bind. Before this gate the surplus was silently truncated to the
8672 // slice the binder could consume — the typed-entry macro-call-gate
8673 // rejected too-few-args loudly (`MissingMacroArg`) but accepted
8674 // too-many silently, an asymmetry the definition-side `&rest <name>
8675 // extra` rejection (`RestParamTrailingTokens`) had no call-side dual.
8676 // After this gate the call-site arity surface is structurally
8677 // complete in both directions; the substitute + bytecode paths share
8678 // `MacroParams::bind`, so both inherit the rejection without drift.
8679
8680 /// Helper for the too-many-args tests — projects to (macro_name,
8681 /// expected, got) for legibility. Sibling of `missing_macro_arg_fields`.
8682 fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
8683 match err {
8684 LispError::TooManyMacroArgs {
8685 macro_name,
8686 expected,
8687 got,
8688 } => (macro_name.as_str(), *expected, *got),
8689 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
8690 }
8691 }
8692
8693 #[test]
8694 fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
8695 // `(defmacro f (a b) ...)` called as `(f 1 2 3)` — `3` has
8696 // nowhere to bind. The rest-less binder rejects via
8697 // `TooManyMacroArgs { macro_name: "f", expected: 2, got: 3 }`,
8698 // NOT silently drops `3`. Pins both the variant identity AND the
8699 // structural fields the typed gate exposes for authoring-tool
8700 // quick-fixes ("you supplied 3 args; the macro takes at most
8701 // 2").
8702 let mut e = Expander::new();
8703 let err = e
8704 .expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8705 .expect_err("surplus arg on rest-less call must error");
8706 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8707 assert_eq!(macro_name, "f");
8708 assert_eq!(expected, 2);
8709 assert_eq!(got, 3);
8710 }
8711
8712 #[test]
8713 fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
8714 // The rest-less binder's fixed maximum arity is `required.len() +
8715 // optional.len()` — the optional section CONTRIBUTES to capacity.
8716 // `(defmacro f (a &optional b) ...)` accepts 1 OR 2 args; 3
8717 // args rejects with `expected: 2` (required + optional, NOT just
8718 // required). Pins the optional-counts-in-capacity contract so a
8719 // regression that omits optionals from the expected calculation
8720 // (and erroneously rejects 2-arg calls) fails-loudly here.
8721 let mut e = Expander::new();
8722 let err = e
8723 .expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8724 .expect_err("surplus arg beyond required+optional must error");
8725 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8726 assert_eq!(macro_name, "f");
8727 assert_eq!(expected, 2);
8728 assert_eq!(got, 3);
8729 }
8730
8731 #[test]
8732 fn too_many_macro_args_required_plus_two_optionals_arity_three() {
8733 // Larger optional section (capacity 1 + 2 = 3). 4 args rejects
8734 // with `expected: 3`. Pins that the capacity calculation scales
8735 // with optional.len(), not just at-most-one. Mixes a bare
8736 // optional with an optional carrying a default form — both shapes
8737 // contribute identically to capacity (the typed `OptionalParam`
8738 // entry's `default: Option<Sexp>` is irrelevant to the arity gate).
8739 let mut e = Expander::new();
8740 let err = e
8741 .expand_program(
8742 read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
8743 )
8744 .expect_err("surplus arg beyond required+two-optional must error");
8745 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8746 assert_eq!(macro_name, "f");
8747 assert_eq!(expected, 3);
8748 assert_eq!(got, 4);
8749 }
8750
8751 #[test]
8752 fn too_many_macro_args_does_not_fire_when_rest_is_present() {
8753 // Negative control: a rest-PRESENT param list has no maximum
8754 // arity — the `&rest` slot collects every trailing arg into a
8755 // `Sexp::List`. `(defmacro f (a &rest xs) ...)` called as
8756 // `(f 1 2 3 4)` MUST succeed; the new gate fires ONLY when
8757 // `MacroParams.rest` is `None`. Pins the rest-present-path
8758 // remains permissive — a regression that wrongly fires the
8759 // too-many gate for any surplus (including the rest-collecting
8760 // path) would break every `&rest`-using macro.
8761 let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
8762 let mut subst = Expander::new_substitute_only();
8763 let mut bytecode = Expander::new();
8764 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8765 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8766 assert_eq!(out_subst, out_byte);
8767 assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
8768 }
8769
8770 #[test]
8771 fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
8772 // Negative control: the rest-less gate fires STRICTLY when
8773 // `args.len() > expected` — at exact arity the binder accepts.
8774 // `(defmacro f (a &optional b) ...)` called as `(f 1 2)` binds
8775 // a=1, b=2 successfully (the optional takes its supplied arg,
8776 // not the default). Pins the boundary condition so a regression
8777 // that flips the comparison to `>=` (rejecting exact-arity
8778 // calls) fails-loudly here.
8779 let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
8780 let mut e = Expander::new();
8781 let out = e.expand_program(read(src).unwrap()).unwrap();
8782 assert_eq!(out[0], parse("(list 1 2)"));
8783 }
8784
8785 #[test]
8786 fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
8787 // Path-uniform rejection: the SAME source emits the SAME
8788 // structural variant under both expansion strategies. The
8789 // shared `MacroParams::bind` makes the rejection lands once and
8790 // both paths inherit it. Mirror of
8791 // `missing_macro_arg_substitute_and_bytecode_paths_agree` —
8792 // both close `THEORY.md §II.1 invariant 2 — free middle` for
8793 // one failure mode each.
8794 let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
8795 let mut subst = Expander::new_substitute_only();
8796 let mut bytecode = Expander::new();
8797 let err_subst = subst
8798 .expand_program(read(src).unwrap())
8799 .expect_err("substitute must error");
8800 let err_byte = bytecode
8801 .expand_program(read(src).unwrap())
8802 .expect_err("bytecode must error");
8803 assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
8804 assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
8805 }
8806
8807 #[test]
8808 fn too_many_macro_args_fires_after_missing_required_priority_held() {
8809 // Priority discipline: the required walk fires
8810 // `MissingMacroArg` BEFORE the rest-less surplus gate is
8811 // reached. `(defmacro f (a b c) …) (f 1)` is `MissingMacroArg
8812 // { param: "b" }`, NOT `TooManyMacroArgs` (and certainly not a
8813 // collision). The two failure modes are structurally disjoint:
8814 // too-few-required vs. too-many-with-no-rest. Pins the bail-on-
8815 // first-missing-required contract so a regression that swaps
8816 // the two gates' order would emit the wrong variant.
8817 let mut e = Expander::new();
8818 let err = e
8819 .expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
8820 .expect_err("missing required must error");
8821 assert!(
8822 matches!(err, LispError::MissingMacroArg { .. }),
8823 "expected MissingMacroArg (priority), got: {err:?}"
8824 );
8825 }
8826
8827 #[test]
8828 fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
8829 // Degenerate case: a nullary macro `(defmacro f () ...)` has
8830 // capacity 0; ANY supplied arg rejects with `expected: 0`. Pins
8831 // the gate fires even when the rest-less max-arity is zero —
8832 // i.e. the rejection is structural, not conditional on a
8833 // non-empty required+optional.
8834 let mut e = Expander::new();
8835 let err = e
8836 .expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
8837 .expect_err("nullary macro called with arg must error");
8838 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8839 assert_eq!(macro_name, "f");
8840 assert_eq!(expected, 0);
8841 assert_eq!(got, 1);
8842 }
8843
8844 #[test]
8845 fn too_many_macro_args_display_renders_legacy_compile_substring() {
8846 // The rendered Display matches the legacy `Compile`-shaped
8847 // diagnostic style — `"compile error in call to {macro_name}:
8848 // too many args: expected at most {expected}, got {got}"` — so
8849 // the existing `"compile error in call to"` substring authoring
8850 // tools' assertions key on stays unchanged. Pins the byte-level
8851 // rendered shape so a regression that drifts the prefix /
8852 // separator / labels fails-loudly here.
8853 let err = LispError::TooManyMacroArgs {
8854 macro_name: "pair".into(),
8855 expected: 2,
8856 got: 5,
8857 };
8858 assert_eq!(
8859 err.to_string(),
8860 "compile error in call to pair: too many args: expected at most 2, got 5"
8861 );
8862 }
8863
8864 #[test]
8865 fn too_many_macro_args_position_is_none_today() {
8866 // Negative control for the future-spans move: until `Sexp`
8867 // carries source positions, `position()` returns `None` for
8868 // this variant. A future run that gives `Sexp` source spans
8869 // adds `pos: Option<usize>` to ONE place; this test gives that
8870 // change a deliberate fail-before/pass-after delta. Parallel to
8871 // `missing_macro_arg_position_is_none_today`.
8872 let err = LispError::TooManyMacroArgs {
8873 macro_name: "pair".into(),
8874 expected: 2,
8875 got: 3,
8876 };
8877 assert_eq!(err.position(), None);
8878 }
8879
8880 /// Helper for the non-symbol-param tests — pins the variant shape and
8881 /// carries the failing position + offending element up to the assert
8882 /// site for legibility. Sibling of `missing_macro_arg_fields`.
8883 fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
8884 match err {
8885 LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
8886 other => panic!("expected NonSymbolParam, got: {other:?}"),
8887 }
8888 }
8889
8890 #[test]
8891 fn non_symbol_param_at_first_position_emits_structural_variant() {
8892 // `(defmacro f (5) ...)` — the first element of the param list is
8893 // an integer literal, not a symbol. Pins variant identity AND
8894 // that `position` is the loop index inside `parse_params` (0 for
8895 // the first slot) AND that `got` is the offending element via
8896 // `Sexp::Display` (`5`). A regression that re-inlines the legacy
8897 // `LispError::Compile` shape (which named neither the position
8898 // nor the offending element) fails-loudly here.
8899 let mut e = Expander::new();
8900 let err = e
8901 .expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
8902 .expect_err("non-symbol param must error");
8903 let (position, got) = non_symbol_param_fields(&err);
8904 assert_eq!(position, 0);
8905 assert_eq!(got, "5");
8906 }
8907
8908 #[test]
8909 fn non_symbol_param_at_second_position_emits_structural_variant() {
8910 // `(defmacro f (a 5) ...)` — `a` parses fine, `5` at position 1
8911 // misfires. Pins that `position` advances with the loop index, so
8912 // an LSP quick-fix that wants to point at "the second element of
8913 // your param list" gains the index as data, no source re-parse
8914 // required.
8915 let mut e = Expander::new();
8916 let err = e
8917 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8918 .expect_err("non-symbol param must error");
8919 let (position, got) = non_symbol_param_fields(&err);
8920 assert_eq!(position, 1);
8921 assert_eq!(got, "5");
8922 }
8923
8924 #[test]
8925 fn non_symbol_param_carries_keyword_value_unchanged() {
8926 // `:k` at a param-list position. `Sexp::Display` for
8927 // `Atom::Keyword(s)` writes `:s`; pins that the variant's `got`
8928 // field round-trips the keyword form unchanged so an LSP that
8929 // surfaces "you wrote `:k` where a symbol was expected" gains
8930 // the literal keyword value as data, no re-parsing required.
8931 let mut e = Expander::new();
8932 let err = e
8933 .expand_program(read("(defmacro f (:k) `(list))").unwrap())
8934 .expect_err("non-symbol param must error");
8935 let (position, got) = non_symbol_param_fields(&err);
8936 assert_eq!(position, 0);
8937 assert_eq!(got, ":k");
8938 }
8939
8940 #[test]
8941 fn non_symbol_param_carries_nested_list_value_unchanged() {
8942 // A nested list at a param-list position. `Sexp::Display` for
8943 // `List(xs)` writes `(<x1> <x2> ...)`; pins that the variant's
8944 // `got` field carries the nested form's full Display projection
8945 // unchanged so the operator sees what they wrote.
8946 let mut e = Expander::new();
8947 let err = e
8948 .expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
8949 .expect_err("non-symbol param must error");
8950 let (position, got) = non_symbol_param_fields(&err);
8951 assert_eq!(position, 0);
8952 assert_eq!(got, "(nested)");
8953 }
8954
8955 #[test]
8956 fn non_symbol_param_in_defpoint_template_emits_same_variant() {
8957 // `defpoint-template` shares `parse_params` with `defmacro` (all
8958 // three head keywords route through `macro_def_from`). Pins that
8959 // the lift fires path-uniformly across the three head keywords
8960 // — `defmacro`, `defpoint-template`, `defcheck` — so the
8961 // structural-completeness floor holds for every defmacro-shaped
8962 // form, not just the one with the `defmacro` head literal.
8963 let mut e = Expander::new();
8964 let err = e
8965 .expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
8966 .expect_err("non-symbol param must error");
8967 let (position, got) = non_symbol_param_fields(&err);
8968 assert_eq!(position, 0);
8969 assert_eq!(got, "5");
8970 }
8971
8972 #[test]
8973 fn non_symbol_param_in_defcheck_emits_same_variant() {
8974 // Sibling of the defpoint-template test — `defcheck` is the
8975 // third head keyword `macro_def_from` recognizes. All three
8976 // route through the same `parse_params` and now reject
8977 // non-symbol params with the same structural variant.
8978 let mut e = Expander::new();
8979 let err = e
8980 .expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
8981 .expect_err("non-symbol param must error");
8982 let (position, got) = non_symbol_param_fields(&err);
8983 assert_eq!(position, 1);
8984 assert_eq!(got, "5");
8985 }
8986
8987 #[test]
8988 fn non_symbol_param_position_is_none_today() {
8989 // Negative control for the future-spans move: until `Sexp`
8990 // carries source positions, `position()` on `LispError` returns
8991 // `None` for this variant. A future run that gives `Sexp`
8992 // source spans adds `pos: Option<usize>` to ONE place; this
8993 // test gives that change a deliberate fail-before/pass-after
8994 // delta. Parallel to `missing_macro_arg_position_is_none_today`.
8995 let mut e = Expander::new();
8996 let err = e
8997 .expand_program(read("(defmacro f (5) `(list))").unwrap())
8998 .expect_err("non-symbol param must error");
8999 assert_eq!(err.position(), None);
9000 }
9001
9002 #[test]
9003 fn non_symbol_param_message_renders_legacy_substring_with_position() {
9004 // End-to-end through Display — pins the rendered diagnostic that
9005 // downstream tools (REPL, `tatara-check`) see today. Legacy
9006 // substrings `"defmacro params"` AND `"expected symbol"` are
9007 // preserved verbatim; the appended `at position {position}, got
9008 // {got}` clause is the new structural detail. Tools that
9009 // pattern-match on the variant gain structural binding to
9010 // `position` / `got`.
9011 let mut e = Expander::new();
9012 let err = e
9013 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
9014 .expect_err("non-symbol param must error");
9015 assert_eq!(
9016 format!("{err}"),
9017 "compile error in defmacro params: \
9018 expected symbol at position 1, got 5"
9019 );
9020 }
9021
9022 #[test]
9023 fn non_symbol_param_substitute_and_bytecode_paths_agree() {
9024 // Path-uniform rejection: the SAME source emits the SAME
9025 // structural variant under both expansion strategies. The
9026 // defmacro-syntax-gate fires inside `macro_def_from` →
9027 // `parse_params`, BEFORE either strategy's expansion path runs;
9028 // so both `Expander::new()` (bytecode) and
9029 // `Expander::new_substitute_only()` (substitute) reject the
9030 // SAME malformed defmacro at the SAME gate. Sibling of
9031 // `missing_macro_arg_substitute_and_bytecode_paths_agree`.
9032 let src = "(defmacro f (a 5) `(,a))";
9033 let mut subst = Expander::new_substitute_only();
9034 let mut bytecode = Expander::new();
9035 let err_subst = subst
9036 .expand_program(read(src).unwrap())
9037 .expect_err("substitute must error");
9038 let err_byte = bytecode
9039 .expand_program(read(src).unwrap())
9040 .expect_err("bytecode must error");
9041 assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
9042 assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
9043 }
9044
9045 /// Helper for the rest-param-missing-name tests — pins the variant
9046 /// shape and carries the marker position + offending follower (or
9047 /// its absence) up to the assert site for legibility. Sibling of
9048 /// `non_symbol_param_fields`.
9049 fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
9050 match err {
9051 LispError::RestParamMissingName { rest_position, got } => {
9052 (*rest_position, got.as_ref().map(|w| w.display.as_str()))
9053 }
9054 other => panic!("expected RestParamMissingName, got: {other:?}"),
9055 }
9056 }
9057
9058 #[test]
9059 fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
9060 // `(defmacro f (&rest))` — the marker is the only param-list
9061 // element; nothing follows. Pins variant identity AND that
9062 // `rest_position == 0` (the first slot) AND that `got == None`
9063 // (no follower exists). A regression that re-inlines the legacy
9064 // `LispError::Compile` shape (which named neither field) fails-
9065 // loudly here.
9066 let mut e = Expander::new();
9067 let err = e
9068 .expand_program(read("(defmacro f (&rest) `(list))").unwrap())
9069 .expect_err("&rest with no follower must error");
9070 let (rest_position, got) = rest_param_missing_name_fields(&err);
9071 assert_eq!(rest_position, 0);
9072 assert_eq!(got, None);
9073 }
9074
9075 #[test]
9076 fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
9077 // `(defmacro f (a &rest))` — `a` parses fine, `&rest` at param-list
9078 // position 1 has no follower at all. Pins that `rest_position`
9079 // advances with the loop index, so an LSP quick-fix that wants to
9080 // point at "your `&rest` at position 1 has no name" gains the
9081 // marker position as data, no source re-parse required.
9082 let mut e = Expander::new();
9083 let err = e
9084 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9085 .expect_err("&rest with no follower must error");
9086 let (rest_position, got) = rest_param_missing_name_fields(&err);
9087 assert_eq!(rest_position, 1);
9088 assert_eq!(got, None);
9089 }
9090
9091 #[test]
9092 fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
9093 // `(defmacro f (&rest 5))` — `&rest` at position 0 followed by
9094 // `5` (an integer literal, not a symbol). Pins that the variant's
9095 // `got` field is `Some` and carries the offending follower's
9096 // `Sexp::Display` projection; the bifurcation between "missing
9097 // entirely" and "present but non-symbol" is in the renderable
9098 // detail, not in what the gate rejects.
9099 let mut e = Expander::new();
9100 let err = e
9101 .expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
9102 .expect_err("&rest followed by non-symbol must error");
9103 let (rest_position, got) = rest_param_missing_name_fields(&err);
9104 assert_eq!(rest_position, 0);
9105 assert_eq!(got, Some("5"));
9106 }
9107
9108 #[test]
9109 fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
9110 // `(defmacro f (a &rest :foo))` — keyword follower at the rest-name
9111 // slot. `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pins
9112 // that the variant's `got` field round-trips the keyword form
9113 // unchanged so an LSP that surfaces "you wrote `:foo` where a
9114 // rest-name was expected" gains the literal keyword value as
9115 // data, no re-parsing required.
9116 let mut e = Expander::new();
9117 let err = e
9118 .expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
9119 .expect_err("&rest followed by keyword must error");
9120 let (rest_position, got) = rest_param_missing_name_fields(&err);
9121 assert_eq!(rest_position, 1);
9122 assert_eq!(got, Some(":foo"));
9123 }
9124
9125 #[test]
9126 fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
9127 // `(defmacro f (&rest (nested)))` — nested-list follower at the
9128 // rest-name slot. `Sexp::Display` for `List(xs)` writes
9129 // `(<x1> <x2> ...)`; pins that the variant's `got` field carries
9130 // the nested form's full Display projection unchanged so the
9131 // operator sees what they wrote.
9132 let mut e = Expander::new();
9133 let err = e
9134 .expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
9135 .expect_err("&rest followed by list must error");
9136 let (rest_position, got) = rest_param_missing_name_fields(&err);
9137 assert_eq!(rest_position, 0);
9138 assert_eq!(got, Some("(nested)"));
9139 }
9140
9141 #[test]
9142 fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
9143 // `defpoint-template` shares `parse_params` with `defmacro` (all
9144 // three head keywords route through `macro_def_from`). Pins that
9145 // the lift fires path-uniformly across the three head keywords —
9146 // a regression that handles `defpoint-template`'s param list
9147 // differently from `defmacro`'s would fail-loudly here.
9148 let mut e = Expander::new();
9149 let err = e
9150 .expand_program(read("(defpoint-template t (a &rest) `(,a))").unwrap())
9151 .expect_err("&rest with no follower must error");
9152 let (rest_position, got) = rest_param_missing_name_fields(&err);
9153 assert_eq!(rest_position, 1);
9154 assert_eq!(got, None);
9155 }
9156
9157 #[test]
9158 fn rest_param_missing_name_in_defcheck_emits_same_variant() {
9159 // Sibling for the `defcheck` head; rounds out the three-head-
9160 // keyword coverage so the lift is path-uniform across
9161 // `defmacro` / `defpoint-template` / `defcheck`. After this
9162 // test the defmacro-syntax-gate rejects `&rest`-without-name
9163 // identically across all three head keywords — the
9164 // typed-entry surface is single-shape across the cluster.
9165 let mut e = Expander::new();
9166 let err = e
9167 .expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
9168 .expect_err("&rest followed by non-symbol must error");
9169 let (rest_position, got) = rest_param_missing_name_fields(&err);
9170 assert_eq!(rest_position, 0);
9171 assert_eq!(got, Some("5"));
9172 }
9173
9174 #[test]
9175 fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
9176 // Path-uniform rejection: the SAME source emits the SAME
9177 // structural variant under both expansion strategies. The
9178 // defmacro-syntax-gate fires inside `macro_def_from` →
9179 // `parse_params`, BEFORE either strategy's expansion path
9180 // runs; so both `Expander::new()` (bytecode) and
9181 // `Expander::new_substitute_only()` (substitute) reject the
9182 // SAME malformed defmacro at the SAME gate. Sibling of
9183 // `non_symbol_param_substitute_and_bytecode_paths_agree`.
9184 let src = "(defmacro f (a &rest 5) `(,a))";
9185 let mut subst = Expander::new_substitute_only();
9186 let mut bytecode = Expander::new();
9187 let err_subst = subst
9188 .expand_program(read(src).unwrap())
9189 .expect_err("substitute must error");
9190 let err_byte = bytecode
9191 .expand_program(read(src).unwrap())
9192 .expect_err("bytecode must error");
9193 assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
9194 assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
9195 }
9196
9197 #[test]
9198 fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
9199 // End-to-end through Display — pins the rendered diagnostic
9200 // consumers see today (REPL, tatara-check) AND the new `(rest
9201 // marker at position {rest_position}, got {got})` clause. The
9202 // legacy `"&rest needs a name"` substring rides through
9203 // verbatim.
9204 let mut e = Expander::new();
9205 let err = e
9206 .expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
9207 .expect_err("&rest followed by non-symbol must error");
9208 assert_eq!(
9209 format!("{err}"),
9210 "compile error in defmacro params: &rest needs a name \
9211 (rest marker at position 1, got 5)"
9212 );
9213 }
9214
9215 #[test]
9216 fn rest_param_missing_name_message_renders_none_provided_when_follower_absent() {
9217 // Same as the prior test but for the "missing entirely" branch.
9218 // The renderable detail is `(rest marker at position
9219 // {rest_position}, none provided)` — naming the absence
9220 // structurally instead of an empty / partial parenthetical.
9221 let mut e = Expander::new();
9222 let err = e
9223 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9224 .expect_err("&rest with no follower must error");
9225 assert_eq!(
9226 format!("{err}"),
9227 "compile error in defmacro params: &rest needs a name \
9228 (rest marker at position 1, none provided)"
9229 );
9230 }
9231
9232 #[test]
9233 fn rest_param_missing_name_position_is_none_today() {
9234 // Pins that `position()` returns `None` so the future `pos:
9235 // Option<usize>` add (once `Sexp` carries source spans) lands
9236 // as a deliberate fail-before/pass-after delta rather than a
9237 // silent default. Parallel to
9238 // `non_symbol_param_position_is_none_today` and
9239 // `missing_macro_arg_position_is_none_today`.
9240 let err_missing = LispError::RestParamMissingName {
9241 rest_position: 1,
9242 got: None,
9243 };
9244 assert_eq!(err_missing.position(), None);
9245 let err_got = LispError::RestParamMissingName {
9246 rest_position: 0,
9247 got: Some(crate::error::SexpWitness::new(
9248 crate::error::SexpShape::Int,
9249 "5",
9250 )),
9251 };
9252 assert_eq!(err_got.position(), None);
9253 }
9254
9255 // --- RestParamTrailingTokens: the parse_params gate's third (and
9256 // final) definition-site failure mode ---
9257 //
9258 // A `&rest <name>` absorbs every remaining call arg, so it is the LAST
9259 // thing a param list can name. Before this variant `parse_params`
9260 // returned the moment it bound the rest name, SILENTLY DROPPING any
9261 // trailing tokens — `(a &rest xs extra)` parsed as if `extra` weren't
9262 // there. These tests pin the loud rejection that replaces the silent
9263 // drop; the symbol `RestParamTrailingTokens` exists only after this
9264 // change, so the whole block is fail-before/pass-after by construction
9265 // (compile-time edge) and the end-to-end regression guard below pins
9266 // that the malformed defmacro no longer expands cleanly.
9267
9268 /// Helper mirroring `rest_param_missing_name_fields` — pins the variant
9269 /// shape and lifts the marker position, trailing count, and first
9270 /// offender's display up to the assert site.
9271 fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
9272 match err {
9273 LispError::RestParamTrailingTokens {
9274 rest_position,
9275 extra,
9276 first,
9277 } => (*rest_position, *extra, first.display.as_str()),
9278 other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
9279 }
9280 }
9281
9282 #[test]
9283 fn parse_params_rejects_single_trailing_token_after_rest_name() {
9284 // `(a &rest c extra)` — `&rest c` is well-formed, but `extra`
9285 // follows. The rest name is bound at position 2, the marker at 1;
9286 // the lone trailing token `extra` is reported (extra == 1, first ==
9287 // "extra"). Before this variant `parse_params` returned at the rest
9288 // name and `extra` vanished.
9289 let err = parse_params(&read("a &rest c extra").unwrap())
9290 .expect_err("a trailing token after the rest name must error");
9291 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9292 }
9293
9294 #[test]
9295 fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
9296 // `(&rest c x y z)` — three tokens follow the rest name. `extra`
9297 // counts ALL of them (3), `first` is the first (`x`), and the
9298 // marker is at position 0. A regression that reports only the
9299 // first trailing token's presence (extra hard-coded to 1) fails
9300 // loudly here.
9301 let err = parse_params(&read("&rest c x y z").unwrap())
9302 .expect_err("multiple trailing tokens must error");
9303 assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
9304 }
9305
9306 #[test]
9307 fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
9308 // `(a &rest c 5)` — the rest NAME `c` is a valid symbol, so this is
9309 // NOT a `RestParamMissingName`; the integer `5` is a trailing token
9310 // AFTER a well-formed `&rest c`. Pins that the two sibling failure
9311 // modes don't collide: a malformed rest-name is `RestParamMissingName`,
9312 // a well-formed rest-name followed by junk is
9313 // `RestParamTrailingTokens`. `first` round-trips `5` via the typed
9314 // witness's `Sexp::Display` projection.
9315 let err = parse_params(&read("a &rest c 5").unwrap())
9316 .expect_err("a trailing non-symbol after the rest name must error");
9317 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
9318 }
9319
9320 #[test]
9321 fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
9322 // The fidelity fix, end-to-end through `expand_program`: a defmacro
9323 // whose param list carries a stray token after `&rest <name>` now
9324 // ERRORS at the typed-entry gate instead of expanding as though the
9325 // stray token weren't there. This is the regression guard for the
9326 // silent-drop bug — before this change the same source expanded
9327 // cleanly and `extra` was discarded with no signal.
9328 let mut e = Expander::new();
9329 let err = e
9330 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9331 .expect_err("trailing token after &rest name must error");
9332 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9333 }
9334
9335 #[test]
9336 fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
9337 // Path-uniform rejection: the gate fires inside `macro_def_from` →
9338 // `parse_params`, BEFORE either expansion strategy runs, so both
9339 // `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
9340 // (substitute) reject the SAME malformed defmacro at the SAME gate.
9341 // Sibling of `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9342 let src = "(defmacro f (a &rest xs extra) `(,a))";
9343 let mut subst = Expander::new_substitute_only();
9344 let mut bytecode = Expander::new();
9345 let err_subst = subst
9346 .expand_program(read(src).unwrap())
9347 .expect_err("substitute must error");
9348 let err_byte = bytecode
9349 .expand_program(read(src).unwrap())
9350 .expect_err("bytecode must error");
9351 assert_eq!(
9352 rest_param_trailing_tokens_fields(&err_subst),
9353 (1, 1, "extra")
9354 );
9355 assert_eq!(
9356 rest_param_trailing_tokens_fields(&err_byte),
9357 (1, 1, "extra")
9358 );
9359 }
9360
9361 #[test]
9362 fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
9363 // End-to-end through Display — pins the rendered diagnostic AND the
9364 // new `(rest marker at position {n}, {extra} trailing after name,
9365 // first: {first})` clause. The `compile error in defmacro params:`
9366 // prefix matches the sibling `&rest needs a name` rendering's shape.
9367 let mut e = Expander::new();
9368 let err = e
9369 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9370 .expect_err("trailing token after &rest name must error");
9371 assert_eq!(
9372 format!("{err}"),
9373 "compile error in defmacro params: &rest name must be last \
9374 (rest marker at position 1, 1 trailing after name, first: extra)"
9375 );
9376 }
9377
9378 #[test]
9379 fn rest_param_trailing_tokens_position_is_none_today() {
9380 // Pins `position() == None` so the future `pos: Option<usize>` add
9381 // (once `Sexp` carries source spans) lands as a deliberate
9382 // fail-before/pass-after delta. Parallel to
9383 // `rest_param_missing_name_position_is_none_today`.
9384 let err = LispError::RestParamTrailingTokens {
9385 rest_position: 1,
9386 extra: 1,
9387 first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
9388 };
9389 assert_eq!(err.position(), None);
9390 }
9391
9392 // --- MacroDefHead enum (the closed-set lift) ---
9393 //
9394 // The next nine tests pin the typed-enum lift that closes the
9395 // three-times rule on the `head: &str → &'static str` projection
9396 // idiom previously inlined at FOUR sites (the `matches!` gate at
9397 // the top of `macro_def_from` plus the projection match inside
9398 // each of `defmacro_arity`, `defmacro_non_symbol_name`,
9399 // `defmacro_non_list_params`). Every test in this block names
9400 // `MacroDefHead` directly — the symbol exists only after the
9401 // lift, so the entire block is fail-before/pass-after by
9402 // construction (compile-time edge). Theory anchor: THEORY.md
9403 // §VI.1 — three-times rule; THEORY.md §V.1 — the closed set is
9404 // a TYPE rather than a `matches!` literal.
9405
9406 #[test]
9407 fn macro_def_head_from_keyword_recognizes_defmacro() {
9408 // Pins that `MacroDefHead::from_keyword("defmacro")` returns
9409 // `Some(MacroDefHead::Defmacro)` — the first of the three
9410 // canonical macro-definition head keywords. A regression that
9411 // re-inlines a `matches!`-only gate (without the typed-enum
9412 // projection) deletes `from_keyword` and fails-loudly here.
9413 assert_eq!(
9414 MacroDefHead::from_keyword("defmacro"),
9415 Some(MacroDefHead::Defmacro)
9416 );
9417 }
9418
9419 #[test]
9420 fn macro_def_head_from_keyword_recognizes_defpoint_template() {
9421 // Pins that `MacroDefHead::from_keyword("defpoint-template")`
9422 // returns `Some(MacroDefHead::DefpointTemplate)` — the second
9423 // of the three canonical head keywords. The `defpoint-template`
9424 // form is the K8s-as-processes authoring surface (see
9425 // tatara-process); `macro_def_from` must recognize it
9426 // identically to `defmacro` so the `(defpoint-template …)`
9427 // form's macro-style binding works the same way.
9428 assert_eq!(
9429 MacroDefHead::from_keyword("defpoint-template"),
9430 Some(MacroDefHead::DefpointTemplate)
9431 );
9432 }
9433
9434 #[test]
9435 fn macro_def_head_from_keyword_recognizes_defcheck() {
9436 // Pins that `MacroDefHead::from_keyword("defcheck")` returns
9437 // `Some(MacroDefHead::Defcheck)` — the third and final
9438 // canonical head keyword. The `defcheck` form is the
9439 // workspace-coherence authoring surface (see
9440 // tatara-reconciler/checks.lisp); `macro_def_from` must
9441 // recognize it identically to `defmacro` so user-defined
9442 // checks inherit the macro-style binding semantics.
9443 assert_eq!(
9444 MacroDefHead::from_keyword("defcheck"),
9445 Some(MacroDefHead::Defcheck)
9446 );
9447 }
9448
9449 #[test]
9450 fn macro_def_head_from_keyword_rejects_unknown() {
9451 // Pins that `MacroDefHead::from_keyword` returns `None` for
9452 // anything outside the closed set — a non-symbol keyword
9453 // (`"if"`), a near-miss spelling (`"defmacroo"`,
9454 // `"defcheckk"`), and the empty string. `macro_def_from`
9455 // depends on this `None` projection to mean "this form is
9456 // not a defmacro form" and walk past — a regression that
9457 // accidentally accepts a near-miss head (e.g. via a
9458 // lower-cased `EqualFold` match) would route `(defmacroo …)`
9459 // through the arity gate, which is wrong. Pins all four
9460 // canonical near-miss / non-canonical inputs.
9461 assert_eq!(MacroDefHead::from_keyword("if"), None);
9462 assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
9463 assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
9464 assert_eq!(MacroDefHead::from_keyword(""), None);
9465 }
9466
9467 #[test]
9468 fn macro_def_head_keyword_round_trips_each_variant() {
9469 // Pins that `MacroDefHead::keyword` returns the canonical
9470 // `&'static str` literal for each variant. Together with
9471 // `from_keyword` this closes the bidirectional projection:
9472 // for every canonical head keyword `s`, `MacroDefHead::
9473 // from_keyword(s).unwrap().keyword() == s`. The `&'static
9474 // str` lifetime on the return type is load-bearing — it's
9475 // what lets the `LispError::Defmacro*` variants carry
9476 // `head: &'static str` slots without an arbitrary owned
9477 // `String`. Pinning the `: &'static str` binding here
9478 // makes the lifetime requirement load-bearing in the test.
9479 let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
9480 let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
9481 let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
9482 assert_eq!(s_defmacro, "defmacro");
9483 assert_eq!(s_defpoint, "defpoint-template");
9484 assert_eq!(s_defcheck, "defcheck");
9485 }
9486
9487 #[test]
9488 fn macro_def_head_keyword_round_trips_through_from_keyword() {
9489 // Pins that the two halves of the projection compose to the
9490 // identity on the closed set: for every canonical head
9491 // keyword, projecting `&str → MacroDefHead → &'static str`
9492 // returns the original literal. Sibling of
9493 // `macro_def_head_keyword_round_trips_each_variant` —
9494 // together they pin both directions of the bidirection.
9495 for kw in ["defmacro", "defpoint-template", "defcheck"] {
9496 let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
9497 assert_eq!(head.keyword(), kw);
9498 }
9499 }
9500
9501 #[test]
9502 fn macro_def_head_threads_through_defmacro_arity_helper() {
9503 // Pins that `defmacro_arity` accepts a typed `MacroDefHead`
9504 // and threads it through to the variant's typed `head` slot
9505 // unchanged — no `&str` projection at the helper boundary
9506 // (the projection through `MacroDefHead::keyword()` happens at
9507 // Display rendering time inside the `#[error(...)]`
9508 // annotation). A regression that drops the `MacroDefHead`
9509 // parameter type (e.g. by reverting to `head: &str`) breaks
9510 // compilation here. Pinning each of the three variants gives
9511 // the typed-head threading the same path-uniformity edge the
9512 // existing `defmacro_arity_in_*_emits_same_variant` tests pin
9513 // for the call-site path through `macro_def_from`.
9514 for head in [
9515 MacroDefHead::Defmacro,
9516 MacroDefHead::DefpointTemplate,
9517 MacroDefHead::Defcheck,
9518 ] {
9519 let err = defmacro_arity(head, 2);
9520 match err {
9521 LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
9522 other => panic!("expected DefmacroArity, got: {other:?}"),
9523 }
9524 }
9525 }
9526
9527 #[test]
9528 fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
9529 // Sibling of the `defmacro_arity` threading test — pins that
9530 // `defmacro_non_symbol_name` accepts a typed `MacroDefHead`
9531 // and threads it through to the variant's typed `head` slot
9532 // unchanged. The `got: &Sexp` parameter rides through
9533 // `crate::domain::sexp_witness` into the variant's typed
9534 // `got: SexpWitness` slot so BOTH the structural shape AND
9535 // the rendered literal are preserved across the helper
9536 // boundary, parallel to how `non_symbol_param` and
9537 // `non_symbol_unquote_target` project their `&Sexp` arguments
9538 // through the same typed joint primitive.
9539 let got = parse("5");
9540 for head in [
9541 MacroDefHead::Defmacro,
9542 MacroDefHead::DefpointTemplate,
9543 MacroDefHead::Defcheck,
9544 ] {
9545 let err = defmacro_non_symbol_name(head, &got);
9546 match err {
9547 LispError::DefmacroNonSymbolName { head: h, got: g } => {
9548 assert_eq!(h, head);
9549 assert_eq!(g.shape, crate::error::SexpShape::Int);
9550 assert_eq!(g.display, "5");
9551 }
9552 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9553 }
9554 }
9555 }
9556
9557 #[test]
9558 fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
9559 // Sibling of the `defmacro_arity` and
9560 // `defmacro_non_symbol_name` threading tests — pins that
9561 // `defmacro_non_list_params` accepts a typed `MacroDefHead`
9562 // and threads it through to the variant's typed `head` slot
9563 // unchanged. Together the three threading tests close the
9564 // typed-enum lift across all three error helpers — every
9565 // call site that constructs a `LispError::Defmacro*` variant
9566 // takes its `head` from a `MacroDefHead`, never from a `&str`
9567 // match. The `got: &Sexp` parameter rides through
9568 // `crate::domain::sexp_witness` into the variant's typed
9569 // `got: SexpWitness` slot so BOTH the structural shape AND
9570 // the rendered literal are preserved across the helper
9571 // boundary, parallel to how `defmacro_non_symbol_name`,
9572 // `non_symbol_param`, and `non_symbol_unquote_target` project
9573 // their `&Sexp` arguments through the same typed joint
9574 // primitive.
9575 let got = parse("x");
9576 for head in [
9577 MacroDefHead::Defmacro,
9578 MacroDefHead::DefpointTemplate,
9579 MacroDefHead::Defcheck,
9580 ] {
9581 let err = defmacro_non_list_params(head, &got);
9582 match err {
9583 LispError::DefmacroNonListParams { head: h, got: g } => {
9584 assert_eq!(h, head);
9585 assert_eq!(g.shape, crate::error::SexpShape::Symbol);
9586 assert_eq!(g.display, "x");
9587 }
9588 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9589 }
9590 }
9591 }
9592
9593 /// Helper for the defmacro-arity tests — pins the variant shape and
9594 /// carries the head / arity up to the assert site for legibility.
9595 /// Sibling of `non_symbol_param_fields` and
9596 /// `rest_param_missing_name_fields`.
9597 fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
9598 match err {
9599 LispError::DefmacroArity { head, arity } => (*head, *arity),
9600 other => panic!("expected DefmacroArity, got: {other:?}"),
9601 }
9602 }
9603
9604 #[test]
9605 fn defmacro_arity_with_head_only_emits_structural_variant() {
9606 // `(defmacro)` — only the head, no name / params / body. Pins
9607 // variant identity AND that `arity == 1` (just the head
9608 // element) AND that `head == "defmacro"`. A regression that
9609 // re-inlines the legacy `LispError::Compile` shape (which
9610 // named neither field) fails-loudly here.
9611 let mut e = Expander::new();
9612 let err = e
9613 .expand_program(read("(defmacro)").unwrap())
9614 .expect_err("defmacro arity gate must error");
9615 let (head, arity) = defmacro_arity_fields(&err);
9616 assert_eq!(head, MacroDefHead::Defmacro);
9617 assert_eq!(arity, 1);
9618 }
9619
9620 #[test]
9621 fn defmacro_arity_with_head_and_name_emits_structural_variant() {
9622 // `(defmacro f)` — head + name, missing params + body. Pins
9623 // that `arity` advances with the actual form length (2 for
9624 // this case) so an LSP quick-fix that wants to surface "you
9625 // wrote 2 elements; need 4" gains the count as data, no
9626 // source re-parse required.
9627 let mut e = Expander::new();
9628 let err = e
9629 .expand_program(read("(defmacro f)").unwrap())
9630 .expect_err("defmacro arity gate must error");
9631 let (head, arity) = defmacro_arity_fields(&err);
9632 assert_eq!(head, MacroDefHead::Defmacro);
9633 assert_eq!(arity, 2);
9634 }
9635
9636 #[test]
9637 fn defmacro_arity_with_head_name_params_emits_structural_variant() {
9638 // `(defmacro f ())` — head + name + params, missing body
9639 // (the most-complete partial defmacro that still trips the
9640 // arity gate). Pins that `arity == 3` exactly so an LSP
9641 // quick-fix that wants to surface "your defmacro is one
9642 // element short — body is missing" gains the count as data.
9643 let mut e = Expander::new();
9644 let err = e
9645 .expand_program(read("(defmacro f ())").unwrap())
9646 .expect_err("defmacro arity gate must error");
9647 let (head, arity) = defmacro_arity_fields(&err);
9648 assert_eq!(head, MacroDefHead::Defmacro);
9649 assert_eq!(arity, 3);
9650 }
9651
9652 #[test]
9653 fn defmacro_arity_in_defpoint_template_emits_same_variant() {
9654 // `defpoint-template` shares `macro_def_from` with `defmacro`
9655 // (all three head keywords route through the same gate). Pins
9656 // that the lift fires path-uniformly across the three head
9657 // keywords AND that the variant's `head` slot carries the
9658 // actual head literal — `defpoint-template`, not `defmacro`
9659 // — so an LSP that wants to point at "your defpoint-template
9660 // form is missing elements" gains the head as data.
9661 let mut e = Expander::new();
9662 let err = e
9663 .expand_program(read("(defpoint-template t)").unwrap())
9664 .expect_err("defpoint-template arity gate must error");
9665 let (head, arity) = defmacro_arity_fields(&err);
9666 assert_eq!(head, MacroDefHead::DefpointTemplate);
9667 assert_eq!(arity, 2);
9668 }
9669
9670 #[test]
9671 fn defmacro_arity_in_defcheck_emits_same_variant() {
9672 // Sibling of the defpoint-template test — `defcheck` is the
9673 // third head keyword `macro_def_from` recognizes. All three
9674 // route through the same arity gate and now reject too-short
9675 // forms with the same structural variant.
9676 let mut e = Expander::new();
9677 let err = e
9678 .expand_program(read("(defcheck)").unwrap())
9679 .expect_err("defcheck arity gate must error");
9680 let (head, arity) = defmacro_arity_fields(&err);
9681 assert_eq!(head, MacroDefHead::Defcheck);
9682 assert_eq!(arity, 1);
9683 }
9684
9685 #[test]
9686 fn defmacro_arity_substitute_and_bytecode_paths_agree() {
9687 // Path-uniform rejection: the SAME source emits the SAME
9688 // structural variant under both expansion strategies. The
9689 // arity gate fires inside `macro_def_from` BEFORE either
9690 // strategy's expansion path runs; so both `Expander::new()`
9691 // (bytecode) and `Expander::new_substitute_only()`
9692 // (substitute) reject the SAME malformed defmacro at the
9693 // SAME gate. Sibling of
9694 // `non_symbol_param_substitute_and_bytecode_paths_agree` and
9695 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9696 let src = "(defmacro f)";
9697 let mut subst = Expander::new_substitute_only();
9698 let mut bytecode = Expander::new();
9699 let err_subst = subst
9700 .expand_program(read(src).unwrap())
9701 .expect_err("substitute must error");
9702 let err_byte = bytecode
9703 .expand_program(read(src).unwrap())
9704 .expect_err("bytecode must error");
9705 assert_eq!(
9706 defmacro_arity_fields(&err_subst),
9707 (MacroDefHead::Defmacro, 2)
9708 );
9709 assert_eq!(
9710 defmacro_arity_fields(&err_byte),
9711 (MacroDefHead::Defmacro, 2)
9712 );
9713 }
9714
9715 #[test]
9716 fn defmacro_arity_message_renders_legacy_substring_with_arity() {
9717 // End-to-end through Display — pins the rendered diagnostic
9718 // consumers see today (REPL, `tatara-check`) AND the new
9719 // `(got {arity} elements, need 4)` clause. The legacy
9720 // `"(defmacro name (params) body) required"` substring
9721 // rides through verbatim. Tools that pattern-match on the
9722 // variant gain structural binding to `head` / `arity`.
9723 let mut e = Expander::new();
9724 let err = e
9725 .expand_program(read("(defmacro f)").unwrap())
9726 .expect_err("defmacro arity gate must error");
9727 assert_eq!(
9728 format!("{err}"),
9729 "compile error in defmacro: (defmacro name (params) body) required \
9730 (got 2 elements, need 4)"
9731 );
9732 }
9733
9734 #[test]
9735 fn defmacro_arity_position_is_none_today() {
9736 // Negative control for the future-spans move: until `Sexp`
9737 // carries source positions, `position()` on `LispError`
9738 // returns `None` for this variant. A future run that gives
9739 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9740 // this test gives that change a deliberate fail-before/pass-
9741 // after delta. Parallel to
9742 // `non_symbol_param_position_is_none_today` and
9743 // `rest_param_missing_name_position_is_none_today`.
9744 let mut e = Expander::new();
9745 let err = e
9746 .expand_program(read("(defmacro)").unwrap())
9747 .expect_err("defmacro arity gate must error");
9748 assert_eq!(err.position(), None);
9749 }
9750
9751 #[test]
9752 fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
9753 // Negative control: a defmacro with exactly 4 elements (head
9754 // + name + params + body) passes the arity gate. Pins that
9755 // the lift is scoped to the arity-deficient case, not to
9756 // every defmacro form. After this test, a regression that
9757 // tightens the arity gate to >= 5 (e.g. spuriously requiring
9758 // a docstring slot) fails-loudly here.
9759 let mut e = Expander::new();
9760 let out = e
9761 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9762 .expect("well-formed defmacro must succeed");
9763 assert_eq!(out[0], Sexp::int(42));
9764 }
9765
9766 /// Helper for the defmacro-non-symbol-name tests — pins variant
9767 /// shape and carries the head / got up to the assert site for
9768 /// legibility. Sibling of `defmacro_arity_fields`,
9769 /// `non_symbol_param_fields`, and `rest_param_missing_name_fields`.
9770 fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
9771 match err {
9772 LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
9773 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9774 }
9775 }
9776
9777 #[test]
9778 fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
9779 // `(defmacro 5 () body)` — the form passes the arity gate
9780 // (4 elements) but list[1] is `5`, not a symbol. Pins variant
9781 // identity AND that `head == "defmacro"` AND that `got ==
9782 // "5"`. A regression that re-inlines the legacy
9783 // `LispError::Compile { form: "defmacro", message: "expected
9784 // name symbol" }` shape (which named the failure mode but
9785 // not the offending element) fails-loudly here.
9786 let mut e = Expander::new();
9787 let err = e
9788 .expand_program(read("(defmacro 5 () body)").unwrap())
9789 .expect_err("defmacro non-symbol name gate must error");
9790 let (head, got) = defmacro_non_symbol_name_fields(&err);
9791 assert_eq!(head, MacroDefHead::Defmacro);
9792 assert_eq!(got, "5");
9793 }
9794
9795 #[test]
9796 fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
9797 // `(defmacro :foo () body)` — list[1] is the keyword `:foo`,
9798 // not a symbol. Pins that `Sexp::Display` for
9799 // `Atom::Keyword(s)` writes `:s` and the variant's `got` slot
9800 // carries the keyword form unchanged. An LSP that wants to
9801 // surface "you wrote `:foo` where a name symbol was expected"
9802 // gains the literal keyword value as data, no source re-parse
9803 // required.
9804 let mut e = Expander::new();
9805 let err = e
9806 .expand_program(read("(defmacro :foo () body)").unwrap())
9807 .expect_err("defmacro non-symbol name gate must error");
9808 let (head, got) = defmacro_non_symbol_name_fields(&err);
9809 assert_eq!(head, MacroDefHead::Defmacro);
9810 assert_eq!(got, ":foo");
9811 }
9812
9813 #[test]
9814 fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
9815 // `(defmacro "name" () body)` — list[1] is the string
9816 // literal `"name"`, not a symbol. Pins that `Sexp::Display`
9817 // for `Atom::String(s)` writes `"s"` (with quotes) and the
9818 // variant's `got` slot carries the quoted form unchanged.
9819 let mut e = Expander::new();
9820 let err = e
9821 .expand_program(read("(defmacro \"name\" () body)").unwrap())
9822 .expect_err("defmacro non-symbol name gate must error");
9823 let (head, got) = defmacro_non_symbol_name_fields(&err);
9824 assert_eq!(head, MacroDefHead::Defmacro);
9825 assert_eq!(got, "\"name\"");
9826 }
9827
9828 #[test]
9829 fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
9830 // `(defmacro (nested) () body)` — list[1] is a nested list,
9831 // not a symbol. Pins that `Sexp::Display` for a list writes
9832 // `(elements)` and the variant's `got` slot carries the
9833 // parenthesized form unchanged.
9834 let mut e = Expander::new();
9835 let err = e
9836 .expand_program(read("(defmacro (nested) () body)").unwrap())
9837 .expect_err("defmacro non-symbol name gate must error");
9838 let (head, got) = defmacro_non_symbol_name_fields(&err);
9839 assert_eq!(head, MacroDefHead::Defmacro);
9840 assert_eq!(got, "(nested)");
9841 }
9842
9843 #[test]
9844 fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
9845 // `defpoint-template` shares `macro_def_from` with `defmacro`
9846 // (all three head keywords route through the same gate).
9847 // Pins that the lift fires path-uniformly across the three
9848 // head keywords AND that the variant's `head` slot carries
9849 // the actual head literal — `defpoint-template`, not
9850 // `defmacro` — so an LSP that wants to point at "your
9851 // defpoint-template form's name slot isn't a symbol" gains
9852 // the head as data.
9853 let mut e = Expander::new();
9854 let err = e
9855 .expand_program(read("(defpoint-template 7 () body)").unwrap())
9856 .expect_err("defpoint-template non-symbol name gate must error");
9857 let (head, got) = defmacro_non_symbol_name_fields(&err);
9858 assert_eq!(head, MacroDefHead::DefpointTemplate);
9859 assert_eq!(got, "7");
9860 }
9861
9862 #[test]
9863 fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
9864 // Sibling for the `defcheck` head — third head keyword
9865 // `macro_def_from` recognizes. Rounds out the three-head-
9866 // keyword coverage so the lift is path-uniform across
9867 // `defmacro` / `defpoint-template` / `defcheck`.
9868 let mut e = Expander::new();
9869 let err = e
9870 .expand_program(read("(defcheck :k () body)").unwrap())
9871 .expect_err("defcheck non-symbol name gate must error");
9872 let (head, got) = defmacro_non_symbol_name_fields(&err);
9873 assert_eq!(head, MacroDefHead::Defcheck);
9874 assert_eq!(got, ":k");
9875 }
9876
9877 #[test]
9878 fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
9879 // Path-uniform rejection: the SAME source emits the SAME
9880 // structural variant under both expansion strategies. The
9881 // name-symbol gate fires inside `macro_def_from` BEFORE
9882 // either expansion strategy runs, so the gate is naturally
9883 // path-uniform; pinning it gives a regression that drifts
9884 // either strategy's handling of non-symbol-name defmacros (or
9885 // makes one strategy accept what the other rejects) a fail-
9886 // before/pass-after edge. Sibling of
9887 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
9888 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
9889 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9890 let src = "(defmacro 5 () body)";
9891 let mut subst = Expander::new_substitute_only();
9892 let mut bytecode = Expander::new();
9893 let err_subst = subst
9894 .expand_program(read(src).unwrap())
9895 .expect_err("substitute must error");
9896 let err_byte = bytecode
9897 .expand_program(read(src).unwrap())
9898 .expect_err("bytecode must error");
9899 assert_eq!(
9900 defmacro_non_symbol_name_fields(&err_subst),
9901 (MacroDefHead::Defmacro, "5")
9902 );
9903 assert_eq!(
9904 defmacro_non_symbol_name_fields(&err_byte),
9905 (MacroDefHead::Defmacro, "5")
9906 );
9907 }
9908
9909 #[test]
9910 fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
9911 // End-to-end through Display — pins the rendered diagnostic
9912 // consumers see today (REPL, `tatara-check`) AND the new
9913 // `, got {got}` clause. The legacy `"expected name symbol"`
9914 // substring rides through verbatim; the prefix matches the
9915 // legacy `Compile { form: "defmacro", message: "expected name
9916 // symbol" }` byte-for-byte. Tools that pattern-match on the
9917 // variant gain structural binding to `head` / `got`.
9918 let mut e = Expander::new();
9919 let err = e
9920 .expand_program(read("(defmacro 5 () body)").unwrap())
9921 .expect_err("defmacro non-symbol name gate must error");
9922 assert_eq!(
9923 format!("{err}"),
9924 "compile error in defmacro: expected name symbol, got 5"
9925 );
9926 }
9927
9928 #[test]
9929 fn defmacro_non_symbol_name_position_is_none_today() {
9930 // Negative control for the future-spans move: until `Sexp`
9931 // carries source positions, `position()` on `LispError`
9932 // returns `None` for this variant. A future run that gives
9933 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9934 // this test gives that change a deliberate fail-before/pass-
9935 // after delta. Parallel to
9936 // `defmacro_arity_position_is_none_today`,
9937 // `non_symbol_param_position_is_none_today`, and
9938 // `rest_param_missing_name_position_is_none_today`.
9939 let mut e = Expander::new();
9940 let err = e
9941 .expand_program(read("(defmacro 5 () body)").unwrap())
9942 .expect_err("defmacro non-symbol name gate must error");
9943 assert_eq!(err.position(), None);
9944 }
9945
9946 #[test]
9947 fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
9948 // Negative control: a defmacro whose name slot IS a symbol
9949 // passes the name-symbol gate. Pins that the lift is scoped
9950 // to the non-symbol-name case, not to every defmacro form.
9951 // After this test, a regression that tightens the gate to
9952 // reject e.g. kebab-cased names fails-loudly here.
9953 let mut e = Expander::new();
9954 let out = e
9955 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9956 .expect("well-formed defmacro must succeed");
9957 assert_eq!(out[0], Sexp::int(42));
9958 }
9959
9960 #[test]
9961 fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
9962 // Pins the gate ordering: a 4-element defmacro whose name
9963 // slot is non-symbol fires `DefmacroNonSymbolName`, NOT
9964 // `DefmacroArity`. The arity gate (>= 4 elements) admits
9965 // this form; the name-symbol gate is the next checkpoint.
9966 // A regression that swaps the gate ordering (e.g. checks
9967 // name-symbol before arity, so `(defmacro 5)` would emit
9968 // `DefmacroNonSymbolName` instead of `DefmacroArity`) fails-
9969 // loudly here.
9970 let mut e = Expander::new();
9971 let err = e
9972 .expand_program(read("(defmacro 5 () body)").unwrap())
9973 .expect_err("name-symbol gate must error");
9974 assert!(
9975 matches!(err, LispError::DefmacroNonSymbolName { .. }),
9976 "expected DefmacroNonSymbolName, got: {err:?}"
9977 );
9978
9979 let err_arity = e
9980 .expand_program(read("(defmacro 5)").unwrap())
9981 .expect_err("arity gate must error");
9982 assert!(
9983 matches!(err_arity, LispError::DefmacroArity { .. }),
9984 "expected DefmacroArity (arity < 4 short-circuits before name check), \
9985 got: {err_arity:?}"
9986 );
9987 }
9988
9989 /// Helper for the defmacro-non-list-params tests — pins variant
9990 /// shape and carries the head / got up to the assert site for
9991 /// legibility. Sibling of `defmacro_arity_fields`,
9992 /// `defmacro_non_symbol_name_fields`, `non_symbol_param_fields`,
9993 /// and `rest_param_missing_name_fields`.
9994 fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
9995 match err {
9996 LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
9997 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9998 }
9999 }
10000
10001 #[test]
10002 fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
10003 // `(defmacro f x body)` — the form passes both the arity gate
10004 // (4 elements) AND the name-symbol gate (`f` is a symbol) but
10005 // list[2] is the symbol `x`, not a list. Pins variant identity
10006 // AND that `head == "defmacro"` AND that `got == "x"`. A
10007 // regression that re-inlines the legacy `LispError::Compile {
10008 // form: "defmacro", message: "expected param list" }` shape
10009 // (which named the failure mode but not the offending element)
10010 // fails-loudly here.
10011 let mut e = Expander::new();
10012 let err = e
10013 .expand_program(read("(defmacro f x body)").unwrap())
10014 .expect_err("defmacro non-list params gate must error");
10015 let (head, got) = defmacro_non_list_params_fields(&err);
10016 assert_eq!(head, MacroDefHead::Defmacro);
10017 assert_eq!(got, "x");
10018 }
10019
10020 #[test]
10021 fn defmacro_non_list_params_with_int_emits_structural_variant() {
10022 // `(defmacro f 5 body)` — list[2] is `5`, not a list. Pins
10023 // that `Sexp::Display` for `Atom::Int(n)` writes `n` and the
10024 // variant's `got` slot carries the integer form unchanged. An
10025 // LSP that surfaces "you wrote `5` where a param list was
10026 // expected" gains the literal value as data, no source
10027 // re-parse required.
10028 let mut e = Expander::new();
10029 let err = e
10030 .expand_program(read("(defmacro f 5 body)").unwrap())
10031 .expect_err("defmacro non-list params gate must error");
10032 let (head, got) = defmacro_non_list_params_fields(&err);
10033 assert_eq!(head, MacroDefHead::Defmacro);
10034 assert_eq!(got, "5");
10035 }
10036
10037 #[test]
10038 fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
10039 // `(defmacro f :foo body)` — list[2] is the keyword `:foo`,
10040 // not a list. Pins that `Sexp::Display` for `Atom::Keyword(s)`
10041 // writes `:s` and the variant's `got` slot carries the
10042 // keyword form unchanged.
10043 let mut e = Expander::new();
10044 let err = e
10045 .expand_program(read("(defmacro f :foo body)").unwrap())
10046 .expect_err("defmacro non-list params gate must error");
10047 let (head, got) = defmacro_non_list_params_fields(&err);
10048 assert_eq!(head, MacroDefHead::Defmacro);
10049 assert_eq!(got, ":foo");
10050 }
10051
10052 #[test]
10053 fn defmacro_non_list_params_with_string_emits_structural_variant() {
10054 // `(defmacro f "params" body)` — list[2] is the string literal
10055 // `"params"`, not a list. Pins that `Sexp::Display` for
10056 // `Atom::String(s)` writes `"s"` (with quotes) and the
10057 // variant's `got` slot carries the quoted form unchanged.
10058 let mut e = Expander::new();
10059 let err = e
10060 .expand_program(read("(defmacro f \"params\" body)").unwrap())
10061 .expect_err("defmacro non-list params gate must error");
10062 let (head, got) = defmacro_non_list_params_fields(&err);
10063 assert_eq!(head, MacroDefHead::Defmacro);
10064 assert_eq!(got, "\"params\"");
10065 }
10066
10067 #[test]
10068 fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
10069 // `defpoint-template` shares `macro_def_from` with `defmacro`
10070 // (all three head keywords route through the same gate).
10071 // Pins that the lift fires path-uniformly across the three
10072 // head keywords AND that the variant's `head` slot carries
10073 // the actual head literal — `defpoint-template`, not
10074 // `defmacro` — so an LSP that wants to point at "your
10075 // defpoint-template form's param-list slot isn't a list"
10076 // gains the head as data.
10077 let mut e = Expander::new();
10078 let err = e
10079 .expand_program(read("(defpoint-template t x body)").unwrap())
10080 .expect_err("defpoint-template non-list params gate must error");
10081 let (head, got) = defmacro_non_list_params_fields(&err);
10082 assert_eq!(head, MacroDefHead::DefpointTemplate);
10083 assert_eq!(got, "x");
10084 }
10085
10086 #[test]
10087 fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
10088 // Sibling for the `defcheck` head — third head keyword
10089 // `macro_def_from` recognizes. Rounds out the three-head-
10090 // keyword coverage so the lift is path-uniform across
10091 // `defmacro` / `defpoint-template` / `defcheck`.
10092 let mut e = Expander::new();
10093 let err = e
10094 .expand_program(read("(defcheck c 7 body)").unwrap())
10095 .expect_err("defcheck non-list params gate must error");
10096 let (head, got) = defmacro_non_list_params_fields(&err);
10097 assert_eq!(head, MacroDefHead::Defcheck);
10098 assert_eq!(got, "7");
10099 }
10100
10101 #[test]
10102 fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
10103 // Path-uniform rejection: the SAME source emits the SAME
10104 // structural variant under both expansion strategies. The
10105 // param-list gate fires inside `macro_def_from` BEFORE either
10106 // expansion strategy runs, so the gate is naturally path-
10107 // uniform; pinning it gives a regression that drifts either
10108 // strategy's handling of non-list-params defmacros (or makes
10109 // one strategy accept what the other rejects) a fail-before/
10110 // pass-after edge. Sibling of
10111 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
10112 // `defmacro_non_symbol_name_substitute_and_bytecode_paths_agree`,
10113 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
10114 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
10115 let src = "(defmacro f x body)";
10116 let mut subst = Expander::new_substitute_only();
10117 let mut bytecode = Expander::new();
10118 let err_subst = subst
10119 .expand_program(read(src).unwrap())
10120 .expect_err("substitute must error");
10121 let err_byte = bytecode
10122 .expand_program(read(src).unwrap())
10123 .expect_err("bytecode must error");
10124 assert_eq!(
10125 defmacro_non_list_params_fields(&err_subst),
10126 (MacroDefHead::Defmacro, "x")
10127 );
10128 assert_eq!(
10129 defmacro_non_list_params_fields(&err_byte),
10130 (MacroDefHead::Defmacro, "x")
10131 );
10132 }
10133
10134 #[test]
10135 fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
10136 // End-to-end through Display — pins the rendered diagnostic
10137 // consumers see today (REPL, `tatara-check`) AND the new
10138 // `, got {got}` clause. The legacy `"expected param list"`
10139 // substring rides through verbatim; the prefix matches the
10140 // legacy `Compile { form: "defmacro", message: "expected
10141 // param list" }` byte-for-byte. Tools that pattern-match on
10142 // the variant gain structural binding to `head` / `got`.
10143 let mut e = Expander::new();
10144 let err = e
10145 .expand_program(read("(defmacro f x body)").unwrap())
10146 .expect_err("defmacro non-list params gate must error");
10147 assert_eq!(
10148 format!("{err}"),
10149 "compile error in defmacro: expected param list, got x"
10150 );
10151 }
10152
10153 #[test]
10154 fn defmacro_non_list_params_position_is_none_today() {
10155 // Negative control for the future-spans move: until `Sexp`
10156 // carries source positions, `position()` on `LispError`
10157 // returns `None` for this variant. A future run that gives
10158 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
10159 // this test gives that change a deliberate fail-before/pass-
10160 // after delta. Parallel to
10161 // `defmacro_arity_position_is_none_today`,
10162 // `defmacro_non_symbol_name_position_is_none_today`,
10163 // `non_symbol_param_position_is_none_today`, and
10164 // `rest_param_missing_name_position_is_none_today`.
10165 let mut e = Expander::new();
10166 let err = e
10167 .expand_program(read("(defmacro f x body)").unwrap())
10168 .expect_err("defmacro non-list params gate must error");
10169 assert_eq!(err.position(), None);
10170 }
10171
10172 #[test]
10173 fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
10174 // Negative control: a defmacro whose param-list slot IS a
10175 // list passes the param-list gate. Pins that the lift is
10176 // scoped to the non-list-params case, not to every defmacro
10177 // form. After this test, a regression that tightens the gate
10178 // to reject e.g. empty param lists fails-loudly here.
10179 let mut e = Expander::new();
10180 let out = e
10181 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10182 .expect("well-formed defmacro must succeed");
10183 assert_eq!(out[0], Sexp::int(42));
10184 }
10185
10186 #[test]
10187 fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
10188 // Pins the gate ordering: a 4-element defmacro whose name
10189 // slot IS a symbol but whose param-list slot is non-list
10190 // fires `DefmacroNonListParams`, NOT `DefmacroNonSymbolName`.
10191 // The name-symbol gate admits this form; the param-list gate
10192 // is the next checkpoint. A regression that swaps the gate
10193 // ordering (e.g. checks param-list before name-symbol, so
10194 // `(defmacro 5 x body)` would emit `DefmacroNonListParams`
10195 // instead of `DefmacroNonSymbolName`) fails-loudly here.
10196 let mut e = Expander::new();
10197 let err = e
10198 .expand_program(read("(defmacro f x body)").unwrap())
10199 .expect_err("param-list gate must error");
10200 assert!(
10201 matches!(err, LispError::DefmacroNonListParams { .. }),
10202 "expected DefmacroNonListParams, got: {err:?}"
10203 );
10204
10205 let err_name = e
10206 .expand_program(read("(defmacro 5 x body)").unwrap())
10207 .expect_err("name-symbol gate must error");
10208 assert!(
10209 matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
10210 "expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
10211 got: {err_name:?}"
10212 );
10213 }
10214
10215 #[test]
10216 fn defmacro_non_list_params_fires_after_arity_gate_passes() {
10217 // Pins the full gate ordering: a 4-element defmacro whose
10218 // first three slots are head/symbol/non-list fires
10219 // `DefmacroNonListParams`, NOT `DefmacroArity`. The arity
10220 // gate (>= 4 elements) admits this form; the name-symbol
10221 // gate admits the symbol; the param-list gate is the third
10222 // checkpoint. A regression that drifts the gate sequence
10223 // (e.g. fires `DefmacroArity` for a 4-element form) fails-
10224 // loudly here. Parallel to
10225 // `defmacro_non_symbol_name_fires_after_arity_gate_passes`
10226 // — together they pin the full
10227 // arity → name-symbol → param-list ordering inside
10228 // `macro_def_from`.
10229 let mut e = Expander::new();
10230 let err = e
10231 .expand_program(read("(defmacro f x body)").unwrap())
10232 .expect_err("param-list gate must error");
10233 assert!(
10234 matches!(err, LispError::DefmacroNonListParams { .. }),
10235 "expected DefmacroNonListParams, got: {err:?}"
10236 );
10237
10238 let err_arity = e
10239 .expand_program(read("(defmacro f x)").unwrap())
10240 .expect_err("arity gate must error");
10241 assert!(
10242 matches!(err_arity, LispError::DefmacroArity { .. }),
10243 "expected DefmacroArity (arity < 4 short-circuits before param-list check), \
10244 got: {err_arity:?}"
10245 );
10246 }
10247
10248 #[test]
10249 fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
10250 // Negative control: `&rest` is a symbol (`Atom::Symbol("&rest")`)
10251 // at the parser level, so `as_symbol()` succeeds for it. The
10252 // `NonSymbolParam` variant does NOT fire on the `&rest` marker
10253 // itself; the dedicated `&rest needs a name` rejection (a
10254 // separate failure mode in this cluster) handles malformed
10255 // rest-param shapes. Pins that the lift is scoped to
10256 // non-symbol elements at param-list positions, not to
10257 // every malformed-param shape.
10258 let mut e = Expander::new();
10259 let out = e
10260 .expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
10261 .expect("&rest with name must succeed");
10262 assert_eq!(out[0], parse("(list 1 2 3)"));
10263 }
10264
10265 #[test]
10266 fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
10267 // End-to-end through the Display impl — pins the rendered diagnostic
10268 // a downstream tool sees today (REPL, tatara-check). The shape is
10269 // parallel to the existing `TypeMismatch` variant: form, expected
10270 // shape, offending literal — all three slots present.
10271 let mut e = Expander::new();
10272 let err = e
10273 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
10274 .expect_err("non-symbol target must error");
10275 assert_eq!(
10276 format!("{err}"),
10277 "compile error in ,: expected symbol, got (list 1 2)"
10278 );
10279 }
10280
10281 // ── template_invariant_violation: structural lift ───────────────
10282 //
10283 // The four byte-identical inline `LispError::Compile { form:
10284 // macro_name.into(), message: <invariant> }` triples in `apply_compiled`
10285 // (Subst-bad-index, Splice-bad-index, EndList-empty-stack,
10286 // final-no-value gates) were lifted to `template_invariant_violation`,
10287 // and the helper's emission was promoted from `LispError::Compile`-
10288 // shape to the structural `LispError::TemplateInvariant { macro_name,
10289 // kind: TemplateInvariantKind }` variant. The index payload of the
10290 // Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
10291 // / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
10292 // kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
10293 // is structurally unrepresentable. Display matches the legacy
10294 // `Compile`-shaped diagnostic byte-for-byte via the closed-set
10295 // `TemplateInvariantKind::message()` projection so authoring-tool
10296 // substring greps see no drift across the lift.
10297 //
10298 // The tests below pin: (a) the helper produces the structural
10299 // `LispError::TemplateInvariant` variant with `macro_name` and `kind`
10300 // first-class; (b) the Subst / Splice gates thread the bad index
10301 // through the typed variants `SubstBadIndex(usize)` / `SpliceBadIndex(usize)`
10302 // unchanged; (c) the two REACHABLE invariant-violation paths through
10303 // `apply_compiled` — Subst with out-of-bounds idx, Splice with
10304 // out-of-bounds idx — route through the helper end-to-end (the
10305 // EndList / no-value paths are guarded by `last_mut().unwrap()`
10306 // ahead of `pop().ok_or_else()` and are not reachable through any
10307 // single CompiledTemplate; they remain defensive against future
10308 // changes to the stack discipline); (d) the legacy Display
10309 // rendering matches byte-for-byte across the lift; (e) positive
10310 // controls: a well-formed CompiledTemplate routes PAST the helper
10311 // cleanly, and unrelated macro errors (missing-required-arg) do
10312 // NOT route through the helper.
10313
10314 #[test]
10315 fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
10316 // Direct unit test of the helper: a fixed macro_name and a
10317 // `TemplateInvariantKind` produce a `LispError::TemplateInvariant`
10318 // variant with the macro_name in the `macro_name` slot and the
10319 // kind passed through verbatim in the `kind` slot. A regression
10320 // that drifts the variant (e.g., back to `LispError::Compile`)
10321 // or swaps the slot positions fails-loudly here.
10322 let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
10323 match err {
10324 LispError::TemplateInvariant { macro_name, kind } => {
10325 assert_eq!(macro_name, "test-macro");
10326 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10327 }
10328 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10329 }
10330 }
10331
10332 #[test]
10333 fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
10334 // The Subst gate's `usize` idx lives INSIDE the
10335 // `TemplateInvariantKind::SubstBadIndex(usize)` variant rather
10336 // than being substring-rendered into a free-form `message`
10337 // slot. Pin that the helper threads the bad index through the
10338 // typed variant unchanged; a regression that drops the index
10339 // payload (e.g., via a `usize -> ()` projection) fails here.
10340 let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
10341 match err {
10342 LispError::TemplateInvariant { macro_name, kind } => {
10343 assert_eq!(macro_name, "wrap");
10344 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10345 }
10346 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10347 }
10348 }
10349
10350 #[test]
10351 fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
10352 // Hand-crafted CompiledTemplate with a Subst(99) op against
10353 // an empty params list: `args_by_index` has length 0, so
10354 // `.get(99)` returns None and the `ok_or_else` triggers
10355 // through the helper. Fail-before-pass-after: this same input
10356 // pre-lift went through `LispError::Compile { form: macro_name,
10357 // message: format!("compiled template referenced bad param
10358 // index {idx}") }`; post-lift it routes through
10359 // `template_invariant_violation` and emits the structural
10360 // `TemplateInvariant { macro_name, kind: SubstBadIndex(99) }`
10361 // variant with the bad index threaded through as typed data.
10362 let tmpl = CompiledTemplate {
10363 ops: vec![TemplateOp::Subst(99)],
10364 };
10365 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10366 .expect_err("bad idx must error");
10367 match err {
10368 LispError::TemplateInvariant { macro_name, kind } => {
10369 assert_eq!(macro_name, "test-macro");
10370 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10371 }
10372 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10373 }
10374 }
10375
10376 #[test]
10377 fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
10378 // Hand-crafted CompiledTemplate with a Splice(42) op against
10379 // an empty params list. Sibling of the Subst-bad-idx test;
10380 // pins the Splice gate routes through the helper with the
10381 // typed `SpliceBadIndex(42)` kind carrying the bad index.
10382 let tmpl = CompiledTemplate {
10383 ops: vec![TemplateOp::Splice(42)],
10384 };
10385 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10386 .expect_err("bad splice idx must error");
10387 match err {
10388 LispError::TemplateInvariant { macro_name, kind } => {
10389 assert_eq!(macro_name, "call-macro");
10390 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10391 }
10392 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10393 }
10394 }
10395
10396 #[test]
10397 fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
10398 // End-to-end through the `LispError` Display impl — pins the
10399 // rendered diagnostic byte-for-byte: `"compile error in
10400 // test-macro: compiled template referenced bad param index 99"`.
10401 // Authoring tools that substring-grep the rendered diagnostic
10402 // (`tatara-check`'s diagnostic capture, REPL substring-greps)
10403 // see no drift across the lift. Parallel to how
10404 // `compile_named_non_symbol_name_renders_legacy_compile_shape`
10405 // pins the sibling-file (compile.rs) lift's Display contract.
10406 let tmpl = CompiledTemplate {
10407 ops: vec![TemplateOp::Subst(99)],
10408 };
10409 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10410 .expect_err("bad idx must error");
10411 assert_eq!(
10412 format!("{err}"),
10413 "compile error in test-macro: compiled template referenced bad param index 99"
10414 );
10415 }
10416
10417 #[test]
10418 fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
10419 // Sibling Display test for the Splice gate. Pins the message
10420 // byte-for-byte through the `LispError` Display impl: `"compile
10421 // error in call-macro: compiled template referenced bad splice
10422 // index 42"`.
10423 let tmpl = CompiledTemplate {
10424 ops: vec![TemplateOp::Splice(42)],
10425 };
10426 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10427 .expect_err("bad splice idx must error");
10428 assert_eq!(
10429 format!("{err}"),
10430 "compile error in call-macro: compiled template referenced bad splice index 42"
10431 );
10432 }
10433
10434 #[test]
10435 fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
10436 // Positive control: a CompiledTemplate produced by the
10437 // bytecode compiler (`compile_template`) for a well-formed
10438 // macro never references an out-of-bounds index nor
10439 // unbalances the stack, so `apply_compiled` routes PAST the
10440 // helper cleanly. A regression that fires the helper on
10441 // well-formed bytecode (e.g., off-by-one in the index
10442 // resolution) would fail here. End-to-end through the public
10443 // `Expander` surface so the test exercises the same code
10444 // path users see.
10445 let mut e = Expander::new();
10446 let out = e
10447 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10448 .expect("well-formed macro expansion must not fire template-invariant-violation");
10449 assert_eq!(out.len(), 1);
10450 assert_eq!(out[0], Sexp::int(42));
10451 }
10452
10453 #[test]
10454 fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
10455 // Negative control: the `missing_macro_arg` gate in the shared
10456 // positional binder (`MacroParams::bind`) fires BEFORE the bytecode
10457 // loop runs,
10458 // so a missing required arg routes through
10459 // `LispError::MissingMacroArg`, NOT through
10460 // `template_invariant_violation`. Pins the helper is
10461 // precisely scoped to bytecode-runtime invariant violations
10462 // (Subst / Splice / stack gates), not to macro-call arity
10463 // errors (the latter has its own structural variant). A
10464 // regression that conflates the two gate clusters would
10465 // route this case through `Compile { ... }` instead of
10466 // `MissingMacroArg` and fail-loudly here.
10467 let mut e = Expander::new();
10468 let err = e
10469 .expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
10470 .expect_err("missing required arg must error");
10471 assert!(
10472 matches!(err, LispError::MissingMacroArg { .. }),
10473 "expected MissingMacroArg, got: {err:?}"
10474 );
10475 }
10476
10477 // ── resolve_bound_arg: bytecode-runtime bound-arg-by-index lookup ──
10478 //
10479 // `resolve_bound_arg(args_by_index, idx, macro_name, kind)` lifts the
10480 // `args_by_index.get(*idx).ok_or_else(|| template_invariant_violation(
10481 // macro_name, KIND(*idx)))?` projection that recurred at BOTH the
10482 // `TemplateOp::Subst` and `TemplateOp::Splice` arms inside
10483 // `apply_compiled`. The arms differ in the kind constructor
10484 // (`SubstBadIndex` vs. `SpliceBadIndex`) and in their post-lookup
10485 // verb (clone+push vs. splice-coerce), but the lookup-and-reject
10486 // prelude is byte-identical modulo the constructor. These tests
10487 // pin the lifted helper's contract directly; the existing
10488 // `apply_compiled_*_bad_idx_*` tests are the path-uniformity
10489 // guards proving both production arms route through it without
10490 // behavior drift.
10491
10492 #[test]
10493 fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
10494 // For an in-range index, the helper returns `Ok(&args[idx])`
10495 // borrowed VERBATIM — same pointer as `args_by_index.get(idx)`.
10496 // Pins the borrow-not-clone contract: a regression that drifts
10497 // the helper to clone+return (`Result<Sexp>` instead of
10498 // `Result<&Sexp>`) would allocate per lookup at the production
10499 // `Subst`/`Splice` hot path. The kind constructor must NOT
10500 // fire on the success path (`FnOnce`'s lazy semantics) — pin
10501 // that the test passes a constructor that would panic if
10502 // called, asserting the helper short-circuits before invoking
10503 // it on the in-range arm.
10504 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10505 let got = resolve_bound_arg(&args, 1, "m", |_| {
10506 panic!("kind constructor must not fire on the in-range path")
10507 })
10508 .expect("in-range lookup must succeed");
10509 assert!(
10510 std::ptr::eq(got, &args[1]),
10511 "resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
10512 );
10513 assert_eq!(*got, Sexp::int(2));
10514 }
10515
10516 #[test]
10517 fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
10518 // For an out-of-range index, the helper raises the structural
10519 // `LispError::TemplateInvariant` variant with the caller-
10520 // supplied `SubstBadIndex` kind constructor applied to the bad
10521 // index. Pins the post-lift emission shape (variant identity
10522 // + the kind constructor threaded with the actual idx); a
10523 // regression that drops the idx payload (e.g., via a `usize ->
10524 // ()` projection) or hard-codes a different kind at the helper
10525 // boundary fails-loudly here. Fail-before-pass-after: this
10526 // assert is contradicted by the pre-lift code path (which
10527 // never called `resolve_bound_arg` because it didn't exist),
10528 // ratifies the post-lift one.
10529 let args: Vec<Sexp> = Vec::new();
10530 let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
10531 .expect_err("out-of-range lookup must error");
10532 match err {
10533 LispError::TemplateInvariant { macro_name, kind } => {
10534 assert_eq!(macro_name, "test-macro");
10535 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10536 }
10537 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10538 }
10539 }
10540
10541 #[test]
10542 fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
10543 // Path-uniformity for the per-call-site kind constructor: the
10544 // SAME out-of-range idx via the `SpliceBadIndex` constructor
10545 // emits `kind: SpliceBadIndex(7)` — distinct from the sibling
10546 // `SubstBadIndex(7)` variant. Pins that the constructor is
10547 // chosen per call site (not hard-coded at the helper boundary),
10548 // closing the structural matrix `{Subst, Splice} × {in-range,
10549 // out-of-range}` the two production arms span across the
10550 // bytecode-runtime's bound-arg-by-index reads. A regression
10551 // that hard-codes a single kind at the helper boundary would
10552 // emit the same variant identity for both call sites and
10553 // fail-loudly here.
10554 let args: Vec<Sexp> = Vec::new();
10555 let err = resolve_bound_arg(
10556 &args,
10557 7,
10558 "test-macro",
10559 TemplateInvariantKind::SpliceBadIndex,
10560 )
10561 .expect_err("out-of-range lookup must error");
10562 match err {
10563 LispError::TemplateInvariant { macro_name, kind } => {
10564 assert_eq!(macro_name, "test-macro");
10565 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
10566 }
10567 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10568 }
10569 }
10570
10571 #[test]
10572 fn resolve_bound_arg_threads_macro_name_verbatim() {
10573 // Path-uniformity for the `macro_name` slot: the helper threads
10574 // the caller's borrow into the variant's owned `String` slot
10575 // verbatim. Pin two distinct macro names route through with no
10576 // mutual interference — a regression that hard-codes a single
10577 // macro_name at the helper boundary or swaps the parameter
10578 // ordering fails-loudly here. Same posture as
10579 // `compiler_spec_io_err_threads_each_stage_through_unchanged`
10580 // pins the typed `stage` slot in the disk-persistence sibling
10581 // lift.
10582 let args: Vec<Sexp> = Vec::new();
10583 for name in ["wrap", "call-macro", "obs"] {
10584 let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
10585 .expect_err("out-of-range lookup must error");
10586 match err {
10587 LispError::TemplateInvariant { macro_name, kind } => {
10588 assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
10589 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10590 }
10591 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10592 }
10593 }
10594 }
10595
10596 #[test]
10597 fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
10598 // Edge case: idx 0 with a single-element args_by_index returns
10599 // `Ok(&args[0])`. Pins the lower-bound of the in-range surface
10600 // — a regression that off-by-ones the lookup (e.g., `get(idx +
10601 // 1)` or `get(idx).filter(|_| idx > 0)`) would fail here.
10602 // Sibling to the upper-bound `resolve_bound_arg_out_of_range_
10603 // with_subst_kind_emits_typed_invariant` test.
10604 let args = vec![Sexp::int(42)];
10605 let got = resolve_bound_arg(&args, 0, "m", |_| {
10606 panic!("kind constructor must not fire on the in-range path")
10607 })
10608 .expect("idx-0 lookup must succeed");
10609 assert!(std::ptr::eq(got, &args[0]));
10610 assert_eq!(*got, Sexp::int(42));
10611 }
10612
10613 #[test]
10614 fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
10615 // Edge case: idx `len - 1` is the highest valid index. Pin
10616 // that it routes through the success arm (NOT the error arm),
10617 // closing the in-range surface end-to-end with the lower-
10618 // bound sibling. A regression that off-by-ones the upper
10619 // bound (e.g., `get(idx).filter(|_| idx < args.len() - 1)`)
10620 // would fail here.
10621 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10622 let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
10623 panic!("kind constructor must not fire on the in-range path")
10624 })
10625 .expect("last-element lookup must succeed");
10626 assert!(std::ptr::eq(got, args.last().unwrap()));
10627 assert_eq!(*got, Sexp::int(3));
10628 }
10629
10630 #[test]
10631 fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
10632 // Boundary case: idx EQUAL to `args.len()` is out-of-range
10633 // (since `get` is 0-indexed). Pin that this routes through
10634 // the error arm with the kind constructor applied to the
10635 // EXACT idx that was tried. A regression that off-by-ones
10636 // the boundary (e.g., admits `idx == len`) would fail here.
10637 // This is the canonical off-by-one trap; the helper's
10638 // contract pins it at the variant-construction boundary.
10639 let args = vec![Sexp::int(1)];
10640 let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
10641 .expect_err("idx == len must error");
10642 match err {
10643 LispError::TemplateInvariant { kind, .. } => {
10644 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
10645 }
10646 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10647 }
10648 }
10649
10650 #[test]
10651 fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
10652 // Boundary case: an empty `args_by_index` slice rejects every
10653 // idx (including 0). Pin that the helper's emission shape is
10654 // uniform regardless of which out-of-range idx fires the
10655 // rejection — `SubstBadIndex(0)` for an empty slice is the
10656 // bytecode-runtime mirror of a zero-arity macro template
10657 // referencing the 0-th param.
10658 let args: Vec<Sexp> = Vec::new();
10659 let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
10660 .expect_err("empty slice rejects every idx");
10661 match err {
10662 LispError::TemplateInvariant { macro_name, kind } => {
10663 assert_eq!(macro_name, "zero-arity");
10664 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10665 }
10666 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10667 }
10668 }
10669
10670 #[test]
10671 fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
10672 // End-to-end path-uniformity: a `Subst(99)` op against a
10673 // zero-arity macro routes the bytecode-runtime's bound-arg
10674 // lookup through `resolve_bound_arg` with the
10675 // `SubstBadIndex` constructor, emitting the structural
10676 // variant with `kind: SubstBadIndex(99)`. The pre-lift
10677 // sibling test `apply_compiled_subst_bad_idx_routes_through_
10678 // template_invariant_violation` pins that the same input
10679 // routes through `template_invariant_violation`; this test
10680 // pins that BOTH still hold under the post-lift composition
10681 // — `resolve_bound_arg` calls `template_invariant_violation`
10682 // internally on the rejection arm. A regression that drifts
10683 // ONE arm's projection from the other (e.g., swaps the
10684 // constructor at one call site, or short-circuits the
10685 // composition) would fail here.
10686 let tmpl = CompiledTemplate {
10687 ops: vec![TemplateOp::Subst(99)],
10688 };
10689 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10690 .expect_err("bad idx must error");
10691 match err {
10692 LispError::TemplateInvariant { macro_name, kind } => {
10693 assert_eq!(macro_name, "test-macro");
10694 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10695 }
10696 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10697 }
10698 }
10699
10700 #[test]
10701 fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
10702 // Sibling end-to-end path-uniformity for the `Splice` arm:
10703 // the post-lift composition routes a `Splice(42)` op through
10704 // `resolve_bound_arg` with the `SpliceBadIndex` constructor,
10705 // emitting `kind: SpliceBadIndex(42)`. Together with the
10706 // `Subst` sibling test above, this pins the structural matrix
10707 // `{Subst, Splice} × resolve_bound_arg` end-to-end through
10708 // the public `apply_compiled` surface, so a regression that
10709 // drifts ONE arm's kind constructor (e.g., the `Splice` arm
10710 // accidentally emits `SubstBadIndex` after a copy-paste
10711 // refactor) fails-loudly here.
10712 let tmpl = CompiledTemplate {
10713 ops: vec![TemplateOp::Splice(42)],
10714 };
10715 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10716 .expect_err("bad splice idx must error");
10717 match err {
10718 LispError::TemplateInvariant { macro_name, kind } => {
10719 assert_eq!(macro_name, "call-macro");
10720 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10721 }
10722 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10723 }
10724 }
10725
10726 #[test]
10727 fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
10728 // Positive control: a `Subst(0)` op against a one-arg macro
10729 // routes through `resolve_bound_arg`'s success arm and the
10730 // `Subst` post-lookup verb (clone + push) emits the bound
10731 // value verbatim. Pin the post-lift composition's success
10732 // path: the clone-and-push semantics live at the call site
10733 // (NOT in `resolve_bound_arg`, which only borrows), and a
10734 // regression that drifts the borrow contract (e.g., the
10735 // helper clones internally + the call site clones again)
10736 // would still pass observationally but would regress the
10737 // hot-path allocation count.
10738 let params = MacroParams {
10739 required: vec!["x".into()],
10740 optional: Vec::new(),
10741 rest: None,
10742 };
10743 let tmpl = CompiledTemplate {
10744 ops: vec![TemplateOp::Subst(0)],
10745 };
10746 let out = apply_compiled("id", ¶ms, &tmpl, &[Sexp::int(42)])
10747 .expect("in-range Subst must succeed");
10748 assert_eq!(out, Sexp::int(42));
10749 }
10750
10751 // ── current_builder_mut: the bytecode-runtime top-of-stack projection ──
10752 //
10753 // `current_builder_mut(stack)` lifts the `stack.last_mut().unwrap()`
10754 // projection that appeared at FOUR sites inside `apply_compiled`'s
10755 // op-loop (Literal, Subst, Splice, post-EndList parent-fold) into ONE
10756 // named primitive. The expect message names the bytecode-runtime
10757 // invariant ("at least one stack frame during op-loop") so a
10758 // regression that drifts the loop's frame management (a new op that
10759 // pops without pushing, an early-return that bypasses EndList's
10760 // stack-check) surfaces a NAMED panic rather than a silent unwrap.
10761 // These tests pin the projection's contract directly; the existing
10762 // `apply_compiled_*` tests + the cross-strategy `expansion_layers_
10763 // agree_on_output_and_cache_wins` benchmark are the path-uniformity
10764 // guards proving the four sites still emit the canonical bytecode-
10765 // runtime output across the lift.
10766
10767 #[test]
10768 fn current_builder_mut_returns_the_top_frame_reference() {
10769 // The simplest projection: on a single-frame stack, the helper
10770 // returns a `&mut Vec<Sexp>` pointing at THAT frame. Pin the
10771 // projection's identity end-to-end via a push that mutates
10772 // through the borrow and observe the original frame carries the
10773 // pushed value back.
10774 let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
10775 current_builder_mut(&mut stack).push(Sexp::int(42));
10776 assert_eq!(stack.len(), 1);
10777 assert_eq!(stack[0], vec![Sexp::int(42)]);
10778 }
10779
10780 #[test]
10781 fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
10782 // The projection MUST target the topmost frame, not the bottom
10783 // one — every `TemplateOp::BeginList` pushes a fresh frame the
10784 // subsequent ops emit into, and a regression that flipped the
10785 // projection to `first_mut` (or to a fixed bottom-frame
10786 // reference) would silently smear all op output into the
10787 // outermost result. Pin path-uniformity with the bytecode-
10788 // runtime's mid-list emission posture: with three frames on
10789 // the stack (one outer + two pending lists), the helper
10790 // returns a borrow into the third frame, leaving frames 0 and
10791 // 1 untouched.
10792 let mut stack: Vec<Vec<Sexp>> = vec![
10793 vec![Sexp::symbol("outer")],
10794 vec![Sexp::symbol("inner-a")],
10795 vec![Sexp::symbol("inner-b")],
10796 ];
10797 current_builder_mut(&mut stack).push(Sexp::int(99));
10798 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10799 assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
10800 assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
10801 }
10802
10803 #[test]
10804 fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
10805 // Structural identity binding the lift to its pre-lift inline
10806 // shape: `current_builder_mut(&mut stack)` IS
10807 // `stack.last_mut().unwrap()` — the same `&mut Vec<Sexp>`,
10808 // pointing at the same allocation. Pin pointer equality via
10809 // `std::ptr::eq` on the projected slice's `as_ptr()` to rule
10810 // out any allocation-shape drift across the lift.
10811 let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
10812 let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
10813 let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
10814 assert!(
10815 std::ptr::eq(via_lift_ptr, via_inline_ptr),
10816 "current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
10817 );
10818 }
10819
10820 #[test]
10821 #[should_panic(
10822 expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
10823 )]
10824 fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
10825 // The bytecode-runtime invariant is encoded in the expect
10826 // message: an empty stack at the projection boundary is
10827 // structurally unreachable inside `apply_compiled`'s op-loop
10828 // (the outermost frame is seeded at entry and every BeginList
10829 // / EndList pair preserves the count >= 1). Pin that the
10830 // NAMED invariant fires on the failure path so a regression
10831 // that drifts the loop's frame management surfaces a
10832 // diagnostic-grade panic rather than a silent unwrap over
10833 // `None`. Authoring tools / future debug-mode hooks can
10834 // pattern-match on the named invariant string instead of
10835 // tracking down an unnamed unwrap site.
10836 let mut empty: Vec<Vec<Sexp>> = Vec::new();
10837 let _ = current_builder_mut(&mut empty);
10838 }
10839
10840 #[test]
10841 fn current_builder_mut_routes_apply_compiled_literal_emit() {
10842 // End-to-end path-uniformity guard: a single-op program
10843 // `TemplateOp::Literal(s)` routes its push through
10844 // `current_builder_mut(&mut stack)` and the literal lands in
10845 // the outermost frame. After the op-loop completes the outer
10846 // `stack.pop().FinalNoValue` gate sees a non-empty top frame
10847 // containing exactly one element, which `apply_compiled`'s
10848 // tail (`top.len() == 1 { top.remove(0) }`) projects back as
10849 // the bound value. Pre-lift the same emission ran through
10850 // `stack.last_mut().unwrap().push(s.clone())`; post-lift it
10851 // runs through `current_builder_mut(&mut stack).push(s.clone())`
10852 // — the byte-identical outcome pins that the Literal arm's
10853 // routing through the new projection preserves the bytecode-
10854 // runtime's emission shape.
10855 let tmpl = CompiledTemplate {
10856 ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
10857 };
10858 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10859 .expect("literal-only template must succeed");
10860 assert_eq!(out, Sexp::symbol("hello"));
10861 }
10862
10863 #[test]
10864 fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
10865 // End-to-end path-uniformity guard for the post-EndList
10866 // parent-fold push: `(BeginList, Literal(a), Literal(b),
10867 // EndList)` builds an inner frame `[a, b]`, pops it on
10868 // EndList, then pushes `Sexp::List([a, b])` into the parent
10869 // (outer) frame via `current_builder_mut`. The outermost
10870 // `stack.pop()` then surfaces that list as the bound result.
10871 // Pre-lift the parent-fold push ran through
10872 // `stack.last_mut().unwrap().push(Sexp::List(items))`; post-
10873 // lift it runs through `current_builder_mut(&mut stack).
10874 // push(Sexp::List(items))` — pin the byte-identical outcome
10875 // so a regression that drifts the parent-fold target (e.g.,
10876 // pushes onto the just-popped frame's pointer instead of the
10877 // new top) fails loudly here.
10878 let tmpl = CompiledTemplate {
10879 ops: vec![
10880 TemplateOp::BeginList,
10881 TemplateOp::Literal(Sexp::symbol("a")),
10882 TemplateOp::Literal(Sexp::symbol("b")),
10883 TemplateOp::EndList,
10884 ],
10885 };
10886 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10887 .expect("BeginList/EndList template must succeed");
10888 assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
10889 }
10890
10891 #[test]
10892 fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
10893 // End-to-end path-uniformity guard for BOTH index-reading
10894 // arms routing through the lifted projection: a one-required +
10895 // one-rest macro `(call f &rest args)` with template
10896 // `(BeginList, Subst(0), Splice(1), EndList)` exercises both
10897 // Subst's clone-and-push AND Splice's splice-value-into
10898 // emit-paths against the current builder via
10899 // `current_builder_mut(&mut stack)`. The composed result is
10900 // `(foo 1 2 3)` — Subst lands the bound `f = foo` and
10901 // Splice flattens `args = (1 2 3)` — and the byte-identical
10902 // outcome pins that BOTH Subst and Splice arms' emits route
10903 // through the SHARED projection. Sibling to
10904 // `apply_compiled_splice_in_range_routes_past_resolve_bound
10905 // _arg_into_splice_value_into` which already exercises this
10906 // shape end-to-end; the addition here is the path-uniformity
10907 // anchor for the `current_builder_mut` lift specifically.
10908 let params = MacroParams {
10909 required: vec!["f".into()],
10910 optional: Vec::new(),
10911 rest: Some("args".into()),
10912 };
10913 let tmpl = CompiledTemplate {
10914 ops: vec![
10915 TemplateOp::BeginList,
10916 TemplateOp::Subst(0),
10917 TemplateOp::Splice(1),
10918 TemplateOp::EndList,
10919 ],
10920 };
10921 let out = apply_compiled(
10922 "call",
10923 ¶ms,
10924 &tmpl,
10925 &[
10926 Sexp::symbol("foo"),
10927 Sexp::int(1),
10928 Sexp::int(2),
10929 Sexp::int(3),
10930 ],
10931 )
10932 .expect("Subst + Splice template must succeed");
10933 assert_eq!(
10934 out,
10935 Sexp::List(vec![
10936 Sexp::symbol("foo"),
10937 Sexp::int(1),
10938 Sexp::int(2),
10939 Sexp::int(3),
10940 ])
10941 );
10942 }
10943
10944 #[test]
10945 fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
10946 // Positive control for the `Splice` arm: a `&rest` macro that
10947 // splices a bound list routes through `resolve_bound_arg`'s
10948 // success arm and the `Splice` post-lookup verb
10949 // (`splice_value_into`) flattens the bound list into the
10950 // builder. Pin the composition's success path end-to-end:
10951 // the bound `Sexp::List([1, 2, 3])` at idx 1 flattens into
10952 // the outer builder's `(call 1 2 3)` shape — the same output
10953 // `rest_param_splices_with_at` pins through the public
10954 // surface, here pinned with the bytecode-runtime composition
10955 // exposed directly.
10956 let params = MacroParams {
10957 required: vec!["f".into()],
10958 optional: Vec::new(),
10959 rest: Some("args".into()),
10960 };
10961 let tmpl = CompiledTemplate {
10962 ops: vec![
10963 TemplateOp::BeginList,
10964 TemplateOp::Subst(0),
10965 TemplateOp::Splice(1),
10966 TemplateOp::EndList,
10967 ],
10968 };
10969 let out = apply_compiled(
10970 "call",
10971 ¶ms,
10972 &tmpl,
10973 &[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
10974 )
10975 .expect("in-range Splice must succeed");
10976 assert_eq!(
10977 out,
10978 Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
10979 );
10980 }
10981
10982 // ── pop_builder_frame: the bytecode-runtime stack-frame consume ─────
10983 //
10984 // `pop_builder_frame(stack, macro_name, kind)` lifts the
10985 // `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
10986 // kind))?` chain that recurred at two sites inside `apply_compiled`
10987 // (the `EndList` arm + the post-loop final pop) into ONE named
10988 // primitive on the bytecode-runtime's stack-frame algebra. Sibling of
10989 // `current_builder_mut` (the top-frame-borrow projection — the same
10990 // `&mut Vec<Vec<Sexp>>` consumed at the borrow face) and
10991 // `resolve_bound_arg` (the bound-arg-by-index lookup primitive that
10992 // also routes through `TemplateInvariantKind` as the per-call-site
10993 // rejection identity). These tests pin the primitive's contract
10994 // directly; the existing `apply_compiled_*` tests are the path-
10995 // uniformity guards proving the two sites route through it without
10996 // behavior drift.
10997
10998 #[test]
10999 fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
11000 // Happy path: a two-frame stack pops the topmost frame off and
11001 // returns it AS-IS while shrinking the stack by exactly one
11002 // element. Pin both the return value (the popped frame's
11003 // contents are byte-identical to what was pushed) AND the
11004 // mutation (`stack.len()` drops from 2 to 1).
11005 let mut stack: Vec<Vec<Sexp>> = vec![
11006 vec![Sexp::symbol("outer")],
11007 vec![Sexp::int(1), Sexp::int(2)],
11008 ];
11009 let popped =
11010 pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
11011 .expect("non-empty stack must pop cleanly");
11012 assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
11013 assert_eq!(stack.len(), 1);
11014 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
11015 }
11016
11017 #[test]
11018 fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
11019 // Empty-stack rejection: an empty stack flows through the
11020 // `EndListEmptyStack` kind constructor into a structural
11021 // `LispError::TemplateInvariant { macro_name, kind:
11022 // EndListEmptyStack }` variant. Fail-before-pass-after: pre-lift
11023 // the same input would route through the inline
11024 // `stack.pop().ok_or_else(|| template_invariant_violation(_,
11025 // EndListEmptyStack))?` chain at the `EndList` arm; post-lift
11026 // it routes through ONE named primitive both pop-emitting
11027 // sites share, and a regression that drops the kind threading
11028 // (e.g. unifies both kinds into one constant) fails here.
11029 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11030 let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
11031 .expect_err("empty stack must reject");
11032 match err {
11033 LispError::TemplateInvariant { macro_name, kind } => {
11034 assert_eq!(macro_name, "wrap");
11035 assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
11036 }
11037 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11038 }
11039 }
11040
11041 #[test]
11042 fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
11043 // Path-uniformity across the closed-set of pop-emitting kinds:
11044 // the same primitive threads `FinalNoValue` verbatim through
11045 // its `TemplateInvariantKind` slot, with the macro_name
11046 // identity preserved across the call boundary. Sibling of the
11047 // `EndListEmptyStack` test above — together they pin the
11048 // primitive's closed-set posture (BOTH reachable
11049 // pop-emitting kinds route through the same primitive, neither
11050 // is hard-coded), so a future `TemplateInvariantKind` variant
11051 // added for a new pop-emitting op (e.g. a hypothetical
11052 // `EndManyEmptyStack`) extends the primitive's reachability
11053 // mechanically by passing the new kind through the same slot.
11054 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11055 let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
11056 .expect_err("empty stack must reject");
11057 match err {
11058 LispError::TemplateInvariant { macro_name, kind } => {
11059 assert_eq!(macro_name, "id");
11060 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
11061 }
11062 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11063 }
11064 }
11065
11066 #[test]
11067 fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
11068 // Closed-set posture check: even kinds the production
11069 // `apply_compiled` op-loop ROUTES through `resolve_bound_arg`
11070 // rather than `pop_builder_frame` (the indexed `SubstBadIndex(_)`
11071 // / `SpliceBadIndex(_)` siblings) MUST compose correctly with
11072 // this primitive's typed slot — they are NOT reachable here
11073 // from the production loop, but `TemplateInvariantKind` does
11074 // not distinguish "indexed" kinds from "stack-gate" kinds at
11075 // the helper's signature, so a regression that special-cases
11076 // one kind family would be a silent type-narrowing the closed-
11077 // set typed enum was lifted to prevent. Pin the universal
11078 // routing: ANY kind variant feeds through the primitive's
11079 // `kind` slot identically. Sibling assertion to
11080 // `template_invariant_violation_threads_subst_idx_through_typed_variant`
11081 // — same compose-the-kind-into-the-variant contract, one
11082 // composition step further down the substrate stack.
11083 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11084 let err = pop_builder_frame(
11085 &mut empty,
11086 "compose",
11087 TemplateInvariantKind::SubstBadIndex(42),
11088 )
11089 .expect_err("empty stack must reject regardless of kind family");
11090 match err {
11091 LispError::TemplateInvariant { macro_name, kind } => {
11092 assert_eq!(macro_name, "compose");
11093 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
11094 }
11095 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11096 }
11097 }
11098
11099 #[test]
11100 fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
11101 // Structural-identity binding the lift to its pre-lift inline
11102 // shape: `pop_builder_frame(stack, macro_name, kind)` IS
11103 // `stack.pop().ok_or_else(|| template_invariant_violation(
11104 // macro_name, kind))?` — both reachable arms (success +
11105 // failure) must produce byte-identical outcomes. The success
11106 // arm checks the popped Vec contents AND the post-pop stack
11107 // length match the inline path; the failure arm checks the
11108 // emitted variant's identity AND its `macro_name` / `kind`
11109 // slots match the inline path. A regression that drifts the
11110 // primitive's projection (e.g. a `stack.swap_remove(_)` typo,
11111 // or a kind-rewrite at the helper boundary) fails on at least
11112 // one of the two arms.
11113 // Success arm:
11114 let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11115 let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11116 let via_lift = pop_builder_frame(
11117 &mut stack_lift,
11118 "macro",
11119 TemplateInvariantKind::EndListEmptyStack,
11120 )
11121 .expect("non-empty stack pops cleanly through lift");
11122 let via_inline = stack_inline.pop().ok_or_else(|| {
11123 template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
11124 });
11125 assert_eq!(
11126 via_lift,
11127 via_inline.unwrap(),
11128 "popped frame must be byte-identical across lift vs inline"
11129 );
11130 assert_eq!(
11131 stack_lift.len(),
11132 stack_inline.len(),
11133 "post-pop stack length must be byte-identical across lift vs inline"
11134 );
11135 // Failure arm:
11136 let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
11137 let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
11138 let err_lift = pop_builder_frame(
11139 &mut empty_lift,
11140 "macro",
11141 TemplateInvariantKind::FinalNoValue,
11142 )
11143 .expect_err("empty stack rejects through lift");
11144 let err_inline = empty_inline
11145 .pop()
11146 .ok_or_else(|| {
11147 template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
11148 })
11149 .expect_err("empty stack rejects through inline");
11150 match (err_lift, err_inline) {
11151 (
11152 LispError::TemplateInvariant {
11153 macro_name: m_lift,
11154 kind: k_lift,
11155 },
11156 LispError::TemplateInvariant {
11157 macro_name: m_inline,
11158 kind: k_inline,
11159 },
11160 ) => {
11161 assert_eq!(m_lift, m_inline);
11162 assert_eq!(k_lift, k_inline);
11163 }
11164 (l, i) => panic!(
11165 "expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
11166 ),
11167 }
11168 }
11169
11170 #[test]
11171 fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
11172 // End-to-end path-uniformity guard for the `EndList` arm: a
11173 // `(BeginList, Literal(x), EndList)` program pushes a child
11174 // frame, populates it with one literal, then routes the child
11175 // frame OUT of the stack via `pop_builder_frame` (kind
11176 // `EndListEmptyStack` — unreachable on this valid input, but
11177 // the kind threads through the primitive identically). The
11178 // post-pop verb (`Sexp::List(items)` push into the parent via
11179 // `current_builder_mut`) yields the same `Sexp::List([x])`
11180 // shape the consumer projected pre-lift. Pin the byte-
11181 // identical outcome so a regression that drifts the EndList
11182 // arm's routing (e.g. swaps the kind constructor, or routes
11183 // through a different stack-mutating primitive) fails loudly
11184 // here. Sibling of
11185 // `current_builder_mut_routes_apply_compiled_end_list_parent_fold`
11186 // — that test pins the parent-fold PUSH; this test pins the
11187 // child-frame POP that immediately precedes it. Together the
11188 // two close the EndList arm's path-uniformity across the
11189 // pop-then-push composition.
11190 let tmpl = CompiledTemplate {
11191 ops: vec![
11192 TemplateOp::BeginList,
11193 TemplateOp::Literal(Sexp::symbol("only")),
11194 TemplateOp::EndList,
11195 ],
11196 };
11197 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11198 .expect("BeginList/EndList one-literal template must succeed");
11199 assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
11200 }
11201
11202 #[test]
11203 fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
11204 // End-to-end path-uniformity guard for the post-loop final
11205 // pop: a single-op `Literal(s)` program emits `s` into the
11206 // outermost (seed) frame, then the post-loop tail routes the
11207 // seed frame OUT via `pop_builder_frame` (kind `FinalNoValue`
11208 // — unreachable on this valid input). The post-pop arity gate
11209 // (`top.len() == 1 { top.remove(0) }`) projects the literal
11210 // back as the bound value. Pre-lift the same emission ran
11211 // through the inline `stack.pop().ok_or_else(|| template_
11212 // invariant_violation(_, FinalNoValue))?` chain at the
11213 // post-loop tail; post-lift it routes through ONE named
11214 // primitive the EndList arm ALSO routes through, and the
11215 // single-literal outcome is byte-identical across both code
11216 // paths. Sibling of
11217 // `current_builder_mut_routes_apply_compiled_literal_emit` —
11218 // that test pins the EMIT into the seed frame; this test pins
11219 // the POP of the same seed frame at the post-loop tail.
11220 // Together the two close the Literal-only program's path-
11221 // uniformity across the emit-then-consume composition.
11222 let tmpl = CompiledTemplate {
11223 ops: vec![TemplateOp::Literal(Sexp::int(123))],
11224 };
11225 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11226 .expect("literal-only template must succeed");
11227 assert_eq!(out, Sexp::int(123));
11228 }
11229
11230 // ── MacroParams: the typed param-list primitive ─────────────────────
11231 //
11232 // `parse_params` now yields a `MacroParams { required, optional, rest }`
11233 // whose shape makes the canonical lambda-list ordering (required →
11234 // optional → rest, "&rest is last + at-most-one", "&optional at most
11235 // once") structural rather than a construction discipline a `Vec<Param>`
11236 // only happened to uphold. These tests pin the parser's mapping into the
11237 // typed shape, the flat-index contract `names()` exposes to the template
11238 // bytecode, and the single positional binder `bind()` both expansion
11239 // strategies now route through. The end-to-end `rest_param_splices_with_at`
11240 // and `compiled_template_matches_substitute_path` tests above are the
11241 // path-uniformity guards proving both strategies still agree.
11242
11243 #[test]
11244 fn parse_params_maps_required_then_rest_into_typed_shape() {
11245 // `(a b &rest c)` — two required, one rest. The rest name lands in
11246 // the `Option`, never in `required`.
11247 let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
11248 assert_eq!(
11249 params,
11250 MacroParams {
11251 required: vec!["a".into(), "b".into()],
11252 optional: Vec::new(),
11253 rest: Some("c".into()),
11254 }
11255 );
11256 }
11257
11258 #[test]
11259 fn parse_params_rest_absent_leaves_none() {
11260 // `(x y)` — no `&rest`, so `rest` is structurally `None`. There is
11261 // no representation in which a rest-less list carries a stray rest.
11262 let params = parse_params(&read("x y").unwrap()).unwrap();
11263 assert_eq!(
11264 params,
11265 MacroParams {
11266 required: vec!["x".into(), "y".into()],
11267 optional: Vec::new(),
11268 rest: None,
11269 }
11270 );
11271 }
11272
11273 #[test]
11274 fn parse_params_maps_optional_section_between_required_and_rest() {
11275 // `(a &optional b c &rest d)` — the canonical lambda-list order. `a`
11276 // is required, `b`/`c` are optional, `d` is rest. The `&optional`
11277 // marker switches collection from `required` to `optional`; `&rest`
11278 // remains terminal.
11279 let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
11280 assert_eq!(
11281 params,
11282 MacroParams {
11283 required: vec!["a".into()],
11284 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11285 rest: Some("d".into()),
11286 }
11287 );
11288 }
11289
11290 #[test]
11291 fn parse_params_optional_with_no_rest_leaves_rest_none() {
11292 // `(&optional x)` — a leading `&optional` (zero required) with no
11293 // rest. `required` is empty, `x` is the sole optional, `rest` None.
11294 let params = parse_params(&read("&optional x").unwrap()).unwrap();
11295 assert_eq!(
11296 params,
11297 MacroParams {
11298 required: Vec::new(),
11299 optional: vec![OptionalParam::bare("x")],
11300 rest: None,
11301 }
11302 );
11303 }
11304
11305 #[test]
11306 fn parse_params_rejects_repeated_optional_marker() {
11307 // `(a &optional b &optional c)` — a second `&optional` is
11308 // unrepresentable (one flat optional section), so the gate REJECTS
11309 // rather than binding args to a marker symbol named `&optional`. The
11310 // two marker positions (1 and 3) are named.
11311 let err = parse_params(&read("a &optional b &optional c").unwrap())
11312 .expect_err("repeated &optional must error");
11313 assert!(
11314 matches!(
11315 err,
11316 LispError::OptionalMarkerRepeated {
11317 first_position: 1,
11318 second_position: 3,
11319 }
11320 ),
11321 "expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
11322 );
11323 }
11324
11325 #[test]
11326 fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
11327 // `(&rest xs &optional y)` — `&rest <name>` is terminal, so the
11328 // `&optional y` tail is REJECTED as trailing tokens (not silently
11329 // dropped, and not a repeated-optional error: the rest gate fires
11330 // first). Pins the interaction the prior run (3627426) signposted.
11331 let err = parse_params(&read("&rest xs &optional y").unwrap())
11332 .expect_err("tokens after &rest <name> must error");
11333 assert!(
11334 matches!(err, LispError::RestParamTrailingTokens { .. }),
11335 "expected RestParamTrailingTokens, got: {err:?}"
11336 );
11337 }
11338
11339 #[test]
11340 fn names_are_required_then_optional_then_rest_in_flat_index_order() {
11341 // The flat-index contract the bytecode `Subst(idx)`/`Splice(idx)`
11342 // depends on: required names at 0.., then optional names, then the
11343 // rest name last.
11344 let params = MacroParams {
11345 required: vec!["a".into(), "b".into()],
11346 optional: vec![OptionalParam::bare("c")],
11347 rest: Some("d".into()),
11348 };
11349 assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
11350 // Optional names occupy the indices immediately after the required run.
11351 assert_eq!(params.names()[params.required.len()], "c");
11352 // The rest name is last, after required + optional — i.e. at the
11353 // structural `fixed_arity()` boundary the typed primitive names.
11354 assert_eq!(params.names()[params.fixed_arity()], "d");
11355 }
11356
11357 // ── `MacroParams::{REST_MARKER, OPTIONAL_MARKER,
11358 // LAMBDA_LIST_KEYWORD_LEAD}` — the typed CL lambda-list-keyword
11359 // marker algebra ────────────────────────────────────────────────
11360 //
11361 // The three `pub const`s close the Common-Lisp lambda-list keyword
11362 // family at the typed [`MacroParams`] algebra: `REST_MARKER` and
11363 // `OPTIONAL_MARKER` are the two `&'static str` markers the parser's
11364 // typed dispatch specialises on; `LAMBDA_LIST_KEYWORD_LEAD` is the
11365 // canonical `'&'` char shared as the LEAD byte of both markers.
11366 // Pre-lift the same two `&'static str` markers lived as two inline
11367 // `s == "..."` comparisons at `parse_params` — post-lift both
11368 // comparisons route through the typed constants so a delimiter swap
11369 // (e.g. a Racket-compat `#!rest` port, a Clojure-compat `&` port)
11370 // lands at ONE constant on the typed algebra.
11371 //
11372 // These pins cover: (a) the exact `&'static str` / `char` values,
11373 // (b) the structural round-trip law binding each `&'static str`
11374 // marker to its shared `char` LEAD byte, (c) the pairwise
11375 // disjointness of the two `&'static str` markers, (d) the cross-
11376 // axis disjointness of the `char` LEAD byte against every sibling
11377 // outer-marker `char` the substrate's other closed-set algebras
11378 // specialise on, and (e) the path-uniformity of `parse_params`
11379 // through the typed constants.
11380
11381 #[test]
11382 fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
11383 // Pins the constant's exact `&'static str` bytes so a typo
11384 // (`"&res"`, `"&Rest"`, `"&rst"`) or an accidental redefinition
11385 // surfaces immediately. Sibling-shape pin to
11386 // `macro_params_optional_marker_projects_canonical_ampersand_optional_str`
11387 // on the peer `&optional` axis.
11388 assert_eq!(
11389 MacroParams::REST_MARKER,
11390 "&rest",
11391 "MacroParams::REST_MARKER drifted from the substrate- \
11392 canonical CL lambda-list `&rest` marker — the parser's \
11393 `parse_params` rest-slot dispatch AND every downstream \
11394 authoring / rendering surface binds to this ONE typed \
11395 constant.",
11396 );
11397 }
11398
11399 #[test]
11400 fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
11401 // Pins the constant's exact `&'static str` bytes so a typo
11402 // (`"&opt"`, `"&Optional"`, `"&option"`) or an accidental
11403 // redefinition surfaces immediately. Sibling-shape pin to
11404 // `macro_params_rest_marker_projects_canonical_ampersand_rest_str`
11405 // on the peer `&rest` axis.
11406 assert_eq!(
11407 MacroParams::OPTIONAL_MARKER,
11408 "&optional",
11409 "MacroParams::OPTIONAL_MARKER drifted from the substrate- \
11410 canonical CL lambda-list `&optional` marker — the parser's \
11411 `parse_params` optional-section dispatch AND every \
11412 downstream authoring / rendering surface binds to this \
11413 ONE typed constant.",
11414 );
11415 }
11416
11417 #[test]
11418 fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
11419 // Pins the constant's exact `char` value so a typo (`'#'`,
11420 // `'@'`, `'!'`) or an accidental redefinition surfaces
11421 // immediately. Sibling-shape pin to
11422 // `atom_keyword_marker_lead_projects_canonical_colon_char`,
11423 // `atom_bool_literal_lead_projects_canonical_hash_char`,
11424 // `atom_str_delimiter_projects_canonical_double_quote_char`
11425 // on the peer per-role LEAD-byte axes across the substrate's
11426 // closed-set outer algebras.
11427 assert_eq!(
11428 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11429 '&',
11430 "LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
11431 canonical `&` LEAD byte — the CL lambda-list-keyword \
11432 family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
11433 typed constant as their common LEAD byte.",
11434 );
11435 }
11436
11437 #[test]
11438 fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
11439 // STRUCTURAL ROUND-TRIP CONTRACT: the `&'static str` marker
11440 // `MacroParams::REST_MARKER` starts with the `char` LEAD byte
11441 // `MacroParams::LAMBDA_LIST_KEYWORD_LEAD` — the projection law
11442 // binding the two typed constants on the [`MacroParams`]
11443 // algebra. A regression that renames the `&'static str` (e.g.
11444 // to Racket's `"#!rest"` keyword-args) OR the `char` (e.g. to
11445 // `'#'` for the `#!` shebang lead) without updating the other
11446 // fails HERE. Sibling-shape pin to
11447 // `atom_keyword_marker_lead_prefixes_keyword_marker` on the
11448 // peer `Atom`-algebra Keyword-prefix LEAD-byte axis.
11449 assert!(
11450 MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11451 "MacroParams::REST_MARKER `{}` does NOT start with \
11452 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11453 typed constants have drifted apart on the [`MacroParams`] \
11454 algebra; the CL lambda-list-keyword family disjointness \
11455 contract can no longer bind to ONE shared LEAD byte.",
11456 MacroParams::REST_MARKER,
11457 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11458 );
11459 }
11460
11461 #[test]
11462 fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
11463 // STRUCTURAL ROUND-TRIP CONTRACT: peer to
11464 // `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
11465 // on the `&optional` axis. Both `&'static str` markers of the
11466 // CL lambda-list-keyword family MUST share the canonical `char`
11467 // LEAD byte so the typed-marker disjointness contract can bind
11468 // to ONE shared LEAD byte on the [`MacroParams`] algebra.
11469 assert!(
11470 MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11471 "MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
11472 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11473 typed constants have drifted apart on the [`MacroParams`] \
11474 algebra; the CL lambda-list-keyword family disjointness \
11475 contract can no longer bind to ONE shared LEAD byte.",
11476 MacroParams::OPTIONAL_MARKER,
11477 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11478 );
11479 }
11480
11481 #[test]
11482 fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
11483 // PAIRWISE DISJOINTNESS PIN: the two `&'static str` markers on
11484 // the CL lambda-list-keyword algebra MUST differ so the
11485 // parser's typed dispatch cascade at `parse_params` (which
11486 // tests `REST_MARKER` FIRST, then `OPTIONAL_MARKER`) cannot
11487 // silently route both dispatches through the same arm. A
11488 // regression that aliases the two markers (e.g. both to
11489 // `"&rest"` after a typo) would silently drop the optional
11490 // section's structural distinction — every `&optional` name
11491 // would misclassify as a rest-slot marker.
11492 assert_ne!(
11493 MacroParams::REST_MARKER,
11494 MacroParams::OPTIONAL_MARKER,
11495 "REST_MARKER and OPTIONAL_MARKER collide — the parser's \
11496 typed dispatch cascade at `parse_params` can no longer \
11497 distinguish the rest-slot boundary from the optional- \
11498 section boundary.",
11499 );
11500 }
11501
11502 #[test]
11503 fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
11504 // CROSS-AXIS DISJOINTNESS PIN: `MacroParams::LAMBDA_LIST_KEYWORD_LEAD`
11505 // MUST NOT alias any sibling outer-marker `char` on the
11506 // substrate's other closed-set algebras — the Atom-payload
11507 // markers (`STR_DELIMITER`, `STR_ESCAPE_LEAD`,
11508 // `KEYWORD_MARKER_LEAD`, `BOOL_LITERAL_LEAD`), the paired list
11509 // delimiters (`Sexp::LIST_OPEN` / `Sexp::LIST_CLOSE`), the
11510 // paired line-comment delimiters (`Sexp::COMMENT_LEAD` /
11511 // `Sexp::COMMENT_TERM`), every `QuoteForm::lead_char`
11512 // projection, AND `QuoteForm::SPLICE_DISCRIMINATOR`. A
11513 // collision would silently break the reader's outer dispatch:
11514 // an `&`-prefixed bare atom `&rest` / `&optional` would collide
11515 // with whichever marker it aliased. Sibling-shape pin to
11516 // `atom_keyword_marker_lead_distinct_from_every_other_algebra_marker`
11517 // on the peer `Atom`-algebra Keyword-prefix LEAD-byte axis —
11518 // pins the SAME shape on the CL lambda-list-keyword LEAD-byte
11519 // axis. A future outer-marker extension that collided with
11520 // `'&'` fails HERE at the cross-axis enumeration.
11521 use crate::ast::{Atom, QuoteForm, Sexp};
11522
11523 assert_ne!(
11524 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11525 Atom::STR_DELIMITER,
11526 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
11527 bare `&rest` at a param-list position would ambiguously \
11528 begin a lambda-list keyword AND open a string.",
11529 );
11530 assert_ne!(
11531 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11532 Atom::STR_ESCAPE_LEAD,
11533 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
11534 the reader's Str-escape lead byte would alias the CL \
11535 lambda-list-keyword LEAD byte.",
11536 );
11537 assert_ne!(
11538 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11539 Atom::KEYWORD_MARKER_LEAD,
11540 "LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
11541 — a bare `&rest` at a param-list position would \
11542 ambiguously begin a lambda-list keyword AND begin an \
11543 `:foo` keyword.",
11544 );
11545 assert_ne!(
11546 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11547 Atom::BOOL_LITERAL_LEAD,
11548 "LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
11549 a bare `&rest` at a param-list position would ambiguously \
11550 begin a lambda-list keyword AND classify as a Bool prefix.",
11551 );
11552 assert_ne!(
11553 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11554 Sexp::LIST_OPEN,
11555 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
11556 `&rest` at a param-list position would ambiguously begin \
11557 a lambda-list keyword AND open a list.",
11558 );
11559 assert_ne!(
11560 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11561 Sexp::LIST_CLOSE,
11562 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
11563 `&rest` at a param-list position would ambiguously begin \
11564 a lambda-list keyword AND close a list.",
11565 );
11566 assert_ne!(
11567 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11568 Sexp::COMMENT_LEAD,
11569 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
11570 bare `&rest` at a param-list position would ambiguously \
11571 begin a lambda-list keyword AND begin a comment.",
11572 );
11573 assert_ne!(
11574 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11575 Sexp::COMMENT_TERM,
11576 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
11577 reader's line-comment discard loop would terminate on the \
11578 SAME byte the parser's lambda-list-keyword LEAD dispatch \
11579 binds to.",
11580 );
11581 for qf in QuoteForm::ALL {
11582 assert_ne!(
11583 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11584 qf.lead_char(),
11585 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11586 QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
11587 param-list position would ambiguously begin a lambda- \
11588 list keyword AND begin a quote-family prefix.",
11589 );
11590 }
11591 assert_ne!(
11592 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11593 QuoteForm::SPLICE_DISCRIMINATOR,
11594 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11595 SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
11596 peek byte would alias the CL lambda-list-keyword LEAD \
11597 byte.",
11598 );
11599 }
11600
11601 #[test]
11602 fn parse_params_recognizes_rest_marker_via_typed_constant() {
11603 // PATH-UNIFORMITY PIN: the parser's rest-slot dispatch at
11604 // `parse_params` MUST classify the typed constant
11605 // `MacroParams::REST_MARKER` as the rest-slot boundary —
11606 // authoring surfaces that assemble a param list through the
11607 // typed constant (rather than through an inline string literal
11608 // read from user source) must reach the SAME rest-slot binding
11609 // the reader-driven path does.
11610 //
11611 // The `read(REST_MARKER)` composition here is the load-bearing
11612 // structural link: we build the param-list source through the
11613 // typed constant, then parse it through the reader, then the
11614 // parser MUST bind `xs` at the `rest` slot. A regression that
11615 // drifts the parser's dispatch away from the typed constant
11616 // (e.g. re-inlines a string literal that goes stale against
11617 // the constant) fails HERE.
11618 let src = format!("a {} xs", MacroParams::REST_MARKER);
11619 let params = parse_params(&read(&src).unwrap()).unwrap();
11620 assert_eq!(
11621 params,
11622 MacroParams {
11623 required: vec!["a".into()],
11624 optional: Vec::new(),
11625 rest: Some("xs".into()),
11626 },
11627 "parse_params dispatch drifted away from REST_MARKER — the \
11628 typed constant no longer routes to the rest-slot arm.",
11629 );
11630 }
11631
11632 #[test]
11633 fn parse_params_recognizes_optional_marker_via_typed_constant() {
11634 // PATH-UNIFORMITY PIN: peer to
11635 // `parse_params_recognizes_rest_marker_via_typed_constant` on
11636 // the `&optional` axis. The parser's optional-section dispatch
11637 // at `parse_params` MUST classify the typed constant
11638 // `MacroParams::OPTIONAL_MARKER` as the section-switch
11639 // boundary that reroutes subsequent bare-symbol names from the
11640 // `required` bin to the `optional` bin.
11641 let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
11642 let params = parse_params(&read(&src).unwrap()).unwrap();
11643 assert_eq!(
11644 params,
11645 MacroParams {
11646 required: vec!["a".into()],
11647 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11648 rest: None,
11649 },
11650 "parse_params dispatch drifted away from OPTIONAL_MARKER — \
11651 the typed constant no longer routes to the optional- \
11652 section arm.",
11653 );
11654 }
11655
11656 // ── `MacroParams::LAMBDA_LIST_KEYWORDS` + `is_lambda_list_keyword` —
11657 // the closed-set forced-arity ALL array + membership gate that closes
11658 // the CL lambda-list-keyword family at the typed [`MacroParams`]
11659 // algebra. Sibling posture to the closed set of `pub const ALL: [Self;
11660 // N]` forced-arity arrays across the substrate's other closed-set
11661 // outer algebras (`AtomKind::ALL`, `QuoteForm::ALL`, `SexpShape::ALL`,
11662 // `UnquoteForm::ALL`, `MacroDefHead::ALL`); a future third marker
11663 // (`&key`, `&aux`, `&body`) extends the array's arity + adds ONE `pub
11664 // const` for the marker and every downstream family-wide contract
11665 // sweep (LEAD-byte round-trip, pairwise disjointness, membership
11666 // gate) picks up the extension at ONE structural site.
11667 //
11668 // These pins cover: (a) the cardinality contract (the ALL array's
11669 // length matches the pre-lift per-role marker count), (b) the
11670 // ordering contract (each ALL entry matches its corresponding per-
11671 // role `pub const` by-index), (c) the family-wide structural round-
11672 // trip (every ALL element starts_with LEAD), (d) the family-wide
11673 // pairwise disjointness (every distinct ALL index pair yields
11674 // distinct markers), (e) the membership-gate acceptance side (every
11675 // ALL element classifies as `true`), and (f) the membership-gate
11676 // rejection side (the bare LEAD byte + unrecognised `&`-prefixed
11677 // names classify as `false`).
11678 //
11679 // Fail-before/pass-after: every test below references
11680 // `MacroParams::LAMBDA_LIST_KEYWORDS` or
11681 // `MacroParams::is_lambda_list_keyword`, which simply did not exist
11682 // on `MacroParams` before this lift — every assertion was a
11683 // compile-time error against the prior surface.
11684
11685 #[test]
11686 fn macro_params_lambda_list_keywords_has_expected_cardinality() {
11687 // CARDINALITY PIN: the ALL array closes the family at exactly TWO
11688 // entries — the two `&'static str` markers the parser's typed
11689 // dispatch specialises on today (`&rest` + `&optional`). A future
11690 // third marker (`&key`, `&aux`, `&body`) extends this arity to
11691 // 3 AND every downstream family-wide contract sweep picks up the
11692 // extension at ONE structural site. A regression that drops a
11693 // marker from the ALL array (silently narrowing the family) OR
11694 // aliases two markers to the same index (silently narrowing the
11695 // typed dispatch) fails HERE at the cardinality contract before
11696 // the structural round-trip / pairwise-disjointness contracts
11697 // even fire.
11698 assert_eq!(
11699 MacroParams::LAMBDA_LIST_KEYWORDS.len(),
11700 2,
11701 "LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
11702 lambda-list-keyword family closure now names a different \
11703 number of markers than the two the parser's typed dispatch \
11704 specialises on.",
11705 );
11706 }
11707
11708 #[test]
11709 fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
11710 // ORDERING PIN: each ALL entry matches its corresponding per-role
11711 // `pub const` by-index. A regression that swaps the ordering
11712 // (`[OPTIONAL_MARKER, REST_MARKER]` after a rebase) would keep
11713 // the cardinality + membership contracts intact but silently
11714 // reorder downstream consumers that iterate `LAMBDA_LIST_KEYWORDS`
11715 // in canonical order (a future authoring surface that renders
11716 // "supported CL lambda-list keywords: &rest, &optional" would
11717 // silently render "supported CL lambda-list keywords: &optional,
11718 // &rest" instead). Pins the array's declaration-order binding
11719 // structurally so a reorder fails HERE at each element rather
11720 // than only at consumer sites downstream.
11721 assert_eq!(
11722 MacroParams::LAMBDA_LIST_KEYWORDS[0],
11723 MacroParams::REST_MARKER,
11724 "LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
11725 array's declaration-order binding to the per-role `pub \
11726 const` broke at the rest-slot marker slot.",
11727 );
11728 assert_eq!(
11729 MacroParams::LAMBDA_LIST_KEYWORDS[1],
11730 MacroParams::OPTIONAL_MARKER,
11731 "LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
11732 ALL array's declaration-order binding to the per-role \
11733 `pub const` broke at the optional-section marker slot.",
11734 );
11735 }
11736
11737 #[test]
11738 fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
11739 // FAMILY-WIDE STRUCTURAL ROUND-TRIP PIN: every element of the ALL
11740 // array MUST start with the canonical LEAD byte. Where the two
11741 // per-marker `_prefixed_by_lambda_list_keyword_lead` pins each
11742 // named ONE marker inline as a duplicate 3-line
11743 // `assert!(A.starts_with(LEAD), ...)` shape, this family-wide
11744 // sweep routes the SAME contract through the closed-set ALL
11745 // array — a future third marker (`&key`, `&aux`, `&body`)
11746 // extends the array AND is automatically covered by this sweep
11747 // without adding a third `_prefixed_by_...` per-marker pin. The
11748 // per-marker pins stay as fine-grained fail-loud sites; this
11749 // pin is the algebra-wide closure the family-extending refactor
11750 // routes through.
11751 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11752 assert!(
11753 m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11754 "LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
11755 LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
11756 list-keyword family's structural round-trip contract \
11757 no longer binds every marker to its shared LEAD byte.",
11758 lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11759 );
11760 }
11761 }
11762
11763 #[test]
11764 fn macro_params_lambda_list_keywords_pairwise_distinct() {
11765 // FAMILY-WIDE PAIRWISE DISJOINTNESS PIN: every distinct index pair
11766 // `(i, j)` in the ALL array yields distinct markers. Where the
11767 // pre-lift `_rest_and_optional_markers_pairwise_disjoint` pin
11768 // named the TWO markers as a hand-rolled `assert_ne!` pair, this
11769 // family-wide sweep routes the SAME contract through the closed-
11770 // set ALL array so a future third marker automatically extends
11771 // the sweep to `(3 * 2) / 2 == 3` distinct pairs without re-
11772 // deriving per-marker `assert_ne!` calls. The pre-lift pin
11773 // stays as the fine-grained fail-loud site; this pin is the
11774 // family-wide closure the algebra-extending refactor routes
11775 // through.
11776 for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11777 for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11778 if i == j {
11779 continue;
11780 }
11781 assert_ne!(
11782 a, b,
11783 "LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
11784 LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
11785 list-keyword family's pairwise disjointness \
11786 contract no longer binds distinct index pairs \
11787 to distinct markers.",
11788 );
11789 }
11790 }
11791 }
11792
11793 #[test]
11794 fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
11795 // ACCEPTANCE-SIDE MEMBERSHIP PIN: every element of the ALL array
11796 // MUST classify as `true` through the typed membership gate. The
11797 // gate is defined as `LAMBDA_LIST_KEYWORDS.contains(&s)`, so this
11798 // pin is the structural closure binding the gate's return value
11799 // to the ALL array's element set. A regression that specialises
11800 // the gate to a subset of the array (e.g. a `matches!(s,
11801 // "&rest")` inline that silently drops the `&optional` branch)
11802 // fails HERE at the ALL sweep. Sibling-shape acceptance sweep to
11803 // the closed-set `ClosedSet::parse_label` roundtrip pin — every
11804 // element of `Self::ALL` decodes through the projection back to
11805 // itself.
11806 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11807 assert!(
11808 MacroParams::is_lambda_list_keyword(m),
11809 "is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
11810 element `{m}` — the closed-set membership gate's \
11811 acceptance side drifted from the ALL array.",
11812 );
11813 }
11814 }
11815
11816 #[test]
11817 fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
11818 // REJECTION-SIDE MEMBERSHIP PIN (LEAD byte alone): the bare LEAD
11819 // byte `"&"` MUST classify as `false`. A future Clojure-compat
11820 // port might land a bare `&` marker — but until such an
11821 // extension explicitly lands on the ALL array, the substrate
11822 // MUST NOT silently classify the LEAD byte alone as a
11823 // recognised CL lambda-list keyword. Pins the gate's
11824 // rejection-side contract against a plausible future
11825 // near-miss extension.
11826 let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
11827 assert!(
11828 !MacroParams::is_lambda_list_keyword(&bare_lead),
11829 "is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
11830 the closed-set membership gate silently classifies the bare `&` \
11831 LEAD byte as a recognised CL lambda-list keyword despite the ALL \
11832 array containing only the two suffixed markers.",
11833 );
11834 }
11835
11836 #[test]
11837 fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
11838 // REJECTION-SIDE MEMBERSHIP PIN (unrecognised near-misses): the
11839 // three plausible future CL lambda-list keywords (`&key` for
11840 // keyword-args, `&aux` for auxiliaries, `&body` for the
11841 // docstring-carrying tail of `defmacro` bodies) MUST classify as
11842 // `false` UNTIL they explicitly land on the ALL array. Pins the
11843 // gate's conservative rejection contract — the closed-set
11844 // membership gate is not a "starts with `&`" heuristic; it is a
11845 // typed enumeration over the currently-recognised markers. A
11846 // regression that silently loosens the gate to a `starts_with`
11847 // check (dropping the closed-set discipline) fails HERE.
11848 for candidate in ["&key", "&aux", "&body"] {
11849 assert!(
11850 !MacroParams::is_lambda_list_keyword(candidate),
11851 "is_lambda_list_keyword accepted the unrecognised \
11852 `&`-prefixed name `{candidate}` — the closed-set \
11853 membership gate silently loosened its acceptance beyond \
11854 the two markers the ALL array names today.",
11855 );
11856 }
11857 }
11858
11859 #[test]
11860 fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
11861 // REJECTION-SIDE MEMBERSHIP PIN (bare identifiers + empty): a
11862 // bare identifier (`"a"`, `"xs"`, `"foo"`) and the empty string
11863 // MUST classify as `false`. These are the shapes the parser's
11864 // bare-symbol dispatch cascade sees at `parse_params` when
11865 // walking a param list, so pinning them here binds the
11866 // membership gate's rejection contract to the parser's most
11867 // common non-marker input shapes.
11868 for candidate in ["a", "xs", "foo", ""] {
11869 assert!(
11870 !MacroParams::is_lambda_list_keyword(candidate),
11871 "is_lambda_list_keyword accepted the bare non-marker \
11872 input `{candidate}` — the closed-set membership gate's \
11873 rejection side no longer excludes bare identifiers \
11874 the parser routes through the fall-through cascade.",
11875 );
11876 }
11877 }
11878
11879 // ── fixed_arity: the rest-start / rest-less max-arity primitive ─────
11880 //
11881 // `fixed_arity()` lifts the `self.required.len() + self.optional.len()`
11882 // arithmetic that recurred three times inside `MacroParams::bind` — at
11883 // the `Vec::with_capacity` site (where it adds `usize::from(rest.is_some())`
11884 // to get the bound-values count), at the `rest_start` site (inside the
11885 // `if let Some(rest)` branch), and at the `expected` site (inside the
11886 // rest-less `else`). The latter two sites live in mutually-exclusive
11887 // branches yet name ONE structural concept; lifting them collapses the
11888 // arithmetic to one named primitive. These tests pin the primitive's
11889 // contract directly; the existing `bind_*` tests are the path-uniformity
11890 // guards proving `bind`'s sites route through the same value without
11891 // behavior drift.
11892 //
11893 // Fail-before/pass-after: every test below references
11894 // `params.fixed_arity()`, which simply did not exist on `MacroParams`
11895 // before this lift — every assertion's `expect: ___ == params.fixed_arity()`
11896 // line was a compile-time error against the prior surface.
11897
11898 #[test]
11899 fn fixed_arity_is_zero_for_the_empty_param_list() {
11900 // `()` — a nullary macro has fixed arity 0, the rest-less binder
11901 // boundary at which the FIRST surplus arg already rejects.
11902 let params = MacroParams::default();
11903 assert_eq!(params.fixed_arity(), 0);
11904 }
11905
11906 #[test]
11907 fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
11908 // `(a b c)` — three required, no optional, no rest. fixed_arity is
11909 // exactly the required length.
11910 let params = MacroParams {
11911 required: vec!["a".into(), "b".into(), "c".into()],
11912 optional: Vec::new(),
11913 rest: None,
11914 };
11915 assert_eq!(params.fixed_arity(), 3);
11916 }
11917
11918 #[test]
11919 fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
11920 // `(&optional x y)` — two optional, no required. fixed_arity is the
11921 // optional length; the optional section participates in the fixed
11922 // run because supplied positional args bind to it.
11923 let params = MacroParams {
11924 required: Vec::new(),
11925 optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
11926 rest: None,
11927 };
11928 assert_eq!(params.fixed_arity(), 2);
11929 }
11930
11931 #[test]
11932 fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
11933 // `(a b &optional c d e)` — two required + three optional, no rest.
11934 // fixed_arity is 5: the maximum arity a rest-less call can supply.
11935 let params = MacroParams {
11936 required: vec!["a".into(), "b".into()],
11937 optional: vec![
11938 OptionalParam::bare("c"),
11939 OptionalParam::bare("d"),
11940 OptionalParam::bare("e"),
11941 ],
11942 rest: None,
11943 };
11944 assert_eq!(params.fixed_arity(), 5);
11945 }
11946
11947 #[test]
11948 fn fixed_arity_ignores_rest_slot_by_construction() {
11949 // `(a &optional b &rest r)` and `(a &optional b)` — identical fixed
11950 // arity (2). The `&rest` slot has NO maximum and is structurally
11951 // excluded from `fixed_arity`. Naming this invariant pins that a
11952 // regression that drifts the primitive to "required + optional +
11953 // rest.is_some() as usize" fails loudly here — that drift would
11954 // collapse `fixed_arity` into `names().len()`, losing the rest-start
11955 // vs total-bound-values distinction the typed shape relies on.
11956 let with_rest = MacroParams {
11957 required: vec!["a".into()],
11958 optional: vec![OptionalParam::bare("b")],
11959 rest: Some("r".into()),
11960 };
11961 let without_rest = MacroParams {
11962 required: vec!["a".into()],
11963 optional: vec![OptionalParam::bare("b")],
11964 rest: None,
11965 };
11966 assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
11967 assert_eq!(with_rest.fixed_arity(), 2);
11968 }
11969
11970 #[test]
11971 fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
11972 // When `rest` is `Some`, `names()[fixed_arity()]` IS the rest name
11973 // — the rest-start reading of the primitive. Same arithmetic the
11974 // bytecode index would hit (`Subst(fixed_arity())` resolves to the
11975 // rest-bound `Sexp::List`).
11976 let params = MacroParams {
11977 required: vec!["a".into(), "b".into()],
11978 optional: vec![OptionalParam::bare("c")],
11979 rest: Some("r".into()),
11980 };
11981 assert_eq!(params.fixed_arity(), 3);
11982 assert_eq!(params.names()[params.fixed_arity()], "r");
11983 }
11984
11985 #[test]
11986 fn fixed_arity_equals_names_length_when_rest_is_absent() {
11987 // When `rest` is `None`, `names().len() == fixed_arity()` — there
11988 // is no rest-name slot to extend the flat run past the fixed
11989 // boundary. Pins the structural identity
11990 // `names().len() == fixed_arity() + usize::from(rest.is_some())`
11991 // for the rest-less case; the rest-present case is pinned by the
11992 // sibling test above (where the boundary is the rest-name index,
11993 // i.e. one short of `names().len()`).
11994 let params = MacroParams {
11995 required: vec!["a".into(), "b".into()],
11996 optional: vec![OptionalParam::bare("c")],
11997 rest: None,
11998 };
11999 assert_eq!(params.names().len(), params.fixed_arity());
12000 assert_eq!(params.names().len(), 3);
12001 }
12002
12003 #[test]
12004 fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
12005 // The `expected` field of `TooManyMacroArgs` IS `fixed_arity()` —
12006 // the rest-less binder rejects iff `args.len() > fixed_arity()`.
12007 // This pin is the path-uniformity guard binding the typed primitive
12008 // to the binder's rejection contract: a regression that drifts
12009 // `bind`'s `expected` arithmetic from `fixed_arity()` would silently
12010 // surface a different boundary in the diagnostic without touching
12011 // the primitive — and this assertion fails loudly. Mirror of the
12012 // sibling rest-less surplus pin (`bind_rest_less_params_reject_
12013 // surplus_args`); this test pins WHAT the `expected` slot's value
12014 // structurally IS, that pin checks the variant SHAPE.
12015 let params = MacroParams {
12016 required: vec!["a".into(), "b".into()],
12017 optional: vec![OptionalParam::bare("c")],
12018 rest: None,
12019 };
12020 assert_eq!(params.fixed_arity(), 3);
12021 let err = params
12022 .bind(
12023 "m",
12024 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12025 )
12026 .expect_err("4 args against fixed_arity 3 must reject");
12027 match err {
12028 LispError::TooManyMacroArgs {
12029 expected,
12030 got,
12031 macro_name,
12032 } => {
12033 assert_eq!(expected, params.fixed_arity());
12034 assert_eq!(got, 4);
12035 assert_eq!(macro_name, "m");
12036 }
12037 other => panic!("expected TooManyMacroArgs, got {other:?}"),
12038 }
12039 }
12040
12041 #[test]
12042 fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
12043 // When `rest` is `Some`, `bind` collects `args[fixed_arity()..]`
12044 // into the rest's `Sexp::List`. Pin that the rest list contents
12045 // are exactly the suffix beginning at `fixed_arity()` — the
12046 // primitive's rest-start reading IS the slice index the binder
12047 // consumes. A regression that drifts `bind`'s rest-collection
12048 // slice from `fixed_arity()` would surface as a misaligned rest
12049 // list (off-by-one in either direction) and this assertion fails
12050 // loudly. Sibling of `fixed_arity_is_the_rest_less_surplus_
12051 // rejection_boundary` on the rest-PRESENT branch.
12052 let params = MacroParams {
12053 required: vec!["a".into()],
12054 optional: vec![OptionalParam::bare("b")],
12055 rest: Some("r".into()),
12056 };
12057 assert_eq!(params.fixed_arity(), 2);
12058 let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
12059 let vals = params.bind("m", &args).unwrap();
12060 // Bound vec: [a=1, b=2, r=(3 4)] — the rest list IS args[fixed_arity()..].
12061 let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
12062 assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
12063 }
12064
12065 #[test]
12066 fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
12067 // Exactly-saturated rest-present call: `args.len() == fixed_arity()`.
12068 // The rest slot collects the empty slice; bind succeeds, never
12069 // misaligning to an off-by-one underflow. Pin the boundary on the
12070 // rest-PRESENT path — the rest-less mirror is
12071 // `too_many_macro_args_does_not_fire_at_exact_max_arity` above.
12072 let params = MacroParams {
12073 required: vec!["a".into()],
12074 optional: vec![OptionalParam::bare("b")],
12075 rest: Some("r".into()),
12076 };
12077 assert_eq!(params.fixed_arity(), 2);
12078 let vals = params
12079 .bind("m", &[Sexp::int(1), Sexp::int(2)])
12080 .expect("rest-present at exact fixed_arity must bind cleanly");
12081 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
12082 }
12083
12084 #[test]
12085 fn bind_threads_required_positionally_and_collects_rest_as_list() {
12086 // `(a b &rest c)` bound to `1 2 3 4`: a=1, b=2, c=(3 4). The bound
12087 // vec is parallel to `names()`, so the rest list sits at the rest's
12088 // flat index.
12089 let params = MacroParams {
12090 required: vec!["a".into(), "b".into()],
12091 optional: Vec::new(),
12092 rest: Some("c".into()),
12093 };
12094 let vals = params
12095 .bind(
12096 "m",
12097 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12098 )
12099 .unwrap();
12100 assert_eq!(
12101 vals,
12102 vec![
12103 Sexp::int(1),
12104 Sexp::int(2),
12105 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12106 ]
12107 );
12108 }
12109
12110 #[test]
12111 fn bind_supplied_optional_takes_its_positional_arg() {
12112 // `(a &optional b)` bound to `1 2`: a=1, b=2. A supplied optional
12113 // behaves exactly like a positional — only its ABSENCE differs.
12114 let params = MacroParams {
12115 required: vec!["a".into()],
12116 optional: vec![OptionalParam::bare("b")],
12117 rest: None,
12118 };
12119 let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
12120 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
12121 }
12122
12123 #[test]
12124 fn bind_unsupplied_optional_defaults_to_nil() {
12125 // `(a &optional b c)` bound to just `1`: a=1, then b and c run out of
12126 // args and bind to `Sexp::Nil` — CL's default for an `&optional` with
12127 // no supplied default-form. The bound vec is still parallel to
12128 // `names()`, so the template's `,b` / `,c` resolve to nil, not a
12129 // missing-arg error.
12130 let params = MacroParams {
12131 required: vec!["a".into()],
12132 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
12133 rest: None,
12134 };
12135 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12136 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
12137 }
12138
12139 #[test]
12140 fn bind_rest_collects_args_beyond_required_and_optional() {
12141 // `(a &optional b &rest c)` bound to `1 2 3 4`: a=1, b=2 (supplied),
12142 // c=(3 4). The rest starts AFTER the required+optional run, so the
12143 // optional's supplied arg is not swept into the rest.
12144 let params = MacroParams {
12145 required: vec!["a".into()],
12146 optional: vec![OptionalParam::bare("b")],
12147 rest: Some("c".into()),
12148 };
12149 let vals = params
12150 .bind(
12151 "m",
12152 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12153 )
12154 .unwrap();
12155 assert_eq!(
12156 vals,
12157 vec![
12158 Sexp::int(1),
12159 Sexp::int(2),
12160 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12161 ]
12162 );
12163 }
12164
12165 #[test]
12166 fn bind_unsupplied_optional_then_empty_rest() {
12167 // `(a &optional b &rest c)` bound to just `1`: a=1, b=nil (absent),
12168 // c=() (nothing left). Both the optional default AND the empty-rest
12169 // contract hold in the same bind.
12170 let params = MacroParams {
12171 required: vec!["a".into()],
12172 optional: vec![OptionalParam::bare("b")],
12173 rest: Some("c".into()),
12174 };
12175 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12176 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
12177 }
12178
12179 #[test]
12180 fn bind_rest_with_no_remaining_args_is_the_empty_list() {
12181 // Exactly-saturated required args + a rest that captures nothing →
12182 // the rest binds to the empty list, never errors. Mirrors the
12183 // splice contract `,@()` contributes nothing.
12184 let params = MacroParams {
12185 required: vec!["a".into()],
12186 optional: Vec::new(),
12187 rest: Some("c".into()),
12188 };
12189 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12190 assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
12191 }
12192
12193 #[test]
12194 fn bind_missing_required_errors_before_any_rest_collection() {
12195 // A required name with no arg at its position is a
12196 // `MissingMacroArg` — the gate fires during the required walk,
12197 // before the rest is ever collected.
12198 let params = MacroParams {
12199 required: vec!["a".into(), "b".into()],
12200 optional: Vec::new(),
12201 rest: Some("c".into()),
12202 };
12203 let err = params
12204 .bind("m", &[Sexp::int(1)])
12205 .expect_err("missing required `b` must error");
12206 assert!(
12207 matches!(err, LispError::MissingMacroArg { .. }),
12208 "expected MissingMacroArg, got: {err:?}"
12209 );
12210 }
12211
12212 #[test]
12213 fn bind_missing_required_errors_even_with_optional_present() {
12214 // An absent REQUIRED arg errors even when the param list has an
12215 // optional section: the required walk fires `MissingMacroArg` before
12216 // the optional arm (which would otherwise default to nil) is reached.
12217 // Required absence is an error; optional absence is a nil default.
12218 let params = MacroParams {
12219 required: vec!["a".into(), "b".into()],
12220 optional: vec![OptionalParam::bare("c")],
12221 rest: None,
12222 };
12223 let err = params
12224 .bind("m", &[Sexp::int(1)])
12225 .expect_err("missing required `b` must error before optional defaulting");
12226 assert!(
12227 matches!(err, LispError::MissingMacroArg { .. }),
12228 "expected MissingMacroArg, got: {err:?}"
12229 );
12230 }
12231
12232 #[test]
12233 fn bind_rest_less_params_reject_surplus_args() {
12234 // The rest-less binder REJECTS surplus call args via the
12235 // structural `TooManyMacroArgs { macro_name, expected, got }`
12236 // rejection — the call-site mirror of `RestParamTrailingTokens`
12237 // (the definition-site rejection lifted at the parse_params
12238 // boundary). Closes the asymmetry where the typed-entry
12239 // macro-call-gate rejected too-few-args loudly
12240 // (`MissingMacroArg`) but silently truncated too-many. `expected`
12241 // is the rest-less binder's fixed maximum arity
12242 // (`required.len() + optional.len()`); `got` is the actual
12243 // call-site arg count.
12244 let params = MacroParams {
12245 required: vec!["a".into()],
12246 optional: Vec::new(),
12247 rest: None,
12248 };
12249 let err = params
12250 .bind("m", &[Sexp::int(1), Sexp::int(2)])
12251 .expect_err("rest-less surplus must error");
12252 match err {
12253 LispError::TooManyMacroArgs {
12254 macro_name,
12255 expected,
12256 got,
12257 } => {
12258 assert_eq!(macro_name, "m");
12259 assert_eq!(expected, 1);
12260 assert_eq!(got, 2);
12261 }
12262 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
12263 }
12264 }
12265
12266 // ── OptionalParam: per-param default forms — `&optional (x DEFAULT)` ──
12267 //
12268 // The `&optional` section now admits both bare-symbol entries (`x`) AND
12269 // list-form entries (`(x DEFAULT)`). The typed `OptionalParam.default:
12270 // Option<Sexp>` slot makes the per-param default a FIELD on each
12271 // optional entry, not a discipline a sibling `Vec<Sexp>` would have had
12272 // to maintain in lock-step with `Vec<String>`. These tests pin: the
12273 // parser admits both shapes side-by-side; the four malformed list-spec
12274 // shapes (empty / missing-default / extra-elements / non-symbol-name)
12275 // are rejected via `OptionalParamMalformed` with the typed
12276 // `OptionalParamMalformedReason`; the binder consults the default form
12277 // when the arg is absent and ignores it when supplied; and the end-to-
12278 // end expansion agrees between the bytecode and substitute strategies
12279 // (invariant 2 — free middle).
12280
12281 #[test]
12282 fn parse_params_admits_optional_list_spec_with_default() {
12283 // `(a &optional (b 5))` — one bare optional becomes
12284 // `OptionalParam { name: "b", default: Some(Int(5)) }`. The
12285 // surrounding `MacroParams` shape is otherwise identical.
12286 let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
12287 assert_eq!(
12288 params,
12289 MacroParams {
12290 required: vec!["a".into()],
12291 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12292 rest: None,
12293 }
12294 );
12295 }
12296
12297 #[test]
12298 fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
12299 // `(a &optional b (c "x") d (e 9) &rest r)` — the optional section
12300 // interleaves bare and list-form specs. Each lands in its own
12301 // `OptionalParam` entry; `names()` still yields the flat
12302 // required-then-optional-then-rest order.
12303 let params =
12304 parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
12305 assert_eq!(
12306 params,
12307 MacroParams {
12308 required: vec!["a".into()],
12309 optional: vec![
12310 OptionalParam::bare("b"),
12311 OptionalParam::with_default("c", Sexp::string("x")),
12312 OptionalParam::bare("d"),
12313 OptionalParam::with_default("e", Sexp::int(9)),
12314 ],
12315 rest: Some("r".into()),
12316 }
12317 );
12318 assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
12319 }
12320
12321 #[test]
12322 fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
12323 // `(&optional (x (list 1 2)))` — the default form is itself a list.
12324 // Without an evaluator, the literal Sexp is parked verbatim into
12325 // `default`; the binder produces it for any absent call.
12326 let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
12327 let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12328 assert_eq!(
12329 params,
12330 MacroParams {
12331 required: Vec::new(),
12332 optional: vec![OptionalParam::with_default("x", want_default)],
12333 rest: None,
12334 }
12335 );
12336 }
12337
12338 #[test]
12339 fn parse_params_rejects_empty_list_optional_spec() {
12340 // `(&optional ())` — a zero-element list is the empty-list rejection.
12341 // Without the gate the loop would `as_symbol()` on a `Sexp::List` and
12342 // fall through to `NonSymbolParam`, which mis-classifies the failure
12343 // (this is a malformed DEFAULT-FORM spec, not a "param must be a
12344 // symbol" rejection).
12345 let err = parse_params(&read("&optional ()").unwrap())
12346 .expect_err("empty list optional spec must error");
12347 assert!(
12348 matches!(
12349 err,
12350 LispError::OptionalParamMalformed {
12351 position: 1,
12352 reason: crate::error::OptionalParamMalformedReason::EmptyList,
12353 ..
12354 }
12355 ),
12356 "expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
12357 );
12358 }
12359
12360 #[test]
12361 fn parse_params_rejects_one_element_optional_list_as_missing_default() {
12362 // `(&optional (x))` — a one-element list. REJECTED with reason
12363 // `MissingDefault` rather than reinterpreted as `&optional x`, because
12364 // a parenthesized single-element spec is structurally ambiguous and
12365 // the bare-symbol form `x` IS the canonical "no default" shape.
12366 let err = parse_params(&read("&optional (x)").unwrap())
12367 .expect_err("one-element list optional spec must error");
12368 assert!(
12369 matches!(
12370 err,
12371 LispError::OptionalParamMalformed {
12372 position: 1,
12373 reason: crate::error::OptionalParamMalformedReason::MissingDefault,
12374 ..
12375 }
12376 ),
12377 "expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
12378 );
12379 }
12380
12381 #[test]
12382 fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
12383 // `(&optional (x 5 6))` — a three-element list. CL's `(name default
12384 // supplied-p)` shape is not yet supported (no evaluator → no
12385 // supplied-p variable binding), so the third element is structurally
12386 // surplus. REJECTED with reason `ExtraElements{length: 3}`.
12387 let err = parse_params(&read("&optional (x 5 6)").unwrap())
12388 .expect_err("three-element list optional spec must error");
12389 assert!(
12390 matches!(
12391 err,
12392 LispError::OptionalParamMalformed {
12393 position: 1,
12394 reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
12395 ..
12396 }
12397 ),
12398 "expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
12399 );
12400 }
12401
12402 #[test]
12403 fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
12404 // `(&optional (5 default))` — the name slot must be a symbol; a
12405 // numeric literal is REJECTED with reason `NonSymbolName`. Without
12406 // this branch the gate would silently populate
12407 // `OptionalParam.name` from a stringified non-symbol value (`"5"`),
12408 // breaking the invariant that param names are symbols.
12409 let err = parse_params(&read("&optional (5 default)").unwrap())
12410 .expect_err("non-symbol-name optional spec must error");
12411 assert!(
12412 matches!(
12413 err,
12414 LispError::OptionalParamMalformed {
12415 position: 1,
12416 reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
12417 ..
12418 }
12419 ),
12420 "expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
12421 );
12422 }
12423
12424 #[test]
12425 fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
12426 // `((a 5))` — a list in the REQUIRED section is NOT a default-form
12427 // spec; default forms are an optional-section affordance. The gate
12428 // must fall through to `NonSymbolParam` (parity with the prior
12429 // behavior on lists in the required section), not silently admit
12430 // the list as a default-form spec.
12431 let err =
12432 parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
12433 assert!(
12434 matches!(err, LispError::NonSymbolParam { position: 0, .. }),
12435 "expected NonSymbolParam{{position: 0}}, got: {err:?}"
12436 );
12437 }
12438
12439 #[test]
12440 fn bind_unsupplied_optional_with_default_takes_the_default() {
12441 // `(a &optional (b 5))` bound to just `1`: a=1, b=5 (the declared
12442 // default), not nil. The default form is consulted ONLY when the
12443 // call ran out of args.
12444 let params = MacroParams {
12445 required: vec!["a".into()],
12446 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12447 rest: None,
12448 };
12449 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12450 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
12451 }
12452
12453 #[test]
12454 fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
12455 // `(&optional (b 5))` bound to `42`: b=42, NOT the default. A
12456 // supplied optional ALWAYS takes its arg; the default is the
12457 // absence-only fallback. Pins that the default form does not
12458 // shadow a supplied call arg.
12459 let params = MacroParams {
12460 required: Vec::new(),
12461 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12462 rest: None,
12463 };
12464 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12465 assert_eq!(vals, vec![Sexp::int(42)]);
12466 }
12467
12468 #[test]
12469 fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
12470 // `(a &optional (b 5) c (d "z"))` bound to just `1`: a=1, b=5
12471 // (default), c=nil (bare floor), d="z" (default). The three
12472 // absence cases coexist in one bind: per-default fill, nil floor,
12473 // and a tail with a literal-string default.
12474 let params = MacroParams {
12475 required: vec!["a".into()],
12476 optional: vec![
12477 OptionalParam::with_default("b", Sexp::int(5)),
12478 OptionalParam::bare("c"),
12479 OptionalParam::with_default("d", Sexp::string("z")),
12480 ],
12481 rest: None,
12482 };
12483 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12484 assert_eq!(
12485 vals,
12486 vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
12487 );
12488 }
12489
12490 // ── OptionalParam::resolved_default: the absent-call binder accessor ──
12491 //
12492 // `resolved_default` lifts the `param.default.clone().unwrap_or(Sexp::Nil)`
12493 // two-arm fallback that previously inlined at `MacroParams::bind`'s
12494 // optional arm into ONE named accessor on the typed `OptionalParam`.
12495 // The constructor pair `bare` / `with_default` defines the typed
12496 // shapes of the `default` slot; this accessor names the symmetric
12497 // bound-value projection both shapes yield at the absence boundary.
12498 // Tests pin: (a) `bare(name).resolved_default()` is the `Sexp::Nil`
12499 // no-default floor; (b) `with_default(name, d).resolved_default()` is
12500 // `d.clone()`; (c) the projection is `Clone`-stable across repeated
12501 // calls (the typed `default` field is not consumed); (d) path-
12502 // uniformity at the binder — `bind`'s optional arm routes through
12503 // `resolved_default` for both shapes; (e) end-to-end through both
12504 // expansion strategies, the absent-call binding agrees.
12505
12506 #[test]
12507 fn resolved_default_is_nil_for_bare_optional() {
12508 // `OptionalParam::bare(name).default` is `None`, so
12509 // `resolved_default()` projects to `Sexp::Nil` — the CL
12510 // `&optional` no-default-form floor. Fail-before/pass-after: this
12511 // assert is meaningless pre-lift because the helper does not
12512 // exist; post-lift it pins the typed accessor's `Sexp::Nil` arm
12513 // at the named primitive. Sibling of `bare` itself: the
12514 // constructor defines the shape (`default: None`); the accessor
12515 // names the bound-value projection of that shape.
12516 let p = OptionalParam::bare("x");
12517 assert_eq!(p.resolved_default(), Sexp::Nil);
12518 }
12519
12520 #[test]
12521 fn resolved_default_clones_declared_default_for_with_default_optional() {
12522 // `OptionalParam::with_default(name, d).default` is `Some(d)`, so
12523 // `resolved_default()` projects to `d.clone()` — the declared
12524 // default form. Sibling of the bare-floor pin: the closed-set
12525 // `default: Option<Sexp>` slot's two shapes correspond 1:1 with
12526 // the two arms of `resolved_default`. Pins the closed-set
12527 // exhaustive coverage of `Option<Sexp>` × `{Some, None}`.
12528 let p = OptionalParam::with_default("x", Sexp::int(5));
12529 assert_eq!(p.resolved_default(), Sexp::int(5));
12530 }
12531
12532 #[test]
12533 fn resolved_default_clones_arbitrary_sexp_default_form() {
12534 // The declared default can be any `Sexp` — a literal list, a
12535 // keyword, a string, a quasi-quoted form — because v0 has no
12536 // evaluator and the typed slot parks the literal verbatim. Pin
12537 // that `resolved_default()` is faithful to the parked literal
12538 // regardless of shape: a regression that special-cases an arm
12539 // (e.g., projecting `Sexp::List(_)` to `Sexp::Nil`, or "normalizing"
12540 // a `Sexp::Quote`) fails here. The accessor is exactly
12541 // `default.clone()` for the `Some` arm — no shape rewriting.
12542 let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12543 let p = OptionalParam::with_default("x", arbitrary.clone());
12544 assert_eq!(p.resolved_default(), arbitrary);
12545 }
12546
12547 #[test]
12548 fn resolved_default_is_clone_stable_across_repeated_calls() {
12549 // The accessor takes `&self` and projects through `Clone`, so
12550 // repeated calls yield IDENTICAL values — the typed `default`
12551 // field is not consumed. Pins that the accessor is idempotent
12552 // for the same `OptionalParam`, which is the contract the binder
12553 // relies on across multiple `bind` invocations of the same
12554 // macro: every call that leaves the optional unfilled yields
12555 // the SAME bound value, never a partially-consumed shape. A
12556 // regression that converted the accessor to `self.default.take()`
12557 // (consuming the field) would still type-check at the call site
12558 // but would silently desync repeated absent-call bindings; this
12559 // test catches that drift.
12560 let p = OptionalParam::with_default("x", Sexp::string("hi"));
12561 let first = p.resolved_default();
12562 let second = p.resolved_default();
12563 assert_eq!(first, second);
12564 assert_eq!(first, Sexp::string("hi"));
12565 }
12566
12567 #[test]
12568 fn resolved_default_is_the_binders_absent_optional_projection() {
12569 // Path-uniformity pin at the binder boundary: `MacroParams::bind`'s
12570 // optional arm consults `param.resolved_default()` for any
12571 // absent slot. Two-arm coverage: a bare optional (`b`) binds to
12572 // `Sexp::Nil` via the `None` arm; a with-default optional
12573 // (`(c 5)`) binds to `Sexp::int(5)` via the `Some` arm. The
12574 // single bind call exercises both arms in one walk, and the
12575 // bound values vec is parallel to `names()` so position
12576 // checking pins the arm-to-slot mapping. A regression that
12577 // re-inlines the two-arm fallback at the binder, drifting it
12578 // independently from the accessor, would still type-check but
12579 // a future shape change to `resolved_default` (e.g., adding a
12580 // typed `&supplied-p` companion slot) would silently desync
12581 // the binder from the accessor — this test catches that drift.
12582 let params = MacroParams {
12583 required: Vec::new(),
12584 optional: vec![
12585 OptionalParam::bare("b"),
12586 OptionalParam::with_default("c", Sexp::int(5)),
12587 ],
12588 rest: None,
12589 };
12590 // Empty args → both optionals are absent → both arms fire.
12591 let vals = params.bind("m", &[]).unwrap();
12592 assert_eq!(vals.len(), 2);
12593 assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
12594 assert_eq!(
12595 vals[1],
12596 OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
12597 );
12598 // And the absolute identities pin the projection's arms:
12599 assert_eq!(vals[0], Sexp::Nil);
12600 assert_eq!(vals[1], Sexp::int(5));
12601 }
12602
12603 #[test]
12604 fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
12605 // End-to-end path-uniformity: a macro with both an `&optional
12606 // (g "hi")` (with-default) and an `&optional h` (bare) param
12607 // expands the same way under bytecode AND substitute
12608 // strategies, because both strategies route through the SHARED
12609 // `MacroParams::bind` which now consults `resolved_default`
12610 // for absent slots. Pins that the accessor's contract is
12611 // structurally observable at the strategy boundary — a
12612 // regression that bifurcated the accessor's behavior between
12613 // the two paths (impossible, since they share `bind`) would
12614 // surface here.
12615 let src = r#"
12616 (defmacro greet (n &optional (g "hi") h)
12617 `(list ,g ,n ,h))
12618 (greet world)
12619 "#;
12620 let expected = vec![Sexp::List(vec![
12621 Sexp::symbol("list"),
12622 Sexp::string("hi"),
12623 Sexp::symbol("world"),
12624 Sexp::Nil,
12625 ])];
12626 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12627 let substitute = Expander::new_substitute_only()
12628 .expand_program(read(src).unwrap())
12629 .unwrap();
12630 assert_eq!(
12631 bytecode, expected,
12632 "bytecode resolved_default expansion drifted"
12633 );
12634 assert_eq!(
12635 substitute, expected,
12636 "substitute resolved_default expansion drifted"
12637 );
12638 assert_eq!(
12639 bytecode, substitute,
12640 "the two strategies disagree on resolved_default expansion"
12641 );
12642 }
12643
12644 #[test]
12645 fn resolved_default_supplied_optional_does_not_consult_accessor() {
12646 // A SUPPLIED optional binds to its CALL ARG, never to the
12647 // accessor's projection. Pins the contract: `resolved_default`
12648 // is the absence-only fallback; a present arg shadows the
12649 // accessor at the binder. Sibling negative control to
12650 // `resolved_default_is_the_binders_absent_optional_projection`:
12651 // that test exercises the absence arm at every slot; this test
12652 // exercises the presence arm at every slot and proves the
12653 // accessor's `Sexp::int(5)` projection is NOT consulted when
12654 // the optional is supplied with `Sexp::int(42)`. A regression
12655 // that wired the binder to always consult the accessor (the
12656 // wrong direction — the default would shadow supplied args) is
12657 // caught here.
12658 let params = MacroParams {
12659 required: Vec::new(),
12660 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12661 rest: None,
12662 };
12663 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12664 assert_eq!(vals, vec![Sexp::int(42)]);
12665 // And the accessor's would-be projection IS NOT the bound value:
12666 let p = OptionalParam::with_default("b", Sexp::int(5));
12667 assert_ne!(vals[0], p.resolved_default());
12668 }
12669
12670 #[test]
12671 fn optional_default_macro_expands_end_to_end_under_both_strategies() {
12672 // The end-to-end path: a macro with `&optional (g "hi")` expands to
12673 // the default literal when unsupplied, and to the supplied arg when
12674 // present. Both the bytecode and substitute strategies must agree
12675 // (invariant 2 — free middle); they share `MacroParams::bind`, so
12676 // the default arm lands once in `bind` and both strategies inherit
12677 // it unable to drift. This is the test the prior run (611a682)
12678 // signposted as the next-change-that-benefits.
12679 let src = r#"
12680 (defmacro greet (n &optional (g "hi"))
12681 `(list ,g ,n))
12682 (greet world)
12683 (greet world there)
12684 "#;
12685 let expected = vec![
12686 Sexp::List(vec![
12687 Sexp::symbol("list"),
12688 Sexp::string("hi"),
12689 Sexp::symbol("world"),
12690 ]),
12691 Sexp::List(vec![
12692 Sexp::symbol("list"),
12693 Sexp::symbol("there"),
12694 Sexp::symbol("world"),
12695 ]),
12696 ];
12697 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12698 let substitute = Expander::new_substitute_only()
12699 .expand_program(read(src).unwrap())
12700 .unwrap();
12701 assert_eq!(
12702 bytecode, expected,
12703 "bytecode optional-default expansion drifted"
12704 );
12705 assert_eq!(
12706 substitute, expected,
12707 "substitute optional-default expansion drifted"
12708 );
12709 assert_eq!(
12710 bytecode, substitute,
12711 "the two strategies disagree on optional-default expansion"
12712 );
12713 }
12714
12715 #[test]
12716 fn optional_macro_expands_end_to_end_under_both_strategies() {
12717 // The end-to-end path: a macro with an `&optional` param expands
12718 // correctly whether the optional is supplied or defaulted, and the
12719 // bytecode and substitute strategies agree (invariant 2 — free
12720 // middle). `,b` resolves to the supplied arg when present, to
12721 // `Sexp::Nil` when absent (CL's `&optional` default).
12722 let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
12723 // (cons 1 2) — optional supplied; (cons 3 <Nil>) — optional defaulted.
12724 // The defaulted slot is the canonical `Sexp::Nil`, distinct in the AST
12725 // from a reader-produced empty list `()` even though both Display as
12726 // `()`.
12727 let expected = vec![
12728 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
12729 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
12730 ];
12731 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12732 let substitute = Expander::new_substitute_only()
12733 .expand_program(read(src).unwrap())
12734 .unwrap();
12735 assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
12736 assert_eq!(
12737 substitute, expected,
12738 "substitute optional expansion drifted"
12739 );
12740 assert_eq!(
12741 bytecode, substitute,
12742 "the two strategies disagree on optional expansion"
12743 );
12744 }
12745
12746 // ── MacroDef::template_body: the shared body-projection primitive ──
12747 //
12748 // `template_body` lifts the `match &def.body { Sexp::Quasiquote(inner)
12749 // => inner.as_ref(), other => other }` inline peel — present
12750 // byte-identically at the bytecode (`compile_template`) AND substitute
12751 // (`apply`'s fallback) path entries — into ONE named projection both
12752 // strategies share. The existing `compiled_template_matches_substitute
12753 // _path` and `expansion_layers_agree_on_output_and_cache_wins` tests
12754 // are the path-uniformity guards covering the SHARED-PROJECTION shape;
12755 // the four tests below pin the projection's contract DIRECTLY:
12756 // (a) a quasi-quoted body unwraps to the inner; (b) a non-quasi-quoted
12757 // body returns the body verbatim; (c) the borrow is rooted in the
12758 // body field (single-level peel); (d) the projection is the same
12759 // `&Sexp` both strategies route on (so a regression that drifts the
12760 // body-peel from one strategy to the other becomes a type-level
12761 // change at this helper, not a silent two-site divergence).
12762
12763 #[test]
12764 fn template_body_unwraps_outer_quasiquote_to_inner() {
12765 // The canonical authoring shape: `(defmacro f (a) `(list ,a))` —
12766 // the reader wraps the `` ` `` form into `Sexp::Quasiquote(inner)`,
12767 // and `template_body` peels the outer marker. The returned `&Sexp`
12768 // is the inner walker-payload both expansion strategies consume.
12769 let inner = Sexp::List(vec![
12770 Sexp::symbol("list"),
12771 Sexp::Unquote(Box::new(Sexp::symbol("a"))),
12772 ]);
12773 let def = MacroDef {
12774 name: "f".into(),
12775 params: MacroParams::default(),
12776 body: Sexp::Quasiquote(Box::new(inner.clone())),
12777 };
12778 assert_eq!(def.template_body(), &inner);
12779 }
12780
12781 #[test]
12782 fn template_body_returns_non_quasiquote_body_verbatim() {
12783 // A body authored WITHOUT the outer `` ` `` affordance — a bare
12784 // `Sexp::List` body — returns verbatim. The "other" arm of the
12785 // legacy match. Pin parity with the pre-lift code path so a
12786 // regression that drifts the body-peel into "always peel
12787 // something" (which would break literal-body macros) fails here.
12788 let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
12789 let def = MacroDef {
12790 name: "f".into(),
12791 params: MacroParams::default(),
12792 body: body.clone(),
12793 };
12794 assert_eq!(def.template_body(), &body);
12795 // Atom bodies too — the projection is a single-arm match, not a
12796 // recursive descent. A `Sexp::Atom` body is its own template payload.
12797 let atom_def = MacroDef {
12798 name: "g".into(),
12799 params: MacroParams::default(),
12800 body: Sexp::symbol("nil-template"),
12801 };
12802 assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
12803 }
12804
12805 #[test]
12806 fn template_body_peels_single_level_only() {
12807 // A nested `` ``form `` body — `Sexp::Quasiquote(Box::new(
12808 // Sexp::Quasiquote(...)))` — unwraps ONE outer quasi-quote and
12809 // returns the inner `Sexp::Quasiquote(...)` as-is. The v0 module
12810 // preamble declares "Nested quasi-quotes: Not yet supported"; the
12811 // single-level peel matches the legacy inline match's posture
12812 // (which only matched ONE outer `Sexp::Quasiquote(_)` arm, not a
12813 // recursive loop). A regression that drifts to a recursive peel
12814 // would project too far and the inner `Sexp::Quasiquote` marker —
12815 // which the substitute walker treats as an atomic leaf returned
12816 // verbatim (line ~830, `Sexp::Quote(_) | Sexp::Quasiquote(_)
12817 // => Ok(form.clone())`) — would silently disappear from the
12818 // expansion's emitted form. Pin the single-level contract here.
12819 let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
12820 let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
12821 let def = MacroDef {
12822 name: "nested".into(),
12823 params: MacroParams::default(),
12824 body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
12825 };
12826 // Outer peel returns the INNER quasi-quote, NOT its inner payload.
12827 assert_eq!(def.template_body(), &inner_qq);
12828 assert_ne!(def.template_body(), &inner_payload);
12829 }
12830
12831 #[test]
12832 fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
12833 // A `Sexp::Quote(_)` body — not a `Sexp::Quasiquote(_)` — returns
12834 // verbatim through the "other" arm. The two close-cousin shapes
12835 // share an outer-marker character (`'` vs `` ` ``) at the reader
12836 // boundary but differ semantically: a `Quote` body is a literal
12837 // template (no substitution semantics), a `Quasiquote` body is a
12838 // substitution-walker entry. A regression that conflated the two
12839 // — peeling Quote as if it were Quasiquote — would silently turn
12840 // every quoted-body macro into a template-walked macro. Pin the
12841 // discrimination.
12842 let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
12843 let body = Sexp::Quote(Box::new(inner.clone()));
12844 let def = MacroDef {
12845 name: "quoted".into(),
12846 params: MacroParams::default(),
12847 body: body.clone(),
12848 };
12849 assert_eq!(def.template_body(), &body);
12850 // The inner is NOT what comes back — only Quasiquote-bodied macros
12851 // would peel to the inner.
12852 assert_ne!(def.template_body(), &inner);
12853 }
12854
12855 #[test]
12856 fn template_body_is_the_shared_projection_both_strategies_walk() {
12857 // End-to-end path-uniformity at the projection boundary: a macro
12858 // authored with the canonical quasi-quoted body expands
12859 // IDENTICALLY under bytecode and substitute strategies because
12860 // both route their walker's body through `template_body()` — the
12861 // SAME `&Sexp` projection. Sibling of
12862 // `compiled_template_matches_substitute_path` (which observes
12863 // agreement on the EMITTED form); this test pins agreement on
12864 // the projection ENTRY: `compile_template` and the substitute
12865 // fallback now consume the same `&Sexp` (`def.template_body()`),
12866 // never a divergent inline match the two paths could regress
12867 // independently.
12868 let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
12869 let expected = vec![Sexp::List(vec![
12870 Sexp::symbol("list"),
12871 Sexp::int(5),
12872 Sexp::int(5),
12873 ])];
12874 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12875 let substitute = Expander::new_substitute_only()
12876 .expand_program(read(src).unwrap())
12877 .unwrap();
12878 assert_eq!(bytecode, expected, "bytecode body-projection drifted");
12879 assert_eq!(substitute, expected, "substitute body-projection drifted");
12880 assert_eq!(
12881 bytecode, substitute,
12882 "the two strategies disagree on the body-projection's emission"
12883 );
12884 }
12885
12886 // ── Expander::expand: macro-call dispatch routes through `as_call_to_any` ──
12887 //
12888 // `expand` lifts its macro-call recognition to route through the
12889 // substrate's typed-decoded call decomposition: `as_call_to_any(|h|
12890 // self.macros.get(h))` answers "is this form an invocation of any
12891 // registered macro?" in ONE structural query on the Sexp algebra,
12892 // and a HashMap-backed lookup as its classifier. Sibling consumer to
12893 // `macro_def_from` (the typed-macro-definition dispatcher already
12894 // routing through `as_call_to_any(MacroDefHead::from_keyword)` with
12895 // a closed-set enum classifier). With both in place, BOTH dispatch
12896 // sites in the macro expander project through the SAME family
12897 // primitive — each binding the classifier that fits its candidate
12898 // set. The tests below pin the consumer's path-uniformity contract
12899 // at the new boundary: a hand-rolled `as_call_to_any(|h| macros.get
12900 // (h))` dispatch observes the SAME `(def, args)` decomposition the
12901 // `Expander::expand` consumer routes through.
12902
12903 #[test]
12904 fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
12905 // Structural identity: on a registered-macro call, the consumer's
12906 // expansion is observably equivalent to: classify the form via
12907 // `as_call_to_any(|h| macros.get(h))` → some `(def, args)` →
12908 // apply the def to args. Pin path-uniformity: a hand-rolled
12909 // `as_call_to_any` lookup against the same registry the expander
12910 // walks produces the SAME `MacroDef` reference for the SAME
12911 // input form. A regression that drifts the consumer back to an
12912 // inline `as_list + as_call + macros.get` chain (which would
12913 // fragment the family adoption) is caught structurally — the
12914 // hand-rolled `as_call_to_any` and the consumer's dispatch must
12915 // observe the same decomposition.
12916 let mut e = Expander::new();
12917 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12918 .unwrap();
12919 let call_form = parse("(wrap 42)");
12920
12921 // Hand-rolled family-primitive lookup mirrors the lifted consumer.
12922 let (def_via_family, args_via_family) = call_form
12923 .as_call_to_any(|h| e.macros.get(h))
12924 .expect("registered macro call must decompose via as_call_to_any");
12925 assert_eq!(def_via_family.name, "wrap");
12926 assert_eq!(args_via_family, &[Sexp::int(42)]);
12927
12928 // Consumer's expand observes the SAME decomposition: the expanded
12929 // form is `(list 42 42)`, derived from the SAME def + args the
12930 // hand-rolled lookup found. Path-uniform with the family
12931 // primitive at the dispatch boundary.
12932 let expanded = e.expand(&call_form).unwrap();
12933 assert_eq!(
12934 expanded,
12935 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
12936 );
12937 }
12938
12939 #[test]
12940 fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
12941 // Path-uniformity for the non-registered-head path: `as_call_to_any
12942 // (|h| macros.get(h))` returns `None` for a call whose head ISN'T
12943 // a registered macro, and the consumer falls through to the
12944 // children-walk (which expands any nested macro calls). Pin
12945 // both halves: the hand-rolled lookup returns `None`, AND the
12946 // consumer's expand walks into the children. A regression that
12947 // accidentally short-circuits the children-walk for non-macro
12948 // calls (e.g. by treating `as_call_to_any` = `None` as
12949 // "non-expandable" globally) would fail here.
12950 let mut e = Expander::new();
12951 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12952 .unwrap();
12953 let outer = parse("(foo (wrap 5))");
12954
12955 // Hand-rolled family-primitive lookup rejects the outer head.
12956 assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
12957
12958 // Consumer walks children — the inner `(wrap 5)` IS a macro call
12959 // and expands to `(list 5 5)`; the outer `foo` head is preserved.
12960 let expanded = e.expand(&outer).unwrap();
12961 assert_eq!(
12962 expanded,
12963 Sexp::List(vec![
12964 Sexp::symbol("foo"),
12965 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
12966 ])
12967 );
12968 }
12969
12970 #[test]
12971 fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
12972 // Path-uniformity for the non-call path: every shape `as_call`
12973 // rejects (atoms across all 6 kinds, Nil, Quote-family wrappers)
12974 // ALSO routes past `as_call_to_any` into the `as_list()`
12975 // fallthrough, where the not-a-list arm returns `form.clone()`
12976 // verbatim. Pin both halves: the hand-rolled lookup rejects
12977 // every non-call shape regardless of decoder, AND the consumer
12978 // preserves each shape unchanged. A regression that drifts the
12979 // consumer's dispatch order (e.g. checking `as_list()` BEFORE
12980 // `as_call_to_any` in a way that mis-handles Quote-family
12981 // wrappers) would fail here.
12982 let e = Expander::new();
12983 let shapes = [
12984 Sexp::symbol("foo"),
12985 Sexp::int(5),
12986 Sexp::keyword("k"),
12987 Sexp::string("s"),
12988 Sexp::boolean(true),
12989 Sexp::float(1.5),
12990 Sexp::Nil,
12991 Sexp::Quote(Box::new(Sexp::symbol("x"))),
12992 Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
12993 Sexp::Unquote(Box::new(Sexp::symbol("x"))),
12994 Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
12995 ];
12996 for s in &shapes {
12997 // Hand-rolled family-primitive lookup rejects non-call shapes
12998 // even for a promiscuous decoder — the call-shape gate fires
12999 // BEFORE the decoder runs.
13000 assert!(
13001 s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
13002 "non-call shape must yield None for as_call_to_any: {s}"
13003 );
13004 // Consumer preserves the shape verbatim — the not-a-list
13005 // arm at the fallthrough returns `form.clone()`.
13006 assert_eq!(
13007 e.expand(s).unwrap(),
13008 s.clone(),
13009 "non-call shape must round-trip unchanged through expand: {s}"
13010 );
13011 }
13012 }
13013
13014 #[test]
13015 fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
13016 // The empty list `()` has no operator and no children. Pin that
13017 // `as_call_to_any` rejects it (no head to feed the decoder), the
13018 // consumer falls through to `as_list()` which returns `Some(&[])`,
13019 // and the children-walk emits `Sexp::List(vec![])` (an empty
13020 // list, not `form.clone()` of `Sexp::List(vec![])` — both happen
13021 // to be observationally identical, but the path is the
13022 // children-walk arm, NOT the not-a-list arm). Path-uniformity
13023 // gate for the singleton-list edge case the `compile_named_from_
13024 // forms` rejection chain relies on `as_call_to(KEYWORD)` to yield
13025 // `Some(&[])` for — same posture, different family member.
13026 let e = Expander::new();
13027 let empty = Sexp::List(vec![]);
13028
13029 // Hand-rolled family-primitive lookup rejects the empty list.
13030 assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
13031
13032 // Consumer walks children (zero of them) — output is the empty
13033 // list, same as input.
13034 assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
13035 }
13036
13037 // ── Expander::expand_and_collect_calls_to: program-level walk ───────
13038 //
13039 // `expand_and_collect_calls_to(forms, keyword, project)` composes the
13040 // expander's program-level expansion (`expand_program`) with the
13041 // substrate's slice-side typed-keyword projection (`iter_calls_to`)
13042 // and a caller-supplied per-form mapper into ONE method on the
13043 // `Expander` surface. Both `compile_typed::<T>` and
13044 // `compile_named_from_forms::<T>` (compile.rs) route through it; the
13045 // tests below pin its contract directly. The existing compile.rs
13046 // dispatch tests are the path-uniformity guards proving the two
13047 // production sites route through it without behavior drift.
13048
13049 #[test]
13050 fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
13051 // Three forms — two match `defmonitor`, one matches `defalert`.
13052 // The mapper records `args.len()` from each matching form's tail.
13053 // Pin: only the two `defmonitor` matches flow through `project`,
13054 // in source order (so the recorded lengths are 2, 1).
13055 let forms = vec![
13056 Sexp::List(vec![
13057 Sexp::symbol("defmonitor"),
13058 Sexp::keyword("name"),
13059 Sexp::string("first"),
13060 ]),
13061 Sexp::List(vec![
13062 Sexp::symbol("defalert"),
13063 Sexp::keyword("name"),
13064 Sexp::string("not-a-match"),
13065 ]),
13066 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
13067 ];
13068 let mut e = Expander::new();
13069 let lengths: Vec<usize> = e
13070 .expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
13071 .expect("matching forms must compose");
13072 assert_eq!(lengths, vec![2, 1]);
13073 }
13074
13075 #[test]
13076 fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13077 // Path-uniformity for the soft-projection posture inherited from
13078 // `iter_calls_to`: non-matching forms are skipped silently, and
13079 // `project` is never invoked on them. Pin via a counter the
13080 // closure increments per invocation — the counter is the
13081 // observable that distinguishes "skipped silently" from
13082 // "projected to a no-op".
13083 let forms = vec![
13084 Sexp::symbol("bare-atom"),
13085 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
13086 Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
13087 Sexp::List(vec![]),
13088 Sexp::Nil,
13089 ];
13090 let mut count = 0usize;
13091 let mut e = Expander::new();
13092 let out: Vec<()> = e
13093 .expand_and_collect_calls_to(forms, "defmonitor", |_args| {
13094 count += 1;
13095 Ok(())
13096 })
13097 .expect("non-matching-only slice must collect to empty Vec");
13098 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13099 assert_eq!(count, 0, "project must never run for non-matching forms");
13100 }
13101
13102 #[test]
13103 fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13104 // Three matching forms; the closure errors on the SECOND. Pin: the
13105 // collect's `Result<Vec<_>, _>` short-circuit fires at the second
13106 // form's error, the third form's `project` is NEVER invoked, and
13107 // the returned error is exactly the one the closure raised. This
13108 // mirrors `compile_named_from_forms`'s short-circuit on the first
13109 // `NamedFormMissingName` / `NamedFormNonSymbolName` / typed-entry
13110 // kwargs rejection.
13111 let forms = vec![
13112 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13113 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
13114 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13115 ];
13116 let mut seen = Vec::new();
13117 let mut e = Expander::new();
13118 let err = e
13119 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
13120 let n = args[0].as_int().expect("test args are ints");
13121 seen.push(n);
13122 if n == 2 {
13123 Err(LispError::Compile {
13124 form: "test".to_string(),
13125 message: "stop at two".to_string(),
13126 })
13127 } else {
13128 Ok(())
13129 }
13130 })
13131 .expect_err("project's error must short-circuit collect");
13132 // Only the first two forms were ever projected — the third
13133 // never reached `project`.
13134 assert_eq!(seen, vec![1, 2]);
13135 assert!(
13136 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13137 "short-circuit must propagate the project's error verbatim, got {err:?}"
13138 );
13139 }
13140
13141 #[test]
13142 fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
13143 // The `expand_program` step runs BEFORE `iter_calls_to` walks
13144 // anything — so an `expand_program` error (e.g., a malformed
13145 // `defmacro` head) must short-circuit the entire composition
13146 // and `project` must NEVER run. Pin via the closure-running
13147 // counter: an error from the FIRST step never invokes `project`.
13148 // `(defmacro 5 (x) `,x)` — the macro NAME slot is an int, not a
13149 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13150 // fires during `expand_program`.
13151 let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
13152 let mut count = 0usize;
13153 let mut e = Expander::new();
13154 let err = e
13155 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
13156 count += 1;
13157 Ok(())
13158 })
13159 .expect_err("expand_program error must short-circuit before project");
13160 assert_eq!(
13161 count, 0,
13162 "project must never run when expand_program errors"
13163 );
13164 // Sanity: the error IS the expand_program-stage rejection, NOT
13165 // a project-stage error (path-uniformity for the ordering).
13166 let rendered = format!("{err}");
13167 assert!(
13168 rendered.contains("NAME") || rendered.contains("symbol"),
13169 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13170 );
13171 }
13172
13173 #[test]
13174 fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
13175 // Boundary: empty forms slice yields zero items regardless of
13176 // keyword. Pin the degenerate boundary — empty in, empty out —
13177 // and the closure is never invoked. Sibling of
13178 // `iter_calls_to_yields_nothing_for_empty_slice` in ast.rs,
13179 // one level up the composition: the program-level walk is
13180 // fused-empty when `expand_program` yields an empty `Vec<Sexp>`.
13181 let mut count = 0usize;
13182 let mut e = Expander::new();
13183 let out: Vec<()> = e
13184 .expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
13185 count += 1;
13186 Ok(())
13187 })
13188 .expect("empty forms is not an error");
13189 assert!(out.is_empty());
13190 assert_eq!(count, 0);
13191 }
13192
13193 #[test]
13194 fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13195 // Critical ordering: `expand_program` runs BEFORE the keyword
13196 // filter, so a `defmacro` whose expansion produces a form with
13197 // a matching head IS visible to `project`. Pin the pipeline
13198 // order: `(defmacro emit-monitor (n) `(defmonitor :name ,n))
13199 // (emit-monitor "alpha") (defmonitor :name "beta")`
13200 // expands the macro call to `(defmonitor :name "alpha")` and
13201 // the keyword walk then sees TWO `defmonitor` forms (the
13202 // macro-emitted one AND the directly-authored one). A
13203 // pre-expansion filter would miss the macro-emitted form.
13204 let forms = read(
13205 "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13206 (emit-monitor \"alpha\")
13207 (defmonitor :name \"beta\")",
13208 )
13209 .unwrap();
13210 let mut e = Expander::new();
13211 let names: Vec<String> = e
13212 .expand_and_collect_calls_to(forms, "defmonitor", |args| {
13213 // Each defmonitor form's tail is `(:name "X")`; project
13214 // the second element's string to capture the name.
13215 Ok(args[1].as_string().unwrap().to_string())
13216 })
13217 .expect("macroexpanded + directly-authored forms must both flow");
13218 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13219 }
13220
13221 #[test]
13222 fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13223 // The `keyword` argument flows straight into `iter_calls_to`'s
13224 // head-comparison gate — a regression that drifts the keyword
13225 // (e.g., uses `T::KEYWORD` from a different `T` at the helper,
13226 // or lowercases / trims the keyword en route) fails here. Pin
13227 // by passing TWO different keywords against the SAME forms
13228 // input and asserting each picks up only its matching forms.
13229 let forms = vec![
13230 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13231 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
13232 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13233 Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
13234 ];
13235
13236 let mut e = Expander::new();
13237 let monitors: Vec<i64> = e
13238 .expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
13239 Ok(args[0].as_int().unwrap())
13240 })
13241 .unwrap();
13242 assert_eq!(monitors, vec![1, 3]);
13243
13244 let mut e2 = Expander::new();
13245 let alerts: Vec<i64> = e2
13246 .expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
13247 Ok(args[0].as_int().unwrap())
13248 })
13249 .unwrap();
13250 assert_eq!(alerts, vec![2]);
13251
13252 // A keyword nothing matches collects to the empty Vec — same
13253 // shape as the empty-forms case at the slice level.
13254 let mut e3 = Expander::new();
13255 let none: Vec<i64> = e3
13256 .expand_and_collect_calls_to(forms, "missing-keyword", |args| {
13257 Ok(args[0].as_int().unwrap())
13258 })
13259 .unwrap();
13260 assert!(none.is_empty());
13261 }
13262
13263 #[test]
13264 fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
13265 // Structural identity: for any (forms, keyword, project) triple
13266 // whose pieces succeed, the method's result equals the inlined
13267 // pre-lift pipeline `expand_program + iter_calls_to + map +
13268 // collect`. Pin shape AND ordering on a mixed forms input
13269 // (matching + non-matching + macroexpand-emitted matches) so a
13270 // regression that drifts the method's pipeline from the inlined
13271 // closed-form fails here. This is the lift's load-bearing
13272 // path-uniformity gate — the SAME assertion that holds for
13273 // both production consumers (compile.rs) holds at the
13274 // primitive's contract level.
13275 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13276 (foo :idx 1)
13277 (emit-foo 2)
13278 (bar :idx 99)
13279 (foo :idx 3)";
13280 let forms = read(src).unwrap();
13281
13282 // Inlined pre-lift pipeline.
13283 let mut exp_inline = Expander::new();
13284 let expanded = exp_inline.expand_program(forms.clone()).unwrap();
13285 let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
13286 .map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
13287 .collect::<Result<Vec<_>>>()
13288 .unwrap();
13289
13290 // Method pipeline.
13291 let mut exp_method = Expander::new();
13292 let via_method: Vec<i64> = exp_method
13293 .expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13294 .unwrap();
13295
13296 assert_eq!(via_inline, via_method);
13297 assert_eq!(via_inline, vec![1, 2, 3]);
13298 }
13299
13300 // ── Expander::expand_and_collect_calls_to_any: typed-decoded sibling ──
13301 //
13302 // The typed-decoded classifier sibling of `expand_and_collect_calls_to`:
13303 // composes `expand_program` with `iter_calls_to_any` and a per-form
13304 // mapper that receives BOTH the typed witness AND the args tail.
13305 // Closes the (keyword, classifier) 2×2 of expander-surface
13306 // compose-on-iter projections at the typed-decoded corner the prior
13307 // runs' slice-side `iter_calls_to_any` lift left open. The keyword
13308 // sibling now ROUTES through this primitive via a constant-classifier
13309 // composition — the tests below pin both the typed-decoded primitive's
13310 // contract directly AND the routing identity binding the keyword
13311 // sibling to it.
13312 //
13313 // The existing `expand_and_collect_calls_to_*` tests above remain
13314 // load-bearing — they pin the keyword sibling's contract through
13315 // its ROUTED implementation, so a regression that drifts the routing
13316 // composition surfaces at the existing keyword tests too.
13317
13318 #[test]
13319 fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
13320 {
13321 // The typed-decoded primitive's happy path: a closed-set
13322 // classifier decodes head symbols to a typed enum, and the
13323 // per-form projection receives `(decoded, args)` for every
13324 // matched form in source order. Sweep across THREE distinct
13325 // classifier outcomes — `Foo` / `Bar` / `Baz` — interleaved with
13326 // a non-matching form to pin both the typed-witness threading
13327 // AND the source-order yield AND the rejection of non-classifier
13328 // forms in ONE assertion. The decoder runs once per call form
13329 // (`FnMut`); the projection runs once per matched form.
13330 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
13331 enum Op {
13332 Foo,
13333 Bar,
13334 Baz,
13335 }
13336 let src = "(foo 1)
13337 (bar 2)
13338 (other 99)
13339 (baz 3)
13340 (foo 4)";
13341 let forms = read(src).unwrap();
13342 let mut e = Expander::new();
13343 let pairs: Vec<(Op, i64)> = e
13344 .expand_and_collect_calls_to_any(
13345 forms,
13346 |h| match h {
13347 "foo" => Some(Op::Foo),
13348 "bar" => Some(Op::Bar),
13349 "baz" => Some(Op::Baz),
13350 _ => None,
13351 },
13352 |op, args| Ok((op, args[0].as_int().unwrap())),
13353 )
13354 .unwrap();
13355 assert_eq!(
13356 pairs,
13357 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
13358 );
13359 }
13360
13361 #[test]
13362 fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13363 // The soft-projection contract — every shape `iter_calls_to_any`
13364 // rejects (non-list, empty list, list whose head is not a
13365 // symbol, list whose head decodes through the classifier to
13366 // `None`) skips the projection silently. Pin a deliberately-
13367 // panicking projection so the assertion fires loudly if a
13368 // non-matching form reaches it. The decoder accepts only
13369 // `"defmonitor"`; every other shape (atom, list whose head is a
13370 // keyword, list whose head is a symbol the decoder rejects)
13371 // short-circuits before the projection runs.
13372 let src = r#":bare-keyword
13373 "bare-string"
13374 42
13375 ()
13376 (foo bar)
13377 (defmonitor :name "matches")"#;
13378 let forms = read(src).unwrap();
13379 let mut e = Expander::new();
13380 let lengths: Vec<usize> = e
13381 .expand_and_collect_calls_to_any(
13382 forms,
13383 |h| (h == "defmonitor").then_some(()),
13384 |(), args| {
13385 // The projection must run EXACTLY once — for the
13386 // single matched form. Any non-matching form that
13387 // reaches the projection panics here.
13388 assert_eq!(args.len(), 2, "projection ran on non-matching form");
13389 Ok(args.len())
13390 },
13391 )
13392 .unwrap();
13393 assert_eq!(lengths, vec![2]);
13394 }
13395
13396 #[test]
13397 fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13398 // The Result projection's short-circuit contract — when the
13399 // projection returns `Err` on a matched form, the walk
13400 // short-circuits and subsequent matched forms are NOT projected.
13401 // Pin the source-order short-circuit via a counter the
13402 // projection increments before deciding to fail: the counter
13403 // sits at exactly the index of the failing form (the second
13404 // match), proving the third match never reached the projection.
13405 let src = "(foo 1) (foo 2) (foo 3)";
13406 let forms = read(src).unwrap();
13407 let mut count = 0usize;
13408 let mut e = Expander::new();
13409 let err = e
13410 .expand_and_collect_calls_to_any::<i64, _, _, ()>(
13411 forms,
13412 |h| (h == "foo").then_some(()),
13413 |(), args| {
13414 count += 1;
13415 let v = args[0].as_int().unwrap();
13416 if v == 2 {
13417 return Err(crate::error::LispError::Missing("test-failure"));
13418 }
13419 Ok(v)
13420 },
13421 )
13422 .expect_err("projection must short-circuit on first Err");
13423 assert_eq!(
13424 count, 2,
13425 "projection must have run on first AND failing form, then stopped",
13426 );
13427 // Sanity: the error is the project-stage rejection — the same
13428 // typed `LispError` the projection returned, propagated verbatim.
13429 assert!(
13430 matches!(err, crate::error::LispError::Missing("test-failure")),
13431 "expected the projection's typed Err verbatim, got {err:?}",
13432 );
13433 }
13434
13435 #[test]
13436 fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
13437 // Ordering contract — `expand_program` runs BEFORE
13438 // `iter_calls_to_any` walks the slice. A `(defmacro …)` form
13439 // that emits a classifier-decoded body must have its expanded
13440 // body decoded by the classifier (not the unexpanded macro-call
13441 // head). Pin the ordering with a macro `(emit-foo n)` that
13442 // expands to `(foo :idx n)` — the classifier sees the expanded
13443 // `foo` head, not `emit-foo`. Mirrors the parallel keyword test
13444 // `expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword`
13445 // — the SAME composition contract holds on the typed-decoded
13446 // sibling.
13447 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13448 (foo :idx 1)
13449 (emit-foo 2)
13450 (bar :idx 99)
13451 (foo :idx 3)";
13452 let forms = read(src).unwrap();
13453 let mut e = Expander::new();
13454 // Classifier returns `()` for "foo" only; the keyword sibling's
13455 // routing identity is the same composition we exercise here
13456 // directly.
13457 let idxs: Vec<i64> = e
13458 .expand_and_collect_calls_to_any(
13459 forms,
13460 |h| (h == "foo").then_some(()),
13461 |(), args| Ok(args[1].as_int().unwrap()),
13462 )
13463 .unwrap();
13464 // 1 from the literal call, 2 from the macro-emitted call, 3 from
13465 // the final literal call. The macro-emitted `(foo :idx 2)` is
13466 // present BECAUSE `expand_program` ran first.
13467 assert_eq!(idxs, vec![1, 2, 3]);
13468 }
13469
13470 #[test]
13471 fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
13472 // The `FnMut` constraint — the slice walk calls the classifier
13473 // once per call form, and a decoder that captures mutable state
13474 // (a counter, a registry cache, a visited-set) maintains that
13475 // state across the walk. Pin the `FnMut` contract with a counter
13476 // closure that increments per shape-gate-passing form (calls
13477 // whose head is a symbol), asserting the counter equals the
13478 // total call count regardless of whether the classifier accepts
13479 // each form. Mirrors the parallel slice-side test
13480 // `iter_calls_to_any_admits_fnmut_classifier_maintaining_state_across_batch_walk`
13481 // — the SAME `FnMut` contract holds on the expander surface.
13482 let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
13483 let forms = read(src).unwrap();
13484 let mut e = Expander::new();
13485 let mut classifier_calls = 0usize;
13486 let projected: Vec<i64> = e
13487 .expand_and_collect_calls_to_any(
13488 forms,
13489 |h| {
13490 classifier_calls += 1;
13491 (h == "foo").then_some(())
13492 },
13493 |(), args| Ok(args[0].as_int().unwrap()),
13494 )
13495 .unwrap();
13496 assert_eq!(
13497 classifier_calls, 5,
13498 "classifier must run once per call form (5 forms, all calls with symbol heads)",
13499 );
13500 assert_eq!(
13501 projected,
13502 vec![1, 3, 5],
13503 "projection must run only for classifier-accepted forms in source order",
13504 );
13505 }
13506
13507 #[test]
13508 fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
13509 ) {
13510 // Structural identity: the keyword sibling
13511 // `expand_and_collect_calls_to` ROUTES through the typed-decoded
13512 // primitive via a constant-classifier composition — the
13513 // post-lift composition law binding the two siblings:
13514 //
13515 // expand_and_collect_calls_to(forms, k, p) ==
13516 // expand_and_collect_calls_to_any(forms,
13517 // |h| (h == k).then_some(()),
13518 // |(), args| p(args))
13519 //
13520 // Pin shape AND ordering across THREE representative keyword
13521 // values: one that matches multiple forms, one that matches
13522 // none, one that matches exactly one form. Same mixed input
13523 // (literal + macroexpand-emitted + non-matching) the keyword
13524 // path-uniformity test exercises, so the routing identity holds
13525 // on the SAME input shape the keyword sibling's contract pins.
13526 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13527 (foo :idx 1)
13528 (emit-foo 2)
13529 (bar :idx 99)
13530 (foo :idx 3)";
13531 let forms = read(src).unwrap();
13532
13533 for keyword in ["foo", "bar", "absent"] {
13534 let mut exp_keyword = Expander::new();
13535 let via_keyword: Vec<i64> = exp_keyword
13536 .expand_and_collect_calls_to(forms.clone(), keyword, |args| {
13537 Ok(args[1].as_int().unwrap())
13538 })
13539 .unwrap();
13540 let mut exp_classifier = Expander::new();
13541 let via_classifier: Vec<i64> = exp_classifier
13542 .expand_and_collect_calls_to_any(
13543 forms.clone(),
13544 |h| (h == keyword).then_some(()),
13545 |(), args| Ok(args[1].as_int().unwrap()),
13546 )
13547 .unwrap();
13548 assert_eq!(
13549 via_keyword, via_classifier,
13550 "routing identity drifted for keyword {keyword:?}",
13551 );
13552 }
13553 }
13554
13555 #[test]
13556 fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
13557 {
13558 // The expand_program-stage short-circuit contract — when
13559 // `expand_program` rejects (e.g. a `(defmacro …)` form whose
13560 // param list is malformed), the walk short-circuits BEFORE the
13561 // classifier or projection runs. Pin the ordering with a
13562 // deliberately-panicking classifier AND projection: any
13563 // post-expand-program execution fires the panic. Mirrors the
13564 // parallel keyword test
13565 // `expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs`
13566 // — the SAME composition contract holds on the typed-decoded
13567 // sibling.
13568 // `(defmacro 5 (x) `,x)` — macro NAME slot is an int, not a
13569 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13570 // fires during `expand_program`. Same rejection shape the
13571 // parallel keyword test uses, so the ordering contract observed
13572 // here is structurally identical to the keyword sibling's.
13573 let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
13574 let mut e = Expander::new();
13575 let err = e
13576 .expand_and_collect_calls_to_any::<(), _, _, ()>(
13577 forms,
13578 |_h| -> Option<()> {
13579 panic!("classifier must not run when expand_program errors");
13580 },
13581 |(), _args| {
13582 panic!("project must not run when expand_program errors");
13583 },
13584 )
13585 .expect_err("expand_program error must short-circuit before classifier or project");
13586 // Sanity: the error IS an expand_program-stage rejection — not a
13587 // classifier or projection error — so the ordering is observable.
13588 let rendered = format!("{err}");
13589 assert!(
13590 rendered.contains("NAME") || rendered.contains("symbol"),
13591 "expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
13592 );
13593 }
13594
13595 // ── Expander::expand_source_and_collect_calls_to: from-source walk ──
13596 //
13597 // The from-source sibling of `expand_and_collect_calls_to`: composes
13598 // `crate::reader::read(src)?` with the from-forms primitive in ONE
13599 // method on the `Expander` surface. All four typed-program dispatchers
13600 // (free-function `compile_typed` / `compile_named`, preloaded-expander
13601 // `RealizedCompiler::compile_typed` / `compile_named`) route through
13602 // it; the tests below pin its contract directly. The existing
13603 // compile.rs + compiler_spec.rs dispatch tests are the path-uniformity
13604 // guards proving the four production sites route through it without
13605 // behavior drift.
13606
13607 #[test]
13608 fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
13609 // Happy path: a source string with three top-level forms (two
13610 // matching `defmonitor`, one matching `defalert`) flows through
13611 // `read` then through the from-forms primitive. Pin the SAME
13612 // emission shape the from-forms test pins for the matching
13613 // sub-set, sourced from a `&str` rather than a `Vec<Sexp>`.
13614 let src = r#"(defmonitor :name "first")
13615 (defalert :name "not-a-match")
13616 (defmonitor :solo)"#;
13617 let mut e = Expander::new();
13618 let lengths: Vec<usize> = e
13619 .expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
13620 .expect("matching forms must compose");
13621 assert_eq!(lengths, vec![2, 1]);
13622 }
13623
13624 #[test]
13625 fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
13626 // The reader runs BEFORE `expand_program` — so a reader error (an
13627 // unterminated string, an unbalanced paren, an unknown escape)
13628 // must short-circuit the entire composition and `expand_program`
13629 // / the keyword filter / `project` must NEVER run. Pin the
13630 // ordering: an unbalanced open-paren is rejected by the reader
13631 // at parse time, never reaches the from-forms primitive.
13632 let mut count = 0usize;
13633 let mut e = Expander::new();
13634 let err = e
13635 .expand_source_and_collect_calls_to::<(), _>(
13636 "(defmonitor :name \"unbalanced",
13637 "defmonitor",
13638 |_args| {
13639 count += 1;
13640 Ok(())
13641 },
13642 )
13643 .expect_err("reader error must short-circuit before expand_program");
13644 assert_eq!(
13645 count, 0,
13646 "project must never run when reader errors at parse time"
13647 );
13648 // Sanity: the error IS a reader-stage rejection — the rendered
13649 // diagnostic mentions the lexer's gate, not the expander's or
13650 // projector's (path-uniformity for the read-then-walk ordering).
13651 let rendered = format!("{err}");
13652 assert!(
13653 rendered.to_lowercase().contains("string")
13654 || rendered.to_lowercase().contains("paren")
13655 || rendered.to_lowercase().contains("eof")
13656 || rendered.to_lowercase().contains("unexpected")
13657 || rendered.to_lowercase().contains("unterminated")
13658 || rendered.to_lowercase().contains("unclosed"),
13659 "error must be the reader-stage rejection, got: {rendered}"
13660 );
13661 }
13662
13663 #[test]
13664 fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
13665 ) {
13666 // Reader succeeds, but `expand_program` rejects a malformed
13667 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13668 // ordering: `expand_program` rejects BEFORE the keyword filter
13669 // walks anything, `project` must NEVER run. Sibling of the
13670 // from-forms test of the same name — one level up the
13671 // composition (sourced from `&str` rather than `Vec<Sexp>`).
13672 let mut count = 0usize;
13673 let mut e = Expander::new();
13674 let err = e
13675 .expand_source_and_collect_calls_to::<(), _>(
13676 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13677 "defmonitor",
13678 |_args| {
13679 count += 1;
13680 Ok(())
13681 },
13682 )
13683 .expect_err("expand_program error must short-circuit before project");
13684 assert_eq!(
13685 count, 0,
13686 "project must never run when expand_program errors"
13687 );
13688 let rendered = format!("{err}");
13689 assert!(
13690 rendered.contains("NAME") || rendered.contains("symbol"),
13691 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13692 );
13693 }
13694
13695 #[test]
13696 fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13697 // Reader + `expand_program` both succeed; the per-form `project`
13698 // errors on the SECOND matched form. Pin: the collect's short-
13699 // circuit fires at the second match's error, the third match's
13700 // `project` is NEVER invoked, and the returned error is exactly
13701 // the one the closure raised. Mirrors `compile_named`'s
13702 // short-circuit on the first `NamedFormMissingName` /
13703 // `NamedFormNonSymbolName` / typed-entry rejection — sourced
13704 // from `&str`.
13705 let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
13706 let mut seen = Vec::new();
13707 let mut e = Expander::new();
13708 let err = e
13709 .expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
13710 let n = args[1].as_int().expect("test args are ints");
13711 seen.push(n);
13712 if n == 2 {
13713 Err(LispError::Compile {
13714 form: "test".to_string(),
13715 message: "stop at two".to_string(),
13716 })
13717 } else {
13718 Ok(())
13719 }
13720 })
13721 .expect_err("project's error must short-circuit collect");
13722 assert_eq!(seen, vec![1, 2]);
13723 assert!(
13724 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13725 "short-circuit must propagate the project's error verbatim, got {err:?}"
13726 );
13727 }
13728
13729 #[test]
13730 fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
13731 // Boundary: empty source string yields zero items regardless of
13732 // keyword. Pin the degenerate boundary — empty in, empty out —
13733 // and the closure is never invoked. Sibling of the from-forms
13734 // test of the same name, one level up the composition: the
13735 // from-source posture is fused-empty when `read` yields an
13736 // empty `Vec<Sexp>` (which, fed into `expand_program`, yields
13737 // an empty `Vec<Sexp>`, which iter_calls_to walks to zero
13738 // matches).
13739 let mut count = 0usize;
13740 let mut e = Expander::new();
13741 let out: Vec<()> = e
13742 .expand_source_and_collect_calls_to("", "anything", |_args| {
13743 count += 1;
13744 Ok(())
13745 })
13746 .expect("empty source is not an error");
13747 assert!(out.is_empty());
13748 assert_eq!(count, 0);
13749 }
13750
13751 #[test]
13752 fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13753 // Critical ordering preserved across the from-source posture:
13754 // `defmacro` source registers in `expand_program`, its call
13755 // sites expand to the matching keyword, AND the keyword walk
13756 // sees both the macro-emitted form and any directly-authored
13757 // matches in source order. Sibling of the from-forms test of
13758 // the same name, sourced from `&str`.
13759 let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13760 (emit-monitor \"alpha\")
13761 (defmonitor :name \"beta\")";
13762 let mut e = Expander::new();
13763 let names: Vec<String> = e
13764 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13765 Ok(args[1].as_string().unwrap().to_string())
13766 })
13767 .expect("macroexpanded + directly-authored forms must both flow");
13768 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13769 }
13770
13771 #[test]
13772 fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
13773 // Structural identity: for any (src, keyword, project) triple
13774 // whose pieces succeed, the from-source method's result equals
13775 // the inlined pre-lift pipeline `read + expand_and_collect_
13776 // calls_to`. Pin shape AND ordering on a mixed source
13777 // (matching + non-matching + macroexpand-emitted matches) so a
13778 // regression that drifts the method's pipeline from the
13779 // inlined closed-form fails here. This is the lift's
13780 // load-bearing path-uniformity gate — the SAME assertion that
13781 // holds for all four production consumers (free-function
13782 // `compile_typed`/`compile_named` + `RealizedCompiler::compile_typed`/`compile_named`)
13783 // holds at the primitive's contract level.
13784 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13785 (foo :idx 1)
13786 (emit-foo 2)
13787 (bar :idx 99)
13788 (foo :idx 3)";
13789
13790 // Inlined pre-lift pipeline.
13791 let mut exp_inline = Expander::new();
13792 let inline_forms = read(src).unwrap();
13793 let via_inline: Vec<i64> = exp_inline
13794 .expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13795 .unwrap();
13796
13797 // From-source method pipeline.
13798 let mut exp_method = Expander::new();
13799 let via_method: Vec<i64> = exp_method
13800 .expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
13801 .unwrap();
13802
13803 assert_eq!(via_inline, via_method);
13804 assert_eq!(via_inline, vec![1, 2, 3]);
13805 }
13806
13807 #[test]
13808 fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13809 // The `keyword` argument flows straight into the from-forms
13810 // primitive's head-comparison gate, which inherits from
13811 // `iter_calls_to` — a regression that drifts the keyword (e.g.,
13812 // lowercases / trims it en route through the read step) fails
13813 // here. Pin by passing TWO different keywords against the SAME
13814 // source and asserting each picks up only its matching forms.
13815 let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
13816
13817 let mut e1 = Expander::new();
13818 let monitors: Vec<i64> = e1
13819 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13820 Ok(args[1].as_int().unwrap())
13821 })
13822 .unwrap();
13823 assert_eq!(monitors, vec![1, 3]);
13824
13825 let mut e2 = Expander::new();
13826 let alerts: Vec<i64> = e2
13827 .expand_source_and_collect_calls_to(src, "defalert", |args| {
13828 Ok(args[1].as_int().unwrap())
13829 })
13830 .unwrap();
13831 assert_eq!(alerts, vec![2]);
13832
13833 let mut e3 = Expander::new();
13834 let none: Vec<i64> = e3
13835 .expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
13836 Ok(args[1].as_int().unwrap())
13837 })
13838 .unwrap();
13839 assert!(none.is_empty());
13840 }
13841
13842 #[test]
13843 fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13844 // Path-uniformity for the soft-projection posture inherited
13845 // from `iter_calls_to`: non-matching forms in the source are
13846 // skipped silently, and `project` is never invoked on them.
13847 // Sibling of the from-forms test of the same name, sourced
13848 // from `&str` with a mix of atoms-and-lists that the reader
13849 // emits at the top level.
13850 let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
13851 let mut count = 0usize;
13852 let mut e = Expander::new();
13853 let out: Vec<()> = e
13854 .expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
13855 count += 1;
13856 Ok(())
13857 })
13858 .expect("non-matching-only source must collect to empty Vec");
13859 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13860 assert_eq!(count, 0, "project must never run for non-matching forms");
13861 }
13862
13863 // ── Expander::expand_source_and_collect_calls_to_any — from-source × classifier ─
13864 //
13865 // The from-source posture of the typed-decoded classifier sibling on
13866 // the `Expander` surface — closes the (from-forms, from-source) ×
13867 // (keyword, classifier) 2×2 of compose-on-iter projections at the
13868 // from-source classifier corner the prior runs left open. The
13869 // pre-existing `expand_source_and_collect_calls_to` (from-source ×
13870 // keyword) is the constant-classifier projection of this primitive:
13871 // its body composes this function with a `|h| (h == keyword).
13872 // then_some(())` decoder, so the from-source pipeline lives at ONE
13873 // site. These tests pin both the typed-decoded primitive's contract
13874 // directly AND the post-lift routing of
13875 // `expand_source_and_collect_calls_to` through it.
13876
13877 #[test]
13878 fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
13879 ) {
13880 // Happy path: a source string with four top-level forms (two
13881 // matching "foo", one matching "bar", one rejected-head
13882 // "defmonitor") flows through `read` → `expand_program` → the
13883 // typed-decoded classifier walk. The closed-set classifier
13884 // `Op::from_keyword` admits "foo" and "bar" but rejects
13885 // "defmonitor". Pin the typed-decoded yield shape (decoded
13886 // witness alongside args tail) AND source-order preservation
13887 // sourced from `&str` rather than a pre-parsed `Vec<Sexp>`.
13888 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13889 enum Op {
13890 Foo,
13891 Bar,
13892 }
13893 fn op_from_keyword(head: &str) -> Option<Op> {
13894 match head {
13895 "foo" => Some(Op::Foo),
13896 "bar" => Some(Op::Bar),
13897 _ => None,
13898 }
13899 }
13900 let src = r#"(foo :idx 1)
13901 (defmonitor :idx 99)
13902 (bar :idx 2)
13903 (foo :idx 3)"#;
13904 let mut e = Expander::new();
13905 let yielded: Vec<(Op, i64)> = e
13906 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
13907 Ok((op, args[1].as_int().expect("test args are ints")))
13908 })
13909 .expect("matching forms must compose through from-source classifier walk");
13910 assert_eq!(
13911 yielded,
13912 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
13913 "yields must be in source order, decoded witness paired with per-form mapper output"
13914 );
13915 }
13916
13917 #[test]
13918 fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
13919 ) {
13920 // The reader runs BEFORE `expand_program` AND BEFORE the
13921 // classifier — so a reader error (an unterminated string)
13922 // short-circuits the entire composition and both the classifier
13923 // AND `project` must NEVER run. Pin the ordering with BOTH
13924 // decoder and projection explicitly panicking — any post-reader
13925 // execution fires the panic. Sibling of
13926 // `expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program`
13927 // one corner over in the 2×2 (the classifier version of the
13928 // same ordering pin).
13929 let mut e = Expander::new();
13930 let err = e
13931 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13932 "(defmonitor :name \"unbalanced",
13933 |_h: &str| -> Option<()> {
13934 panic!("classifier must not run when reader errors at parse time")
13935 },
13936 |(), _args| -> Result<()> { panic!("project must not run when reader errors") },
13937 )
13938 .expect_err("reader error must short-circuit before classifier");
13939 // Sanity: the error is a reader-stage rejection.
13940 let rendered = format!("{err}");
13941 assert!(
13942 rendered.to_lowercase().contains("string")
13943 || rendered.to_lowercase().contains("paren")
13944 || rendered.to_lowercase().contains("eof")
13945 || rendered.to_lowercase().contains("unexpected")
13946 || rendered.to_lowercase().contains("unterminated")
13947 || rendered.to_lowercase().contains("unclosed"),
13948 "error must be the reader-stage rejection, got: {rendered}"
13949 );
13950 }
13951
13952 #[test]
13953 fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
13954 ) {
13955 // Reader succeeds, but `expand_program` rejects a malformed
13956 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13957 // ordering: `expand_program` rejects BEFORE the classifier walks
13958 // anything, both decoder AND `project` must NEVER run. Sibling
13959 // of the keyword-from-source variant — one corner over in the
13960 // 2×2 with both classifier and projection explicitly panicking.
13961 let mut e = Expander::new();
13962 let err = e
13963 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13964 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13965 |_h: &str| -> Option<()> {
13966 panic!("classifier must not run when expand_program errors")
13967 },
13968 |(), _args| -> Result<()> {
13969 panic!("project must not run when expand_program errors")
13970 },
13971 )
13972 .expect_err("expand_program error must short-circuit before classifier");
13973 let rendered = format!("{err}");
13974 assert!(
13975 rendered.contains("NAME") || rendered.contains("symbol"),
13976 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13977 );
13978 }
13979
13980 #[test]
13981 fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13982 // Path-uniformity for the soft-projection posture inherited
13983 // from `iter_calls_to_any`: forms whose head the classifier
13984 // rejects are skipped silently, and `project` is never invoked
13985 // on them. Pin via an explicitly-panicking projection across a
13986 // mix of forms (atom, list-with-non-symbol-head, list with
13987 // unrecognized-by-classifier symbol head). A regression that
13988 // wrongly invokes `project` on a rejected form fires the panic.
13989 let src = r#"bare-atom
13990 (5 :not-a-symbol-head)
13991 (defmonitor :name "decoder-rejects-me")"#;
13992 let mut e = Expander::new();
13993 // The closed-set classifier admits NOTHING in this source.
13994 let out: Vec<()> = e
13995 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13996 src,
13997 |_h: &str| -> Option<()> { None },
13998 |(), _args| -> Result<()> {
13999 panic!("project must not run for classifier-rejected forms")
14000 },
14001 )
14002 .expect("classifier-rejects-all source must collect to empty Vec");
14003 assert!(
14004 out.is_empty(),
14005 "no classifier-accepted forms — empty Vec, got {out:?}"
14006 );
14007 }
14008
14009 #[test]
14010 fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
14011 // Reader + `expand_program` + classifier all succeed; the
14012 // per-form `project` errors on the SECOND matched form. Pin: the
14013 // collect's short-circuit fires at the second match's error, the
14014 // third match's `project` is NEVER invoked, and the returned
14015 // error is exactly the one the closure raised. Sibling of the
14016 // keyword-from-source variant — one corner over in the 2×2.
14017 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14018 enum Op {
14019 Foo,
14020 }
14021 fn op_from_keyword(head: &str) -> Option<Op> {
14022 (head == "foo").then_some(Op::Foo)
14023 }
14024 let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
14025 let mut seen = Vec::new();
14026 let mut e = Expander::new();
14027 let err = e
14028 .expand_source_and_collect_calls_to_any::<(), _, _, _>(
14029 src,
14030 op_from_keyword,
14031 |_op: Op, args: &[Sexp]| -> Result<()> {
14032 let n = args[1].as_int().expect("test args are ints");
14033 seen.push(n);
14034 if n == 2 {
14035 Err(LispError::Compile {
14036 form: "test".to_string(),
14037 message: "stop at two".to_string(),
14038 })
14039 } else {
14040 Ok(())
14041 }
14042 },
14043 )
14044 .expect_err("project's error must short-circuit collect");
14045 assert_eq!(seen, vec![1, 2], "third match's project must never run");
14046 assert!(
14047 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
14048 "short-circuit must propagate the project's error verbatim, got {err:?}"
14049 );
14050 }
14051
14052 #[test]
14053 fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
14054 ) {
14055 // The post-lift composition law binding the keyword-from-source
14056 // sibling to the typed-decoded primitive:
14057 // `expand_source_and_collect_calls_to(src, k, project) ==
14058 // expand_source_and_collect_calls_to_any(src,
14059 // |h| (h == k).then_some(()),
14060 // |(), args| project(args))`
14061 // Pin shape AND ordering across THREE representative keywords
14062 // (matching some, matching exactly one, matching none) on the
14063 // SAME mixed source the keyword path-uniformity test exercises.
14064 // A regression that re-implements the keyword-from-source filter
14065 // as a parallel `read + expand_and_collect_calls_to` pipeline
14066 // (rather than routing through the typed-decoded primitive)
14067 // fails the value pin.
14068 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
14069 (foo :idx 1)
14070 (emit-foo 2)
14071 (bar :idx 99)
14072 (foo :idx 3)";
14073 for k in ["foo", "bar", "missing"] {
14074 let mut e_keyword = Expander::new();
14075 let via_keyword: Vec<i64> = e_keyword
14076 .expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
14077 .unwrap();
14078
14079 let mut e_classifier = Expander::new();
14080 let via_classifier: Vec<i64> = e_classifier
14081 .expand_source_and_collect_calls_to_any(
14082 src,
14083 |h: &str| (h == k).then_some(()),
14084 |(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
14085 )
14086 .unwrap();
14087
14088 assert_eq!(
14089 via_keyword, via_classifier,
14090 "keyword from-source path must equal classifier from-source path for {k:?}"
14091 );
14092 }
14093 }
14094
14095 #[test]
14096 fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
14097 // Critical ordering preserved across the typed-decoded
14098 // from-source posture: `defmacro` source registers in
14099 // `expand_program`, its call sites expand to a classifier-
14100 // accepted form, AND the classifier walk sees both the
14101 // macro-emitted form and any directly-authored matches in
14102 // source order. Sibling of the keyword-from-source ordering
14103 // test, one corner over in the 2×2.
14104 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14105 enum Op {
14106 Foo,
14107 }
14108 fn op_from_keyword(head: &str) -> Option<Op> {
14109 (head == "foo").then_some(Op::Foo)
14110 }
14111 let src = "(defmacro emit-foo (n) `(foo :name ,n))
14112 (emit-foo \"alpha\")
14113 (foo :name \"beta\")";
14114 let mut e = Expander::new();
14115 let names: Vec<(Op, String)> = e
14116 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
14117 Ok((op, args[1].as_string().unwrap().to_string()))
14118 })
14119 .expect("macroexpanded + directly-authored forms must both flow through classifier");
14120 assert_eq!(
14121 names,
14122 vec![
14123 (Op::Foo, "alpha".to_string()),
14124 (Op::Foo, "beta".to_string()),
14125 ]
14126 );
14127 }
14128
14129 // ── expand_source_program — from-source × yield-all primitive ───
14130 //
14131 // Closes the 2×2 program-level walk family on the `Expander` surface:
14132 // from-forms × yield-all (`expand_program`), from-forms × keyword-
14133 // projected (`expand_and_collect_calls_to`), from-source × keyword-
14134 // projected (`expand_source_and_collect_calls_to`), AND now
14135 // from-source × yield-all (`expand_source_program`). Pins (a) the
14136 // happy path (mixed forms flow through reader then expand_program),
14137 // (b) reader-stage short-circuit (no expand_program side-effects),
14138 // (c) expand_program-stage short-circuit (defmacro-NAME-not-symbol
14139 // is rejected at its offending form), (d) defmacro absorption (the
14140 // primitive's side-effect on `self.macros` matches the from-forms
14141 // posture's side-effect verbatim), (e) the empty-source boundary,
14142 // and (f) structural identity vs. the inlined pre-lift `read +
14143 // expand_program` pipeline (path-uniformity gate).
14144
14145 #[test]
14146 fn expand_source_program_routes_through_reader_then_expand_program() {
14147 // Happy path: a source string with mixed forms (a `defmacro`
14148 // definition, a `defmonitor` form, a plain symbol literal) flows
14149 // through `read` then through the from-forms primitive. Pin the
14150 // SAME emission shape the from-forms test pins, sourced from a
14151 // `&str` rather than a `Vec<Sexp>`.
14152 let src = r#"(defmacro id (x) `,x)
14153 (defmonitor :name "alpha")
14154 bare-symbol"#;
14155 let mut e = Expander::new();
14156 let out = e
14157 .expand_source_program(src)
14158 .expect("mixed forms must compose");
14159 // `(defmacro id …)` is consumed as a side-effect (registers `id`
14160 // in `e.macros`), so the returned `Vec<Sexp>` contains the
14161 // remaining two top-level forms in source order.
14162 assert_eq!(out.len(), 2);
14163 assert_eq!(
14164 out[0].as_call_to("defmonitor").map(<[_]>::len),
14165 Some(2),
14166 "first surviving form is the defmonitor with two args, got: {:?}",
14167 out[0]
14168 );
14169 assert_eq!(
14170 out[1].as_symbol(),
14171 Some("bare-symbol"),
14172 "second surviving form is the bare symbol literal, got: {:?}",
14173 out[1]
14174 );
14175 // The defmacro side-effect landed in the expander's macro table.
14176 assert!(
14177 e.has("id"),
14178 "defmacro must register `id` in the expander's macro table"
14179 );
14180 }
14181
14182 #[test]
14183 fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
14184 // The reader runs BEFORE `expand_program` — so a reader error (an
14185 // unterminated string, an unbalanced paren) must short-circuit
14186 // the entire composition and `expand_program` must NEVER run.
14187 // Pin the ordering: a `defmacro` BEFORE the unterminated string
14188 // is NOT registered, because the reader fails at parse time
14189 // before `expand_program` sees any form.
14190 let mut e = Expander::new();
14191 let err = e
14192 .expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
14193 .expect_err("reader error must short-circuit before expand_program");
14194 // Sanity: the error IS a reader-stage rejection.
14195 let rendered = format!("{err}").to_lowercase();
14196 assert!(
14197 rendered.contains("string")
14198 || rendered.contains("paren")
14199 || rendered.contains("eof")
14200 || rendered.contains("unexpected")
14201 || rendered.contains("unterminated")
14202 || rendered.contains("unclosed"),
14203 "error must be the reader-stage rejection, got: {err}"
14204 );
14205 // Path-uniformity gate: the `defmacro` lexically BEFORE the
14206 // unterminated string must NOT have been absorbed — `read`'s
14207 // failure short-circuits the entire pipeline before
14208 // `expand_program` walks any forms.
14209 assert!(
14210 !e.has("should-not-register"),
14211 "reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
14212 );
14213 }
14214
14215 #[test]
14216 fn expand_source_program_short_circuits_on_expand_program_error() {
14217 // Reader succeeds, but `expand_program` rejects a malformed
14218 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14219 // ordering: the expand_program-stage rejection bubbles up
14220 // verbatim, sourced from `&str` rather than `Vec<Sexp>`.
14221 let mut e = Expander::new();
14222 let err = e
14223 .expand_source_program("(defmacro 5 (x) `,x)")
14224 .expect_err("expand_program error must propagate");
14225 let rendered = format!("{err}");
14226 assert!(
14227 rendered.contains("NAME") || rendered.contains("symbol"),
14228 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14229 );
14230 }
14231
14232 #[test]
14233 fn expand_source_program_yields_empty_vec_for_empty_source() {
14234 // Boundary: empty source string yields zero items. Pin the
14235 // degenerate boundary — empty in, empty out — sibling of the
14236 // from-source × keyword-projected test of the same name, one
14237 // level down the composition (no keyword filter).
14238 let mut e = Expander::new();
14239 let out = e
14240 .expand_source_program("")
14241 .expect("empty source is not an error");
14242 assert!(
14243 out.is_empty(),
14244 "empty source must yield empty Vec, got: {out:?}"
14245 );
14246 }
14247
14248 #[test]
14249 fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
14250 // Critical pipeline order: a `defmacro` in the source registers
14251 // AND a subsequent call to it expands AND the expanded form
14252 // surfaces in the returned Vec. Sourced from `&str`, the
14253 // from-source posture preserves the side-effect-then-expansion
14254 // semantics of the from-forms primitive verbatim.
14255 let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
14256 (emit-monitor "alpha")
14257 (emit-monitor "beta")"#;
14258 let mut e = Expander::new();
14259 let out = e
14260 .expand_source_program(src)
14261 .expect("defmacro absorption then expansion must compose");
14262 // Two surviving forms (the `defmacro` itself is consumed as a
14263 // side-effect), each expanded to a `(defmonitor :name "<n>")`.
14264 assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
14265 for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
14266 let args = out[i]
14267 .as_call_to("defmonitor")
14268 .unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
14269 assert_eq!(args[0].as_keyword(), Some("name"));
14270 assert_eq!(args[1].as_string(), Some(expected_name));
14271 }
14272 // The macro is now in the table — a subsequent call would
14273 // expand against this SAME expander.
14274 assert!(e.has("emit-monitor"));
14275 }
14276
14277 #[test]
14278 fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
14279 // Structural identity: for any `src` whose pieces succeed, the
14280 // from-source method's result equals the inlined pre-lift
14281 // pipeline `read + expand_program`. Pin shape AND ordering on a
14282 // mixed source (defmacro definition + macro call + plain form)
14283 // so a regression that drifts the method's pipeline from the
14284 // inlined closed-form fails here. This is the lift's
14285 // load-bearing path-uniformity gate — the SAME assertion holds
14286 // for both production consumers (`RealizedCompiler::compile` +
14287 // `realize_in_memory`'s `:macros` loop) at the primitive's
14288 // contract level.
14289 let src = r#"(defmacro id (x) `,x)
14290 (id (foo 1 2))
14291 (bar)"#;
14292
14293 // Inlined pre-lift pipeline.
14294 let mut e_inline = Expander::new();
14295 let inline_forms = read(src).unwrap();
14296 let via_inline = e_inline
14297 .expand_program(inline_forms)
14298 .expect("inlined pipeline must succeed");
14299
14300 // From-source method pipeline.
14301 let mut e_method = Expander::new();
14302 let via_method = e_method
14303 .expand_source_program(src)
14304 .expect("from-source method pipeline must succeed");
14305
14306 assert_eq!(
14307 via_inline, via_method,
14308 "from-source method must emit byte-identical result to inlined read+expand_program"
14309 );
14310 // Both pipelines registered the same defmacro side-effect.
14311 assert_eq!(e_inline.has("id"), e_method.has("id"));
14312 assert!(e_method.has("id"));
14313 }
14314
14315 #[test]
14316 fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
14317 // Per-`&mut self` absorption posture pin: a `defmacro` registered
14318 // in call 1 SURVIVES into call 2 on the SAME `Expander`. This is
14319 // the load-bearing semantic `realize_in_memory`'s per-spec-macro
14320 // build-up depends on — every iteration's `expand_source_program(
14321 // macro_src)?` lands its `defmacro` heads into the SAME mutable
14322 // `preloaded` expander, so a follow-up `:macros` source can
14323 // invoke macros defined in earlier ones.
14324 let mut e = Expander::new();
14325 let _ = e
14326 .expand_source_program("(defmacro outer (n) `(inner ,n))")
14327 .unwrap();
14328 assert!(e.has("outer"));
14329 // Call 2: defines `inner` AND invokes `outer`, which expands to
14330 // `(inner <n>)`. The expansion proves call 1's `outer` survived
14331 // into call 2's expansion.
14332 let out = e
14333 .expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
14334 .unwrap();
14335 assert_eq!(out.len(), 1, "call 2 yields one expanded form");
14336 // (outer 42) → (inner 42) → (wrapped 42)
14337 let args = out[0]
14338 .as_call_to("wrapped")
14339 .expect("nested expansion must reach `wrapped`");
14340 assert_eq!(args[0].as_int(), Some(42));
14341 }
14342
14343 // ── Expander::register_macro_def: the macro-registration primitive ──
14344 //
14345 // `register_macro_def(&mut self, def: MacroDef) -> Result<()>` lifts
14346 // the byte-identical two-step block —
14347 //
14348 // if self.compile_templates {
14349 // self.templates.insert(def.name.clone(), compile_template(&def)?);
14350 // }
14351 // self.macros.insert(def.name.clone(), def);
14352 //
14353 // — that lived inline at `with_macros` (the bulk-from-iterator
14354 // constructor) AND `expand_program`'s `(defmacro …)`-head arm (the
14355 // program-walk-time registration site) into ONE named method on the
14356 // `Expander` surface. The tests below pin:
14357 //
14358 // (a) the bytecode-default expander (`Expander::new()`) populates
14359 // BOTH `macros` AND `templates` keyed by `def.name`,
14360 // (b) the substitute-only expander (`Expander::new_substitute_only()`)
14361 // populates `macros` but SKIPS `templates` — the
14362 // `compile_templates: false` gate fires structurally,
14363 // (c) a template that fails to compile (`,foo` against `params:
14364 // []`) short-circuits BEFORE `self.macros.insert` runs, so the
14365 // failed registration leaves both tables untouched — no
14366 // partial-write window in which `self.macros.has(name)` is
14367 // true but `self.templates.has(name)` is missing,
14368 // (d) `with_macros([def])` and `register_macro_def(def)` on a fresh
14369 // expander produce structurally identical state (both tables'
14370 // sets of keys + the same `MacroDef` body under each key),
14371 // (e) `expand_program` of one `(defmacro …)` form and
14372 // `register_macro_def` of the parsed `MacroDef` produce
14373 // structurally identical state — closing path-uniformity
14374 // across the lift's two consumers AT the registration site.
14375 //
14376 // The tests bind on `Expander::has(name)` / `Expander::len()` for
14377 // the macros table and on bytecode-vs-substitute output equivalence
14378 // (a registered macro must expand to the SAME result regardless of
14379 // whether the bytecode path's `self.templates` entry exists) for the
14380 // templates table.
14381
14382 fn macro_def_id() -> MacroDef {
14383 // `(defmacro id (x) `,x)` — the simplest well-formed MacroDef:
14384 // one required param, a single-symbol unquote body. Compiles to
14385 // a valid `CompiledTemplate` (no unbound vars), so
14386 // `register_macro_def` succeeds on every Expander posture.
14387 MacroDef {
14388 name: "id".into(),
14389 params: MacroParams {
14390 required: vec!["x".into()],
14391 optional: vec![],
14392 rest: None,
14393 },
14394 // ` ` quasi-quoted `,x` — `Quasiquote(Unquote(Symbol("x")))`.
14395 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14396 }
14397 }
14398
14399 fn macro_def_bad_template() -> MacroDef {
14400 // `(defmacro bad () `,unbound)` — a quasi-quote body with `,unbound`
14401 // against an EMPTY required-params list. `compile_template`
14402 // rejects it with `UnboundTemplateVar` because `unbound` is not
14403 // in `params.names()`. Used to exercise the
14404 // `compile_template`-failed-but-self.macros-still-pristine
14405 // path-uniformity pin.
14406 MacroDef {
14407 name: "bad".into(),
14408 params: MacroParams::default(),
14409 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
14410 }
14411 }
14412
14413 #[test]
14414 fn register_macro_def_bytecode_default_populates_macros_and_templates() {
14415 // The bytecode-default `Expander::new()` carries
14416 // `compile_templates: true`; `register_macro_def` MUST populate
14417 // BOTH `self.macros` (the substitute path's registry) AND
14418 // `self.templates` (the bytecode path's pre-compiled bytecode
14419 // index) keyed by `def.name`. Fail-before-pass-after: this
14420 // assert requires the new method to exist AND to compose the
14421 // two side-effects in canonical order on a single-call
14422 // registration — pre-lift `register_macro_def` did not exist.
14423 let mut e = Expander::new();
14424 e.register_macro_def(macro_def_id())
14425 .expect("well-formed MacroDef must register");
14426 assert!(
14427 e.has("id"),
14428 "self.macros must carry the registered name after register_macro_def"
14429 );
14430 assert!(
14431 e.templates.contains_key("id"),
14432 "self.templates must carry the compiled bytecode under the bytecode-default posture"
14433 );
14434 // The registered macro must expand correctly through the
14435 // bytecode strategy — proves the inserted template is the right
14436 // bytecode for the body.
14437 let out = e
14438 .expand_program(read("(id 42)").unwrap())
14439 .expect("registered macro must expand");
14440 assert_eq!(out.len(), 1);
14441 assert_eq!(out[0], Sexp::int(42));
14442 }
14443
14444 #[test]
14445 fn register_macro_def_substitute_only_skips_templates() {
14446 // `Expander::new_substitute_only()` carries `compile_templates:
14447 // false`; `register_macro_def` MUST populate `self.macros` for
14448 // the substitute path but SKIP `self.templates` (no bytecode
14449 // pre-compile). Pin the `compile_templates: false` gate fires
14450 // structurally — a regression that drifts the conditional from
14451 // the registration primitive (e.g. a future emitter that
14452 // unconditionally pre-compiles) would silently double the
14453 // benchmark baseline's allocation footprint.
14454 let mut e = Expander::new_substitute_only();
14455 e.register_macro_def(macro_def_id())
14456 .expect("well-formed MacroDef must register under substitute-only");
14457 assert!(
14458 e.has("id"),
14459 "self.macros must carry the registered name even under substitute-only"
14460 );
14461 assert!(
14462 !e.templates.contains_key("id"),
14463 "self.templates MUST be empty under compile_templates: false — the gate fires"
14464 );
14465 // The registered macro must still expand correctly through the
14466 // substitute strategy — proves the inserted MacroDef is the
14467 // right body for substitute-walking.
14468 let out = e
14469 .expand_program(read("(id 42)").unwrap())
14470 .expect("registered macro must expand via substitute path");
14471 assert_eq!(out.len(), 1);
14472 assert_eq!(out[0], Sexp::int(42));
14473 }
14474
14475 #[test]
14476 fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
14477 // The structural ordering pin: when `compile_template(&def)?`
14478 // rejects (e.g. `,unbound` against empty params), the `?` MUST
14479 // short-circuit BEFORE `self.macros.insert(def.name.clone(),
14480 // def)` runs, so a failed registration leaves BOTH tables
14481 // exactly as they were. Without this ordering a future
14482 // bytecode-strategy lookup would resolve `self.macros.get(name)`
14483 // (the body was inserted) AND find `self.templates.get(name)`
14484 // returning `None` (the pre-compile failed), silently coercing
14485 // the macro onto the substitute fallback path despite
14486 // `compile_templates: true`. The lift preserves the pre-lift
14487 // ordering (`compile_template` precedes `self.macros.insert`)
14488 // structurally — the test pins it across the lift.
14489 let mut e = Expander::new();
14490 let err = e
14491 .register_macro_def(macro_def_bad_template())
14492 .expect_err("unbound-template body must reject");
14493 // The rejection is the structural variant the bytecode
14494 // strategy's template-gate emits.
14495 assert!(
14496 matches!(err, LispError::UnboundTemplateVar { .. }),
14497 "expected UnboundTemplateVar, got: {err:?}"
14498 );
14499 // Both tables MUST be pristine — no partial write.
14500 assert!(
14501 !e.has("bad"),
14502 "self.macros must be untouched after compile_template failure"
14503 );
14504 assert!(
14505 !e.templates.contains_key("bad"),
14506 "self.templates must be untouched after compile_template failure"
14507 );
14508 }
14509
14510 #[test]
14511 fn with_macros_routes_through_register_macro_def_path_uniformity() {
14512 // Path-uniformity pin across the bulk-constructor consumer:
14513 // `with_macros([def])` MUST produce the same final state as
14514 // `Expander::new()` + `register_macro_def(def)`. A regression
14515 // that drifts the constructor's per-MacroDef inline block from
14516 // the registration primitive (e.g. a future emitter that
14517 // re-inlines the two-step block at `with_macros` rather than
14518 // delegating) would fail loudly here.
14519 let mut via_register = Expander::new();
14520 via_register
14521 .register_macro_def(macro_def_id())
14522 .expect("register must succeed");
14523 let mut via_with_macros =
14524 Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
14525 assert_eq!(via_register.len(), via_with_macros.len());
14526 assert!(via_register.has("id"));
14527 assert!(via_with_macros.has("id"));
14528 // Both tables key on the same set across both postures.
14529 assert_eq!(
14530 via_register.templates.contains_key("id"),
14531 via_with_macros.templates.contains_key("id"),
14532 "self.templates key-presence must agree across with_macros and register_macro_def"
14533 );
14534 // Both registered expanders expand the registered macro
14535 // identically — the strongest behavioral parity.
14536 let out_a = via_register
14537 .expand_program(read("(id 99)").unwrap())
14538 .unwrap();
14539 let out_b = via_with_macros
14540 .expand_program(read("(id 99)").unwrap())
14541 .unwrap();
14542 assert_eq!(out_a, out_b);
14543 assert_eq!(out_a, vec![Sexp::int(99)]);
14544 }
14545
14546 #[test]
14547 fn expand_program_routes_through_register_macro_def_path_uniformity() {
14548 // Path-uniformity pin across the program-walk consumer:
14549 // `expand_program` of `(defmacro id (x) `,x)` MUST produce the
14550 // same final state as a direct
14551 // `register_macro_def(macro_def_id())`. A regression that
14552 // drifts `expand_program`'s `(defmacro …)`-head arm from the
14553 // registration primitive (e.g. a future emitter that re-inlines
14554 // the two-step block at `expand_program` rather than delegating)
14555 // would fail loudly here.
14556 let mut via_register = Expander::new();
14557 via_register
14558 .register_macro_def(macro_def_id())
14559 .expect("register must succeed");
14560 let mut via_expand_program = Expander::new();
14561 let yielded = via_expand_program
14562 .expand_program(read("(defmacro id (x) `,x)").unwrap())
14563 .expect("expand_program of one defmacro must succeed");
14564 // expand_program drops the (defmacro …) form from its returned
14565 // Vec<Sexp> (defmacro is a definition, not a program form), so
14566 // the yielded list is empty — pin the side-effect-only posture.
14567 assert!(
14568 yielded.is_empty(),
14569 "(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
14570 );
14571 assert!(via_register.has("id"));
14572 assert!(via_expand_program.has("id"));
14573 assert_eq!(via_register.len(), via_expand_program.len());
14574 // Both tables key on the same set across both postures.
14575 assert_eq!(
14576 via_register.templates.contains_key("id"),
14577 via_expand_program.templates.contains_key("id"),
14578 "self.templates key-presence must agree across expand_program and register_macro_def"
14579 );
14580 // Both registered expanders expand the registered macro
14581 // identically — the strongest behavioral parity, closing
14582 // path-uniformity across BOTH consumers (with_macros above,
14583 // expand_program here) at ONE primitive.
14584 let out_a = via_register
14585 .expand_program(read("(id 7)").unwrap())
14586 .unwrap();
14587 let out_b = via_expand_program
14588 .expand_program(read("(id 7)").unwrap())
14589 .unwrap();
14590 assert_eq!(out_a, out_b);
14591 assert_eq!(out_a, vec![Sexp::int(7)]);
14592 }
14593
14594 // ── as_unquote: path-uniformity across the three substitute / compile_node sites ──
14595 //
14596 // The new typed-marker projection `Sexp::as_unquote` lifts the
14597 // (Sexp::Unquote/UnquoteSplice variant, UnquoteForm::Unquote/Splice
14598 // literal) pair into ONE structural query. These tests pin behavioral
14599 // parity end-to-end across BOTH expansion strategies:
14600 // * `compile_node` — the bytecode-template strategy's typed marker
14601 // dispatch routes through as_unquote at the compile step.
14602 // * `substitute` (top-level + list-inner) — the substitute-walker
14603 // fallback strategy's typed marker dispatch routes through
14604 // as_unquote at both per-form sites.
14605 // The structural invariant the prior runs' `expansion_layers_agree_on_
14606 // output_and_cache_wins` benchmark observes — bytecode AND substitute
14607 // produce byte-identical output — is anchored here at the macro-template
14608 // level for every distinct unquote-family shape the substitute and
14609 // bytecode strategies discriminate.
14610
14611 #[test]
14612 fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
14613 // A template body whose only marker is a top-level `,x`. Both
14614 // expansion strategies route through `as_unquote` (compile_node for
14615 // bytecode, substitute for the fallback walker), each pairing
14616 // Sexp::Unquote ↔ UnquoteForm::Unquote at ONE typed projection.
14617 // Pin byte-identical output across both strategies — the
14618 // structural-invariant the new projection's lift was designed to
14619 // make load-bearing structural rather than per-site discipline.
14620 let src = "(defmacro id (x) ,x) (id 42)";
14621 let mut bc = Expander::new();
14622 let mut sub = Expander::new_substitute_only();
14623 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14624 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14625 assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
14626 assert_eq!(out_bc, vec![Sexp::int(42)]);
14627 }
14628
14629 #[test]
14630 fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
14631 // A template body whose marker is a list-inner `,@xs`. The substitute
14632 // strategy's list-inner Splice arm routes through `as_unquote`
14633 // matching Some((UnquoteForm::Splice, inner)); the bytecode strategy's
14634 // compile_node Splice arm routes through `as_unquote` matching the
14635 // same. Pin byte-identical output across both strategies for the
14636 // splice path — the projection's typed-marker pairing
14637 // Sexp::UnquoteSplice ↔ UnquoteForm::Splice is structurally
14638 // identical at every consumer post-lift.
14639 let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
14640 let mut bc = Expander::new();
14641 let mut sub = Expander::new_substitute_only();
14642 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14643 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14644 assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
14645 let expected = Sexp::List(vec![
14646 Sexp::symbol("list"),
14647 Sexp::int(0),
14648 Sexp::int(1),
14649 Sexp::int(2),
14650 Sexp::int(3),
14651 Sexp::int(99),
14652 ]);
14653 assert_eq!(out_bc, vec![expected]);
14654 }
14655
14656 #[test]
14657 fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
14658 // The substitute strategy's top-level `,@x` arm rejects with
14659 // LispError::SpliceOutsideList (a splice marker with no containing
14660 // list to flatten into). Pre-lift the arm was
14661 // `Sexp::UnquoteSplice(inner) => Err(splice_outside_list(inner))`;
14662 // post-lift the arm routes through `as_unquote` matching
14663 // Some((UnquoteForm::Splice, inner)) and dispatches on the typed
14664 // marker. Pin path-uniformity: the rejection MUST fire identically
14665 // through the new projection. The substitute-only strategy bypasses
14666 // the bytecode path, exposing this gate directly.
14667 //
14668 // `(defmacro bad (xs) ,@xs) (bad (1 2 3))` — the body is a bare
14669 // `,@xs` at top level (NOT wrapped in a containing list), so the
14670 // substitute fallback's top-level Splice arm fires.
14671 let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
14672 let mut sub = Expander::new_substitute_only();
14673 let err = sub.expand_program(read(src).unwrap()).unwrap_err();
14674 assert!(
14675 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14676 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14677 );
14678 }
14679
14680 #[test]
14681 fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
14682 // A `,unbound` template body — the inner symbol isn't a param —
14683 // fires gate-2 (must-be-bound-in-scope). The typed marker
14684 // `UnquoteForm::Unquote` MUST thread through `as_unquote` →
14685 // `resolve_unquote_in_params(inner, params, form)` → gate-2's
14686 // rejection-builder. Pre-lift the marker was bound at the per-arm
14687 // literal `UnquoteForm::Unquote`; post-lift it's bound at the
14688 // typed projection. Pin: the rejection's `prefix` slot is
14689 // UnquoteForm::Unquote, structurally derived from the typed
14690 // projection's typed marker, NOT a literal at the arm body.
14691 let src = "(defmacro bad (x) ,unbound)";
14692 let mut bc = Expander::new();
14693 let err = bc.expand_program(read(src).unwrap()).unwrap_err();
14694 match err {
14695 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14696 assert_eq!(
14697 prefix,
14698 UnquoteForm::Unquote,
14699 "typed marker drifted from UnquoteForm::Unquote at gate-2"
14700 );
14701 }
14702 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14703 }
14704 // Sibling negative control: `,@unbound` inside a containing list
14705 // threads UnquoteForm::Splice through the same projection. The
14706 // splice MUST be inside a list — `compile_template` rejects
14707 // top-level `,@X` bodies with SpliceOutsideList before compile_node
14708 // runs, so the typed-marker dispatch on Splice is observable only
14709 // for well-positioned splice bodies. Closes path-uniformity across
14710 // BOTH typed marker variants at the compile path.
14711 let src_splice = "(defmacro bad (x) (foo ,@unbound))";
14712 let mut bc2 = Expander::new();
14713 let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
14714 match err_splice {
14715 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14716 assert_eq!(
14717 prefix,
14718 UnquoteForm::Splice,
14719 "typed marker drifted from UnquoteForm::Splice at gate-2"
14720 );
14721 }
14722 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14723 }
14724 }
14725
14726 // ── compile_template + contains_unquote path-uniformity through as_unquote ──
14727 //
14728 // The prior `as_unquote` projection lift (eb00684) routed `compile_node`'s
14729 // two arms and `substitute`'s top-level + list-inner arms through the
14730 // typed-marker projection but LEFT `compile_template`'s top-level
14731 // splice-outside-list gate and `contains_unquote`'s family check inline
14732 // matching `Sexp::UnquoteSplice(inner)` / `Sexp::Unquote(_) |
14733 // Sexp::UnquoteSplice(_)`. After this lift both production sites route
14734 // through `as_unquote`: `compile_template` matches `Some((UnquoteForm::
14735 // Splice, inner))` (same shape as `substitute`'s list-inner Splice arm),
14736 // `contains_unquote` uses `as_unquote().is_some()` for the family check.
14737 // Every production-site recognizer of an unquote-family wrapper now
14738 // shares ONE typed-marker projection — the (Sexp variant, UnquoteForm
14739 // variant) pairing for `,@-outside-list` is bound at ONE structural
14740 // query across all FOUR reachable splice-recognition sites, and a future
14741 // regression that drifts the marker pairing at any production site
14742 // becomes a type error at the helper boundary rather than a silent
14743 // per-site divergence.
14744
14745 #[test]
14746 fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
14747 // The bytecode path's `compile_template` gate pre-rejects top-level
14748 // `,@X` bodies via `as_unquote()` matching `Some((UnquoteForm::Splice,
14749 // inner))`. Pre-lift the arm was `if let Sexp::UnquoteSplice(inner) =
14750 // body` — the LAST production-site inline `Sexp::UnquoteSplice(_)`
14751 // match outside the projection itself. Post-lift the (Sexp variant,
14752 // UnquoteForm variant) pairing for the splice-outside-list gate is
14753 // bound at ONE projection function across all three reachable
14754 // emission sites (compile_template top-level, substitute top-level,
14755 // substitute list-inner). Path-uniformity guard at the new boundary:
14756 // the bytecode-path compile-time gate now routes through the same
14757 // shape `substitute`'s list-inner Splice arm uses.
14758 //
14759 // `(defmacro bad (xs) \`,@xs)` — the body's outer quasi-quote is
14760 // peeled by `template_body`, leaving `,@xs` at top level for
14761 // `compile_template` to gate before `compile_node` walks anything.
14762 // The bytecode strategy is default-on (`Expander::new()` sets
14763 // `compile_templates: true`), so the gate fires at
14764 // `register_macro_def` time.
14765 let mut e = Expander::new();
14766 let err = e
14767 .expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
14768 .expect_err("compile_template must reject top-level ,@X via as_unquote");
14769 assert!(
14770 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14771 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14772 );
14773 }
14774
14775 #[test]
14776 fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
14777 // Negative control on the typed-marker dispatch at `compile_template`'s
14778 // top-level gate. A top-level `,X` body (Unquote, NOT Splice) is a
14779 // valid template — `compile_node` lowers it to a single `Subst(idx)`
14780 // op. The new `Some((UnquoteForm::Splice, inner))` pattern at the
14781 // gate MUST NOT fire on the Unquote variant — only Splice — so the
14782 // typed marker is observably load-bearing: a regression that drifts
14783 // the pattern from `UnquoteForm::Splice` to the wider
14784 // `Some((_, inner))` would mis-reject `(defmacro id (x) ,x)` here.
14785 //
14786 // `(defmacro id (x) ,x) (id 42)` — `id`'s body is bare `,x` at top
14787 // level; `compile_template` admits it via the typed-marker gate,
14788 // `compile_node` emits `Subst(0)`, and the call expands to `42`.
14789 let src = "(defmacro id (x) ,x) (id 42)";
14790 let mut e = Expander::new();
14791 let expanded = e
14792 .expand_program(read(src).unwrap())
14793 .expect("top-level ,X body must compile through as_unquote-typed gate");
14794 assert_eq!(expanded, vec![Sexp::int(42)]);
14795 }
14796
14797 #[test]
14798 fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
14799 // The fast-path optimizer in `compile_node` short-circuits on
14800 // `contains_unquote(node)` — true iff the subtree carries an
14801 // unquote-family wrapper. Pre-lift the family check inlined
14802 // `Sexp::Unquote(_) | Sexp::UnquoteSplice(_) => true`; post-lift it
14803 // routes through `as_unquote().is_some()`. Pin path-uniformity at
14804 // the family-recognition boundary: every production-site unquote
14805 // recognizer (compile_template's gate, compile_node's per-arm
14806 // dispatch, substitute's top-level + list-inner arms, AND this
14807 // fast-path predicate) now shares ONE typed-marker projection.
14808 //
14809 // Both variants must trigger the fast-path bail — the optimizer's
14810 // gate keys on the family, not the inner. The recursion into
14811 // Quote/Quasiquote/List subtrees ALSO observes the same family
14812 // gate at every level, so a `,@xs` buried under a `\`...` outer
14813 // wrapper still fires it.
14814 let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14815 let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
14816 let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
14817 Sexp::symbol("foo"),
14818 Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
14819 ])));
14820 // The projection's `is_some()` face MUST agree with the pre-lift
14821 // `matches!()` discriminant on every variant — closed-set guarantee
14822 // shared with `as_unquote`'s contract pin in `ast.rs`.
14823 assert!(super::contains_unquote(&bare_unquote));
14824 assert!(super::contains_unquote(&bare_splice));
14825 assert!(super::contains_unquote(&nested));
14826 // Negative control: shapes the projection rejects (Nil, Atom,
14827 // bare List, Quote-family without inner unquote) must NOT trigger
14828 // the fast-path bail — the projection's None face stays observably
14829 // distinct from its Some face.
14830 assert!(!super::contains_unquote(&Sexp::Nil));
14831 assert!(!super::contains_unquote(&Sexp::symbol("plain")));
14832 assert!(!super::contains_unquote(&Sexp::int(5)));
14833 assert!(!super::contains_unquote(&Sexp::List(vec![
14834 Sexp::symbol("plain"),
14835 Sexp::int(1),
14836 ])));
14837 assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
14838 Sexp::symbol("inert")
14839 ))));
14840 }
14841
14842 #[test]
14843 fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
14844 // Pin the lift: `contains_unquote`'s quote-family recognition
14845 // now routes through `Sexp::as_quote_form` (the 4-of-4 wrapper
14846 // projection) AND `QuoteForm::as_unquote_form` (the 2-of-4
14847 // substitution-subset gate) at ONE site — not through the pair
14848 // of `as_unquote().is_some()` + inline
14849 // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm. The
14850 // path-uniformity assertion: for every quote-family wrapper, the
14851 // function's behavior agrees with the manual composition through
14852 // the two typed-marker projections. A regression that re-inlines
14853 // either arm (e.g. drops the `as_unquote_form().is_some()` gate
14854 // and just returns true for any `as_quote_form().is_some()`, or
14855 // restores the per-variant `Quote | Quasiquote` arm) drifts from
14856 // this composition and surfaces here.
14857 use crate::ast::QuoteForm;
14858
14859 // Sweep every QuoteForm variant under both axes:
14860 // * unquote-only-inner (inner = symbol):
14861 // Unquote(s) → true via `as_unquote_form() == Some`
14862 // UnquoteSplice(s) → true via `as_unquote_form() == Some`
14863 // Quote(s) → false (inner has no unquote, recurses to false)
14864 // Quasiquote(s) → false (same)
14865 // * unquote-inner-inside-quote-wrapper:
14866 // Quote(Unquote(s)) → true (Quote arm recurses via `as_quote_form`,
14867 // inner `Unquote` returns true through the
14868 // SAME projection — both arms share the
14869 // ONE typed-marker site post-lift)
14870 let inner_plain = Sexp::symbol("x");
14871 let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14872
14873 for qf in QuoteForm::ALL {
14874 let wrapped_plain = match qf {
14875 QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
14876 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
14877 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
14878 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
14879 };
14880 // Path-uniformity: behavior derives from the manual two-marker
14881 // composition through `as_quote_form` + `as_unquote_form`.
14882 // The substitution-subset gate (`as_unquote_form().is_some()`)
14883 // returns true for Unquote/UnquoteSplice and false for
14884 // Quote/Quasiquote — directly encoding the 2-of-4 partition.
14885 let via_manual =
14886 qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
14887 assert_eq!(
14888 super::contains_unquote(&wrapped_plain),
14889 via_manual,
14890 "contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
14891 );
14892
14893 let wrapped_unquote = match qf {
14894 QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
14895 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
14896 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
14897 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
14898 };
14899 // EVERY quote-family wrapper around an inner `Unquote` must
14900 // contain unquote — either the outer projects as `Some` on
14901 // the substitution subset (Unquote/UnquoteSplice arms), OR
14902 // the outer is Quote/Quasiquote and the recursion into the
14903 // inner re-fires the projection. The pre-lift Quote arm
14904 // recursed via the inline `Sexp::Quote(inner)` match;
14905 // post-lift the SAME recursion fires via `as_quote_form`'s
14906 // typed projection. Both yield `true`.
14907 assert!(
14908 super::contains_unquote(&wrapped_unquote),
14909 "contains_unquote missed an inner Unquote under {qf:?} — \
14910 the quote-family recursion through as_quote_form drifted"
14911 );
14912 }
14913
14914 // Negative control: a non-quote-family wrapper (List, Atom,
14915 // Nil) must NOT route through `as_quote_form` at all. The
14916 // `if let Some(...) = node.as_quote_form()` gate stays `None`
14917 // and falls through to the List/_ arms. This is the structural
14918 // partition: the projection's `None` face MUST agree with
14919 // `!matches!(node, Sexp::Quote(_) | Sexp::Quasiquote(_) |
14920 // Sexp::Unquote(_) | Sexp::UnquoteSplice(_))`.
14921 let nested_in_list = Sexp::List(vec![
14922 Sexp::symbol("outer"),
14923 Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14924 ]);
14925 assert!(
14926 super::contains_unquote(&nested_in_list),
14927 "contains_unquote failed to descend into a List subtree containing a \
14928 Quasiquote(Unquote(_)) — list recursion arm drifted"
14929 );
14930 }
14931
14932 // ── expand_and_collect_named_calls_to_any (from-forms × named ────
14933 // × typed-decoded classifier) ─────────────────────────────────────
14934 //
14935 // The (named NAME-then-kwargs × typed-decoded classifier) cell of
14936 // the dispatcher matrix on the `Expander` surface — pre-lift the
14937 // matrix had the bare-kwargs row closed (expand_and_collect_calls_to
14938 // + expand_and_collect_calls_to_any) AND the constant-keyword
14939 // column closed on the named axis (expand_to_named +
14940 // expand_source_to_named) but the (named × classifier) cell was
14941 // open — every consumer that wanted to walk a program by typed-
14942 // decoded classifier AND extract the NAME slot per matched form
14943 // had to compose `expand_and_collect_calls_to_any` with
14944 // `split_name_slot` inline. Post-lift the cell is ONE named primitive
14945 // on the `Expander` surface, and the constant-`T::KEYWORD` named
14946 // sibling routes through it as the (constant-classifier × named)
14947 // specialization.
14948 //
14949 // The tests below pin: (a) the happy path yields the decoded triple
14950 // `(T, &str, &[Sexp])` for every matched form in source order; (b)
14951 // non-matching forms are skipped (soft-projection contract inherited
14952 // from the typed-decoded primitive); (c) the `NamedFormMissingName`
14953 // variant fires for matched forms with no NAME slot, threading the
14954 // classifier-supplied `&'static str` keyword; (d) the
14955 // `NamedFormNonSymbolName` variant fires for matched forms with a
14956 // non-symbol-or-string NAME slot, again threading the classifier-
14957 // supplied keyword; (e) the projection's `Err` short-circuits at
14958 // the first failing matched form; (f) `expand_program` runs BEFORE
14959 // the classifier filter walks — a `(defmacro …)` emitting a named
14960 // form is decoded by the classifier on the EXPANDED head, not the
14961 // macro-call head; (g) the from-source sibling agrees with `read +
14962 // from-forms` on the same source.
14963
14964 #[test]
14965 fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
14966 ) {
14967 // The typed-decoded named primitive's happy path: a closed-set
14968 // classifier decodes head symbols to a typed enum PAIRED with
14969 // its canonical static keyword, and the per-form projection
14970 // receives `(decoded, name, spec_args)` for every matched form
14971 // in source order. Sweep across TWO distinct classifier outcomes
14972 // — `Monitor` / `Notify` — interleaved with a non-matching form
14973 // to pin the typed-witness threading AND the NAME-slot
14974 // projection AND the source-order yield AND the rejection of
14975 // non-classifier forms in ONE assertion.
14976 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14977 enum Kind {
14978 Monitor,
14979 Notify,
14980 }
14981 let src = r#"(defmonitor cpu :threshold 80)
14982 (other-form 99)
14983 (defnotify email :to "ops@example.com")
14984 (defmonitor mem :threshold 90)"#;
14985 let forms = read(src).unwrap();
14986 let mut e = Expander::new();
14987 let rows: Vec<(Kind, String, usize)> = e
14988 .expand_and_collect_named_calls_to_any(
14989 forms,
14990 |h| match h {
14991 "defmonitor" => Some((Kind::Monitor, "defmonitor")),
14992 "defnotify" => Some((Kind::Notify, "defnotify")),
14993 _ => None,
14994 },
14995 |kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
14996 )
14997 .unwrap();
14998 assert_eq!(
14999 rows,
15000 vec![
15001 (Kind::Monitor, "cpu".to_string(), 2),
15002 (Kind::Notify, "email".to_string(), 2),
15003 (Kind::Monitor, "mem".to_string(), 2),
15004 ],
15005 );
15006 }
15007
15008 #[test]
15009 fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
15010 // Soft-projection contract — every shape the typed-decoded
15011 // primitive rejects (non-list, empty list, list whose head is
15012 // not a symbol, list whose head decodes to `None`) skips the
15013 // projection silently. The named gate inside the wrapper
15014 // projection runs ONLY for classifier-matched forms; a non-
15015 // matching form that reaches the projection panics here.
15016 let src = r#":bare-keyword
15017 "bare-string"
15018 42
15019 ()
15020 (foo bar)
15021 (defmonitor cpu :threshold 80)"#;
15022 let forms = read(src).unwrap();
15023 let mut e = Expander::new();
15024 let names: Vec<String> = e
15025 .expand_and_collect_named_calls_to_any(
15026 forms,
15027 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15028 |(), name, _args| Ok(name.to_string()),
15029 )
15030 .unwrap();
15031 assert_eq!(names, vec!["cpu".to_string()]);
15032 }
15033
15034 #[test]
15035 fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
15036 ) {
15037 // `(defmonitor)` matches the classifier but has no NAME slot —
15038 // `split_name_slot` fires `NamedFormMissingName { keyword:
15039 // "defmonitor" }`. The classifier's `&'static str` keyword is
15040 // threaded VERBATIM through the named gate; a regression that
15041 // hardcodes a different keyword (e.g. the matched head text or
15042 // a default) would fail the structural variant identity
15043 // assertion. Fail-before-pass-after: pre-lift the primitive did
15044 // not exist; the inline `expand_and_collect_calls_to_any +
15045 // split_name_slot` composition at the consumer site was the
15046 // only path, with the keyword bound at the consumer's call
15047 // boundary rather than at the named primitive's body.
15048 let forms = read("(defmonitor)").unwrap();
15049 let mut e = Expander::new();
15050 let err = e
15051 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15052 forms,
15053 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15054 |(), _name, _args| Ok(()),
15055 )
15056 .unwrap_err();
15057 assert!(
15058 matches!(
15059 err,
15060 crate::error::LispError::NamedFormMissingName {
15061 keyword: "defmonitor"
15062 }
15063 ),
15064 "expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
15065 );
15066 }
15067
15068 #[test]
15069 fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
15070 ) {
15071 // `(defmonitor 42 :threshold 80)` matches the classifier but
15072 // the NAME slot is an int — `split_name_slot` fires
15073 // `NamedFormNonSymbolName { keyword: "defmonitor", got:
15074 // SexpShape::Int }`. The classifier's `&'static str` keyword is
15075 // threaded through the structural rejection variant verbatim,
15076 // and the typed `SexpShape` projection on the offending slot
15077 // is preserved across the named primitive.
15078 let forms = read("(defmonitor 42 :threshold 80)").unwrap();
15079 let mut e = Expander::new();
15080 let err = e
15081 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15082 forms,
15083 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15084 |(), _name, _args| Ok(()),
15085 )
15086 .unwrap_err();
15087 assert!(
15088 matches!(
15089 err,
15090 crate::error::LispError::NamedFormNonSymbolName {
15091 keyword: "defmonitor",
15092 got: crate::error::SexpShape::Int,
15093 }
15094 ),
15095 "expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
15096 );
15097 }
15098
15099 #[test]
15100 fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
15101 // Result projection's short-circuit contract — when the
15102 // projection returns `Err` on a matched named form, the walk
15103 // stops and subsequent matched forms are NOT projected. Pin
15104 // source-order short-circuit via a counter the projection
15105 // increments before deciding to fail: the counter sits at
15106 // exactly the index of the failing form (second match),
15107 // proving the third match never reached the projection.
15108 let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
15109 let mut count = 0usize;
15110 let mut e = Expander::new();
15111 let err = e
15112 .expand_and_collect_named_calls_to_any::<String, _, _, ()>(
15113 forms,
15114 |h| (h == "defmon").then_some(((), "defmon")),
15115 |(), name, _args| {
15116 count += 1;
15117 if name == "b" {
15118 return Err(crate::error::LispError::Missing("test-failure"));
15119 }
15120 Ok(name.to_string())
15121 },
15122 )
15123 .expect_err("projection must short-circuit on first Err");
15124 assert_eq!(
15125 count, 2,
15126 "projection must have run on first matched form AND the failing form, then stopped"
15127 );
15128 assert!(
15129 matches!(err, crate::error::LispError::Missing("test-failure")),
15130 "expected the projection's typed Err verbatim, got: {err:?}"
15131 );
15132 }
15133
15134 #[test]
15135 fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
15136 // Ordering contract — `expand_program` runs BEFORE the
15137 // typed-decoded named walk. A `(defmacro …)` form that emits a
15138 // classifier-decoded named form must have its EXPANDED head
15139 // decoded by the classifier, not the macro-call head. Pin the
15140 // ordering with a macro `(emit-mon n thr)` expanding to
15141 // `(defmonitor ,n :threshold ,thr)` — the classifier sees the
15142 // expanded `defmonitor` head and the NAME slot's symbol value
15143 // from the macro arg.
15144 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15145 (defmonitor cpu :threshold 80)
15146 (emit-mon mem 90)
15147 (other-form 99)
15148 (emit-mon disk 70)"#;
15149 let forms = read(src).unwrap();
15150 let mut e = Expander::new();
15151 let names: Vec<String> = e
15152 .expand_and_collect_named_calls_to_any(
15153 forms,
15154 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15155 |(), name, _args| Ok(name.to_string()),
15156 )
15157 .unwrap();
15158 // `cpu` from the literal call, `mem` from the first macro
15159 // emit, `disk` from the second macro emit. The macro-emitted
15160 // forms are present BECAUSE `expand_program` ran first.
15161 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15162 }
15163
15164 #[test]
15165 fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
15166 // From-source posture parity: feeding source through
15167 // `expand_source_and_collect_named_calls_to_any` is byte-
15168 // identical to feeding `read(src)?` through the from-forms
15169 // sibling on a fresh expander, because the from-source posture
15170 // is `read(src)? + from-forms` by construction. A regression
15171 // that drifts the from-source posture's pipeline from the
15172 // from-forms posture's pipeline would fail here.
15173 let src = r#"(defmonitor cpu :threshold 80)
15174 (defmonitor mem :threshold 90)"#;
15175 let via_src: Vec<String> = Expander::new()
15176 .expand_source_and_collect_named_calls_to_any(
15177 src,
15178 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15179 |(), name, _args| Ok(name.to_string()),
15180 )
15181 .unwrap();
15182 let forms = read(src).unwrap();
15183 let via_forms: Vec<String> = Expander::new()
15184 .expand_and_collect_named_calls_to_any(
15185 forms,
15186 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15187 |(), name, _args| Ok(name.to_string()),
15188 )
15189 .unwrap();
15190 assert_eq!(via_src, via_forms);
15191 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15192 }
15193
15194 #[test]
15195 fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
15196 ) {
15197 // `?`-routing through `read` short-circuits BEFORE the
15198 // classifier or the named gate runs. Unbalanced paren — reader
15199 // error fires; classifier panic-decoder MUST NOT execute,
15200 // proving the read step gates the entire pipeline.
15201 let mut e = Expander::new();
15202 let err = e
15203 .expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
15204 "(defmonitor cpu :threshold 80",
15205 |_h| panic!("classifier must not run when reader fails"),
15206 |(), _name, _args| Ok(()),
15207 )
15208 .unwrap_err();
15209 // The named gate's variants MUST NOT fire — the reader error
15210 // is structurally distinct.
15211 assert!(
15212 !matches!(
15213 err,
15214 crate::error::LispError::NamedFormMissingName { .. }
15215 | crate::error::LispError::NamedFormNonSymbolName { .. }
15216 ),
15217 "expected reader error, not named-gate variant; got: {err:?}"
15218 );
15219 }
15220
15221 // ── expand_and_collect_named_calls_to (from-forms × named × ──────
15222 // constant-keyword × untyped R) ─────────────────────────────────
15223 //
15224 // The (named NAME-then-kwargs × constant-keyword × untyped `R`)
15225 // cell of the `Expander` typed-walk family — pre-lift the cell was
15226 // reachable ONLY through `expand_to_named<T>` with the
15227 // `T: TataraDomain` parameter baking BOTH the `T::KEYWORD` filter
15228 // AND the `T::compile_from_args`-based projection through
15229 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15230 // named_form_projection::<T>)`. Post-lift the cell surfaces as ONE
15231 // method that takes the keyword and the `(name, args) -> R`
15232 // projection as caller-supplied parameters, AND
15233 // `expand_to_named<T>` routes through it as the typed
15234 // `T::KEYWORD`-constant specialization — so the named-form
15235 // `split_name_slot` composition lives at ONE site
15236 // (`expand_and_collect_named_calls_to_any` body) post-lift rather
15237 // than at TWO sites pre-lift (the bare-kwargs path through
15238 // `named_form_projection<T>` AND the classifier path).
15239 //
15240 // The tests below pin: (a) the happy path yields the `(name, args)`
15241 // pair for every matched form in source order; (b) non-matching
15242 // forms are skipped (soft-projection contract inherited from the
15243 // named typed-decoded primitive); (c) the `NamedFormMissingName`
15244 // variant fires for matched forms with no NAME slot, threading the
15245 // primitive's `&'static str` keyword; (d) the
15246 // `NamedFormNonSymbolName` variant fires for matched forms with a
15247 // non-symbol-or-string NAME slot, again threading the primitive's
15248 // keyword; (e) the projection's `Err` short-circuits at the first
15249 // failing matched form; (f) `expand_program` runs BEFORE the
15250 // keyword filter walks — a `(defmacro …)` emitting a named form
15251 // has its EXPANDED head matched by the constant-keyword filter,
15252 // not the macro-call head; (g) the from-source sibling agrees with
15253 // `read + from-forms` on the same source; (h) the constant-
15254 // classifier composition law binds the runtime-keyword cell
15255 // (`expand_and_collect_named_calls_to`) to the typed-decoded
15256 // classifier cell (`expand_and_collect_named_calls_to_any`) via a
15257 // constant-classifier decoder.
15258
15259 #[test]
15260 fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
15261 ) {
15262 // The constant-keyword named primitive's happy path: a runtime
15263 // `&'static str` keyword filters matched forms, and the per-
15264 // form projection receives `(name, spec_args)` for every
15265 // matched form in source order. Three matched forms
15266 // interleaved with a non-matcher pin the source-order yield,
15267 // the NAME-slot projection, AND the rejection of non-keyword
15268 // forms in ONE assertion.
15269 let src = r#"(defmonitor cpu :threshold 80)
15270 (other-form 99)
15271 (defmonitor mem :threshold 90 :unit "MiB")
15272 (defmonitor disk :threshold 70)"#;
15273 let forms = read(src).unwrap();
15274 let mut e = Expander::new();
15275 let rows: Vec<(String, usize)> = e
15276 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
15277 Ok((name.to_string(), spec_args.len()))
15278 })
15279 .unwrap();
15280 assert_eq!(
15281 rows,
15282 vec![
15283 ("cpu".to_string(), 2),
15284 ("mem".to_string(), 4),
15285 ("disk".to_string(), 2),
15286 ],
15287 );
15288 }
15289
15290 #[test]
15291 fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
15292 // Soft-projection contract — every shape the named typed-
15293 // decoded primitive rejects (non-list, empty list, list whose
15294 // head is not a symbol, list whose head doesn't match the
15295 // constant keyword) skips the projection silently. The named
15296 // gate inside the wrapper projection runs ONLY for matched
15297 // forms; a non-matching form that reaches the projection
15298 // panics here.
15299 let src = r#":bare-keyword
15300 "bare-string"
15301 42
15302 ()
15303 (foo bar)
15304 (defmonitor cpu :threshold 80)"#;
15305 let forms = read(src).unwrap();
15306 let mut e = Expander::new();
15307 let names: Vec<String> = e
15308 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15309 Ok(name.to_string())
15310 })
15311 .unwrap();
15312 assert_eq!(names, vec!["cpu".to_string()]);
15313 }
15314
15315 #[test]
15316 fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
15317 // The named-form gate's missing-NAME variant must thread the
15318 // primitive's `&'static str` keyword (NOT a hardcoded literal,
15319 // NOT the projection's own copy) — the `NamedFormMissingName
15320 // { keyword }` slot inside `split_name_slot` is bound at the
15321 // call site BY THE PRIMITIVE itself, via the constant-
15322 // classifier decoder threading `((), keyword)` into
15323 // `expand_and_collect_named_calls_to_any`.
15324 let src = r#"(defmonitor)"#;
15325 let forms = read(src).unwrap();
15326 let mut e = Expander::new();
15327 let err = e
15328 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15329 .unwrap_err();
15330 assert!(
15331 matches!(
15332 err,
15333 crate::error::LispError::NamedFormMissingName {
15334 keyword: "defmonitor"
15335 }
15336 ),
15337 "expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
15338 );
15339 }
15340
15341 #[test]
15342 fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
15343 ) {
15344 // The named-form gate's non-symbol-NAME variant must thread
15345 // the primitive's `&'static str` keyword AND the typed-shape
15346 // witness (`SexpShape::Int` for an integer NAME slot). The
15347 // shape projection through `split_name_slot`'s
15348 // `as_symbol_or_string()` gate fires on the first matched
15349 // form whose NAME slot is not a symbol or string.
15350 let src = r#"(defmonitor 42 :threshold 80)"#;
15351 let forms = read(src).unwrap();
15352 let mut e = Expander::new();
15353 let err = e
15354 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15355 .unwrap_err();
15356 match err {
15357 crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
15358 assert_eq!(keyword, "defmonitor");
15359 assert_eq!(got, crate::error::SexpShape::Int);
15360 }
15361 other => panic!(
15362 "expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
15363 ),
15364 }
15365 }
15366
15367 #[test]
15368 fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
15369 // `Iterator::collect::<Result<Vec<R>, _>>()` short-circuits at
15370 // the first failing projection. The second matched form's
15371 // projection error MUST fire AND the third matched form's
15372 // projection MUST NOT run. Pin both halves with a counter +
15373 // a fail-on-call sentinel.
15374 let src = r#"(defmonitor cpu :threshold 80)
15375 (defmonitor mem :threshold 90)
15376 (defmonitor disk :threshold 70)"#;
15377 let forms = read(src).unwrap();
15378 let mut count: usize = 0;
15379 let mut e = Expander::new();
15380 let err = e
15381 .expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
15382 count += 1;
15383 if name == "mem" {
15384 Err(crate::error::LispError::Compile {
15385 form: "test".into(),
15386 message: format!("rejecting at NAME={name}"),
15387 })
15388 } else {
15389 Ok(name.to_string())
15390 }
15391 })
15392 .unwrap_err();
15393 assert_eq!(count, 2, "third matched form must not be projected");
15394 match err {
15395 crate::error::LispError::Compile { message, .. } => {
15396 assert!(message.contains("NAME=mem"));
15397 }
15398 other => panic!("expected projection-driven Compile error; got: {other:?}"),
15399 }
15400 }
15401
15402 #[test]
15403 fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
15404 // Ordering contract — `expand_program` runs BEFORE the
15405 // constant-keyword named walk. A `(defmacro …)` form that
15406 // emits a matched named form must have its EXPANDED head
15407 // matched by the constant-keyword filter, not the macro-call
15408 // head. Pin the ordering with a macro `(emit-mon n thr)`
15409 // expanding to `(defmonitor ,n :threshold ,thr)` — the
15410 // constant-keyword filter matches the expanded `defmonitor`
15411 // head and the NAME slot projects the symbol-author value.
15412 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15413 (defmonitor cpu :threshold 80)
15414 (emit-mon mem 90)
15415 (other-form 99)
15416 (emit-mon disk 70)"#;
15417 let forms = read(src).unwrap();
15418 let mut e = Expander::new();
15419 let names: Vec<String> = e
15420 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15421 Ok(name.to_string())
15422 })
15423 .unwrap();
15424 // `cpu` from the literal call, `mem` from the first macro
15425 // emit, `disk` from the second macro emit. The macro-emitted
15426 // forms are present BECAUSE `expand_program` ran first.
15427 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15428 }
15429
15430 #[test]
15431 fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
15432 // From-source posture parity: feeding source through
15433 // `expand_source_and_collect_named_calls_to` is byte-identical
15434 // to feeding `read(src)?` through the from-forms sibling on a
15435 // fresh expander, because the from-source posture is
15436 // `read(src)? + from-forms` by construction. A regression that
15437 // drifts the from-source posture's pipeline from the from-
15438 // forms posture's pipeline would fail here.
15439 let src = r#"(defmonitor cpu :threshold 80)
15440 (defmonitor mem :threshold 90)"#;
15441 let via_src: Vec<String> = Expander::new()
15442 .expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
15443 Ok(name.to_string())
15444 })
15445 .unwrap();
15446 let forms = read(src).unwrap();
15447 let via_forms: Vec<String> = Expander::new()
15448 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15449 Ok(name.to_string())
15450 })
15451 .unwrap();
15452 assert_eq!(via_src, via_forms);
15453 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15454 }
15455
15456 #[test]
15457 fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
15458 ) {
15459 // `?`-routing through `read` short-circuits BEFORE the
15460 // constant-keyword filter, the named gate, or the projection
15461 // runs. Unbalanced paren — reader error fires; the projection
15462 // panic-payload MUST NOT execute, proving the read step gates
15463 // the entire pipeline.
15464 let mut e = Expander::new();
15465 let err = e
15466 .expand_source_and_collect_named_calls_to::<(), _>(
15467 "(defmonitor cpu :threshold 80",
15468 "defmonitor",
15469 |_name, _args| panic!("projection must not run when reader fails"),
15470 )
15471 .unwrap_err();
15472 // The named gate's variants MUST NOT fire — the reader error
15473 // is structurally distinct.
15474 assert!(
15475 !matches!(
15476 err,
15477 crate::error::LispError::NamedFormMissingName { .. }
15478 | crate::error::LispError::NamedFormNonSymbolName { .. }
15479 ),
15480 "expected reader error, not named-gate variant; got: {err:?}",
15481 );
15482 }
15483
15484 #[test]
15485 fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
15486 ) {
15487 // Composition-identity test pinning the runtime-keyword named
15488 // cell (`expand_and_collect_named_calls_to`) to the typed-
15489 // decoded named-classifier cell
15490 // (`expand_and_collect_named_calls_to_any`) via the constant-
15491 // classifier decoder shape. Post-lift the identity:
15492 //
15493 // expand_and_collect_named_calls_to(forms, kw, project) ==
15494 // expand_and_collect_named_calls_to_any(forms,
15495 // |h| (h == kw).then_some(((), kw)),
15496 // |(), name, args| project(name, args))
15497 //
15498 // Pinning the identity here makes the typed-decoded named-
15499 // classifier primitive the CANONICAL composition point the
15500 // runtime-keyword sibling routes through — parallel to how
15501 // `expand_and_collect_calls_to` routes through
15502 // `expand_and_collect_calls_to_any` via a `|h| (h == k).
15503 // then_some(())` decoder on the bare-kwargs axis. A future
15504 // regression that drifts ONE cell's NAME-slot rejection chain
15505 // from the other becomes loudly visible at this assertion.
15506 let src = r#"(defmonitor cpu :threshold 80)
15507 (other-form 99)
15508 (defmonitor mem :threshold 90)"#;
15509 let forms = read(src).unwrap();
15510 let via_constant_keyword: Vec<(String, usize)> = Expander::new()
15511 .expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
15512 Ok((name.to_string(), args.len()))
15513 })
15514 .unwrap();
15515 let via_classifier: Vec<(String, usize)> = Expander::new()
15516 .expand_and_collect_named_calls_to_any(
15517 forms,
15518 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15519 |(), name, args| Ok((name.to_string(), args.len())),
15520 )
15521 .unwrap();
15522 assert_eq!(via_constant_keyword, via_classifier);
15523 assert_eq!(
15524 via_constant_keyword,
15525 vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
15526 );
15527 }
15528
15529 #[test]
15530 fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
15531 ) {
15532 // End-to-end path-uniformity: `Expander::expand_to_named<T>`
15533 // (the typed constant-keyword named cell) must yield the same
15534 // payload as `expand_and_collect_named_calls_to(forms,
15535 // T::KEYWORD, |name, args| { T::compile_from_args(args)? +
15536 // NamedDefinition })` — the lift's structural identity.
15537 //
15538 // Pre-lift `expand_to_named<T>` routed through
15539 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15540 // named_form_projection::<T>)` (the bare-kwargs constant
15541 // primitive with `named_form_projection<T>` doing the NAME
15542 // extraction internally). Post-lift `expand_to_named<T>`
15543 // routes through the named constant-keyword primitive (which
15544 // itself routes through the classifier primitive via a
15545 // constant-classifier decoder) — so the `split_name_slot`
15546 // composition lives at ONE site (the `_any` primitive body)
15547 // post-lift rather than at TWO sites.
15548 use crate::compile::NamedDefinition;
15549 use crate::compiler_spec::CompilerSpec;
15550 let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
15551 (defcompiler beta-compiler :name "y" :dialect "standard")"#;
15552 let forms = read(src).unwrap();
15553 let via_expand_to_named = Expander::new()
15554 .expand_to_named::<CompilerSpec>(forms.clone())
15555 .unwrap();
15556 let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
15557 .expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
15558 let spec =
15559 <CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
15560 Ok(NamedDefinition {
15561 name: name.to_string(),
15562 spec,
15563 })
15564 })
15565 .unwrap();
15566 assert_eq!(via_expand_to_named.len(), 2);
15567 assert_eq!(via_expand_to_named.len(), via_named_constant.len());
15568 for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
15569 assert_eq!(a.name, b.name, "NAME slot must agree across cells");
15570 assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
15571 }
15572 assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
15573 assert_eq!(via_expand_to_named[0].spec.name, "x");
15574 assert_eq!(via_expand_to_named[1].name, "beta-compiler");
15575 assert_eq!(via_expand_to_named[1].spec.name, "y");
15576 }
15577
15578 // ── ExpansionDepthExceeded: runaway macro rejected instead of aborting ──
15579
15580 #[test]
15581 fn expander_default_max_expansion_depth_is_the_module_constant() {
15582 // Both constructors seed `max_expansion_depth` from the module
15583 // constant — a regression that drifts one constructor's default
15584 // from the constant (e.g. a hardcoded `256` at ONE site with the
15585 // other left uninitialized after a `Default` derive addition)
15586 // fails this pin.
15587 assert_eq!(
15588 Expander::new().max_expansion_depth(),
15589 DEFAULT_MAX_EXPANSION_DEPTH
15590 );
15591 assert_eq!(
15592 Expander::new_substitute_only().max_expansion_depth(),
15593 DEFAULT_MAX_EXPANSION_DEPTH
15594 );
15595 assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
15596 }
15597
15598 #[test]
15599 fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
15600 let mut e = Expander::new();
15601 e.set_max_expansion_depth(7);
15602 assert_eq!(e.max_expansion_depth(), 7);
15603 }
15604
15605 #[test]
15606 fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
15607 // `(defmacro loop (x) `(loop ,x))` applied to `(loop hello)`
15608 // pre-lift stack-overflowed the process — a below-floor abort.
15609 // Post-lift the expander returns a typed
15610 // `LispError::ExpansionDepthExceeded` at the ceiling with
15611 // `macro_name` populated from the offending call's head.
15612 let mut e = Expander::new();
15613 e.set_max_expansion_depth(4);
15614 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15615 let err = e.expand_program(forms).unwrap_err();
15616 match err {
15617 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15618 assert_eq!(macro_name, "loop");
15619 assert_eq!(limit, 4);
15620 }
15621 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15622 }
15623 }
15624
15625 #[test]
15626 fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
15627 // The substitute strategy shares the same `expand` outer walker
15628 // with the bytecode strategy, so the ceiling fires at the SAME
15629 // gate regardless of which apply-layer is active. The pin makes
15630 // that shared-outer-walker property structural: a regression
15631 // that ever branches the depth guard on strategy (e.g. moves it
15632 // into `apply_compiled` alone) fails this test on the
15633 // substitute expander.
15634 let mut e = Expander::new_substitute_only();
15635 e.set_max_expansion_depth(4);
15636 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15637 let err = e.expand_program(forms).unwrap_err();
15638 match err {
15639 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15640 assert_eq!(macro_name, "loop");
15641 assert_eq!(limit, 4);
15642 }
15643 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15644 }
15645 }
15646
15647 #[test]
15648 fn expand_lawful_nested_macros_within_ceiling_succeed() {
15649 // Lawful nested macros (`(twice (twice hey))`) accrete depth in
15650 // the low single digits because the tree-child arm doesn't
15651 // count — only the post-apply re-expansion path bumps the
15652 // counter. A ceiling of 3 is enough for a two-deep nested
15653 // expansion. This pin is the fail-before/pass-after negative
15654 // control for the recursive-macro rejection tests above: the
15655 // depth counter must NOT reject lawful macro-nesting within
15656 // the ceiling, and only reject the runaway shape.
15657 let mut e = Expander::new();
15658 e.set_max_expansion_depth(3);
15659 let forms = read(
15660 "(defmacro twice (x) `(list ,x ,x))
15661 (twice (twice hey))",
15662 )
15663 .unwrap();
15664 let out = e.expand_program(forms).unwrap();
15665 assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
15666 }
15667
15668 #[test]
15669 fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
15670 // A macro-free lawful deep tree — five levels of plain nesting —
15671 // is expanded at a ceiling well below the tree's height. The
15672 // tree-child arm does not accrete depth, so tree height is
15673 // orthogonal to the ceiling; only macro re-expansion counts.
15674 // A regression that ever bumps depth on tree-child descent
15675 // fails this pin.
15676 let mut e = Expander::new();
15677 e.set_max_expansion_depth(2);
15678 let forms = read("(a (b (c (d (e f)))))").unwrap();
15679 let out = e.expand_program(forms).unwrap();
15680 assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
15681 }
15682
15683 #[test]
15684 fn expansion_depth_exceeded_position_is_none() {
15685 // The variant joins the non-positional cohort — the offending
15686 // macro's byte offset is not what a runaway diagnostic anchors
15687 // to; the OFFENDING MACRO NAME (`macro_name`) is. This pin
15688 // keeps the variant in the `position() -> None` cohort so a
15689 // future span-carrying edit lands the field in ONE place
15690 // across every non-positional variant simultaneously.
15691 let err = LispError::ExpansionDepthExceeded {
15692 macro_name: "loop".to_string(),
15693 limit: 256,
15694 };
15695 assert_eq!(err.position(), None);
15696 }
15697
15698 #[test]
15699 fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
15700 // Display projection carries both `macro_name` and `limit`, so
15701 // authoring surfaces that substring-grep the rendered
15702 // diagnostic (`tatara-check`, REPL, LSP) see BOTH halves at
15703 // the variant boundary and never need to substring-parse
15704 // free-form text to recover the identity of the offending
15705 // macro or the ceiling it breached.
15706 let err = LispError::ExpansionDepthExceeded {
15707 macro_name: "loop".to_string(),
15708 limit: 256,
15709 };
15710 let rendered = err.to_string();
15711 assert!(rendered.contains("loop"), "rendered: {rendered}");
15712 assert!(rendered.contains("256"), "rendered: {rendered}");
15713 assert!(
15714 rendered.contains("macro expansion depth exceeded"),
15715 "rendered: {rendered}"
15716 );
15717 }
15718
15719 // ── max_cache_entries: bounded-memoization ceiling ──────────────────
15720
15721 #[test]
15722 fn expander_default_max_cache_entries_is_the_module_constant() {
15723 // Both cache-enabling constructors seed `max_cache_entries` from
15724 // the module constant — a regression that drifts one
15725 // constructor's default from the constant (e.g. a hardcoded
15726 // `8192` at ONE site with the other left uninitialized after a
15727 // `Default` derive addition) fails this pin. The peer
15728 // constructor `new_substitute_only` seeds the same ceiling so
15729 // the field stays initialized under `#[derive(Clone)]` (a
15730 // zero-initialized clone would be a silent bifurcation of the
15731 // cache-ceiling surface).
15732 assert_eq!(
15733 Expander::new().max_cache_entries(),
15734 DEFAULT_MAX_CACHE_ENTRIES
15735 );
15736 assert_eq!(
15737 Expander::new_substitute_only().max_cache_entries(),
15738 DEFAULT_MAX_CACHE_ENTRIES
15739 );
15740 assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
15741 }
15742
15743 #[test]
15744 fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
15745 // Setter mirrors accessor — a regression that ever forgets to
15746 // write through to the field, or writes to a shadow field the
15747 // accessor does not read, fails this pin.
15748 let mut e = Expander::new();
15749 e.set_max_cache_entries(7);
15750 assert_eq!(e.max_cache_entries(), 7);
15751 }
15752
15753 #[test]
15754 fn expand_cache_size_is_bounded_by_max_cache_entries() {
15755 // Five distinct `(name, args)` pairs expanded against a cache
15756 // ceiling of `2` populate the cache with EXACTLY the first two
15757 // insertions and skip the remaining three. Every expansion
15758 // still returns the correct result — the cache is a pure
15759 // PERFORMANCE optimization; the ceiling gates memoization,
15760 // never correctness. LOAD-BEARING true-arm catch on the
15761 // bounded-cache contract: a regression that ignored the
15762 // ceiling (unbounded insert) would land `cache_size() == 5`
15763 // here; a regression that skipped insertion entirely (broken
15764 // cache) would still see correct expansion output but would
15765 // fail the interior `cache_size() == 2` pin.
15766 let mut e = Expander::new();
15767 e.set_max_cache_entries(2);
15768 let src = "
15769 (defmacro id (x) `,x)
15770 (id one)
15771 (id two)
15772 (id three)
15773 (id four)
15774 (id five)
15775 ";
15776 let out = e.expand_program(read(src).unwrap()).unwrap();
15777 assert_eq!(out.len(), 5);
15778 assert_eq!(out[0], parse("one"));
15779 assert_eq!(out[1], parse("two"));
15780 assert_eq!(out[2], parse("three"));
15781 assert_eq!(out[3], parse("four"));
15782 assert_eq!(out[4], parse("five"));
15783 assert_eq!(
15784 e.cache_size(),
15785 2,
15786 "cache grew past the max_cache_entries ceiling",
15787 );
15788 }
15789
15790 #[test]
15791 fn expand_zero_cache_ceiling_disables_caching_effectively() {
15792 // Zero ceiling admits every fresh `(name, args)` pair through
15793 // the compute path and never grows the cache — the same
15794 // observable behavior as `set_cache_enabled(false)` but
15795 // reached through the ceiling knob. Correctness is preserved:
15796 // the miss path always recomputes the same value the cache
15797 // would have returned, so an operator that dials the ceiling
15798 // to `0` (e.g. to hunt a cache-related regression) never
15799 // bifurcates the output surface. Peer to
15800 // `set_max_expansion_depth(0)` one RESOURCE axis over — both
15801 // ceilings admit the zero endpoint as a well-defined
15802 // extremum.
15803 let mut e = Expander::new();
15804 e.set_max_cache_entries(0);
15805 let src = "
15806 (defmacro id (x) `,x)
15807 (id one)
15808 (id two)
15809 ";
15810 let out = e.expand_program(read(src).unwrap()).unwrap();
15811 assert_eq!(out.len(), 2);
15812 assert_eq!(out[0], parse("one"));
15813 assert_eq!(out[1], parse("two"));
15814 assert_eq!(
15815 e.cache_size(),
15816 0,
15817 "zero cache ceiling grew the cache — the ceiling must be respected on every insert",
15818 );
15819 }
15820
15821 #[test]
15822 fn expand_cached_hits_survive_past_the_ceiling() {
15823 // Once a `(name, args)` pair sits IN the cache under the
15824 // ceiling, subsequent calls hit it — the ceiling only gates
15825 // NEW-KEY inserts, never lookups. This test seeds the cache
15826 // with two entries under a ceiling of `2`, then re-issues the
15827 // SAME calls to prove the cached hits still fire (cache_size
15828 // stays at `2` because no new keys are inserted, and the
15829 // expansion output is stable). LOAD-BEARING true-arm catch
15830 // that the ceiling gates the INSERT path alone — a regression
15831 // that ever refused cache LOOKUPS at the ceiling would
15832 // silently bifurcate correctness on the second call.
15833 let mut e = Expander::new();
15834 e.set_max_cache_entries(2);
15835 let src = "
15836 (defmacro id (x) `,x)
15837 (id one)
15838 (id two)
15839 (id one)
15840 (id two)
15841 ";
15842 let out = e.expand_program(read(src).unwrap()).unwrap();
15843 assert_eq!(out.len(), 4);
15844 assert_eq!(out[0], parse("one"));
15845 assert_eq!(out[1], parse("two"));
15846 assert_eq!(out[2], parse("one"));
15847 assert_eq!(out[3], parse("two"));
15848 assert_eq!(
15849 e.cache_size(),
15850 2,
15851 "cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
15852 );
15853 }
15854
15855 #[test]
15856 fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
15857 // After the cache reaches its ceiling and subsequent inserts
15858 // are skipped, `clear_cache` empties the memo and RE-OPENS the
15859 // insert path — the next expansion populates the freshly-
15860 // empty cache without operator intervention on the ceiling
15861 // itself. The pin binds the operator-facing recovery path
15862 // documented on [`DEFAULT_MAX_CACHE_ENTRIES`]: "skip cache
15863 // insertion until the operator clears the cache via
15864 // [`Expander::clear_cache`]".
15865 let mut e = Expander::new();
15866 e.set_max_cache_entries(1);
15867 let src_fill = "
15868 (defmacro id (x) `,x)
15869 (id one)
15870 (id two)
15871 ";
15872 let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
15873 assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
15874 e.clear_cache();
15875 assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
15876 let out = e
15877 .expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
15878 .unwrap();
15879 assert_eq!(out.len(), 1);
15880 assert_eq!(out[0], parse("three"));
15881 assert_eq!(
15882 e.cache_size(),
15883 1,
15884 "clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
15885 );
15886 }
15887
15888 // ── max_expansion_size: bounded-output ceiling ─────────────────────
15889
15890 #[test]
15891 fn expander_default_max_expansion_size_is_the_module_constant() {
15892 // Both constructors seed `max_expansion_size` from the module
15893 // constant — a regression that drifts one constructor's
15894 // default from the constant (e.g. a hardcoded `65_536` at
15895 // ONE site with the other left uninitialized after a
15896 // `Default` derive addition) fails this pin. Peer to the
15897 // `max_expansion_depth` + `max_cache_entries` default-
15898 // coherence pins one RESOURCE-DIMENSION axis over.
15899 assert_eq!(
15900 Expander::new().max_expansion_size(),
15901 DEFAULT_MAX_EXPANSION_SIZE
15902 );
15903 assert_eq!(
15904 Expander::new_substitute_only().max_expansion_size(),
15905 DEFAULT_MAX_EXPANSION_SIZE
15906 );
15907 assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
15908 }
15909
15910 #[test]
15911 fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
15912 // Setter mirrors accessor — a regression that ever forgets to
15913 // write through to the field, or writes to a shadow field the
15914 // accessor does not read, fails this pin. Peer to the
15915 // setter/accessor coherence pins on the depth + cache axes.
15916 let mut e = Expander::new();
15917 e.set_max_expansion_size(64);
15918 assert_eq!(e.max_expansion_size(), 64);
15919 }
15920
15921 #[test]
15922 fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
15923 // `(defmacro bomb (x) `(list ,x ,x ,x ,x))` applied to
15924 // `(bomb hey)` produces `(list hey hey hey hey)` — 6 nodes
15925 // (outer List + 5 children). With a ceiling of `4` this
15926 // crosses the OUTPUT-SIZE threshold at the first apply and
15927 // the expander returns a typed
15928 // `LispError::ExpansionSizeExceeded` with `macro_name`,
15929 // `size`, and `limit` all populated. Pre-lift the runaway
15930 // just accumulated in a growing tree until the process
15931 // heap ran out; post-lift the rejection sits at the same
15932 // call boundary as every other macro-expansion failure.
15933 let mut e = Expander::new();
15934 e.set_max_expansion_size(4);
15935 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15936 let err = e.expand_program(forms).unwrap_err();
15937 match err {
15938 LispError::ExpansionSizeExceeded {
15939 macro_name,
15940 size,
15941 limit,
15942 } => {
15943 assert_eq!(macro_name, "bomb");
15944 assert_eq!(size, 6);
15945 assert_eq!(limit, 4);
15946 }
15947 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15948 }
15949 }
15950
15951 #[test]
15952 fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
15953 // The substitute strategy shares the same `expand_with_depth`
15954 // outer walker with the bytecode strategy, so the ceiling
15955 // fires at the SAME gate regardless of which apply-layer is
15956 // active. The pin makes that shared-outer-walker property
15957 // structural: a regression that ever branches the size guard
15958 // on strategy (e.g. moves it into `apply_compiled` alone)
15959 // fails this test on the substitute expander. Peer to
15960 // `expand_recursive_macro_rejects_at_depth_limit_substitute_path`
15961 // one RESOURCE axis over.
15962 let mut e = Expander::new_substitute_only();
15963 e.set_max_expansion_size(4);
15964 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15965 let err = e.expand_program(forms).unwrap_err();
15966 match err {
15967 LispError::ExpansionSizeExceeded {
15968 macro_name,
15969 size,
15970 limit,
15971 } => {
15972 assert_eq!(macro_name, "bomb");
15973 assert_eq!(size, 6);
15974 assert_eq!(limit, 4);
15975 }
15976 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15977 }
15978 }
15979
15980 #[test]
15981 fn expand_lawful_output_within_size_ceiling_succeeds() {
15982 // `(defmacro twice (x) `(list ,x ,x))` applied to `(twice hey)`
15983 // produces `(list hey hey)` — 4 nodes. With a ceiling of `4`
15984 // the output sits AT the ceiling (`<=` admits equality; only
15985 // `>` rejects), so the expansion succeeds. Fail-before/pass-
15986 // after negative control for the bomb rejection tests: a
15987 // regression that ever rejected at-ceiling output would fail
15988 // this pin. Peer to `expand_lawful_nested_macros_within_ceiling_succeed`
15989 // one RESOURCE axis over.
15990 let mut e = Expander::new();
15991 e.set_max_expansion_size(4);
15992 let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
15993 let out = e.expand_program(forms).unwrap();
15994 assert_eq!(out[0], parse("(list hey hey)"));
15995 }
15996
15997 #[test]
15998 fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
15999 // A macro-free lawful large tree is NOT gated by the size
16000 // ceiling — the ceiling gates macro-`apply` outputs alone,
16001 // not tree-child descents through non-macro forms. A `usize::MAX`
16002 // ceiling would trivially admit anything; this pin uses a
16003 // deliberately small ceiling of `4` on a tree with 6 nodes
16004 // to catch a regression that ever bumps the ceiling on tree
16005 // descent. A tree-only walk should NEVER fire the ceiling.
16006 // Peer to `expand_depth_ceiling_ignores_lawful_tree_nesting_depth`
16007 // one RESOURCE axis over.
16008 let mut e = Expander::new();
16009 e.set_max_expansion_size(4);
16010 // `(a (b (c d)))` — 6 nodes, no macros. Passes cleanly.
16011 let forms = read("(a (b (c d)))").unwrap();
16012 let out = e.expand_program(forms).unwrap();
16013 assert_eq!(out[0], parse("(a (b (c d)))"));
16014 }
16015
16016 #[test]
16017 fn expansion_size_exceeded_position_is_none() {
16018 // The variant joins the non-positional cohort — the offending
16019 // macro's byte offset is not what an expansion-bomb diagnostic
16020 // anchors to; the (offending macro name, observed size,
16021 // configured ceiling) triple is. This pin keeps the variant
16022 // in the `position() -> None` cohort so a future span-carrying
16023 // edit lands the field in ONE place across every non-positional
16024 // variant simultaneously. Peer to
16025 // `expansion_depth_exceeded_position_is_none` one RESOURCE
16026 // axis over.
16027 let err = LispError::ExpansionSizeExceeded {
16028 macro_name: "bomb".to_string(),
16029 size: 512,
16030 limit: 256,
16031 };
16032 assert_eq!(err.position(), None);
16033 }
16034
16035 #[test]
16036 fn expansion_size_exceeded_display_matches_typed_variant_shape() {
16037 // Display projection carries `macro_name`, `size`, AND
16038 // `limit`, so authoring surfaces that substring-grep the
16039 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16040 // THREE halves at the variant boundary and never need to
16041 // substring-parse free-form text to recover the identity of
16042 // the offending macro, the observed size, or the ceiling it
16043 // breached. Peer to `expansion_depth_exceeded_display_matches_typed_variant_shape`
16044 // one RESOURCE axis over.
16045 let err = LispError::ExpansionSizeExceeded {
16046 macro_name: "bomb".to_string(),
16047 size: 512,
16048 limit: 256,
16049 };
16050 let rendered = err.to_string();
16051 assert!(rendered.contains("bomb"), "rendered: {rendered}");
16052 assert!(rendered.contains("512"), "rendered: {rendered}");
16053 assert!(rendered.contains("256"), "rendered: {rendered}");
16054 assert!(
16055 rendered.contains("macro expansion output size exceeded"),
16056 "rendered: {rendered}"
16057 );
16058 }
16059
16060 #[test]
16061 fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
16062 // A lawful outer macro that expands to a call of a bomb inner
16063 // macro rejects at the INNER call — `macro_name` at the
16064 // variant boundary is the name of the macro whose `apply`
16065 // output crossed the ceiling, not the outermost caller.
16066 // Load-bearing name-provenance pin: an operator whose bomb
16067 // sits N levels deep in a compose chain needs to see the
16068 // INNER macro's name in the diagnostic, not the wrapping
16069 // shape. A regression that ever populates `macro_name` from
16070 // the outermost caller's head fails this pin.
16071 let mut e = Expander::new();
16072 e.set_max_expansion_size(4);
16073 // `wrapper` re-expands into `bomb`; `bomb` produces a size-6
16074 // output that crosses the ceiling. Expected: the diagnostic
16075 // names `bomb`, not `wrapper`.
16076 let src = "
16077 (defmacro bomb (x) `(list ,x ,x ,x ,x))
16078 (defmacro wrapper (x) `(bomb ,x))
16079 (wrapper hey)
16080 ";
16081 let err = e.expand_program(read(src).unwrap()).unwrap_err();
16082 match err {
16083 LispError::ExpansionSizeExceeded {
16084 macro_name,
16085 size,
16086 limit,
16087 } => {
16088 assert_eq!(macro_name, "bomb");
16089 assert_eq!(size, 6);
16090 assert_eq!(limit, 4);
16091 }
16092 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16093 }
16094 }
16095
16096 // ── max_macro_body_size: REGISTRATION-time body-size ceiling ──────
16097 // Peer to depth (recursion length) + cache (memoization width) +
16098 // expansion size (single-`apply` OUTPUT size) one PIPELINE-STAGE
16099 // axis over — the three prior guards fire at EXPAND time; this
16100 // one fires at REGISTER time. Closes the expander's RESOURCE
16101 // surface at a fourth typed dimension across two pipeline stages.
16102
16103 #[test]
16104 fn expander_default_max_macro_body_size_is_the_module_constant() {
16105 // Both constructors seed `max_macro_body_size` from the
16106 // module constant — a regression that drifts one
16107 // constructor's default from the constant (e.g. a hardcoded
16108 // `16_384` at ONE site with the other left uninitialized
16109 // after a `Default` derive addition) fails this pin. Peer
16110 // to the `expander_default_max_expansion_{depth,size}_is_the_module_constant`
16111 // pins one PIPELINE-STAGE axis over.
16112 assert_eq!(
16113 Expander::new().max_macro_body_size(),
16114 DEFAULT_MAX_MACRO_BODY_SIZE
16115 );
16116 assert_eq!(
16117 Expander::new_substitute_only().max_macro_body_size(),
16118 DEFAULT_MAX_MACRO_BODY_SIZE
16119 );
16120 assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
16121 }
16122
16123 #[test]
16124 fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
16125 // Setter mirrors accessor — a regression that ever forgets to
16126 // write through to the field, or writes to a shadow field the
16127 // accessor does not read, fails this pin. Peer to the
16128 // setter/accessor coherence pins on the depth + cache +
16129 // expansion-size axes.
16130 let mut e = Expander::new();
16131 e.set_max_macro_body_size(64);
16132 assert_eq!(e.max_macro_body_size(), 64);
16133 }
16134
16135 #[test]
16136 fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
16137 // `(defmacro huge (x) `(list a b c d e))` has body
16138 // `` `(list a b c d e) `` — a `Sexp::Quasiquote` wrapping a
16139 // list of 6 nodes (outer List + 5 children); the outer
16140 // quasi-quote wrapper adds one → 8 nodes total for
16141 // `def.body.node_count()`. With a ceiling of `4` this
16142 // crosses the REGISTRATION-time BODY-SIZE threshold and the
16143 // expander returns a typed `LispError::MacroBodySizeExceeded`
16144 // with `macro_name`, `size`, and `limit` all populated. Peer
16145 // to `expand_expansion_bomb_rejects_at_size_limit_bytecode_path`
16146 // one PIPELINE-STAGE axis over.
16147 let mut e = Expander::new();
16148 e.set_max_macro_body_size(4);
16149 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16150 let err = e.expand_program(forms).unwrap_err();
16151 match err {
16152 LispError::MacroBodySizeExceeded {
16153 macro_name,
16154 size,
16155 limit,
16156 } => {
16157 assert_eq!(macro_name, "huge");
16158 assert_eq!(size, 8);
16159 assert_eq!(limit, 4);
16160 }
16161 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16162 }
16163 }
16164
16165 #[test]
16166 fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
16167 // The substitute strategy shares the same
16168 // `register_macro_def` outer registration entry with the
16169 // bytecode strategy, so the REGISTRATION-time BODY-SIZE
16170 // ceiling fires at the SAME gate regardless of which
16171 // apply-layer is active. The pin makes that shared-registration
16172 // property structural: a regression that ever branches the
16173 // body-size guard on strategy (e.g. moves it into the
16174 // `compile_templates` arm alone) fails this test on the
16175 // substitute expander. Peer to
16176 // `expand_expansion_bomb_rejects_at_size_limit_substitute_path`
16177 // one PIPELINE-STAGE axis over.
16178 let mut e = Expander::new_substitute_only();
16179 e.set_max_macro_body_size(4);
16180 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16181 let err = e.expand_program(forms).unwrap_err();
16182 match err {
16183 LispError::MacroBodySizeExceeded {
16184 macro_name,
16185 size,
16186 limit,
16187 } => {
16188 assert_eq!(macro_name, "huge");
16189 assert_eq!(size, 8);
16190 assert_eq!(limit, 4);
16191 }
16192 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16193 }
16194 }
16195
16196 #[test]
16197 fn register_macro_body_at_ceiling_admits() {
16198 // `(defmacro twice (x) `(list ,x))` has body
16199 // `` `(list ,x) `` — `Sexp::Quasiquote` wrapping a 3-node
16200 // list (List + `list` atom + `,x` unquote which is
16201 // 1 + inner atom = 2 nodes) = 1 + (1 + 1 + 2) = 5 nodes.
16202 // With a ceiling of `5` the body sits AT the ceiling
16203 // (`<=` admits equality; only `>` rejects), so the
16204 // registration succeeds and the subsequent expansion
16205 // proceeds normally. Fail-before/pass-after negative
16206 // control for the body-bomb rejection tests: a regression
16207 // that ever rejected at-ceiling bodies would fail this pin.
16208 let mut e = Expander::new();
16209 e.set_max_macro_body_size(5);
16210 let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
16211 let out = e.expand_program(forms).unwrap();
16212 assert_eq!(out[0], parse("(list hey)"));
16213 }
16214
16215 #[test]
16216 fn register_failure_leaves_both_tables_pristine() {
16217 // A rejected registration MUST leave BOTH `self.macros`
16218 // AND `self.templates` exactly as they were — no
16219 // partial-write window in which one table carries the
16220 // over-ceiling entry while the other does not, or in
16221 // which the template pre-compile already ran. This pin
16222 // catches a regression that ever moves the body-size
16223 // gate below `self.templates.insert` or below
16224 // `self.macros.insert`.
16225 let mut e = Expander::new();
16226 e.set_max_macro_body_size(4);
16227 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16228 assert!(e.expand_program(forms).is_err());
16229 assert!(
16230 !e.has("huge"),
16231 "macros table must stay pristine after rejection"
16232 );
16233 assert_eq!(e.len(), 0);
16234 }
16235
16236 #[test]
16237 fn macro_body_size_exceeded_position_is_none() {
16238 // The variant joins the non-positional cohort — the
16239 // offending macro's byte offset is not what a body-bomb
16240 // diagnostic anchors to; the (offending macro name,
16241 // observed body size, configured ceiling) triple is.
16242 // This pin keeps the variant in the `position() -> None`
16243 // cohort so a future span-carrying edit lands the field
16244 // in ONE place across every non-positional variant
16245 // simultaneously. Peer to
16246 // `expansion_{depth,size}_exceeded_position_is_none` one
16247 // PIPELINE-STAGE axis over.
16248 let err = LispError::MacroBodySizeExceeded {
16249 macro_name: "huge".to_string(),
16250 size: 512,
16251 limit: 256,
16252 };
16253 assert_eq!(err.position(), None);
16254 }
16255
16256 #[test]
16257 fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
16258 // Display projection carries `macro_name`, `size`, AND
16259 // `limit`, so authoring surfaces that substring-grep the
16260 // rendered diagnostic (`tatara-check`, REPL, LSP) see
16261 // ALL THREE halves at the variant boundary and never need
16262 // to substring-parse free-form text to recover the
16263 // identity of the offending macro, the observed body
16264 // size, or the ceiling it breached. Peer to
16265 // `expansion_size_exceeded_display_matches_typed_variant_shape`
16266 // one PIPELINE-STAGE axis over.
16267 let err = LispError::MacroBodySizeExceeded {
16268 macro_name: "huge".to_string(),
16269 size: 512,
16270 limit: 256,
16271 };
16272 let rendered = err.to_string();
16273 assert!(rendered.contains("huge"), "rendered: {rendered}");
16274 assert!(rendered.contains("512"), "rendered: {rendered}");
16275 assert!(rendered.contains("256"), "rendered: {rendered}");
16276 assert!(
16277 rendered.contains("macro body size exceeded"),
16278 "rendered: {rendered}"
16279 );
16280 }
16281
16282 #[test]
16283 fn lawful_macro_body_within_ceiling_registers_and_expands() {
16284 // A lawful hand-authored macro body sits well below any
16285 // realistic ceiling — the default `DEFAULT_MAX_MACRO_BODY_SIZE`
16286 // is deliberately generous so operators never brush it on
16287 // real authoring workloads. This pin locks the default
16288 // ceiling's non-interference posture: a regression that
16289 // ever lowered the default enough to reject the reference
16290 // `(defmacro when (cond x) `(if ,cond ,x))` shape would
16291 // fail here.
16292 let mut e = Expander::new();
16293 let forms = read(
16294 "(defmacro when (cond x) `(if ,cond ,x))
16295 (when #t hey)",
16296 )
16297 .unwrap();
16298 let out = e.expand_program(forms).unwrap();
16299 assert_eq!(out[0], parse("(if #t hey)"));
16300 }
16301
16302 #[test]
16303 fn register_macro_def_direct_call_respects_body_size_ceiling() {
16304 // `register_macro_def` is the substrate primitive both
16305 // `expand_program`'s `defmacro` recognition AND
16306 // `with_macros`'s bulk load route through. The body-size
16307 // ceiling fires on the primitive itself, not on any one
16308 // consumer — a regression that ever moved the gate into
16309 // `expand_program`'s arm alone would fail this pin.
16310 let mut e = Expander::new();
16311 e.set_max_macro_body_size(4);
16312 let def = MacroDef {
16313 name: "huge".to_string(),
16314 params: MacroParams {
16315 required: vec!["x".to_string()],
16316 optional: Vec::new(),
16317 rest: None,
16318 },
16319 body: Sexp::List(vec![
16320 Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
16321 Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
16322 Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
16323 Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
16324 Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
16325 ]),
16326 };
16327 // Body node_count: List(5 atoms) = 1 + 5 = 6 nodes.
16328 let err = e.register_macro_def(def).unwrap_err();
16329 match err {
16330 LispError::MacroBodySizeExceeded {
16331 macro_name,
16332 size,
16333 limit,
16334 } => {
16335 assert_eq!(macro_name, "huge");
16336 assert_eq!(size, 6);
16337 assert_eq!(limit, 4);
16338 }
16339 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16340 }
16341 assert!(!e.has("huge"));
16342 }
16343
16344 // ── max_registered_macros: REGISTRATION-time table-count ceiling ──
16345 // Peer to body-size (REGISTER-time SIZE) one RESOURCE-DIMENSION
16346 // axis over — where body-size bounds a per-registration SIZE,
16347 // this ceiling bounds cumulative registration COUNT. Peer to
16348 // cache-entries (EXPAND-time COUNT) one PIPELINE-STAGE axis over
16349 // — the two entry-count guards close the two pipeline stages
16350 // symmetrically. Closes the expander's RESOURCE surface at a
16351 // FIFTH typed dimension across two pipeline stages: the
16352 // REGISTER-time face is now a two-dimensional (size × count)
16353 // closure symmetric with the EXPAND-time (depth + count + size)
16354 // triple.
16355
16356 #[test]
16357 fn expander_default_max_registered_macros_is_the_module_constant() {
16358 // Both constructors seed `max_registered_macros` from the
16359 // module constant — a regression that drifts one
16360 // constructor's default from the constant (e.g. a hardcoded
16361 // `4096` at ONE site with the other left uninitialized
16362 // after a `Default` derive addition) fails this pin. Peer
16363 // to the four prior default-coherence pins on depth,
16364 // cache-entries, expansion-size, and body-size.
16365 assert_eq!(
16366 Expander::new().max_registered_macros(),
16367 DEFAULT_MAX_REGISTERED_MACROS
16368 );
16369 assert_eq!(
16370 Expander::new_substitute_only().max_registered_macros(),
16371 DEFAULT_MAX_REGISTERED_MACROS
16372 );
16373 assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
16374 }
16375
16376 #[test]
16377 fn set_max_registered_macros_takes_effect_on_subsequent_register() {
16378 // Setter mirrors accessor — a regression that ever forgets
16379 // to write through to the field, or writes to a shadow
16380 // field the accessor does not read, fails this pin. Peer
16381 // to the four prior setter/accessor coherence pins.
16382 let mut e = Expander::new();
16383 e.set_max_registered_macros(64);
16384 assert_eq!(e.max_registered_macros(), 64);
16385 }
16386
16387 #[test]
16388 fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
16389 // With a ceiling of `2`, the first two fresh registrations
16390 // land in the table and the third fresh key rejects with
16391 // `LispError::RegisteredMacrosExceeded { macro_name: "c",
16392 // count: 2, limit: 2 }`. `count` is the pre-insert table
16393 // length (which equals `limit` at rejection in the common
16394 // case); `macro_name` is the OFFENDING call's name (the
16395 // fresh key we could not admit), not the outermost caller.
16396 let mut e = Expander::new();
16397 e.set_max_registered_macros(2);
16398 let forms = read(
16399 "(defmacro a (x) `(list ,x))
16400 (defmacro b (x) `(list ,x))
16401 (defmacro c (x) `(list ,x))",
16402 )
16403 .unwrap();
16404 let err = e.expand_program(forms).unwrap_err();
16405 match err {
16406 LispError::RegisteredMacrosExceeded {
16407 macro_name,
16408 count,
16409 limit,
16410 } => {
16411 assert_eq!(macro_name, "c");
16412 assert_eq!(count, 2);
16413 assert_eq!(limit, 2);
16414 }
16415 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16416 }
16417 // First two admitted; third rejected leaves the table at
16418 // its pre-third-call size.
16419 assert!(e.has("a"));
16420 assert!(e.has("b"));
16421 assert!(!e.has("c"));
16422 assert_eq!(e.len(), 2);
16423 }
16424
16425 #[test]
16426 fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
16427 // The substitute strategy shares the same
16428 // `register_macro_def` outer registration entry with the
16429 // bytecode strategy, so the REGISTRATION-time TABLE-COUNT
16430 // ceiling fires at the SAME gate regardless of which
16431 // apply-layer is active. The pin makes that shared-
16432 // registration property structural: a regression that ever
16433 // branches the table-count guard on strategy (e.g. moves
16434 // it into the `compile_templates` arm alone) fails this
16435 // test on the substitute expander. Peer to the four prior
16436 // shared-registration pins.
16437 let mut e = Expander::new_substitute_only();
16438 e.set_max_registered_macros(2);
16439 let forms = read(
16440 "(defmacro a (x) `(list ,x))
16441 (defmacro b (x) `(list ,x))
16442 (defmacro c (x) `(list ,x))",
16443 )
16444 .unwrap();
16445 let err = e.expand_program(forms).unwrap_err();
16446 match err {
16447 LispError::RegisteredMacrosExceeded {
16448 macro_name,
16449 count,
16450 limit,
16451 } => {
16452 assert_eq!(macro_name, "c");
16453 assert_eq!(count, 2);
16454 assert_eq!(limit, 2);
16455 }
16456 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16457 }
16458 assert!(e.has("a"));
16459 assert!(e.has("b"));
16460 assert!(!e.has("c"));
16461 assert_eq!(e.len(), 2);
16462 }
16463
16464 #[test]
16465 fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
16466 // OVERWRITES (a re-registration of an already-registered
16467 // key) do not grow the table and MUST be admitted at any
16468 // ceiling — operators redefining a macro at capacity is a
16469 // legitimate authoring pattern. This pin catches a
16470 // regression that ever treats overwrites the same as fresh
16471 // keys (a `contains_key` check that inverts, or a
16472 // `set_max_registered_macros(0)` that would reject even
16473 // pre-registered overwrites).
16474 let mut e = Expander::new();
16475 // Land two macros with a permissive ceiling.
16476 e.expand_program(
16477 read(
16478 "(defmacro a (x) `(list ,x))
16479 (defmacro b (x) `(list ,x))",
16480 )
16481 .unwrap(),
16482 )
16483 .unwrap();
16484 assert_eq!(e.len(), 2);
16485 // Drop the ceiling to exactly the current size — a fresh
16486 // third key would now reject, but an overwrite of `a` MUST
16487 // admit.
16488 e.set_max_registered_macros(2);
16489 let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
16490 e.expand_program(forms).unwrap();
16491 assert_eq!(e.len(), 2, "overwrite must not grow the table");
16492 // Re-expand a call to `a` — the NEW definition (two
16493 // required params) is active, and the OLD definition (one
16494 // required param) is gone. Under the new definition,
16495 // `(a 1 2)` expands to `(pair 1 2)`.
16496 let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
16497 assert_eq!(expanded[0], parse("(pair foo bar)"));
16498 }
16499
16500 #[test]
16501 fn register_at_table_ceiling_admits_up_to_but_not_past() {
16502 // Fail-before/pass-after negative control on the exact
16503 // boundary: with a ceiling of `3`, exactly three fresh keys
16504 // admit and the fourth rejects. A regression that ever
16505 // shifted the boundary by one (e.g. keyed on `>` instead of
16506 // `>=`, or checked `len() > cap` before insert instead of
16507 // `len() >= cap`) would fail one of the two arms — either
16508 // the third admission or the fourth rejection.
16509 let mut e = Expander::new();
16510 e.set_max_registered_macros(3);
16511 // Three fresh keys admit.
16512 e.expand_program(
16513 read(
16514 "(defmacro a (x) `(list ,x))
16515 (defmacro b (x) `(list ,x))
16516 (defmacro c (x) `(list ,x))",
16517 )
16518 .unwrap(),
16519 )
16520 .unwrap();
16521 assert_eq!(e.len(), 3);
16522 // Fourth fresh key rejects.
16523 let err = e
16524 .expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
16525 .unwrap_err();
16526 match err {
16527 LispError::RegisteredMacrosExceeded {
16528 macro_name,
16529 count,
16530 limit,
16531 } => {
16532 assert_eq!(macro_name, "d");
16533 assert_eq!(count, 3);
16534 assert_eq!(limit, 3);
16535 }
16536 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16537 }
16538 // Table stays at pre-rejection size — no partial write.
16539 assert_eq!(e.len(), 3);
16540 assert!(!e.has("d"));
16541 }
16542
16543 #[test]
16544 fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
16545 // A rejected registration MUST leave BOTH `self.macros` AND
16546 // `self.templates` exactly as they were — no partial-write
16547 // window in which one table carries the over-ceiling entry
16548 // while the other does not, or in which the template
16549 // pre-compile already ran. Table-count gate fires BEFORE
16550 // body-size gate walks the body AND BEFORE any insert lands,
16551 // so a regression that ever moved the table-count gate
16552 // below `self.templates.insert` or below
16553 // `self.macros.insert` would fail this pin. Peer to
16554 // `register_failure_leaves_both_tables_pristine` on the
16555 // body-size axis.
16556 let mut e = Expander::new();
16557 e.set_max_registered_macros(1);
16558 // Land one macro at ceiling.
16559 e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16560 .unwrap();
16561 assert!(e.has("a"));
16562 // Reject a fresh second key.
16563 let err = e
16564 .expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
16565 .unwrap_err();
16566 assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
16567 // Table stays at pre-rejection state — `b` is NOT present
16568 // in either self.macros OR self.templates.
16569 assert_eq!(e.len(), 1);
16570 assert!(e.has("a"));
16571 assert!(!e.has("b"));
16572 // Expanding a call to `a` still works (templates table for
16573 // `a` untouched by the failed `b` registration).
16574 let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
16575 assert_eq!(expanded[0], parse("(list hey)"));
16576 }
16577
16578 #[test]
16579 fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
16580 // Zero ceiling is the "contract-test that this expander
16581 // accepts no new macros" posture — every fresh key rejects
16582 // at the first `defmacro` because `0 >= 0`. Peer to the
16583 // depth-ceiling zero-admits-and-rejects-every-macro-call
16584 // pin one RESOURCE axis over.
16585 let mut e = Expander::new();
16586 e.set_max_registered_macros(0);
16587 let err = e
16588 .expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16589 .unwrap_err();
16590 match err {
16591 LispError::RegisteredMacrosExceeded {
16592 macro_name,
16593 count,
16594 limit,
16595 } => {
16596 assert_eq!(macro_name, "a");
16597 assert_eq!(count, 0);
16598 assert_eq!(limit, 0);
16599 }
16600 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16601 }
16602 assert!(e.is_empty());
16603 }
16604
16605 #[test]
16606 fn registered_macros_exceeded_position_is_none() {
16607 // The variant joins the non-positional cohort — the
16608 // offending macro's byte offset is not what a table-count
16609 // diagnostic anchors to; the (offending fresh macro name,
16610 // observed table count, configured ceiling) triple is.
16611 // This pin keeps the variant in the `position() -> None`
16612 // cohort so a future span-carrying edit lands the field
16613 // in ONE place across every non-positional variant
16614 // simultaneously. Peer to the four prior
16615 // `*_exceeded_position_is_none` pins on the resource-
16616 // ceiling cohort.
16617 let err = LispError::RegisteredMacrosExceeded {
16618 macro_name: "fresh-1000".to_string(),
16619 count: 4096,
16620 limit: 4096,
16621 };
16622 assert_eq!(err.position(), None);
16623 }
16624
16625 #[test]
16626 fn registered_macros_exceeded_display_matches_typed_variant_shape() {
16627 // Display projection carries `macro_name`, `count`, AND
16628 // `limit`, so authoring surfaces that substring-grep the
16629 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16630 // THREE halves at the variant boundary and never need to
16631 // substring-parse free-form text to recover the identity
16632 // of the offending fresh key, the observed table count, or
16633 // the ceiling it breached. Peer to the four prior
16634 // `*_exceeded_display_matches_typed_variant_shape` pins.
16635 let err = LispError::RegisteredMacrosExceeded {
16636 macro_name: "fresh-1000".to_string(),
16637 count: 4096,
16638 limit: 4096,
16639 };
16640 let rendered = err.to_string();
16641 assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
16642 assert!(rendered.contains("4096"), "rendered: {rendered}");
16643 assert!(
16644 rendered.contains("registered macros count exceeded"),
16645 "rendered: {rendered}"
16646 );
16647 }
16648
16649 #[test]
16650 fn lawful_typescape_within_ceiling_registers_and_expands() {
16651 // A lawful hand-authored typescape (a handful of macros)
16652 // sits well below any realistic ceiling — the default
16653 // `DEFAULT_MAX_REGISTERED_MACROS` is deliberately generous
16654 // so operators never brush it on real authoring workloads.
16655 // This pin locks the default ceiling's non-interference
16656 // posture: a regression that ever lowered the default
16657 // enough to reject a small typescape would fail here. Peer
16658 // to `lawful_macro_body_within_ceiling_registers_and_expands`.
16659 let mut e = Expander::new();
16660 let forms = read(
16661 "(defmacro when (cond x) `(if ,cond ,x))
16662 (defmacro twice (x) `(list ,x ,x))
16663 (defmacro pair (x y) `(list ,x ,y))
16664 (when #t (twice hey))",
16665 )
16666 .unwrap();
16667 let out = e.expand_program(forms).unwrap();
16668 assert_eq!(out[0], parse("(if #t (list hey hey))"));
16669 assert_eq!(e.len(), 3);
16670 }
16671
16672 #[test]
16673 fn register_macro_def_direct_call_respects_table_ceiling() {
16674 // `register_macro_def` is the substrate primitive both
16675 // `expand_program`'s `defmacro` recognition AND
16676 // `with_macros`'s bulk load route through. The table-count
16677 // ceiling fires on the primitive itself, not on any one
16678 // consumer — a regression that ever moved the gate into
16679 // `expand_program`'s arm alone would fail this pin. Peer
16680 // to `register_macro_def_direct_call_respects_body_size_ceiling`
16681 // one RESOURCE-DIMENSION axis over.
16682 let mut e = Expander::new();
16683 e.set_max_registered_macros(1);
16684 let def_a = MacroDef {
16685 name: "a".to_string(),
16686 params: MacroParams {
16687 required: vec!["x".to_string()],
16688 optional: Vec::new(),
16689 rest: None,
16690 },
16691 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16692 };
16693 let def_b = MacroDef {
16694 name: "b".to_string(),
16695 params: MacroParams {
16696 required: vec!["x".to_string()],
16697 optional: Vec::new(),
16698 rest: None,
16699 },
16700 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16701 };
16702 // First admits — table sits at ceiling.
16703 e.register_macro_def(def_a).unwrap();
16704 assert!(e.has("a"));
16705 assert_eq!(e.len(), 1);
16706 // Second rejects — fresh key past ceiling.
16707 let err = e.register_macro_def(def_b).unwrap_err();
16708 match err {
16709 LispError::RegisteredMacrosExceeded {
16710 macro_name,
16711 count,
16712 limit,
16713 } => {
16714 assert_eq!(macro_name, "b");
16715 assert_eq!(count, 1);
16716 assert_eq!(limit, 1);
16717 }
16718 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16719 }
16720 assert!(!e.has("b"));
16721 assert_eq!(e.len(), 1);
16722 }
16723
16724 #[test]
16725 fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
16726 // Direct `register_macro_def` calls must ALSO admit
16727 // overwrites at ceiling — the overwrite discipline lives
16728 // on the primitive, not on `expand_program`'s arm alone.
16729 // Sibling of the source-level overwrite admission test.
16730 let mut e = Expander::new();
16731 e.set_max_registered_macros(1);
16732 let def_a_v1 = MacroDef {
16733 name: "a".to_string(),
16734 params: MacroParams {
16735 required: vec!["x".to_string()],
16736 optional: Vec::new(),
16737 rest: None,
16738 },
16739 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16740 };
16741 let def_a_v2 = MacroDef {
16742 name: "a".to_string(),
16743 params: MacroParams {
16744 required: vec!["x".to_string(), "y".to_string()],
16745 optional: Vec::new(),
16746 rest: None,
16747 },
16748 body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
16749 };
16750 e.register_macro_def(def_a_v1).unwrap();
16751 assert_eq!(e.len(), 1);
16752 // Overwrite of `a` at ceiling admits — no growth.
16753 e.register_macro_def(def_a_v2).unwrap();
16754 assert_eq!(e.len(), 1);
16755 }
16756
16757 // ── `MacroParams::total_arity` — the total-slots projection ──
16758 //
16759 // `total_arity()` names `required.len() + optional.len() +
16760 // rest.is_some() as usize` on the typed `MacroParams` algebra —
16761 // the `Vec`-free peer of `names().len()` AND the axis both
16762 // `MacroParams::bind`'s `Vec::with_capacity` hint AND the
16763 // REGISTER-time arity-ceiling gate consult. These pins cover:
16764 // structural identity vs. `names().len()`, the
16765 // `fixed_arity + rest.is_some() as usize` decomposition, and the
16766 // three canonical shapes (empty, all-required, mixed with rest).
16767 #[test]
16768 fn macro_params_total_arity_matches_names_len() {
16769 // The structural identity `total_arity() == names().len()`
16770 // — the `Vec`-free primitive's projection equals the flat-
16771 // index list's length. A regression that ever miscounts
16772 // one slot (drops rest, double-counts optional) fails this
16773 // pin. Sibling to `fixed_arity`'s peer-arithmetic pins.
16774 let params = MacroParams {
16775 required: vec!["a".into(), "b".into()],
16776 optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
16777 rest: Some("e".into()),
16778 };
16779 assert_eq!(params.total_arity(), params.names().len());
16780 assert_eq!(params.total_arity(), 5);
16781 }
16782
16783 #[test]
16784 fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
16785 // The `fixed_arity + rest.is_some() as usize` decomposition
16786 // — pins the composition on the [`MacroParams`] algebra so
16787 // a future consumer that wants "total slots" via
16788 // `fixed_arity + rest?` binds to the same arithmetic
16789 // `total_arity` names.
16790 let rest_none = MacroParams {
16791 required: vec!["a".into()],
16792 optional: vec![OptionalParam::bare("b")],
16793 rest: None,
16794 };
16795 assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
16796 assert_eq!(rest_none.total_arity(), 2);
16797 let rest_some = MacroParams {
16798 required: vec!["a".into()],
16799 optional: vec![OptionalParam::bare("b")],
16800 rest: Some("r".into()),
16801 };
16802 assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
16803 assert_eq!(rest_some.total_arity(), 3);
16804 }
16805
16806 #[test]
16807 fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
16808 // Nullary macro edge: `(defmacro nullary () BODY)` has
16809 // `total_arity() == 0`. Under a `set_max_macro_arity(0)`
16810 // ceiling the arity gate keys on `>` (equality admits), so
16811 // this nullary registration succeeds while any non-nullary
16812 // one rejects — the exact contract the arity-gate
16813 // zero-admits-nullary test below relies on.
16814 let params = MacroParams::default();
16815 assert_eq!(params.total_arity(), 0);
16816 }
16817
16818 // ── max_macro_arity: REGISTRATION-time param-count ceiling ──
16819 // Peer to body-size (REGISTER-time BODY SIZE) one RESOURCE-
16820 // DIMENSION axis over — where body-size bounds a per-registration
16821 // BODY node count, this ceiling bounds a per-registration PARAM
16822 // slot count. Peer to registered-macros (REGISTER-time TABLE
16823 // COUNT) one RESOURCE-DIMENSION axis over — where
16824 // registered-macros bounds cumulative registration COUNT, this
16825 // ceiling bounds a per-registration PARAM-LIST WIDTH. Together
16826 // the three REGISTER-time ceilings close the (per-body SIZE,
16827 // per-body ARITY, per-table COUNT) three-corner surface.
16828
16829 #[test]
16830 fn expander_default_max_macro_arity_is_the_module_constant() {
16831 // Both constructors seed `max_macro_arity` from the module
16832 // constant — a regression that drifts one constructor's
16833 // default from the constant (e.g. a hardcoded `128` at ONE
16834 // site with the other left uninitialized after a `Default`
16835 // derive addition) fails this pin. Peer to the five prior
16836 // default-coherence pins on depth, cache-entries,
16837 // expansion-size, body-size, and registered-macros.
16838 assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
16839 assert_eq!(
16840 Expander::new_substitute_only().max_macro_arity(),
16841 DEFAULT_MAX_MACRO_ARITY
16842 );
16843 assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
16844 }
16845
16846 #[test]
16847 fn set_max_macro_arity_takes_effect_on_subsequent_register() {
16848 // Setter mirrors accessor — a regression that ever forgets to
16849 // write through to the field, or writes to a shadow field the
16850 // accessor does not read, fails this pin. Peer to the five
16851 // prior setter/accessor coherence pins.
16852 let mut e = Expander::new();
16853 e.set_max_macro_arity(16);
16854 assert_eq!(e.max_macro_arity(), 16);
16855 }
16856
16857 #[test]
16858 fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
16859 // `(defmacro huge (a b c) `,a)` declares 3 required params.
16860 // Under `set_max_macro_arity(2)` the arity gate fires:
16861 // 3 > 2 → `LispError::MacroArityExceeded { macro_name: "huge",
16862 // arity: 3, limit: 2 }`. `arity` is the observed
16863 // `def.params.total_arity()`, NOT `required.len()` alone —
16864 // even an optional-only or rest-only overrun trips the gate
16865 // (pinned separately). Peer to the register-time body-size
16866 // rejection tests one RESOURCE-DIMENSION axis over.
16867 let mut e = Expander::new();
16868 e.set_max_macro_arity(2);
16869 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16870 let err = e.expand_program(forms).unwrap_err();
16871 match err {
16872 LispError::MacroArityExceeded {
16873 macro_name,
16874 arity,
16875 limit,
16876 } => {
16877 assert_eq!(macro_name, "huge");
16878 assert_eq!(arity, 3);
16879 assert_eq!(limit, 2);
16880 }
16881 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16882 }
16883 assert!(!e.has("huge"));
16884 }
16885
16886 #[test]
16887 fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
16888 // The substitute strategy shares the same
16889 // `register_macro_def` outer registration entry with the
16890 // bytecode strategy, so the REGISTRATION-time ARITY ceiling
16891 // fires at the SAME gate regardless of which apply-layer is
16892 // active. Peer to
16893 // `register_arity_bomb_rejects_at_arity_limit_bytecode_path`.
16894 let mut e = Expander::new_substitute_only();
16895 e.set_max_macro_arity(2);
16896 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16897 let err = e.expand_program(forms).unwrap_err();
16898 match err {
16899 LispError::MacroArityExceeded {
16900 macro_name,
16901 arity,
16902 limit,
16903 } => {
16904 assert_eq!(macro_name, "huge");
16905 assert_eq!(arity, 3);
16906 assert_eq!(limit, 2);
16907 }
16908 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16909 }
16910 assert!(!e.has("huge"));
16911 }
16912
16913 #[test]
16914 fn register_arity_gate_counts_optional_slots() {
16915 // `(defmacro foo (a &optional b c) `,a)` has 1 required + 2
16916 // optional + 0 rest = arity 3 via `total_arity`. Under a
16917 // ceiling of `2` the gate rejects, matching the required-only
16918 // arity-bomb — the arity axis is `total_arity()`, not
16919 // `required.len()`. A regression that drops the optional slots
16920 // from the arity computation would ADMIT this shape and fail
16921 // this pin.
16922 let mut e = Expander::new();
16923 e.set_max_macro_arity(2);
16924 let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
16925 let err = e.expand_program(forms).unwrap_err();
16926 match err {
16927 LispError::MacroArityExceeded {
16928 macro_name,
16929 arity,
16930 limit,
16931 } => {
16932 assert_eq!(macro_name, "foo");
16933 assert_eq!(arity, 3);
16934 assert_eq!(limit, 2);
16935 }
16936 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16937 }
16938 }
16939
16940 #[test]
16941 fn register_arity_gate_counts_rest_slot() {
16942 // `(defmacro spread (a b &rest r) `,a)` has 2 required + 0
16943 // optional + 1 rest = arity 3 via `total_arity`. Under a
16944 // ceiling of `2` the gate rejects, matching the required-only
16945 // arity-bomb — the arity axis includes the rest slot as ONE
16946 // named param, not zero. A regression that drops the rest slot
16947 // from the arity computation would ADMIT this shape and fail
16948 // this pin.
16949 let mut e = Expander::new();
16950 e.set_max_macro_arity(2);
16951 let forms = read("(defmacro spread (a b &rest r) `,a)").unwrap();
16952 let err = e.expand_program(forms).unwrap_err();
16953 match err {
16954 LispError::MacroArityExceeded {
16955 macro_name,
16956 arity,
16957 limit,
16958 } => {
16959 assert_eq!(macro_name, "spread");
16960 assert_eq!(arity, 3);
16961 assert_eq!(limit, 2);
16962 }
16963 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16964 }
16965 }
16966
16967 #[test]
16968 fn register_arity_at_ceiling_admits() {
16969 // `(defmacro pair (x y) `(list ,x ,y))` has 2 required = arity
16970 // 2 via `total_arity`. Under a ceiling of `2` the arity gate
16971 // keys on `>` (equality admits; only strict overrun rejects),
16972 // so the registration succeeds and the expansion proceeds
16973 // normally. Fail-before/pass-after negative control on the
16974 // exact boundary: a regression that ever shifted the boundary
16975 // by one (e.g. keyed on `>=` instead of `>`) would fail this
16976 // pin. Peer to `register_macro_body_at_ceiling_admits` on the
16977 // body-size axis.
16978 let mut e = Expander::new();
16979 e.set_max_macro_arity(2);
16980 let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
16981 let out = e.expand_program(forms).unwrap();
16982 assert_eq!(out[0], parse("(list a b)"));
16983 assert_eq!(e.len(), 1);
16984 }
16985
16986 #[test]
16987 fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
16988 // A rejected arity-gated registration MUST leave BOTH
16989 // `self.macros` AND `self.templates` exactly as they were —
16990 // no partial-write window in which one table carries the
16991 // over-ceiling entry while the other does not, or in which
16992 // the template pre-compile already ran. Arity gate fires
16993 // BEFORE the body-size gate walks the body AND BEFORE any
16994 // insert lands, so a regression that ever moved the arity
16995 // gate below `self.templates.insert` or below
16996 // `self.macros.insert` would fail this pin. Peer to
16997 // `register_failure_leaves_both_tables_pristine` (body-size)
16998 // and `register_table_ceiling_leaves_both_tables_pristine_on_rejection`
16999 // (table-count).
17000 let mut e = Expander::new();
17001 e.set_max_macro_arity(1);
17002 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17003 assert!(e.expand_program(forms).is_err());
17004 assert!(
17005 !e.has("huge"),
17006 "macros table must stay pristine after rejection"
17007 );
17008 assert_eq!(e.len(), 0);
17009 // Expanding a NEW lawful macro afterwards still works — the
17010 // failed arity-gate rejection has not corrupted expander
17011 // state.
17012 let out = e
17013 .expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
17014 .unwrap();
17015 assert_eq!(out[0], parse("hey"));
17016 }
17017
17018 #[test]
17019 fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
17020 // Zero ceiling is the "contract-test that this expander
17021 // accepts only nullary macros" posture — every fresh
17022 // non-nullary key rejects at the arity gate because
17023 // `total_arity() > 0`, while nullary `(defmacro nullary ()
17024 // BODY)` still admits (its arity is exactly 0, and only
17025 // strict overrun rejects). Peer to the body-size zero-rejects-
17026 // every-non-empty-body pin one RESOURCE-DIMENSION axis over
17027 // (though body-size zero rejects even nullary-body shapes,
17028 // since `Sexp::Nil::node_count() == 1 > 0`; arity zero
17029 // preserves the nullary registration surface).
17030 let mut e = Expander::new();
17031 e.set_max_macro_arity(0);
17032 // Nullary admits — arity 0 <= 0.
17033 e.expand_program(read("(defmacro nullary () `unit)").unwrap())
17034 .unwrap();
17035 assert!(e.has("nullary"));
17036 // Non-nullary rejects — arity 1 > 0.
17037 let err = e
17038 .expand_program(read("(defmacro id (x) `,x)").unwrap())
17039 .unwrap_err();
17040 match err {
17041 LispError::MacroArityExceeded {
17042 macro_name,
17043 arity,
17044 limit,
17045 } => {
17046 assert_eq!(macro_name, "id");
17047 assert_eq!(arity, 1);
17048 assert_eq!(limit, 0);
17049 }
17050 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17051 }
17052 assert!(!e.has("id"));
17053 }
17054
17055 #[test]
17056 fn macro_arity_exceeded_position_is_none() {
17057 // The variant joins the non-positional cohort — the offending
17058 // macro's byte offset is not what an arity-bomb diagnostic
17059 // anchors to; the (macro name, observed arity, configured
17060 // ceiling) triple is. This pin keeps the variant in the
17061 // `position() -> None` cohort so a future span-carrying edit
17062 // lands the field in ONE place across every non-positional
17063 // variant simultaneously. Peer to the five prior
17064 // `*_exceeded_position_is_none` pins on the resource-ceiling
17065 // cohort.
17066 let err = LispError::MacroArityExceeded {
17067 macro_name: "huge".to_string(),
17068 arity: 512,
17069 limit: 128,
17070 };
17071 assert_eq!(err.position(), None);
17072 }
17073
17074 #[test]
17075 fn macro_arity_exceeded_display_matches_typed_variant_shape() {
17076 // Display projection carries `macro_name`, `arity`, AND
17077 // `limit`, so authoring surfaces that substring-grep the
17078 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
17079 // THREE halves at the variant boundary and never need to
17080 // substring-parse free-form text to recover the identity of
17081 // the offending macro, the observed arity, or the ceiling it
17082 // breached. Peer to the five prior
17083 // `*_exceeded_display_matches_typed_variant_shape` pins.
17084 let err = LispError::MacroArityExceeded {
17085 macro_name: "huge".to_string(),
17086 arity: 512,
17087 limit: 128,
17088 };
17089 let rendered = err.to_string();
17090 assert!(rendered.contains("huge"), "rendered: {rendered}");
17091 assert!(rendered.contains("512"), "rendered: {rendered}");
17092 assert!(rendered.contains("128"), "rendered: {rendered}");
17093 assert!(
17094 rendered.contains("macro arity exceeded"),
17095 "rendered: {rendered}"
17096 );
17097 }
17098
17099 #[test]
17100 fn lawful_macro_arity_within_ceiling_registers_and_expands() {
17101 // A lawful hand-authored macro arity sits well below any
17102 // realistic ceiling — the default `DEFAULT_MAX_MACRO_ARITY`
17103 // is deliberately generous so operators never brush it on
17104 // real authoring workloads. This pin locks the default
17105 // ceiling's non-interference posture: a regression that
17106 // ever lowered the default enough to reject the reference
17107 // multi-slot `(defmacro when (cond x) …)` shape would fail
17108 // here. Peer to `lawful_macro_body_within_ceiling_registers_and_expands`
17109 // and `lawful_typescape_within_ceiling_registers_and_expands`.
17110 let mut e = Expander::new();
17111 let forms = read(
17112 "(defmacro when (cond x) `(if ,cond ,x))
17113 (defmacro triple (x y z) `(list ,x ,y ,z))
17114 (when #t (triple a b c))",
17115 )
17116 .unwrap();
17117 let out = e.expand_program(forms).unwrap();
17118 assert_eq!(out[0], parse("(if #t (list a b c))"));
17119 }
17120
17121 #[test]
17122 fn register_macro_def_direct_call_respects_arity_ceiling() {
17123 // `register_macro_def` is the substrate primitive both
17124 // `expand_program`'s `defmacro` recognition AND `with_macros`'s
17125 // bulk load route through. The arity ceiling fires on the
17126 // primitive itself, not on any one consumer — a regression
17127 // that ever moved the gate into `expand_program`'s arm alone
17128 // would fail this pin. Peer to
17129 // `register_macro_def_direct_call_respects_body_size_ceiling`
17130 // AND `register_macro_def_direct_call_respects_table_ceiling`
17131 // one RESOURCE-DIMENSION axis over.
17132 let mut e = Expander::new();
17133 e.set_max_macro_arity(2);
17134 let def = MacroDef {
17135 name: "huge".to_string(),
17136 params: MacroParams {
17137 required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
17138 optional: Vec::new(),
17139 rest: None,
17140 },
17141 body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
17142 };
17143 // Arity 3 > ceiling 2 — rejected before body-size gate.
17144 let err = e.register_macro_def(def).unwrap_err();
17145 match err {
17146 LispError::MacroArityExceeded {
17147 macro_name,
17148 arity,
17149 limit,
17150 } => {
17151 assert_eq!(macro_name, "huge");
17152 assert_eq!(arity, 3);
17153 assert_eq!(limit, 2);
17154 }
17155 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17156 }
17157 assert!(!e.has("huge"));
17158 }
17159
17160 #[test]
17161 fn arity_gate_fires_before_body_size_gate() {
17162 // ORDERING PIN — the arity gate fires BEFORE the body-size
17163 // gate. With both `set_max_macro_arity(1)` AND
17164 // `set_max_macro_body_size(2)` set below a shape that violates
17165 // BOTH ceilings — `(defmacro dual (a b) `(list ,a ,b))` has
17166 // arity 2 AND body node count > 2 — the arity gate's
17167 // rejection wins, so the returned variant is
17168 // `MacroArityExceeded`, not `MacroBodySizeExceeded`. A
17169 // regression that ever swapped the two gates' order would fail
17170 // this pin. The ordering isn't cosmetic: arity is O(1)
17171 // (three-arm addition on `def.params` fields), body-size is
17172 // O(body_nodes) (`def.body.node_count()` walks the AST), so
17173 // the O(1)-first order minimises the cost of rejecting the
17174 // worst case (a large-body large-arity macro).
17175 let mut e = Expander::new();
17176 e.set_max_macro_arity(1);
17177 e.set_max_macro_body_size(2);
17178 let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
17179 let err = e.expand_program(forms).unwrap_err();
17180 assert!(
17181 matches!(err, LispError::MacroArityExceeded { .. }),
17182 "arity gate must fire before body-size gate; got: {err:?}"
17183 );
17184 }
17185
17186 #[test]
17187 fn arity_gate_fires_after_table_count_gate() {
17188 // ORDERING PIN — the table-count gate fires BEFORE the arity
17189 // gate. Under `set_max_registered_macros(1)` at capacity AND
17190 // `set_max_macro_arity(1)`, a fresh over-arity registration
17191 // trips the table-count gate FIRST (its `HashMap::len`
17192 // comparison is O(1) cache-friendly and gates cumulative
17193 // registration cost); the arity gate is O(1) but on `def`,
17194 // not on shared state, so it sits AFTER the table-count gate
17195 // on the O(1)-first ordering of the REGISTER-time chain. A
17196 // regression that ever swapped the two would fail this pin.
17197 let mut e = Expander::new();
17198 e.set_max_registered_macros(1);
17199 e.set_max_macro_arity(1);
17200 // Land one macro at table ceiling.
17201 e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
17202 .unwrap();
17203 assert_eq!(e.len(), 1);
17204 // Fresh registration with BOTH violations — table-count wins.
17205 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17206 let err = e.expand_program(forms).unwrap_err();
17207 assert!(
17208 matches!(err, LispError::RegisteredMacrosExceeded { .. }),
17209 "table-count gate must fire before arity gate; got: {err:?}"
17210 );
17211 }
17212
17213 // ── ResourceLimits: bundled resource-ceiling snapshot ────────────
17214
17215 #[test]
17216 fn default_resource_limits_binds_each_field_to_matching_module_constant() {
17217 // Pin the (constant × aggregation) corner: the const-form
17218 // aggregate MUST bind every field to its individually-defined
17219 // module constant. A re-tuning of ONE module constant
17220 // (e.g. `DEFAULT_MAX_EXPANSION_DEPTH` raised from 256 to 512)
17221 // has to propagate through the aggregate at ONE site; this
17222 // test fires the divergence loudly if a future edit sets a
17223 // module constant to a fresh value but forgets to update the
17224 // matching aggregate slot. Field-by-field equality carries
17225 // the message better than one deep-equality assertion.
17226 assert_eq!(
17227 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
17228 DEFAULT_MAX_EXPANSION_DEPTH
17229 );
17230 assert_eq!(
17231 DEFAULT_RESOURCE_LIMITS.max_cache_entries,
17232 DEFAULT_MAX_CACHE_ENTRIES
17233 );
17234 assert_eq!(
17235 DEFAULT_RESOURCE_LIMITS.max_expansion_size,
17236 DEFAULT_MAX_EXPANSION_SIZE
17237 );
17238 assert_eq!(
17239 DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
17240 DEFAULT_MAX_MACRO_BODY_SIZE
17241 );
17242 assert_eq!(
17243 DEFAULT_RESOURCE_LIMITS.max_registered_macros,
17244 DEFAULT_MAX_REGISTERED_MACROS
17245 );
17246 assert_eq!(
17247 DEFAULT_RESOURCE_LIMITS.max_macro_arity,
17248 DEFAULT_MAX_MACRO_ARITY
17249 );
17250 }
17251
17252 #[test]
17253 fn resource_limits_default_impl_matches_const_form() {
17254 // The two projections of the shipped posture — the `const`
17255 // form [`DEFAULT_RESOURCE_LIMITS`] and the runtime
17256 // `Default::default()` form — must reach the SAME value. Pins
17257 // that a future edit which flips one to a new default cannot
17258 // silently drift the other.
17259 assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
17260 }
17261
17262 #[test]
17263 fn expander_new_resource_limits_matches_shipped_defaults() {
17264 // Fresh-expander posture on the bundled-getter surface — the
17265 // six ceilings [`Expander::new`] seeds MUST snapshot to
17266 // [`DEFAULT_RESOURCE_LIMITS`] verbatim. Peer of the six
17267 // individual `expander_default_max_*_is_the_module_constant`
17268 // pins one AGGREGATION axis over — where each individual
17269 // pin catches ONE field drift, this pin catches ANY of the
17270 // six drifting AND the snapshot projection itself losing a
17271 // field.
17272 assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
17273 }
17274
17275 #[test]
17276 fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
17277 // Substitute-only expander seeds the SAME six ceilings from
17278 // the SAME module constants as [`Expander::new`] — the two
17279 // constructors share the resource-ceiling posture even
17280 // though they diverge on `compile_templates` +
17281 // `cache_enabled`. Pins that a future constructor which
17282 // forgets to seed one of the six ceilings from its module
17283 // constant fails loudly through the bundled getter.
17284 assert_eq!(
17285 Expander::new_substitute_only().resource_limits(),
17286 DEFAULT_RESOURCE_LIMITS
17287 );
17288 }
17289
17290 #[test]
17291 fn resource_limits_snapshot_reflects_each_individual_setter() {
17292 // Coherence pin between the six individual setters and the
17293 // bundled getter — flipping ONE ceiling via its individual
17294 // setter must project through [`Expander::resource_limits`]
17295 // to the SAME value the individual getter returns. A
17296 // regression that leaves ONE field out of the snapshot
17297 // projection (a copy-paste omission in the struct literal
17298 // constructor) fires here. Distinct low-non-default values
17299 // so a stray field carrying the default fails loudly.
17300 let mut e = Expander::new();
17301 e.set_max_expansion_depth(3);
17302 e.set_max_cache_entries(5);
17303 e.set_max_expansion_size(7);
17304 e.set_max_macro_body_size(11);
17305 e.set_max_registered_macros(13);
17306 e.set_max_macro_arity(17);
17307 let snap = e.resource_limits();
17308 assert_eq!(snap.max_expansion_depth, 3);
17309 assert_eq!(snap.max_cache_entries, 5);
17310 assert_eq!(snap.max_expansion_size, 7);
17311 assert_eq!(snap.max_macro_body_size, 11);
17312 assert_eq!(snap.max_registered_macros, 13);
17313 assert_eq!(snap.max_macro_arity, 17);
17314 // And the individual getters see the same values — the
17315 // snapshot is a projection, not a divergent copy.
17316 assert_eq!(e.max_expansion_depth(), 3);
17317 assert_eq!(e.max_cache_entries(), 5);
17318 assert_eq!(e.max_expansion_size(), 7);
17319 assert_eq!(e.max_macro_body_size(), 11);
17320 assert_eq!(e.max_registered_macros(), 13);
17321 assert_eq!(e.max_macro_arity(), 17);
17322 }
17323
17324 #[test]
17325 fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
17326 // Sibling pin to `resource_limits_snapshot_reflects_...` on the
17327 // WRITE axis — bulk-setting via [`Expander::set_resource_limits`]
17328 // must reach every individual `max_*` getter. A regression
17329 // that drops one field from the destructuring assignment in
17330 // the bulk setter fires here. Distinct low-non-default values
17331 // so a stray field carrying the previous value fails loudly.
17332 let mut e = Expander::new();
17333 e.set_resource_limits(ResourceLimits {
17334 max_expansion_depth: 2,
17335 max_cache_entries: 4,
17336 max_expansion_size: 8,
17337 max_macro_body_size: 16,
17338 max_registered_macros: 32,
17339 max_macro_arity: 64,
17340 });
17341 assert_eq!(e.max_expansion_depth(), 2);
17342 assert_eq!(e.max_cache_entries(), 4);
17343 assert_eq!(e.max_expansion_size(), 8);
17344 assert_eq!(e.max_macro_body_size(), 16);
17345 assert_eq!(e.max_registered_macros(), 32);
17346 assert_eq!(e.max_macro_arity(), 64);
17347 }
17348
17349 #[test]
17350 fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
17351 // Round-trip theorem — for any starting expander, the
17352 // composition `set_resource_limits(resource_limits())` is
17353 // the identity on the resource-ceiling surface. This is the
17354 // ONE typed statement that pins the (getter, setter) pair as
17355 // strict projections of the same six-field surface, no
17356 // hidden lossy field, no ordering asymmetry between the
17357 // struct literal and the destructuring pattern.
17358 let mut e = Expander::new();
17359 e.set_max_expansion_depth(9);
17360 e.set_max_cache_entries(19);
17361 e.set_max_expansion_size(29);
17362 e.set_max_macro_body_size(39);
17363 e.set_max_registered_macros(49);
17364 e.set_max_macro_arity(59);
17365 let snap = e.resource_limits();
17366 // Reset to defaults so any leftover state would show up in
17367 // the post-round-trip snapshot as the default rather than
17368 // the intended value.
17369 e.set_resource_limits(ResourceLimits::default());
17370 assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
17371 // Apply the captured snapshot — the expander must land at
17372 // the same six values `snap` recorded.
17373 e.set_resource_limits(snap);
17374 assert_eq!(e.resource_limits(), snap);
17375 }
17376
17377 #[test]
17378 fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
17379 // The `Copy` posture on [`ResourceLimits`] gives callers the
17380 // cheap ergonomic of a struct-update literal — take the
17381 // shipped posture and override ONE ceiling — that a chain of
17382 // six independent setters cannot express in ONE typed expression.
17383 // Pins the intended use-shape so a future refactor which
17384 // removes `Copy` from the struct fails to compile at THIS
17385 // site rather than at every downstream consumer.
17386 let mut e = Expander::new();
17387 e.set_resource_limits(ResourceLimits {
17388 max_macro_arity: 4,
17389 ..DEFAULT_RESOURCE_LIMITS
17390 });
17391 // Overridden field carries the fresh value.
17392 assert_eq!(e.max_macro_arity(), 4);
17393 // Every other field carries the shipped default — the
17394 // struct-update literal is a full projection, not a
17395 // selective mutation.
17396 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17397 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17398 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17399 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17400 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17401 }
17402
17403 #[test]
17404 fn expander_default_derives_resource_limits_from_bundled_field_default() {
17405 // Post the [`Expander::limits`]-field consolidation the derived
17406 // `Default` on [`Expander`] projects through [`ResourceLimits`]'s
17407 // own `Default` — which returns [`DEFAULT_RESOURCE_LIMITS`] — so
17408 // `Expander::default().resource_limits()` lands at the shipped
17409 // ceiling posture verbatim. Pre-consolidation the six independent
17410 // `max_*: usize` fields defaulted to the bare `usize` zero, so
17411 // `Expander::default()` yielded a below-floor posture with every
17412 // ceiling clamped to `0` — a shape no consumer wanted and every
17413 // test path avoided by routing through [`Expander::new`] instead.
17414 // The consolidation is what turned that latent below-floor
17415 // derived-`Default` shape into a lawful shipped posture on the
17416 // structural axis. Peer of
17417 // `expander_new_resource_limits_matches_shipped_defaults` one
17418 // CONSTRUCTOR axis over — where that test pins the explicit
17419 // [`Expander::new`] constructor, this test pins the derived-
17420 // [`Default`] constructor route to the SAME six values through
17421 // the SAME bundled projection. Sibling of
17422 // `resource_limits_default_impl_matches_const_form` one
17423 // TYPE-LEVEL axis over — that test pins [`ResourceLimits::default`]
17424 // to [`DEFAULT_RESOURCE_LIMITS`]; this test pins [`Expander`]'s
17425 // derived `Default` to project through it.
17426 assert_eq!(
17427 Expander::default().resource_limits(),
17428 DEFAULT_RESOURCE_LIMITS
17429 );
17430 }
17431
17432 #[test]
17433 fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
17434 // Behavioral pin — the bundled setter is not a decorative
17435 // getter/setter pair; the six ceilings it propagates must
17436 // reach the SAME check sites the individual setters do. Set
17437 // `max_macro_arity` to 1 via the bulk setter and confirm the
17438 // arity gate fires on a two-arg macro registration. A
17439 // regression that wired the bulk setter to a shadow field
17440 // rather than the actual `Expander` state fails here.
17441 let mut e = Expander::new();
17442 e.set_resource_limits(ResourceLimits {
17443 max_macro_arity: 1,
17444 ..DEFAULT_RESOURCE_LIMITS
17445 });
17446 let forms = read("(defmacro two (a b) `,a)").unwrap();
17447 let err = e.expand_program(forms).unwrap_err();
17448 assert!(
17449 matches!(
17450 err,
17451 LispError::MacroArityExceeded {
17452 arity: 2,
17453 limit: 1,
17454 ..
17455 }
17456 ),
17457 "bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
17458 );
17459 }
17460
17461 // ── Expander::with_limits — at-construction resource-posture ───────
17462
17463 #[test]
17464 fn expander_with_limits_seeds_the_provided_resource_posture() {
17465 // Round-trip pin at CONSTRUCTION time — the bundle the caller
17466 // passes into [`Expander::with_limits`] MUST project back
17467 // through [`Expander::resource_limits`] verbatim. Peer of the
17468 // `set_resource_limits(l); resource_limits() == l` round-trip
17469 // one CONSTRUCTOR-STAGE axis over — that pin covers the
17470 // POST-construction assignment; this pin covers the
17471 // AT-construction assignment. A regression that dropped the
17472 // `limits` argument on the floor (a copy-paste that left
17473 // `limits: DEFAULT_RESOURCE_LIMITS` in the struct literal
17474 // instead of `limits`) fires here on every non-default field.
17475 // Distinct low-non-default values so a stray field defaulting
17476 // fails loudly.
17477 let want = ResourceLimits {
17478 max_expansion_depth: 3,
17479 max_cache_entries: 5,
17480 max_expansion_size: 7,
17481 max_macro_body_size: 11,
17482 max_registered_macros: 13,
17483 max_macro_arity: 17,
17484 };
17485 assert_eq!(Expander::with_limits(want).resource_limits(), want);
17486 }
17487
17488 #[test]
17489 fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
17490 // Delegation identity — [`Expander::new`] delegates to
17491 // [`Expander::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`] so
17492 // the two constructors MUST agree on the resource-ceiling
17493 // projection. Pins the "no drift between the two paths to a
17494 // fresh default-posture expander" theorem: a future refactor
17495 // that breaks the delegation (an inline field literal reappearing
17496 // in [`Expander::new`], an accidental hardcoded const at ONE
17497 // site with the other unchanged) fires here. Peer of
17498 // `resource_limits_default_impl_matches_const_form` one
17499 // TYPE-LEVEL axis over — that test pins the two projections of
17500 // the shipped posture on [`ResourceLimits`] itself; this test
17501 // pins the two constructor paths on [`Expander`] to reach the
17502 // SAME six values through the SAME bundle.
17503 assert_eq!(
17504 Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
17505 Expander::new().resource_limits()
17506 );
17507 }
17508
17509 #[test]
17510 fn expander_with_limits_composes_with_struct_update_override() {
17511 // The `Copy` posture on [`ResourceLimits`] lets a caller compose
17512 // "the shipped posture with ONE ceiling overridden" as ONE
17513 // struct-update literal AND thread that composition through
17514 // [`Expander::with_limits`] as ONE typed call — the ergonomic
17515 // shape that six independent setter invocations cannot express
17516 // in ONE expression. Overridden ceiling carries the fresh value,
17517 // every other ceiling carries its shipped default. A future
17518 // refactor that removes `Copy` from [`ResourceLimits`] fails to
17519 // compile at THIS site rather than at every downstream consumer.
17520 let e = Expander::with_limits(ResourceLimits {
17521 max_macro_arity: 4,
17522 ..DEFAULT_RESOURCE_LIMITS
17523 });
17524 assert_eq!(e.max_macro_arity(), 4);
17525 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17526 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17527 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17528 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17529 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17530 }
17531
17532 #[test]
17533 fn expander_with_limits_carries_the_new_execution_strategy() {
17534 // The (compile_templates, cache_enabled) execution-strategy pair
17535 // MUST carry the [`Expander::new`] default posture — bytecode +
17536 // cache both on — regardless of which resource posture the
17537 // caller passes in. Cache-count observation is the least-
17538 // invasive check available on the public surface: a fresh
17539 // `with_limits` expander that expands a repeat-call site must
17540 // populate at least one cache entry (bytecode + cache both on),
17541 // and a substitute-only sibling would leave `cache_size()` at
17542 // zero even after every call site expanded. A regression that
17543 // wired the wrong execution strategy into `with_limits`
17544 // (compile_templates: false, cache_enabled: false) fires here.
17545 let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
17546 let forms = read(
17547 "(defmacro id (x) `,x)
17548 (id one)
17549 (id one)",
17550 )
17551 .unwrap();
17552 e.expand_program(forms).unwrap();
17553 assert!(
17554 e.cache_size() >= 1,
17555 "with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
17556 e.cache_size()
17557 );
17558 }
17559
17560 #[test]
17561 fn expander_with_limits_fires_the_arity_gate_from_construction() {
17562 // Behavioral pin — the at-construction bundle assignment MUST
17563 // reach the SAME check sites the individual + bulk POST-
17564 // construction setters do. Construct an expander with
17565 // `max_macro_arity: 1` directly and confirm the arity gate
17566 // fires on a two-arg macro registration. Sibling of
17567 // `resource_limits_bulk_setter_keeps_the_arity_gate_in_effect`
17568 // one CONSTRUCTOR-STAGE axis over — that pin covers the POST-
17569 // construction setter reaching the gate; this pin covers the
17570 // AT-construction assignment reaching the SAME gate through
17571 // the SAME `self.limits.max_macro_arity` load. A regression
17572 // that wired `with_limits` to a shadow field rather than
17573 // `self.limits` fails here.
17574 let mut e = Expander::with_limits(ResourceLimits {
17575 max_macro_arity: 1,
17576 ..DEFAULT_RESOURCE_LIMITS
17577 });
17578 let forms = read("(defmacro two (a b) `,a)").unwrap();
17579 let err = e.expand_program(forms).unwrap_err();
17580 assert!(
17581 matches!(
17582 err,
17583 LispError::MacroArityExceeded {
17584 arity: 2,
17585 limit: 1,
17586 ..
17587 }
17588 ),
17589 "at-construction arity ceiling must reach the register-time gate; got: {err:?}"
17590 );
17591 }
17592
17593 // ── UNBOUNDED_RESOURCE_LIMITS — ceiling-lifted preset posture ──────
17594
17595 #[test]
17596 fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
17597 // Field-level pin — the ceiling-lifted preset carries
17598 // [`usize::MAX`] on every axis of the six-field surface. Pinned
17599 // as six independent field asserts (rather than one struct
17600 // equality) so a drift on ONE field carries a distinct-name
17601 // failure that names the drifting axis. A future extension
17602 // that adds a SEVENTH ceiling requires an additional field
17603 // assert here in lockstep with the const literal — rustc's
17604 // field-exhaustiveness on the const forces the new field to
17605 // appear, and this pin exercises it.
17606 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
17607 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
17608 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
17609 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
17610 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
17611 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
17612 }
17613
17614 #[test]
17615 fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
17616 // Structural-distinctness pin — the two constants
17617 // [`DEFAULT_RESOURCE_LIMITS`] and [`UNBOUNDED_RESOURCE_LIMITS`]
17618 // sit at DISTINCT points on the posture axis of the (posture ×
17619 // aggregation) grid, and the disagreement is EXHAUSTIVE on the
17620 // six-field surface — every shipped `DEFAULT_MAX_*` module
17621 // constant is a concrete positive value strictly less than
17622 // [`usize::MAX`]. Pins that a future re-tuning which cranked one
17623 // shipped default up to [`usize::MAX`] (collapsing the two
17624 // presets on that field) fires here loudly rather than
17625 // silently letting the two presets become indistinguishable
17626 // on that axis. A regression which flipped ONE unbounded-const
17627 // field back to its default value also fires here — either
17628 // half of the disagreement breaks the theorem.
17629 assert_ne!(
17630 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
17631 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
17632 );
17633 assert_ne!(
17634 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
17635 DEFAULT_RESOURCE_LIMITS.max_cache_entries
17636 );
17637 assert_ne!(
17638 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
17639 DEFAULT_RESOURCE_LIMITS.max_expansion_size
17640 );
17641 assert_ne!(
17642 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
17643 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
17644 );
17645 assert_ne!(
17646 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
17647 DEFAULT_RESOURCE_LIMITS.max_registered_macros
17648 );
17649 assert_ne!(
17650 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
17651 DEFAULT_RESOURCE_LIMITS.max_macro_arity
17652 );
17653 }
17654
17655 #[test]
17656 fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
17657 // Round-trip pin at CONSTRUCTION time — an expander built at
17658 // the ceiling-lifted preset MUST project back through
17659 // [`Expander::resource_limits`] to the SAME constant verbatim.
17660 // Peer of `expander_with_limits_seeds_the_provided_resource_posture`
17661 // one PRESET-POSTURE axis over — that pin exercises the
17662 // constructor's carry via a distinct-low-value bundle; this
17663 // pin exercises it via the ceiling-lifted preset constant.
17664 // Cross-pin the six-field snapshot equals the ceiling-lifted
17665 // constant so a regression that broke either the constant OR
17666 // the constructor's carry fires here.
17667 assert_eq!(
17668 Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
17669 UNBOUNDED_RESOURCE_LIMITS
17670 );
17671 }
17672
17673 #[test]
17674 fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
17675 // Behavioral pin — the ceiling-lifted preset MUST reach the
17676 // register-time body-size gate as an actually-lifted ceiling.
17677 // Register a macro whose body node count strictly exceeds
17678 // [`DEFAULT_MAX_MACRO_BODY_SIZE`] (a body which under the
17679 // shipped default would fail [`register_macro_def`] with a
17680 // [`LispError::MacroBodySizeExceeded`]) and confirm the
17681 // registration succeeds under the ceiling-lifted preset. Only
17682 // the body-size ceiling being lifted lets a body of that
17683 // magnitude land; the arity ceiling is left at 1 param via
17684 // struct-update override to prove the preset's other five
17685 // ceilings STILL project cleanly at [`usize::MAX`] alongside
17686 // the ceiling under test. A regression that failed to lift
17687 // ONE ceiling across the preset (a stale `DEFAULT_MAX_*` field
17688 // in the const literal) fires here on the axis it dropped.
17689 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17690 // Build a huge quasi-quoted list body: `(k k k k k … k) whose
17691 // node_count() eclipses DEFAULT_MAX_MACRO_BODY_SIZE = 16384.
17692 let mut body = String::from("(defmacro huge (k) `(");
17693 for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
17694 body.push_str(",k ");
17695 }
17696 body.push_str("))");
17697 let forms = read(&body).unwrap();
17698 e.expand_program(forms)
17699 .expect("unbounded body-size ceiling must admit a body over the default limit");
17700 assert!(e.has("huge"));
17701 }
17702
17703 #[test]
17704 fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
17705 // Behavioral pin sibling of the body-size pin above one
17706 // RESOURCE-DIMENSION axis over on the REGISTER-time surface —
17707 // the ceiling-lifted preset MUST also reach the arity gate as
17708 // an actually-lifted ceiling. Register a macro whose param
17709 // list strictly exceeds [`DEFAULT_MAX_MACRO_ARITY`] (a
17710 // 129-slot lambda list which under the shipped default fails
17711 // with [`LispError::MacroArityExceeded`]) and confirm the
17712 // registration succeeds under the ceiling-lifted preset.
17713 use std::fmt::Write as _;
17714 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17715 let mut src = String::from("(defmacro many-arity (");
17716 for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
17717 write!(src, "a-{i} ").unwrap();
17718 }
17719 src.push_str(") `,a-0)");
17720 let forms = read(&src).unwrap();
17721 e.expand_program(forms)
17722 .expect("unbounded arity ceiling must admit a param list over the default limit");
17723 assert!(e.has("many-arity"));
17724 }
17725
17726 #[test]
17727 fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
17728 // Ergonomic pin — the `Copy` posture on [`ResourceLimits`] lets
17729 // a caller build "every ceiling lifted EXCEPT this ONE" as ONE
17730 // struct-update literal on the ceiling-lifted preset AND thread
17731 // that composition through [`Expander::with_limits`] as ONE
17732 // typed call. This is the "only THIS ceiling gates" fixture
17733 // shape a chain of six independent setters cannot express in
17734 // ONE expression, and the shape the last commit named as a
17735 // future benefit of the (getter, setter, at-construction)
17736 // three-corner face closing at ONE bundle.
17737 //
17738 // Set only `max_macro_arity: 1`; confirm the arity gate fires
17739 // on a two-arg registration while the five other lifted
17740 // ceilings project through the getter as [`usize::MAX`]. A
17741 // regression that broke the ceiling-lifted preset would EITHER
17742 // fail to project [`usize::MAX`] on one of the five other
17743 // fields OR fail to gate the isolated arity ceiling at 1.
17744 let mut e = Expander::with_limits(ResourceLimits {
17745 max_macro_arity: 1,
17746 ..UNBOUNDED_RESOURCE_LIMITS
17747 });
17748 assert_eq!(e.max_macro_arity(), 1);
17749 assert_eq!(e.max_expansion_depth(), usize::MAX);
17750 assert_eq!(e.max_cache_entries(), usize::MAX);
17751 assert_eq!(e.max_expansion_size(), usize::MAX);
17752 assert_eq!(e.max_macro_body_size(), usize::MAX);
17753 assert_eq!(e.max_registered_macros(), usize::MAX);
17754 let forms = read("(defmacro two (a b) `,a)").unwrap();
17755 let err = e.expand_program(forms).unwrap_err();
17756 assert!(
17757 matches!(
17758 err,
17759 LispError::MacroArityExceeded {
17760 arity: 2,
17761 limit: 1,
17762 ..
17763 }
17764 ),
17765 "struct-update override on the ceiling-lifted preset must \
17766 leave the isolated arity ceiling gating; got: {err:?}"
17767 );
17768 }
17769
17770 #[test]
17771 fn unbounded_resource_limits_carries_through_the_bulk_setter() {
17772 // Coherence pin between the AT-construction constructor and
17773 // the POST-construction bulk setter — both paths that reach the
17774 // ceiling-lifted preset must land at the SAME six values on
17775 // the expander state. Pre-lift a caller wanting the unbounded
17776 // posture composed six individual setter invocations at the
17777 // call site; post-lift both `Expander::with_limits(UNBOUNDED_
17778 // RESOURCE_LIMITS)` and `e.set_resource_limits(UNBOUNDED_
17779 // RESOURCE_LIMITS)` reach the SAME snapshot through the SAME
17780 // constant. A regression that wired the bulk setter to a
17781 // shadow field, or the ceiling-lifted preset to a stale field
17782 // literal, fires here on the constructor-vs-setter identity.
17783 let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17784 let mut b = Expander::new();
17785 b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
17786 assert_eq!(a.resource_limits(), b.resource_limits());
17787 assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
17788 }
17789
17790 // ── ResourceLimits::strictest / ::most_permissive — meet/join ──────
17791
17792 /// A hand-authored asymmetric posture used across the lattice-law
17793 /// tests. Every field is a distinct positive value strictly less
17794 /// than [`usize::MAX`] and distinct from every corresponding
17795 /// [`DEFAULT_MAX_*`] module constant, so composition witnesses
17796 /// (meet, join, absorption, distributivity) discriminate across
17797 /// all three postures unambiguously.
17798 const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
17799 max_expansion_depth: 7,
17800 max_cache_entries: 11,
17801 max_expansion_size: 13,
17802 max_macro_body_size: 17,
17803 max_registered_macros: 19,
17804 max_macro_arity: 23,
17805 };
17806
17807 /// A second asymmetric posture whose per-axis values are on both
17808 /// sides of `HAND_AUTHORED_MID_POSTURE`'s values — three axes
17809 /// smaller (so the meet picks THIS posture on those axes and the
17810 /// join picks `MID` on those axes) and three axes larger (mirror).
17811 /// Together the two postures exercise every ordering combination
17812 /// on the six-axis pointwise `min`/`max` cascade.
17813 const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
17814 max_expansion_depth: 3, // smaller than MID's 7
17815 max_cache_entries: 29, // larger than MID's 11
17816 max_expansion_size: 5, // smaller than MID's 13
17817 max_macro_body_size: 31, // larger than MID's 17
17818 max_registered_macros: 2, // smaller than MID's 19
17819 max_macro_arity: 41, // larger than MID's 23
17820 };
17821
17822 #[test]
17823 fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
17824 // Concrete-preset pin — the meet of the (shipped default,
17825 // ceiling-lifted unbounded) preset pair is the DEFAULT preset
17826 // structurally: every `DEFAULT_MAX_*` module constant is a
17827 // concrete positive value strictly less than [`usize::MAX`],
17828 // so on every axis the pointwise-`min` picks the DEFAULT side.
17829 // Peer of `most_permissive_of_default_and_unbounded_projects_the_unbounded`
17830 // one COMBINATOR axis over on the same shipped-preset-pair
17831 // surface.
17832 assert_eq!(
17833 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17834 DEFAULT_RESOURCE_LIMITS,
17835 );
17836 assert_eq!(
17837 UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17838 DEFAULT_RESOURCE_LIMITS,
17839 );
17840 }
17841
17842 #[test]
17843 fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
17844 // Concrete-preset pin — the join of the (shipped default,
17845 // ceiling-lifted unbounded) preset pair is the UNBOUNDED
17846 // preset structurally: every axis's pointwise-`max` picks the
17847 // [`usize::MAX`] side over any concrete default. Peer of
17848 // `strictest_of_default_and_unbounded_projects_the_default`.
17849 assert_eq!(
17850 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17851 UNBOUNDED_RESOURCE_LIMITS,
17852 );
17853 assert_eq!(
17854 UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17855 UNBOUNDED_RESOURCE_LIMITS,
17856 );
17857 }
17858
17859 #[test]
17860 fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
17861 // Field-level pin — the meet operation projects the pointwise
17862 // `min` across the six-field surface. Pinned as six
17863 // independent field asserts (rather than one struct equality)
17864 // so a drift on ONE axis carries a distinct-name failure that
17865 // names the drifting axis; a future SEVENTH ceiling extension
17866 // requires an additional field assert here in lockstep.
17867 let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
17868 assert_eq!(m.max_expansion_depth, 3);
17869 assert_eq!(m.max_cache_entries, 11);
17870 assert_eq!(m.max_expansion_size, 5);
17871 assert_eq!(m.max_macro_body_size, 17);
17872 assert_eq!(m.max_registered_macros, 2);
17873 assert_eq!(m.max_macro_arity, 23);
17874 }
17875
17876 #[test]
17877 fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
17878 // Field-level pin — the join operation projects the pointwise
17879 // `max` across the six-field surface. Pinned as six
17880 // independent field asserts so a drift on ONE axis carries a
17881 // distinct-name failure that names the drifting axis.
17882 let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
17883 assert_eq!(j.max_expansion_depth, 7);
17884 assert_eq!(j.max_cache_entries, 29);
17885 assert_eq!(j.max_expansion_size, 13);
17886 assert_eq!(j.max_macro_body_size, 31);
17887 assert_eq!(j.max_registered_macros, 19);
17888 assert_eq!(j.max_macro_arity, 41);
17889 }
17890
17891 #[test]
17892 fn resource_limits_strictest_is_idempotent() {
17893 // Lattice law — `a ∧ a = a`. Every posture is a fixed point of
17894 // its own meet with itself; a regression that changed the
17895 // per-axis primitive to something other than pointwise `min`
17896 // (a `min - 1` off-by-one, or an arbitrary tie-breaker on
17897 // equal values) fires here.
17898 assert_eq!(
17899 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17900 DEFAULT_RESOURCE_LIMITS,
17901 );
17902 assert_eq!(
17903 UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17904 UNBOUNDED_RESOURCE_LIMITS,
17905 );
17906 assert_eq!(
17907 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17908 HAND_AUTHORED_MID_POSTURE,
17909 );
17910 }
17911
17912 #[test]
17913 fn resource_limits_most_permissive_is_idempotent() {
17914 // Lattice law — `a ∨ a = a`. Sibling of the strictest
17915 // idempotence pin one COMBINATOR axis over.
17916 assert_eq!(
17917 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17918 DEFAULT_RESOURCE_LIMITS,
17919 );
17920 assert_eq!(
17921 UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17922 UNBOUNDED_RESOURCE_LIMITS,
17923 );
17924 assert_eq!(
17925 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17926 HAND_AUTHORED_MID_POSTURE,
17927 );
17928 }
17929
17930 #[test]
17931 fn resource_limits_strictest_is_commutative() {
17932 // Lattice law — `a ∧ b = b ∧ a`. Pointwise `min` is symmetric
17933 // on every axis, so the composition inherits commutativity
17934 // structurally. A regression that broke ordering (e.g. a
17935 // per-axis "prefer left on tie") fires here.
17936 assert_eq!(
17937 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
17938 HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17939 );
17940 assert_eq!(
17941 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
17942 HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
17943 );
17944 }
17945
17946 #[test]
17947 fn resource_limits_most_permissive_is_commutative() {
17948 // Lattice law — `a ∨ b = b ∨ a`. Sibling of the strictest
17949 // commutativity pin one COMBINATOR axis over.
17950 assert_eq!(
17951 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
17952 HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17953 );
17954 assert_eq!(
17955 DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
17956 HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
17957 );
17958 }
17959
17960 #[test]
17961 fn resource_limits_strictest_is_associative() {
17962 // Lattice law — `(a ∧ b) ∧ c = a ∧ (b ∧ c)`. Pointwise `min`
17963 // is associative on every axis, so the composition inherits
17964 // associativity structurally. A regression that broke
17965 // grouping (e.g. an accidental accumulator that skewed the
17966 // order of `min` invocations) fires here.
17967 let a = DEFAULT_RESOURCE_LIMITS;
17968 let b = HAND_AUTHORED_MID_POSTURE;
17969 let c = HAND_AUTHORED_OTHER_POSTURE;
17970 assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
17971 }
17972
17973 #[test]
17974 fn resource_limits_most_permissive_is_associative() {
17975 // Lattice law — `(a ∨ b) ∨ c = a ∨ (b ∨ c)`. Sibling of the
17976 // strictest associativity pin one COMBINATOR axis over.
17977 let a = DEFAULT_RESOURCE_LIMITS;
17978 let b = HAND_AUTHORED_MID_POSTURE;
17979 let c = HAND_AUTHORED_OTHER_POSTURE;
17980 assert_eq!(
17981 a.most_permissive(b).most_permissive(c),
17982 a.most_permissive(b.most_permissive(c)),
17983 );
17984 }
17985
17986 #[test]
17987 fn resource_limits_meet_and_join_satisfy_absorption() {
17988 // Lattice absorption identities — `a ∧ (a ∨ b) = a` AND
17989 // `a ∨ (a ∧ b) = a`. Together with commutativity, idempotence,
17990 // and associativity they define the lattice axioms; the two
17991 // absorption laws are the operator PAIR's interlock (each
17992 // undoes the other's contribution on the shared operand).
17993 // Any per-axis primitive pair for which `min(a, max(a, b)) = a`
17994 // AND `max(a, min(a, b)) = a` inherits this at the pointwise
17995 // composition; pointwise-`min`/`max` on [`usize`] satisfy both.
17996 let a = HAND_AUTHORED_MID_POSTURE;
17997 let b = HAND_AUTHORED_OTHER_POSTURE;
17998 assert_eq!(a.strictest(a.most_permissive(b)), a);
17999 assert_eq!(a.most_permissive(a.strictest(b)), a);
18000 }
18001
18002 #[test]
18003 fn resource_limits_meet_distributes_over_join() {
18004 // Distributive-lattice pin — `a ∧ (b ∨ c) = (a ∧ b) ∨ (a ∧ c)`.
18005 // Pointwise-`min` distributes over pointwise-`max` on [`usize`]
18006 // on every axis, so the composition inherits distributivity
18007 // structurally. A regression that broke the pointwise
18008 // structure (e.g. a per-axis operation that used SUBTRACTION
18009 // rather than MIN/MAX) fires here — subtraction does NOT
18010 // distribute over addition and would break the law on the
18011 // asymmetric-posture witness pair.
18012 let a = DEFAULT_RESOURCE_LIMITS;
18013 let b = HAND_AUTHORED_MID_POSTURE;
18014 let c = HAND_AUTHORED_OTHER_POSTURE;
18015 assert_eq!(
18016 a.strictest(b.most_permissive(c)),
18017 a.strictest(b).most_permissive(a.strictest(c)),
18018 );
18019 }
18020
18021 #[test]
18022 fn resource_limits_join_distributes_over_meet() {
18023 // Distributive-lattice pin, sibling of the meet-over-join
18024 // distributivity pin one COMBINATOR axis over — `a ∨ (b ∧ c) =
18025 // (a ∨ b) ∧ (a ∨ c)`. In a distributive lattice BOTH
18026 // distributivity identities hold; pin both so a regression
18027 // that broke ONE without the other cannot slip through.
18028 let a = DEFAULT_RESOURCE_LIMITS;
18029 let b = HAND_AUTHORED_MID_POSTURE;
18030 let c = HAND_AUTHORED_OTHER_POSTURE;
18031 assert_eq!(
18032 a.most_permissive(b.strictest(c)),
18033 a.most_permissive(b).strictest(a.most_permissive(c)),
18034 );
18035 }
18036
18037 #[test]
18038 fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
18039 // Meet-lower-bound pin — the meet lies at or below BOTH
18040 // operands on every axis. This is the structural GLB property
18041 // of the meet in the pointwise partial order (tighter-first):
18042 // any admissible input under the meet is admissible under
18043 // both operands, so the meet's ceiling must not exceed either
18044 // operand's ceiling on any axis.
18045 let a = HAND_AUTHORED_MID_POSTURE;
18046 let b = HAND_AUTHORED_OTHER_POSTURE;
18047 let m = a.strictest(b);
18048 assert!(m.max_expansion_depth <= a.max_expansion_depth);
18049 assert!(m.max_expansion_depth <= b.max_expansion_depth);
18050 assert!(m.max_cache_entries <= a.max_cache_entries);
18051 assert!(m.max_cache_entries <= b.max_cache_entries);
18052 assert!(m.max_expansion_size <= a.max_expansion_size);
18053 assert!(m.max_expansion_size <= b.max_expansion_size);
18054 assert!(m.max_macro_body_size <= a.max_macro_body_size);
18055 assert!(m.max_macro_body_size <= b.max_macro_body_size);
18056 assert!(m.max_registered_macros <= a.max_registered_macros);
18057 assert!(m.max_registered_macros <= b.max_registered_macros);
18058 assert!(m.max_macro_arity <= a.max_macro_arity);
18059 assert!(m.max_macro_arity <= b.max_macro_arity);
18060 }
18061
18062 #[test]
18063 fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
18064 // Join-upper-bound pin — the join lies at or above BOTH
18065 // operands on every axis. Sibling of the strictest-lower-
18066 // bound pin one COMBINATOR axis over; the structural LUB
18067 // property of the join in the pointwise partial order.
18068 let a = HAND_AUTHORED_MID_POSTURE;
18069 let b = HAND_AUTHORED_OTHER_POSTURE;
18070 let j = a.most_permissive(b);
18071 assert!(j.max_expansion_depth >= a.max_expansion_depth);
18072 assert!(j.max_expansion_depth >= b.max_expansion_depth);
18073 assert!(j.max_cache_entries >= a.max_cache_entries);
18074 assert!(j.max_cache_entries >= b.max_cache_entries);
18075 assert!(j.max_expansion_size >= a.max_expansion_size);
18076 assert!(j.max_expansion_size >= b.max_expansion_size);
18077 assert!(j.max_macro_body_size >= a.max_macro_body_size);
18078 assert!(j.max_macro_body_size >= b.max_macro_body_size);
18079 assert!(j.max_registered_macros >= a.max_registered_macros);
18080 assert!(j.max_registered_macros >= b.max_registered_macros);
18081 assert!(j.max_macro_arity >= a.max_macro_arity);
18082 assert!(j.max_macro_arity >= b.max_macro_arity);
18083 }
18084
18085 #[test]
18086 fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
18087 // Const-fn pin — the meet is evaluable in const context, so a
18088 // caller can stitch a new `pub const` preset from two shipped
18089 // presets without deferring the composition to runtime. Pinned
18090 // as a `const` binding at the item level so the compiler
18091 // exercises the const-fn path structurally; the runtime
18092 // assertion is redundant for a `const` value that already
18093 // evaluated at compile time, but pins the identity as a typed
18094 // theorem alongside every OTHER `strictest` pin above.
18095 const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
18096 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
18097 assert_eq!(
18098 DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
18099 min_usize(
18100 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18101 HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
18102 ),
18103 );
18104 // Extra check: the composition MUST project the MID posture's
18105 // small value on every axis where MID is smaller than DEFAULT
18106 // — MID's per-axis values (7, 11, 13, 17, 19, 23) are all
18107 // strictly less than every `DEFAULT_MAX_*` constant, so the
18108 // meet projects MID verbatim.
18109 assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
18110 }
18111
18112 #[test]
18113 fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
18114 // Const-fn pin sibling of the strictest const-composition pin
18115 // one COMBINATOR axis over — the join is also `const fn`, so
18116 // a caller can stitch a new `pub const` preset via the LUB
18117 // combinator at compile time.
18118 const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
18119 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
18120 assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
18121 }
18122
18123 // ── ResourceLimits::leq — pointwise partial order ─────────────────
18124
18125 #[test]
18126 fn resource_limits_leq_is_pointwise_field_conjunction() {
18127 // Field-level pin — the leq relation projects the pointwise
18128 // `<=` conjunction across the six-field surface. `LOOSER` is
18129 // `MID + 1` on every axis, so `MID.leq(LOOSER)` holds; a
18130 // per-axis tighten on ANY field alone flips the relation to
18131 // false. Pinned as six independent tighten-per-axis asserts so
18132 // a drift on ONE axis carries a distinct-name failure that
18133 // names the drifting axis; a future SEVENTH ceiling extension
18134 // requires an additional per-axis tighten assert here in
18135 // lockstep.
18136 const LOOSER: ResourceLimits = ResourceLimits {
18137 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18138 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18139 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18140 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18141 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18142 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18143 };
18144 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18145 assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
18146
18147 // Per-axis exceed pins: raise MID's field ONE axis above LOOSER
18148 // on that axis; the leq must flip to false. Every other axis
18149 // stays at MID, so LOOSER still dominates on those axes —
18150 // isolating the single-axis violation.
18151 let exceed_depth = ResourceLimits {
18152 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18153 ..HAND_AUTHORED_MID_POSTURE
18154 };
18155 assert!(!exceed_depth.leq(LOOSER));
18156 let exceed_cache = ResourceLimits {
18157 max_cache_entries: LOOSER.max_cache_entries + 1,
18158 ..HAND_AUTHORED_MID_POSTURE
18159 };
18160 assert!(!exceed_cache.leq(LOOSER));
18161 let exceed_expansion = ResourceLimits {
18162 max_expansion_size: LOOSER.max_expansion_size + 1,
18163 ..HAND_AUTHORED_MID_POSTURE
18164 };
18165 assert!(!exceed_expansion.leq(LOOSER));
18166 let exceed_body = ResourceLimits {
18167 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18168 ..HAND_AUTHORED_MID_POSTURE
18169 };
18170 assert!(!exceed_body.leq(LOOSER));
18171 let exceed_registered = ResourceLimits {
18172 max_registered_macros: LOOSER.max_registered_macros + 1,
18173 ..HAND_AUTHORED_MID_POSTURE
18174 };
18175 assert!(!exceed_registered.leq(LOOSER));
18176 let exceed_arity = ResourceLimits {
18177 max_macro_arity: LOOSER.max_macro_arity + 1,
18178 ..HAND_AUTHORED_MID_POSTURE
18179 };
18180 assert!(!exceed_arity.leq(LOOSER));
18181 }
18182
18183 #[test]
18184 fn resource_limits_leq_is_reflexive() {
18185 // Partial-order law — `a ≤ a` for every posture. Reflexivity
18186 // is the identity axiom on any lattice's underlying partial
18187 // order; a regression that changed the per-axis primitive to
18188 // strict `<` (or that added an accidental "prefer other on
18189 // tie" branch) fires here on every shipped posture.
18190 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18191 assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18192 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18193 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18194 }
18195
18196 #[test]
18197 fn resource_limits_leq_is_antisymmetric() {
18198 // Partial-order law — `a ≤ b ∧ b ≤ a ⇒ a = b`. The mutual-
18199 // dominance witness on a partial order pins the two operands
18200 // to structural equality. On the pointwise `<=` composition
18201 // this holds because `a.max_X <= b.max_X && b.max_X <= a.max_X
18202 // ⇒ a.max_X == b.max_X` on every axis, and struct equality
18203 // is field-conjunction. Sibling to reflexivity one AXIOM axis
18204 // over on the partial-order axiom surface.
18205 //
18206 // Two distinct constructions of the SAME six-field posture
18207 // (one via struct literal, one via `..DEFAULT_RESOURCE_LIMITS`
18208 // spread of the shipped preset) — mutual leq holds; struct
18209 // equality holds; antisymmetry pin closes.
18210 let a = DEFAULT_RESOURCE_LIMITS;
18211 let b = ResourceLimits {
18212 max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18213 max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
18214 max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
18215 max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
18216 max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
18217 max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
18218 };
18219 assert!(a.leq(b));
18220 assert!(b.leq(a));
18221 assert_eq!(a, b);
18222 }
18223
18224 #[test]
18225 fn resource_limits_leq_is_transitive() {
18226 // Partial-order law — `a ≤ b ∧ b ≤ c ⇒ a ≤ c`. Transitivity
18227 // is the composition axiom on any lattice's partial order.
18228 // Pointwise `<=` on [`usize`] is transitive per axis, so the
18229 // six-field conjunction inherits it structurally.
18230 //
18231 // Chain witness: MID's every-axis `+1` shift is LOOSER, and
18232 // LOOSER's every-axis `+1` shift is LOOSEST; the chain MID
18233 // ≤ LOOSER ≤ LOOSEST implies MID ≤ LOOSEST.
18234 const LOOSER: ResourceLimits = ResourceLimits {
18235 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18236 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18237 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18238 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18239 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18240 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18241 };
18242 const LOOSEST: ResourceLimits = ResourceLimits {
18243 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18244 max_cache_entries: LOOSER.max_cache_entries + 1,
18245 max_expansion_size: LOOSER.max_expansion_size + 1,
18246 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18247 max_registered_macros: LOOSER.max_registered_macros + 1,
18248 max_macro_arity: LOOSER.max_macro_arity + 1,
18249 };
18250 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18251 assert!(LOOSER.leq(LOOSEST));
18252 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
18253 }
18254
18255 #[test]
18256 fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
18257 // Concrete-preset pin — the leq relation on the shipped
18258 // preset pair is STRICT: DEFAULT ≤ UNBOUNDED holds (every
18259 // `DEFAULT_MAX_*` module constant is a concrete positive
18260 // value at most [`usize::MAX`]) AND UNBOUNDED ≤ DEFAULT
18261 // fails (every `DEFAULT_MAX_*` is strictly less than
18262 // [`usize::MAX`], so on every axis [`usize::MAX`] exceeds
18263 // the matching default). Peer of
18264 // `strictest_of_default_and_unbounded_projects_the_default`
18265 // one PRIMITIVE-KIND axis over on the same shipped-preset-
18266 // pair surface: where the meet composition projects DEFAULT,
18267 // this relation asserts DEFAULT is the tighter of the two.
18268 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18269 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18270 }
18271
18272 #[test]
18273 fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
18274 // Partial-order NON-total pin — the two hand-authored asymmetric
18275 // postures have MID smaller on three axes (depth, expansion,
18276 // registered) and OTHER smaller on the other three (cache,
18277 // body, arity); neither dominates the other, so BOTH
18278 // directions of the leq relation fail. The pin discriminates
18279 // a regression that promoted `leq` from partial to total
18280 // (e.g. an accidental `||` swap in the conjunction, which
18281 // would make every pair comparable).
18282 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18283 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18284 }
18285
18286 #[test]
18287 fn resource_limits_leq_agrees_with_meet() {
18288 // Lattice cross-check axiom — `a ≤ b ⇔ a ⊓ b = a`. Every
18289 // classification lattice one crate over
18290 // (`tatara-lattice/src/lib.rs`'s preamble) declares this
18291 // agreement axiom binding the partial order to the meet
18292 // combinator, and the default-impl body on
18293 // `tatara_lattice::Lattice::leq` derives `leq` FROM `meet`
18294 // through it. This pin discharges the same axiom on the
18295 // ResourceLimits algebra as a two-direction structural
18296 // equality across the strict-order preset pair AND the
18297 // reflexivity witness.
18298 //
18299 // Forward (a ≤ b ⇒ a ⊓ b = a):
18300 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18301 assert_eq!(
18302 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18303 DEFAULT_RESOURCE_LIMITS,
18304 );
18305 // Reverse (a ⊓ b = a ⇒ a ≤ b), reflexivity witness:
18306 assert_eq!(
18307 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18308 DEFAULT_RESOURCE_LIMITS,
18309 );
18310 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18311
18312 // Non-agreement witness — the incomparable asymmetric postures
18313 // have meet distinct from EITHER operand, and leq false in
18314 // both directions; agreement holds on the negative side too.
18315 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18316 assert_ne!(
18317 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18318 HAND_AUTHORED_MID_POSTURE,
18319 );
18320 }
18321
18322 #[test]
18323 fn resource_limits_leq_agrees_with_join() {
18324 // Lattice cross-check axiom, sibling of the meet-agreement
18325 // pin one COMBINATOR axis over — `a ≤ b ⇔ a ⊔ b = b`.
18326 //
18327 // Forward (a ≤ b ⇒ a ⊔ b = b):
18328 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18329 assert_eq!(
18330 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18331 UNBOUNDED_RESOURCE_LIMITS,
18332 );
18333 // Reverse (a ⊔ b = b ⇒ a ≤ b), reflexivity witness:
18334 assert_eq!(
18335 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18336 DEFAULT_RESOURCE_LIMITS,
18337 );
18338 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18339
18340 // Non-agreement witness — the incomparable asymmetric postures
18341 // have join distinct from EITHER operand.
18342 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18343 assert_ne!(
18344 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18345 HAND_AUTHORED_OTHER_POSTURE,
18346 );
18347 }
18348
18349 #[test]
18350 fn resource_limits_strictest_is_leq_both_operands() {
18351 // Meet-lower-bound pin, typed lattice-relation companion to
18352 // `resource_limits_strictest_is_dominated_by_both_operands_pointwise`
18353 // one PRIMITIVE-KIND axis over — where the per-axis pin
18354 // discharged the six independent `<=` asserts inline, this pin
18355 // routes through the typed `leq` primitive, so the
18356 // greatest-lower-bound property is stated in the LATTICE
18357 // vocabulary (meet-is-leq-both-operands) rather than the
18358 // six-fold field-conjunction vocabulary.
18359 let a = HAND_AUTHORED_MID_POSTURE;
18360 let b = HAND_AUTHORED_OTHER_POSTURE;
18361 let m = a.strictest(b);
18362 assert!(m.leq(a));
18363 assert!(m.leq(b));
18364 }
18365
18366 #[test]
18367 fn resource_limits_most_permissive_is_geq_both_operands() {
18368 // Join-upper-bound pin, typed lattice-relation companion to
18369 // `resource_limits_most_permissive_dominates_both_operands_pointwise`
18370 // one PRIMITIVE-KIND axis over. Stated in the LATTICE
18371 // vocabulary as `a ≤ a ⊔ b ∧ b ≤ a ⊔ b` — sibling of the
18372 // meet-lower-bound pin one COMBINATOR axis over.
18373 let a = HAND_AUTHORED_MID_POSTURE;
18374 let b = HAND_AUTHORED_OTHER_POSTURE;
18375 let j = a.most_permissive(b);
18376 assert!(a.leq(j));
18377 assert!(b.leq(j));
18378 }
18379
18380 #[test]
18381 fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
18382 // Const-fn pin — the leq relation is evaluable in const
18383 // context, so a caller can pin a preset-relation identity at
18384 // compile time. Sibling of the const-fn composition pins on
18385 // `strictest` / `most_permissive` one PRIMITIVE-KIND axis
18386 // over: those two operations compose two presets into a
18387 // const-time third, and this operation decides a const-time
18388 // yes/no on two presets. `const _: ()` at item scope is a
18389 // compile-time proof — the compiler rejects the module if
18390 // either identity does not hold; a runtime `assert!` over the
18391 // same expression would fire at test time on the same value
18392 // clippy's `assertions_on_constants` correctly names as a
18393 // redundant late check on a compile-time constant.
18394 //
18395 // Strict-order preset-pair pin — DEFAULT ≤ UNBOUNDED (every
18396 // `DEFAULT_MAX_*` module constant is at most [`usize::MAX`]):
18397 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18398 // Reverse direction fails — [`usize::MAX`] exceeds every
18399 // concrete `DEFAULT_MAX_*` constant, so the partial order is
18400 // STRICT on the shipped preset pair:
18401 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18402 }
18403
18404 // ── EMPTY_RESOURCE_LIMITS — bounded-lattice bottom preset ─────────
18405
18406 #[test]
18407 fn empty_resource_limits_binds_every_ceiling_to_zero() {
18408 // Field-level pin — the zero preset carries `0` on every axis
18409 // of the six-field surface. Pinned as six independent field
18410 // asserts (rather than one struct equality) so a drift on ONE
18411 // field carries a distinct-name failure that names the drifting
18412 // axis. A future extension that adds a SEVENTH ceiling requires
18413 // an additional field assert here in lockstep with the const
18414 // literal — rustc's field-exhaustiveness on the const forces
18415 // the new field to appear, and this pin exercises the seed
18416 // value. Structural peer of
18417 // `unbounded_resource_limits_binds_every_ceiling_to_usize_max`
18418 // one LATTICE-POLE axis over on the (bottom, top)
18419 // bounded-lattice preset-pair surface.
18420 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
18421 assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
18422 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
18423 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
18424 assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
18425 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
18426 }
18427
18428 #[test]
18429 fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
18430 // Structural-distinctness pin — the two constants
18431 // [`DEFAULT_RESOURCE_LIMITS`] and [`EMPTY_RESOURCE_LIMITS`] sit
18432 // at DISTINCT points on the preset-posture axis, and the
18433 // disagreement is EXHAUSTIVE on the six-field surface — every
18434 // shipped `DEFAULT_MAX_*` module constant is a concrete
18435 // strictly-positive value, so every axis distinguishes DEFAULT
18436 // from the all-zero bottom. Peer of
18437 // `unbounded_resource_limits_disagrees_with_default_on_every_ceiling`
18438 // one LATTICE-POLE axis over.
18439 assert_ne!(
18440 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18441 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
18442 );
18443 assert_ne!(
18444 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18445 DEFAULT_RESOURCE_LIMITS.max_cache_entries
18446 );
18447 assert_ne!(
18448 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18449 DEFAULT_RESOURCE_LIMITS.max_expansion_size
18450 );
18451 assert_ne!(
18452 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18453 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
18454 );
18455 assert_ne!(
18456 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18457 DEFAULT_RESOURCE_LIMITS.max_registered_macros
18458 );
18459 assert_ne!(
18460 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18461 DEFAULT_RESOURCE_LIMITS.max_macro_arity
18462 );
18463 }
18464
18465 #[test]
18466 fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
18467 // Diagonal-distinctness pin — the (BOTTOM, TOP) preset pair
18468 // disagrees on every axis (`0 != usize::MAX` on every field).
18469 // Structural companion to the DEFAULT-vs-EMPTY and
18470 // UNBOUNDED-vs-DEFAULT disagreement pins one PRESET-PAIR axis
18471 // over — together the three pin the six-field surface at the
18472 // full diagonal of the (EMPTY, DEFAULT, UNBOUNDED) triple.
18473 assert_ne!(
18474 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18475 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
18476 );
18477 assert_ne!(
18478 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18479 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
18480 );
18481 assert_ne!(
18482 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18483 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
18484 );
18485 assert_ne!(
18486 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18487 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
18488 );
18489 assert_ne!(
18490 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18491 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
18492 );
18493 assert_ne!(
18494 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18495 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
18496 );
18497 }
18498
18499 #[test]
18500 fn empty_resource_limits_is_the_join_identity() {
18501 // Lattice-identity law — `a ⊔ ⊥ = a`. Every posture's
18502 // pointwise `max` against 0 returns the posture verbatim, so
18503 // [`EMPTY_RESOURCE_LIMITS`] acts as the identity element of
18504 // the join monoid. Peer of
18505 // `empty_resource_limits_is_the_meet_annihilator` one
18506 // COMBINATOR axis over on the (meet, join) surface.
18507 //
18508 // Cross-preset exhaustion — verify the identity against every
18509 // named preset (DEFAULT, UNBOUNDED, both hand-authored
18510 // asymmetric postures) so a regression that broke ONE axis of
18511 // the algebra fires here on the operand whose field crosses
18512 // that axis's boundary.
18513 assert_eq!(
18514 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18515 DEFAULT_RESOURCE_LIMITS,
18516 );
18517 assert_eq!(
18518 EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18519 DEFAULT_RESOURCE_LIMITS,
18520 );
18521 assert_eq!(
18522 UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18523 UNBOUNDED_RESOURCE_LIMITS,
18524 );
18525 assert_eq!(
18526 EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18527 UNBOUNDED_RESOURCE_LIMITS,
18528 );
18529 assert_eq!(
18530 HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18531 HAND_AUTHORED_MID_POSTURE,
18532 );
18533 assert_eq!(
18534 HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18535 HAND_AUTHORED_OTHER_POSTURE,
18536 );
18537 // Idempotence at the identity itself — sibling witness of the
18538 // `most_permissive_is_idempotent` general pin one PRESET axis
18539 // over.
18540 assert_eq!(
18541 EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18542 EMPTY_RESOURCE_LIMITS,
18543 );
18544 }
18545
18546 #[test]
18547 fn empty_resource_limits_is_the_meet_annihilator() {
18548 // Lattice-annihilator law — `a ⊓ ⊥ = ⊥`. Every posture's
18549 // pointwise `min` against 0 returns 0 on every axis, so
18550 // [`EMPTY_RESOURCE_LIMITS`] acts as the annihilator (absorbing
18551 // element) of the meet operation. Peer of
18552 // `empty_resource_limits_is_the_join_identity` one COMBINATOR
18553 // axis over — the identity on one side of the bounded lattice
18554 // is the annihilator on the other side.
18555 assert_eq!(
18556 DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18557 EMPTY_RESOURCE_LIMITS,
18558 );
18559 assert_eq!(
18560 EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18561 EMPTY_RESOURCE_LIMITS,
18562 );
18563 assert_eq!(
18564 UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18565 EMPTY_RESOURCE_LIMITS,
18566 );
18567 assert_eq!(
18568 EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18569 EMPTY_RESOURCE_LIMITS,
18570 );
18571 assert_eq!(
18572 HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18573 EMPTY_RESOURCE_LIMITS,
18574 );
18575 assert_eq!(
18576 HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18577 EMPTY_RESOURCE_LIMITS,
18578 );
18579 }
18580
18581 #[test]
18582 fn empty_resource_limits_is_the_lattice_minimum() {
18583 // Partial-order minimum law — `⊥ ≤ a` for every `a`. Every
18584 // posture's field is a `usize` and `0 <= x` holds for every
18585 // `usize x`, so [`EMPTY_RESOURCE_LIMITS`] sits at or below
18586 // every posture on every axis of the pointwise `<=` conjunction.
18587 // Peer of the UNBOUNDED-is-maximum property on the
18588 // partial-order face one LATTICE-POLE axis over — those two
18589 // pin the (min, max) bounds of the bounded lattice.
18590 assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18591 assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18592 assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18593 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
18594 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
18595 // Reverse direction FAILS on every non-EMPTY preset (each has
18596 // at least one field strictly greater than 0). The partial
18597 // order is STRICT between EMPTY and every other named preset,
18598 // structurally exhausting the diagonal of the
18599 // (EMPTY, DEFAULT, UNBOUNDED, MID, OTHER) 5-preset ordering
18600 // face at the BOTTOM row.
18601 assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18602 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18603 assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18604 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18605 }
18606
18607 #[test]
18608 fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
18609 // Const-fn pin — the bounded-lattice identity + annihilator +
18610 // minimum laws hold at COMPILE time. Sibling of
18611 // `resource_limits_leq_evaluates_at_compile_time_via_const_fn`
18612 // one LATTICE-STRUCTURE axis over — that pin fixed the strict
18613 // order of the (DEFAULT, UNBOUNDED) preset pair at compile
18614 // time; this pin fixes the bounded-lattice pole role of the
18615 // (EMPTY, UNBOUNDED) preset pair. `const _: ()` at item scope
18616 // is a compile-time proof — a regression that broke either
18617 // identity element or the bottom-is-minimum invariant fires at
18618 // rustc time, not test-run time.
18619 //
18620 // Bottom-is-minimum: EMPTY ≤ DEFAULT ≤ UNBOUNDED (the
18621 // three-preset ordering chain on the shipped preset diagonal).
18622 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18623 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18624 // Reverse strictness — neither DEFAULT nor UNBOUNDED sits
18625 // below EMPTY (each has a strictly-positive field).
18626 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18627 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18628 // Join-identity + meet-annihilator laws cross-pinned through
18629 // the `leq` primitive at const time — the round-trip identity
18630 // (`a ⊔ ⊥ = a ⇔ ⊥ ≤ a`, `a ⊓ ⊥ = ⊥ ⇔ ⊥ ≤ a`) means the two
18631 // leq asserts above discharge both algebraic laws by the
18632 // `leq_agrees_with_meet` + `leq_agrees_with_join` lattice
18633 // cross-check axiom already pinned in this cohort.
18634 }
18635
18636 #[test]
18637 fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
18638 // Fold-identity behavioral pin — a slice of postures folded
18639 // through [`ResourceLimits::most_permissive`] with EMPTY as
18640 // the seed produces the pointwise least upper bound across
18641 // the slice on every axis. The join-identity property
18642 // (`a.most_permissive(EMPTY) == a`) guarantees the seed
18643 // contributes nothing to the fold's result — every field's
18644 // pointwise `max` against the seed's `0` returns the field's
18645 // value verbatim, so the fold reduces to the joined-across-
18646 // -operands upper bound. This is the destination usage the
18647 // constant's docstring names as its fold-identity role.
18648 //
18649 // A regression that seeded from ANY non-empty posture (e.g.
18650 // `DEFAULT_RESOURCE_LIMITS`) would inflate the fold's result
18651 // on every axis where the seed's field exceeds the pointwise
18652 // max of the slice — fires as a distinct-axis inequality here.
18653 let postures: [ResourceLimits; 3] = [
18654 HAND_AUTHORED_MID_POSTURE,
18655 HAND_AUTHORED_OTHER_POSTURE,
18656 DEFAULT_RESOURCE_LIMITS,
18657 ];
18658 let joined = postures
18659 .iter()
18660 .copied()
18661 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
18662 // Every operand is `leq` the joined result — the least
18663 // upper bound dominates each contributor on every axis.
18664 assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
18665 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
18666 assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
18667 // Field-level pin — each axis is the pointwise `max` of the
18668 // three sources' values. Six independent field asserts so a
18669 // drift on ONE axis carries a distinct-name failure.
18670 assert_eq!(
18671 joined.max_expansion_depth,
18672 HAND_AUTHORED_MID_POSTURE
18673 .max_expansion_depth
18674 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18675 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18676 );
18677 assert_eq!(
18678 joined.max_cache_entries,
18679 HAND_AUTHORED_MID_POSTURE
18680 .max_cache_entries
18681 .max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18682 .max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18683 );
18684 assert_eq!(
18685 joined.max_expansion_size,
18686 HAND_AUTHORED_MID_POSTURE
18687 .max_expansion_size
18688 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18689 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18690 );
18691 assert_eq!(
18692 joined.max_macro_body_size,
18693 HAND_AUTHORED_MID_POSTURE
18694 .max_macro_body_size
18695 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18696 .max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18697 );
18698 assert_eq!(
18699 joined.max_registered_macros,
18700 HAND_AUTHORED_MID_POSTURE
18701 .max_registered_macros
18702 .max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18703 .max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18704 );
18705 assert_eq!(
18706 joined.max_macro_arity,
18707 HAND_AUTHORED_MID_POSTURE
18708 .max_macro_arity
18709 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18710 .max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18711 );
18712 // Empty-slice edge case — folding over an EMPTY slice returns
18713 // the seed verbatim (the identity element as the null aggregate).
18714 // A wrapper-type approach (`Option<ResourceLimits>` for the
18715 // aggregate) would need a distinct absent branch here; the
18716 // typed identity-element approach resolves it structurally.
18717 let empty_slice: [ResourceLimits; 0] = [];
18718 assert_eq!(
18719 empty_slice
18720 .iter()
18721 .copied()
18722 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18723 EMPTY_RESOURCE_LIMITS,
18724 );
18725 }
18726
18727 #[test]
18728 fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
18729 // Dual fold-identity pin — a slice of postures folded through
18730 // [`ResourceLimits::strictest`] with UNBOUNDED as the seed
18731 // produces the pointwise greatest lower bound across the slice
18732 // on every axis. Peer of the EMPTY-seeds-most_permissive-fold
18733 // pin one COMBINATOR axis over — the identity element for
18734 // meet is the join's ANNIHILATOR (`UNBOUNDED`), and the
18735 // identity element for join is the meet's annihilator
18736 // (`EMPTY`); the two together bind the bounded-lattice's
18737 // aggregation surface at BOTH poles.
18738 let postures: [ResourceLimits; 3] = [
18739 HAND_AUTHORED_MID_POSTURE,
18740 HAND_AUTHORED_OTHER_POSTURE,
18741 DEFAULT_RESOURCE_LIMITS,
18742 ];
18743 let met = postures
18744 .iter()
18745 .copied()
18746 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
18747 // The greatest lower bound sits `leq` every operand.
18748 assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
18749 assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
18750 assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
18751 // Field-level pin — each axis is the pointwise `min` of the
18752 // three sources' values (the UNBOUNDED seed's `usize::MAX`
18753 // never wins the min against a concrete positive value).
18754 assert_eq!(
18755 met.max_expansion_depth,
18756 HAND_AUTHORED_MID_POSTURE
18757 .max_expansion_depth
18758 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18759 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18760 );
18761 assert_eq!(
18762 met.max_cache_entries,
18763 HAND_AUTHORED_MID_POSTURE
18764 .max_cache_entries
18765 .min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18766 .min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18767 );
18768 assert_eq!(
18769 met.max_expansion_size,
18770 HAND_AUTHORED_MID_POSTURE
18771 .max_expansion_size
18772 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18773 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18774 );
18775 assert_eq!(
18776 met.max_macro_body_size,
18777 HAND_AUTHORED_MID_POSTURE
18778 .max_macro_body_size
18779 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18780 .min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18781 );
18782 assert_eq!(
18783 met.max_registered_macros,
18784 HAND_AUTHORED_MID_POSTURE
18785 .max_registered_macros
18786 .min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18787 .min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18788 );
18789 assert_eq!(
18790 met.max_macro_arity,
18791 HAND_AUTHORED_MID_POSTURE
18792 .max_macro_arity
18793 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18794 .min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18795 );
18796 // Empty-slice edge — folding over an empty slice returns the
18797 // UNBOUNDED seed verbatim (the meet-identity as the null
18798 // aggregate). Symmetric to the EMPTY-seed empty-slice pin.
18799 let empty_slice: [ResourceLimits; 0] = [];
18800 assert_eq!(
18801 empty_slice
18802 .iter()
18803 .copied()
18804 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18805 UNBOUNDED_RESOURCE_LIMITS,
18806 );
18807 }
18808
18809 #[test]
18810 fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
18811 // Null-aggregate pin — the N-ary meet over an empty slice is
18812 // the meet-identity element [`UNBOUNDED_RESOURCE_LIMITS`]. The
18813 // typed identity-element approach resolves the empty-input case
18814 // at the algebra layer; a wrapper-type approach
18815 // (`Option<ResourceLimits>` for the aggregate) would need a
18816 // distinct absent branch here, and a panic-on-empty seed-from-
18817 // -first-element approach would abort the caller.
18818 assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
18819 }
18820
18821 #[test]
18822 fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
18823 // Null-aggregate dual pin — the N-ary join over an empty slice
18824 // is the join-identity element [`EMPTY_RESOURCE_LIMITS`]. Peer
18825 // to `strictest_of_empty_slice` one COMBINATOR axis over on the
18826 // (N-ary meet, N-ary join) surface — the identity element for
18827 // join is the meet's ANNIHILATOR and vice versa; the two
18828 // together bind both bounded-lattice poles as null aggregates.
18829 assert_eq!(
18830 ResourceLimits::most_permissive_of(&[]),
18831 EMPTY_RESOURCE_LIMITS,
18832 );
18833 }
18834
18835 #[test]
18836 fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
18837 // Meet-identity absorption pin at N=1 — the N-ary meet of ONE
18838 // input is the input verbatim, since the meet-identity seed
18839 // (`UNBOUNDED_RESOURCE_LIMITS`) satisfies
18840 // `UNBOUNDED.strictest(a) == a` by the pointwise `min` behavior
18841 // against [`usize::MAX`]. This is the identity-law-at-N=1
18842 // pin: the fold's seed does not distort the 1-input case.
18843 assert_eq!(
18844 ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
18845 HAND_AUTHORED_MID_POSTURE,
18846 );
18847 assert_eq!(
18848 ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18849 HAND_AUTHORED_OTHER_POSTURE,
18850 );
18851 assert_eq!(
18852 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
18853 DEFAULT_RESOURCE_LIMITS,
18854 );
18855 }
18856
18857 #[test]
18858 fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
18859 // Join-identity absorption pin at N=1 — dual of
18860 // `strictest_of_single_element`, using the join-identity seed
18861 // (`EMPTY_RESOURCE_LIMITS`) whose per-axis `0` satisfies
18862 // `EMPTY.most_permissive(a) == a` against any non-zero value.
18863 assert_eq!(
18864 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
18865 HAND_AUTHORED_MID_POSTURE,
18866 );
18867 assert_eq!(
18868 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18869 HAND_AUTHORED_OTHER_POSTURE,
18870 );
18871 assert_eq!(
18872 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
18873 DEFAULT_RESOURCE_LIMITS,
18874 );
18875 }
18876
18877 #[test]
18878 fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
18879 // N=2 reduction pin — the N-ary meet on a 2-element slice
18880 // reduces to the pairwise combinator, since
18881 // `UNBOUNDED.strictest(a).strictest(b) == a.strictest(b)`. The
18882 // strictest_of(&[a, b]) call MUST agree with a.strictest(b) on
18883 // every input pair the algebra distinguishes.
18884 assert_eq!(
18885 ResourceLimits::strictest_of(
18886 &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
18887 ),
18888 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18889 );
18890 assert_eq!(
18891 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
18892 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18893 );
18894 }
18895
18896 #[test]
18897 fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
18898 // N=2 reduction dual pin — the N-ary join on a 2-element slice
18899 // reduces to the pairwise combinator.
18900 assert_eq!(
18901 ResourceLimits::most_permissive_of(&[
18902 HAND_AUTHORED_MID_POSTURE,
18903 HAND_AUTHORED_OTHER_POSTURE,
18904 ]),
18905 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18906 );
18907 assert_eq!(
18908 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
18909 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18910 );
18911 }
18912
18913 #[test]
18914 fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
18915 // Method-agrees-with-fold pin — the sole behavioral claim the
18916 // N-ary aggregator makes is that it IS the inline
18917 // `.iter().copied().fold(UNBOUNDED, strictest)` cascade the
18918 // pre-lift caller composed at every consumption site. A drift
18919 // between the method's implementation and the inline fold
18920 // would break this equality on every non-empty slice.
18921 let postures: [ResourceLimits; 3] = [
18922 HAND_AUTHORED_MID_POSTURE,
18923 HAND_AUTHORED_OTHER_POSTURE,
18924 DEFAULT_RESOURCE_LIMITS,
18925 ];
18926 assert_eq!(
18927 ResourceLimits::strictest_of(&postures),
18928 postures
18929 .iter()
18930 .copied()
18931 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18932 );
18933 }
18934
18935 #[test]
18936 fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
18937 // Method-agrees-with-fold dual pin.
18938 let postures: [ResourceLimits; 3] = [
18939 HAND_AUTHORED_MID_POSTURE,
18940 HAND_AUTHORED_OTHER_POSTURE,
18941 DEFAULT_RESOURCE_LIMITS,
18942 ];
18943 assert_eq!(
18944 ResourceLimits::most_permissive_of(&postures),
18945 postures
18946 .iter()
18947 .copied()
18948 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18949 );
18950 }
18951
18952 #[test]
18953 fn resource_limits_strictest_of_is_order_independent() {
18954 // Order-independence pin — the fold inherits commutativity AND
18955 // associativity of the pairwise `strictest`, so any permutation
18956 // of the input slice yields the same aggregate. Reversal is
18957 // the maximal permutation for a 3-element input (indices
18958 // 0,1,2 → 2,1,0); an aggregator that leaked fold order would
18959 // discriminate here.
18960 let forward = ResourceLimits::strictest_of(&[
18961 HAND_AUTHORED_MID_POSTURE,
18962 HAND_AUTHORED_OTHER_POSTURE,
18963 DEFAULT_RESOURCE_LIMITS,
18964 ]);
18965 let reversed = ResourceLimits::strictest_of(&[
18966 DEFAULT_RESOURCE_LIMITS,
18967 HAND_AUTHORED_OTHER_POSTURE,
18968 HAND_AUTHORED_MID_POSTURE,
18969 ]);
18970 assert_eq!(forward, reversed);
18971 }
18972
18973 #[test]
18974 fn resource_limits_most_permissive_of_is_order_independent() {
18975 let forward = ResourceLimits::most_permissive_of(&[
18976 HAND_AUTHORED_MID_POSTURE,
18977 HAND_AUTHORED_OTHER_POSTURE,
18978 DEFAULT_RESOURCE_LIMITS,
18979 ]);
18980 let reversed = ResourceLimits::most_permissive_of(&[
18981 DEFAULT_RESOURCE_LIMITS,
18982 HAND_AUTHORED_OTHER_POSTURE,
18983 HAND_AUTHORED_MID_POSTURE,
18984 ]);
18985 assert_eq!(forward, reversed);
18986 }
18987
18988 #[test]
18989 fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
18990 // Compile-time-composition pin — `strictest_of` is `const fn`,
18991 // so a caller binds an N-ary aggregate to a `pub const` and the
18992 // fold evaluates at compile time. Every `DEFAULT_MAX_*` is
18993 // strictly less than `usize::MAX`, so the meet of `[DEFAULT,
18994 // UNBOUNDED]` picks DEFAULT on every axis — a preset-pair
18995 // identity already pinned pairwise, verified here through the
18996 // N-ary method's const-context evaluation. The N-ary const-fn
18997 // peer of the pairwise const-fn pins on
18998 // [`ResourceLimits::strictest`]; a regression that turned
18999 // `strictest_of` into a runtime `fn` would break the `const`
19000 // binding below at compile time.
19001 const AGGREGATED: ResourceLimits =
19002 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
19003 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19004 }
19005
19006 #[test]
19007 fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
19008 // Compile-time-composition dual pin — every `DEFAULT_MAX_*` is
19009 // strictly greater than `0`, so the join of `[EMPTY, DEFAULT]`
19010 // picks DEFAULT on every axis.
19011 const AGGREGATED: ResourceLimits =
19012 ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
19013 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19014 }
19015
19016 #[test]
19017 fn resource_limits_strictest_of_result_is_leq_every_operand() {
19018 // Lattice-relation pin — the N-ary meet sits `leq` every
19019 // operand. This is the N-ary extension of the pairwise
19020 // `strictest_is_leq_both_operands` pin; the meet aggregate
19021 // dominates from below (the greatest lower bound of the slice
19022 // sits at-or-below each input on every axis).
19023 let postures: [ResourceLimits; 3] = [
19024 HAND_AUTHORED_MID_POSTURE,
19025 HAND_AUTHORED_OTHER_POSTURE,
19026 DEFAULT_RESOURCE_LIMITS,
19027 ];
19028 let met = ResourceLimits::strictest_of(&postures);
19029 for p in postures {
19030 assert!(met.leq(p));
19031 }
19032 }
19033
19034 #[test]
19035 fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
19036 // Lattice-relation dual pin — every operand sits `leq` the
19037 // N-ary join. The join aggregate is the least upper bound of
19038 // the slice.
19039 let postures: [ResourceLimits; 3] = [
19040 HAND_AUTHORED_MID_POSTURE,
19041 HAND_AUTHORED_OTHER_POSTURE,
19042 DEFAULT_RESOURCE_LIMITS,
19043 ];
19044 let joined = ResourceLimits::most_permissive_of(&postures);
19045 for p in postures {
19046 assert!(p.leq(joined));
19047 }
19048 }
19049
19050 #[test]
19051 fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
19052 // Behavioral pin — an expander constructed from the N-ary meet
19053 // of a slice of postures MUST gate at the tightest ceiling
19054 // across the slice on every axis. Peer of
19055 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19056 // one AGGREGATION-ARITY axis over: where the pairwise pin
19057 // composes exactly two postures, this pin composes THREE
19058 // through the N-ary aggregator, confirming the arity extension
19059 // preserves the "meet gates at tightest" behavioral contract.
19060 //
19061 // The slice tightens macro_arity to 1 (the tightest across the
19062 // three postures) via the middle operand; every other axis
19063 // stays at usize::MAX (the outer two operands supply
19064 // usize::MAX; the middle operand's struct-update literal
19065 // preserves usize::MAX everywhere except macro_arity).
19066 let tightened = ResourceLimits::strictest_of(&[
19067 UNBOUNDED_RESOURCE_LIMITS,
19068 ResourceLimits {
19069 max_macro_arity: 1,
19070 ..UNBOUNDED_RESOURCE_LIMITS
19071 },
19072 UNBOUNDED_RESOURCE_LIMITS,
19073 ]);
19074 assert_eq!(tightened.max_macro_arity, 1);
19075 assert_eq!(tightened.max_expansion_depth, usize::MAX);
19076 assert_eq!(tightened.max_cache_entries, usize::MAX);
19077 assert_eq!(tightened.max_expansion_size, usize::MAX);
19078 assert_eq!(tightened.max_macro_body_size, usize::MAX);
19079 assert_eq!(tightened.max_registered_macros, usize::MAX);
19080 let mut e = Expander::with_limits(tightened);
19081 let forms = read("(defmacro two (a b) `,a)").unwrap();
19082 let err = e.expand_program(forms).unwrap_err();
19083 assert!(
19084 matches!(
19085 err,
19086 LispError::MacroArityExceeded {
19087 arity: 2,
19088 limit: 1,
19089 ..
19090 }
19091 ),
19092 "expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
19093 );
19094 }
19095
19096 #[test]
19097 fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
19098 // Behavioral pin — an expander constructed from the meet of
19099 // two postures MUST gate at the tighter of the two ceilings
19100 // on every axis. Build the meet of the ceiling-lifted preset
19101 // and a hand-authored posture whose `max_macro_arity` sits at
19102 // 1; the meet inherits the 1-slot arity ceiling (tighter than
19103 // [`usize::MAX`]) AND the [`usize::MAX`] ceiling on every
19104 // other axis (tighter than the hand-authored posture's small
19105 // values — no, wait: the OTHER posture's small values ARE
19106 // tighter. So the meet inherits 1 on arity and the OTHER
19107 // small values elsewhere.) Confirm the constructed expander
19108 // rejects a 2-arg macro registration with the arity gate.
19109 let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
19110 max_macro_arity: 1,
19111 ..UNBOUNDED_RESOURCE_LIMITS
19112 });
19113 assert_eq!(tightened.max_macro_arity, 1);
19114 // Other five axes remain at `usize::MAX` (both operands carry
19115 // `usize::MAX` on those, so the pointwise `min` is `usize::MAX`).
19116 assert_eq!(tightened.max_expansion_depth, usize::MAX);
19117 assert_eq!(tightened.max_cache_entries, usize::MAX);
19118 assert_eq!(tightened.max_expansion_size, usize::MAX);
19119 assert_eq!(tightened.max_macro_body_size, usize::MAX);
19120 assert_eq!(tightened.max_registered_macros, usize::MAX);
19121 let mut e = Expander::with_limits(tightened);
19122 let forms = read("(defmacro two (a b) `,a)").unwrap();
19123 let err = e.expand_program(forms).unwrap_err();
19124 assert!(
19125 matches!(
19126 err,
19127 LispError::MacroArityExceeded {
19128 arity: 2,
19129 limit: 1,
19130 ..
19131 }
19132 ),
19133 "the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
19134 );
19135 }
19136
19137 // ── ResourceLimits::clamp — bounded-lattice bracket combinator ────
19138
19139 /// A tighter-than-MID lower-bracket witness — every axis strictly
19140 /// smaller than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the
19141 /// pointwise `max(mid, floor) == mid` on every axis (`floor.leq(mid)`
19142 /// holds structurally). Peer of `HAND_AUTHORED_CLAMP_CEILING` one
19143 /// BRACKET-BOUND axis over on the (floor, ceiling) bracket-witness
19144 /// pair.
19145 const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
19146 max_expansion_depth: 2,
19147 max_cache_entries: 3,
19148 max_expansion_size: 5,
19149 max_macro_body_size: 7,
19150 max_registered_macros: 11,
19151 max_macro_arity: 13,
19152 };
19153
19154 /// A looser-than-MID upper-bracket witness — every axis strictly larger
19155 /// than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the pointwise
19156 /// `min(mid, ceiling) == mid` on every axis (`mid.leq(ceiling)` holds
19157 /// structurally). Peer of `HAND_AUTHORED_CLAMP_FLOOR` one BRACKET-
19158 /// BOUND axis over. Together with `HAND_AUTHORED_CLAMP_FLOOR` the two
19159 /// bracket `HAND_AUTHORED_MID_POSTURE` strictly on every axis:
19160 /// `FLOOR.leq(MID) && MID.leq(CEILING) && FLOOR != MID && MID !=
19161 /// CEILING`.
19162 const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
19163 max_expansion_depth: 47,
19164 max_cache_entries: 53,
19165 max_expansion_size: 59,
19166 max_macro_body_size: 61,
19167 max_registered_macros: 67,
19168 max_macro_arity: 71,
19169 };
19170
19171 #[test]
19172 fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
19173 // In-range identity — when the input already sits within the
19174 // `[lower, upper]` bracket (`lower.leq(a) && a.leq(upper)` per-
19175 // axis), the clamp is the identity function: the pointwise
19176 // `max(a, lower) == a` (since `lower ≤ a` per-axis picks `a`),
19177 // then the pointwise `min(a, upper) == a` (since `a ≤ upper`
19178 // per-axis picks `a`). The primary invariant on the "input
19179 // already satisfies the bracket" arm of the three-arm bracket-
19180 // membership surface.
19181 //
19182 // Prerequisite pins — the two hand-authored bounds bracket
19183 // `MID` strictly on every axis (structural check that discharges
19184 // the "already in range" premise). A regression that inflated
19185 // `FLOOR` or deflated `CEILING` past `MID` would fail these
19186 // asserts before the clamp identity was tested, making the
19187 // premise's failure name itself rather than showing up as a
19188 // downstream misleading clamp mismatch.
19189 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19190 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19191 let clamped =
19192 HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19193 assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
19194 }
19195
19196 #[test]
19197 fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
19198 // Below-lower floor — when the input is tighter than the
19199 // bracket floor on every axis (`a.leq(lower)` per-axis), the
19200 // clamp lifts the input up to `lower`: the pointwise `max(a,
19201 // lower) == lower` (since `a ≤ lower` per-axis picks `lower`),
19202 // then the pointwise `min(lower, upper) == lower` (since
19203 // `lower ≤ upper` per-axis picks `lower`). Peer of
19204 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19205 // one BRACKET-POSITION axis over on the (in-range, below-lower,
19206 // above-upper) three-arm bracket-membership surface.
19207 //
19208 // `EMPTY_RESOURCE_LIMITS` sits `leq` every posture (bounded-
19209 // lattice bottom), so `EMPTY.leq(FLOOR)` holds structurally
19210 // for any FLOOR; a clamp of `EMPTY` into `[FLOOR, CEILING]`
19211 // MUST return `FLOOR`.
19212 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
19213 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19214 let clamped =
19215 EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19216 assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
19217 }
19218
19219 #[test]
19220 fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
19221 // Above-upper ceiling — when the input is looser than the
19222 // bracket ceiling on every axis (`upper.leq(a)` per-axis), the
19223 // clamp lowers the input down to `upper`: the pointwise
19224 // `max(a, lower) == a` (since `lower ≤ upper ≤ a` per-axis picks
19225 // `a`), then the pointwise `min(a, upper) == upper` (since
19226 // `upper ≤ a` per-axis picks `upper`). Peer of the
19227 // below-lower arm one BRACKET-POSITION axis over on the three-
19228 // arm bracket-membership surface.
19229 //
19230 // `UNBOUNDED_RESOURCE_LIMITS` sits `geq` every posture (bounded-
19231 // lattice top), so `CEILING.leq(UNBOUNDED)` holds structurally
19232 // for any CEILING; a clamp of `UNBOUNDED` into `[FLOOR, CEILING]`
19233 // MUST return `CEILING`.
19234 assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
19235 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19236 let clamped =
19237 UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19238 assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
19239 }
19240
19241 #[test]
19242 fn resource_limits_clamp_result_sits_within_the_bracket() {
19243 // Bracket-membership contract — the DEFINING invariant on the
19244 // clamp primitive: for every well-formed bracket
19245 // (`lower.leq(upper)`) and every input `a`, the result sits
19246 // within the bracket `lower.leq(a.clamp(lower, upper)) &&
19247 // a.clamp(lower, upper).leq(upper)`. Exercised on ALL FOUR arm
19248 // positions of the bracket-membership surface via the four
19249 // known-preset inputs (below-lower → in-range → above-upper
19250 // → asymmetric-crossing) so a regression that inflated the
19251 // ceiling arm or deflated the floor arm exhausts all four
19252 // input positions and names the failing arm.
19253 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19254 let inputs = [
19255 EMPTY_RESOURCE_LIMITS, // below-lower
19256 HAND_AUTHORED_MID_POSTURE, // in-range
19257 UNBOUNDED_RESOURCE_LIMITS, // above-upper
19258 HAND_AUTHORED_OTHER_POSTURE, // asymmetric (crosses the bracket)
19259 ];
19260 for a in inputs {
19261 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19262 assert!(
19263 HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
19264 "clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
19265 );
19266 assert!(
19267 clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
19268 "clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
19269 );
19270 }
19271 }
19272
19273 #[test]
19274 fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
19275 // Extrema-bracket identity — clamping against the bounded-
19276 // lattice extrema (`EMPTY_RESOURCE_LIMITS` as the leq-minimum,
19277 // `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum) is the
19278 // identity function on every posture: every posture already
19279 // sits within the widest possible bracket. Cross-checks the
19280 // clamp primitive against the bounded-lattice identity elements
19281 // previously lifted; a regression that swapped the two extrema
19282 // in the impl would collapse every input to
19283 // `UNBOUNDED.strictest(EMPTY) == EMPTY` and fail this pin on
19284 // every non-EMPTY input.
19285 for a in [
19286 DEFAULT_RESOURCE_LIMITS,
19287 HAND_AUTHORED_MID_POSTURE,
19288 HAND_AUTHORED_OTHER_POSTURE,
19289 EMPTY_RESOURCE_LIMITS,
19290 UNBOUNDED_RESOURCE_LIMITS,
19291 ] {
19292 assert_eq!(
19293 a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19294 a,
19295 "clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
19296 );
19297 }
19298 }
19299
19300 #[test]
19301 fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
19302 // Degenerate-bracket collapse — a zero-width bracket
19303 // (`lower == upper`) collapses every input to the single point.
19304 // The pointwise `max(a, x)` lifts every axis to at least `x`,
19305 // then the pointwise `min(., x)` clamps every axis to exactly
19306 // `x` regardless of `a`'s per-axis position (since `x ≤ max(a,
19307 // x) ≤ max(x, x) == x` when `a.leq(x)` and `min(max(a, x), x)
19308 // == x` when `x.leq(a)`).
19309 for x in [
19310 HAND_AUTHORED_MID_POSTURE,
19311 HAND_AUTHORED_OTHER_POSTURE,
19312 DEFAULT_RESOURCE_LIMITS,
19313 ] {
19314 for a in [
19315 EMPTY_RESOURCE_LIMITS,
19316 HAND_AUTHORED_MID_POSTURE,
19317 HAND_AUTHORED_OTHER_POSTURE,
19318 UNBOUNDED_RESOURCE_LIMITS,
19319 ] {
19320 assert_eq!(
19321 a.clamp(x, x),
19322 x,
19323 "zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
19324 );
19325 }
19326 }
19327 }
19328
19329 #[test]
19330 fn resource_limits_clamp_is_idempotent() {
19331 // Idempotence — re-applying the same bracket to an already-
19332 // clamped posture returns the same result. The clamp primitive
19333 // is idempotent because its output already sits within the
19334 // `[lower, upper]` bracket (pinned by
19335 // `resource_limits_clamp_result_sits_within_the_bracket`), so
19336 // the second application hits the in-range identity arm. Peer
19337 // of the idempotence pins on `strictest` and `most_permissive`
19338 // one ARITY axis over (2-input primitive → 3-input bracket) on
19339 // the lattice-algebra idempotence-law surface.
19340 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19341 for a in [
19342 EMPTY_RESOURCE_LIMITS,
19343 HAND_AUTHORED_MID_POSTURE,
19344 HAND_AUTHORED_OTHER_POSTURE,
19345 UNBOUNDED_RESOURCE_LIMITS,
19346 ] {
19347 let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19348 let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19349 assert_eq!(
19350 once, twice,
19351 "clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
19352 );
19353 }
19354 }
19355
19356 #[test]
19357 fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
19358 // Const-fn pin — the bracket combinator is evaluable in const
19359 // context, so a caller can pre-clamp a shipped preset against
19360 // an operator-supplied policy bracket at compile time. Sibling
19361 // of the const-fn composition pins on `strictest` /
19362 // `most_permissive` / `strictest_of` / `most_permissive_of`
19363 // one PRIMITIVE-KIND axis over: those four combinators compose
19364 // presets at const time, and this bracket combinator projects
19365 // a preset into a const-time bracket.
19366 //
19367 // A regression to a runtime `fn` here would fail the `pub
19368 // const` binding below at compile time — a stronger guarantee
19369 // than a runtime `assert!` because the const binding is
19370 // evaluated once at compile time, not per test invocation.
19371 const CLAMPED_DEFAULT: ResourceLimits =
19372 DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
19373 assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
19374
19375 // Compile-time bracket-membership pin — const `assert!` on the
19376 // lattice-relation contract discharges the "clamp result sits
19377 // within the bracket" invariant at compile time on the
19378 // (DEFAULT, EMPTY, UNBOUNDED) preset triple.
19379 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
19380 const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
19381 }
19382
19383 #[test]
19384 fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
19385 // Structural-equivalence pin — the sole behavioral claim the
19386 // clamp primitive makes is that it IS the
19387 // `self.most_permissive(lower).strictest(upper)` two-step
19388 // cascade the pre-lift caller composed. A drift between the
19389 // method and the inline cascade would break this equality on
19390 // every input; the pin exhausts the four bracket-position arms
19391 // of the input surface.
19392 for a in [
19393 EMPTY_RESOURCE_LIMITS,
19394 HAND_AUTHORED_MID_POSTURE,
19395 HAND_AUTHORED_OTHER_POSTURE,
19396 UNBOUNDED_RESOURCE_LIMITS,
19397 ] {
19398 let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19399 let via_cascade = a
19400 .most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
19401 .strictest(HAND_AUTHORED_CLAMP_CEILING);
19402 assert_eq!(via_method, via_cascade);
19403 }
19404 }
19405
19406 #[test]
19407 fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
19408 // Behavioral pin — an expander constructed from a preset
19409 // clamped into `[EMPTY, TIGHT_BRACKET]` MUST gate at the
19410 // bracket ceiling's arity axis. Confirms the clamp primitive
19411 // reaches through to the runtime gates that consume
19412 // `ResourceLimits`, so a future consumer that pre-clamps a
19413 // preset against a policy bracket inherits the bracket's
19414 // ceilings at every guard. Peer of
19415 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19416 // one COMBINATOR-ARITY axis over on the (pairwise-meet,
19417 // N-ary-meet, bracket) combinator-arity face.
19418 //
19419 // The bracket ceiling tightens `max_macro_arity` to 1; every
19420 // other axis stays at `usize::MAX` (the CEILING preset carries
19421 // `usize::MAX` on those, and the input UNBOUNDED preset also
19422 // carries `usize::MAX` on every axis — the clamp's `strictest`
19423 // arm picks `usize::MAX` from either side).
19424 let bracket_ceiling = ResourceLimits {
19425 max_macro_arity: 1,
19426 ..UNBOUNDED_RESOURCE_LIMITS
19427 };
19428 let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19429 assert_eq!(clamped.max_macro_arity, 1);
19430 assert_eq!(clamped.max_expansion_depth, usize::MAX);
19431 assert_eq!(clamped.max_cache_entries, usize::MAX);
19432 assert_eq!(clamped.max_expansion_size, usize::MAX);
19433 assert_eq!(clamped.max_macro_body_size, usize::MAX);
19434 assert_eq!(clamped.max_registered_macros, usize::MAX);
19435 let mut e = Expander::with_limits(clamped);
19436 let forms = read("(defmacro two (a b) `,a)").unwrap();
19437 let err = e.expand_program(forms).unwrap_err();
19438 assert!(
19439 matches!(
19440 err,
19441 LispError::MacroArityExceeded {
19442 arity: 2,
19443 limit: 1,
19444 ..
19445 }
19446 ),
19447 "the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
19448 );
19449 }
19450
19451 // ── ResourceLimits::within — bracket-membership predicate ─────────
19452
19453 #[test]
19454 fn resource_limits_within_of_posture_in_range_is_true() {
19455 // In-range identity — when the input already sits within the
19456 // `[lower, upper]` bracket on every axis (`lower.leq(a) &&
19457 // a.leq(upper)`), the predicate returns `true`. Peer of
19458 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19459 // one PRIMITIVE-KIND axis over on the (combinator, predicate)
19460 // face of the bracket-primitive surface.
19461 //
19462 // Prerequisite pins — the two hand-authored bounds bracket
19463 // `MID` strictly on every axis, structurally discharging the
19464 // "already in range" premise.
19465 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19466 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19467 assert!(HAND_AUTHORED_MID_POSTURE
19468 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19469 }
19470
19471 #[test]
19472 fn resource_limits_within_of_posture_below_lower_is_false() {
19473 // Below-lower rejection — when the input is strictly tighter
19474 // than the bracket floor on some axis (`!lower.leq(a)`), the
19475 // predicate returns `false`. `EMPTY_RESOURCE_LIMITS` has every
19476 // axis at `0`, strictly below `HAND_AUTHORED_CLAMP_FLOOR`'s
19477 // per-axis ceilings; so `FLOOR.leq(EMPTY)` fails on every axis,
19478 // making the conjunction reject regardless of `EMPTY`'s relation
19479 // to `CEILING`. Peer of
19480 // `resource_limits_clamp_of_posture_below_lower_returns_lower`
19481 // one PRIMITIVE-KIND axis over.
19482 assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
19483 assert!(
19484 !EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
19485 );
19486 }
19487
19488 #[test]
19489 fn resource_limits_within_of_posture_above_upper_is_false() {
19490 // Above-upper rejection — when the input is strictly looser
19491 // than the bracket ceiling on some axis (`!a.leq(upper)`), the
19492 // predicate returns `false`. `UNBOUNDED_RESOURCE_LIMITS` has
19493 // every axis at `usize::MAX`, strictly above
19494 // `HAND_AUTHORED_CLAMP_CEILING`'s per-axis ceilings; so
19495 // `UNBOUNDED.leq(CEILING)` fails on every axis. Peer of
19496 // `resource_limits_clamp_of_posture_above_upper_returns_upper`
19497 // one PRIMITIVE-KIND axis over.
19498 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
19499 assert!(!UNBOUNDED_RESOURCE_LIMITS
19500 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19501 }
19502
19503 #[test]
19504 fn resource_limits_within_with_lattice_extrema_is_true() {
19505 // Extrema-bracket identity — every posture sits within the
19506 // widest possible bracket `[EMPTY_RESOURCE_LIMITS,
19507 // UNBOUNDED_RESOURCE_LIMITS]`. Cross-checks the predicate
19508 // against the bounded-lattice identity elements (`EMPTY` is
19509 // the leq-minimum, `UNBOUNDED` is the leq-maximum). Peer of
19510 // `resource_limits_clamp_with_lattice_extrema_returns_the_input`
19511 // one PRIMITIVE-KIND axis over — the predicate view of the
19512 // "clamp is the identity" property.
19513 for a in [
19514 DEFAULT_RESOURCE_LIMITS,
19515 HAND_AUTHORED_MID_POSTURE,
19516 HAND_AUTHORED_OTHER_POSTURE,
19517 EMPTY_RESOURCE_LIMITS,
19518 UNBOUNDED_RESOURCE_LIMITS,
19519 ] {
19520 assert!(
19521 a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19522 "every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
19523 );
19524 }
19525 }
19526
19527 #[test]
19528 fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
19529 // Degenerate-bracket collapse — a zero-width bracket admits
19530 // only the single point at the bounds. `a.within(x, x)` holds
19531 // iff `x.leq(a) && a.leq(x)` iff `a == x` by antisymmetry of
19532 // `leq`. Peer of `resource_limits_clamp_with_equal_bounds_returns_the_bound`
19533 // one PRIMITIVE-KIND axis over: the clamp collapses to `x`
19534 // unconditionally, and the predicate distinguishes the "was
19535 // already equal" case from the "would have been clamped" case.
19536 let bounds = [
19537 HAND_AUTHORED_MID_POSTURE,
19538 HAND_AUTHORED_OTHER_POSTURE,
19539 DEFAULT_RESOURCE_LIMITS,
19540 EMPTY_RESOURCE_LIMITS,
19541 UNBOUNDED_RESOURCE_LIMITS,
19542 ];
19543 for x in bounds {
19544 for a in bounds {
19545 assert_eq!(
19546 a.within(x, x),
19547 a == x,
19548 "a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
19549 );
19550 }
19551 }
19552 }
19553
19554 #[test]
19555 fn resource_limits_within_of_self_is_reflexive() {
19556 // Reflexive-bracket identity — every posture sits within the
19557 // zero-width bracket at itself, since `a.leq(a)` holds by
19558 // reflexivity of `leq`. The reflexive-bracket peer of the
19559 // reflexive-leq pin (`resource_limits_leq_is_reflexive`) one
19560 // ARITY axis over (pairwise → three-input).
19561 for a in [
19562 EMPTY_RESOURCE_LIMITS,
19563 DEFAULT_RESOURCE_LIMITS,
19564 HAND_AUTHORED_MID_POSTURE,
19565 HAND_AUTHORED_OTHER_POSTURE,
19566 UNBOUNDED_RESOURCE_LIMITS,
19567 ] {
19568 assert!(
19569 a.within(a, a),
19570 "a.within(a, a) must hold by reflexivity of leq; a={a:?}",
19571 );
19572 }
19573 }
19574
19575 #[test]
19576 fn resource_limits_within_agrees_with_clamp_fixed_point() {
19577 // Clamp fixed-point theorem — `a.within(lower, upper) ⇔
19578 // a.clamp(lower, upper) == a`. The CANONICAL cross-check axiom
19579 // binding the `within` predicate to the `clamp` combinator,
19580 // analogous to the meet-agreement (`a.leq(b) ⇔ a.strictest(b)
19581 // == a`, pinned by `resource_limits_leq_agrees_with_meet`) and
19582 // join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`,
19583 // pinned by `resource_limits_leq_agrees_with_join`) axioms
19584 // binding the `leq` relation to its combinator peers.
19585 //
19586 // Exercised on all four bracket-position arms of the input
19587 // surface (below-lower / in-range / above-upper / asymmetric-
19588 // crossing) so a regression that broke either direction of the
19589 // fixed-point equivalence exhausts every position and names the
19590 // failing arm.
19591 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19592 for a in [
19593 EMPTY_RESOURCE_LIMITS, // below-lower — clamp != a, within false
19594 HAND_AUTHORED_MID_POSTURE, // in-range — clamp == a, within true
19595 UNBOUNDED_RESOURCE_LIMITS, // above-upper — clamp != a, within false
19596 HAND_AUTHORED_OTHER_POSTURE, // asymmetric — clamp != a, within false
19597 ] {
19598 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19599 let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19600 let clamp_fixed = clamped == a;
19601 assert_eq!(
19602 within_holds, clamp_fixed,
19603 "within(lower, upper) must agree with clamp fixed-point; \
19604 a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
19605 );
19606 }
19607 }
19608
19609 #[test]
19610 fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
19611 // Structural-equivalence pin — the sole behavioral claim the
19612 // predicate makes is that it IS the `lower.leq(a) && a.leq(upper)`
19613 // two-primitive conjunction the pre-lift caller composed. A drift
19614 // between the method and the inline conjunction would break this
19615 // equality on some input; the pin exhausts the four bracket-
19616 // position arms of the input surface. Peer of
19617 // `resource_limits_clamp_agrees_with_direct_two_step_cascade`
19618 // one PRIMITIVE-KIND axis over (combinator ↔ predicate).
19619 for a in [
19620 EMPTY_RESOURCE_LIMITS,
19621 HAND_AUTHORED_MID_POSTURE,
19622 HAND_AUTHORED_OTHER_POSTURE,
19623 UNBOUNDED_RESOURCE_LIMITS,
19624 ] {
19625 let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19626 let via_conjunction =
19627 HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
19628 assert_eq!(via_method, via_conjunction, "a={a:?}");
19629 }
19630 }
19631
19632 #[test]
19633 fn resource_limits_within_composes_at_compile_time_via_const_fn() {
19634 // Const-fn pin — the bracket predicate is evaluable in const
19635 // context, so a caller can pin a preset-bracket-membership
19636 // identity at compile time. Sibling of the const-fn evaluability
19637 // pins on `leq` and `clamp` one PRIMITIVE-KIND axis over: the
19638 // relation and combinator both evaluate at const time, and the
19639 // predicate closing the (combinator, predicate) row does too.
19640 //
19641 // A regression to a runtime `fn` here would fail the `const _:
19642 // () = assert!(...)` bindings below at compile time.
19643 const _: () = assert!(
19644 DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
19645 );
19646 const _: () = assert!(
19647 DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
19648 );
19649 const _: () = assert!(
19650 !UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
19651 );
19652 }
19653
19654 #[test]
19655 fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
19656 // Behavioral pin — a caller that CHECKS bracket-membership
19657 // before constructing an expander can safely reach through to
19658 // the runtime gates: if `within` reports true, `clamp` is a
19659 // no-op (by the fixed-point theorem), so the expander's
19660 // ceilings are the input posture's own. Peer of
19661 // `expander_built_at_clamped_preset_gates_at_the_bracket_ceiling`
19662 // one PRIMITIVE-KIND axis over — the predicate view of the
19663 // "clamp reaches through to the runtime gates" property.
19664 //
19665 // The candidate sits at the bracket ceiling (arity=1, other
19666 // axes at usize::MAX), so it MUST sit within the [EMPTY,
19667 // bracket_ceiling] bracket — the maximal within-membership arm
19668 // of the fixed-point theorem. Every other axis at usize::MAX
19669 // clears the register-time gates, so the arity gate at 1 is
19670 // the one that trips on a 2-parameter macro registration.
19671 let bracket_ceiling = ResourceLimits {
19672 max_macro_arity: 1,
19673 ..UNBOUNDED_RESOURCE_LIMITS
19674 };
19675 let candidate = ResourceLimits {
19676 max_macro_arity: 1,
19677 ..UNBOUNDED_RESOURCE_LIMITS
19678 };
19679 assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
19680 // Since candidate sits within the bracket, clamp is the
19681 // identity; the expander inherits candidate's ceilings verbatim.
19682 let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19683 assert_eq!(clamped, candidate);
19684 let mut e = Expander::with_limits(clamped);
19685 let forms = read("(defmacro two (a b) `,a)").unwrap();
19686 let err = e.expand_program(forms).unwrap_err();
19687 assert!(
19688 matches!(
19689 err,
19690 LispError::MacroArityExceeded {
19691 arity: 2,
19692 limit: 1,
19693 ..
19694 }
19695 ),
19696 "the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
19697 );
19698 }
19699
19700 // ── ResourceLimits::is_lower_bound_of / is_upper_bound_of ─────────
19701 // N-ary boolean-conjunction peers of the pairwise `leq` relation,
19702 // closing the (combinator, predicate) row on the N-ary-aggregation
19703 // face of the lattice-algebra surface. See the docstring on
19704 // `ResourceLimits::is_lower_bound_of` for the (pairwise, N-ary,
19705 // bracket) × (combinator, predicate) placement.
19706
19707 #[test]
19708 fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
19709 // Empty-conjunction identity — every posture is trivially a
19710 // lower bound of the empty set. Peer of `strictest_of(&[]) ==
19711 // UNBOUNDED_RESOURCE_LIMITS`'s empty-slice identity one
19712 // PRIMITIVE-KIND axis over: the empty slice aggregates to the
19713 // identity element of the underlying operation (`true` for
19714 // boolean conjunction, `UNBOUNDED` for pointwise `min`).
19715 assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
19716 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19717 assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19718 assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19719 }
19720
19721 #[test]
19722 fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
19723 // Empty-conjunction dual — every posture is trivially an upper
19724 // bound of the empty set. Peer of
19725 // `is_lower_bound_of_empty_slice_is_vacuously_true` one
19726 // COMBINATOR-DIRECTION axis over, AND peer of
19727 // `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
19728 // slice identity one PRIMITIVE-KIND axis over.
19729 assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
19730 assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19731 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19732 assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19733 }
19734
19735 #[test]
19736 fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
19737 // 1-input identity — the singleton predicate reduces to the
19738 // pairwise partial-order relation. The N-ary predicate on a
19739 // 1-element slice is the pairwise `leq` verbatim, mirroring
19740 // `strictest_of(&[a]) == a`'s reduction on the combinator side.
19741 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19742 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19743 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19744 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19745 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19746 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19747 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19748 ];
19749 for (a, b) in cases {
19750 assert_eq!(
19751 a.is_lower_bound_of(&[b]),
19752 a.leq(b),
19753 "1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
19754 );
19755 }
19756 }
19757
19758 #[test]
19759 fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
19760 // 1-input dual identity — the singleton upper-bound predicate
19761 // is the pairwise `leq` with the direction flipped
19762 // (`b.leq(a)`), since `self` sits ABOVE the operand rather than
19763 // BELOW it.
19764 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19765 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19766 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19767 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19768 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19769 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19770 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19771 ];
19772 for (a, b) in cases {
19773 assert_eq!(
19774 a.is_upper_bound_of(&[b]),
19775 b.leq(a),
19776 "1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
19777 );
19778 }
19779 }
19780
19781 #[test]
19782 fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
19783 // Definitional bridge — the N-ary meet is always a lower bound
19784 // of the slice it aggregates. Pins the link between the N-ary
19785 // COMBINATOR (`strictest_of`) and the N-ary PREDICATE
19786 // (`is_lower_bound_of`): the meet is an ELEMENT of the lower-
19787 // bound set the predicate CHARACTERIZES.
19788 let postures: [ResourceLimits; 3] = [
19789 HAND_AUTHORED_MID_POSTURE,
19790 HAND_AUTHORED_OTHER_POSTURE,
19791 DEFAULT_RESOURCE_LIMITS,
19792 ];
19793 let met = ResourceLimits::strictest_of(&postures);
19794 assert!(met.is_lower_bound_of(&postures));
19795 }
19796
19797 #[test]
19798 fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
19799 // Dual definitional bridge — the N-ary join is always an upper
19800 // bound of the slice it aggregates.
19801 let postures: [ResourceLimits; 3] = [
19802 HAND_AUTHORED_MID_POSTURE,
19803 HAND_AUTHORED_OTHER_POSTURE,
19804 DEFAULT_RESOURCE_LIMITS,
19805 ];
19806 let joined = ResourceLimits::most_permissive_of(&postures);
19807 assert!(joined.is_upper_bound_of(&postures));
19808 }
19809
19810 #[test]
19811 fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
19812 // Universal-bottom witness — the lattice bottom is a common
19813 // lower bound of every slice, since `EMPTY.leq(a) == true` for
19814 // every posture by the lattice-bottom axiom (`0 ≤ x` per-axis).
19815 // The bounded-lattice specialization of the general "the meet-
19816 // identity is a lower bound of every set" theorem.
19817 let postures: [ResourceLimits; 4] = [
19818 HAND_AUTHORED_MID_POSTURE,
19819 HAND_AUTHORED_OTHER_POSTURE,
19820 DEFAULT_RESOURCE_LIMITS,
19821 UNBOUNDED_RESOURCE_LIMITS,
19822 ];
19823 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
19824 // The three preset-carried postures each individually — the
19825 // universal-bottom witness holds on the singleton slice too.
19826 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19827 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19828 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19829 }
19830
19831 #[test]
19832 fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
19833 // Universal-top witness — the lattice top is a common upper
19834 // bound of every slice, since `a.leq(UNBOUNDED) == true` for
19835 // every posture by the lattice-top axiom (`x ≤ usize::MAX`
19836 // per-axis).
19837 let postures: [ResourceLimits; 4] = [
19838 HAND_AUTHORED_MID_POSTURE,
19839 HAND_AUTHORED_OTHER_POSTURE,
19840 DEFAULT_RESOURCE_LIMITS,
19841 EMPTY_RESOURCE_LIMITS,
19842 ];
19843 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
19844 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19845 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19846 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19847 }
19848
19849 #[test]
19850 fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
19851 // Any-operand short-circuit — if `self` fails `leq` against ANY
19852 // operand in the slice, the N-ary conjunction rejects. The dual
19853 // of the "meet is a lower bound" witness on the false side:
19854 // moving `self` STRICTLY ABOVE an operand on any axis costs the
19855 // lower-bound property.
19856 //
19857 // MID sits above the join (element-wise) on some axes and below
19858 // on others, so MID is NEITHER a lower bound of {MID, OTHER}
19859 // (fails against OTHER's smaller-axis values) NOR is OTHER a
19860 // lower bound of {MID, OTHER} (fails against MID's smaller-axis
19861 // values).
19862 assert!(!HAND_AUTHORED_MID_POSTURE
19863 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19864 assert!(!HAND_AUTHORED_OTHER_POSTURE
19865 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19866 // A UNIVERSAL rejection — the lattice top is a lower bound
19867 // ONLY of the empty set OR of slices whose every element equals
19868 // UNBOUNDED. Given a strictly-smaller preset in the slice, the
19869 // top's per-axis usize::MAX loses `leq` on every axis whose
19870 // preset value is strictly smaller.
19871 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19872 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19873 }
19874
19875 #[test]
19876 fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
19877 // Dual any-operand short-circuit — if ANY operand fails `leq`
19878 // against `self`, the N-ary conjunction rejects.
19879 assert!(!HAND_AUTHORED_MID_POSTURE
19880 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19881 assert!(!HAND_AUTHORED_OTHER_POSTURE
19882 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19883 // The bottom cannot bound anything from above except the empty
19884 // set OR a slice of EMPTY-only postures.
19885 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19886 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19887 }
19888
19889 #[test]
19890 fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
19891 // Method-vs-scaffold cross-check — the lifted N-ary predicate
19892 // agrees with the pre-lift two-primitive
19893 // `postures.iter().all(|p| self.leq(*p))` scaffolding on every
19894 // preset-carried posture across the shipped bounded-lattice
19895 // extrema AND the two hand-authored asymmetric witnesses. Pins
19896 // the lift is a semantic no-op — post-lift the consumer routes
19897 // through ONE name instead of composing the conjunction inline,
19898 // but the two paths compute the same boolean.
19899 let candidates: [ResourceLimits; 5] = [
19900 EMPTY_RESOURCE_LIMITS,
19901 DEFAULT_RESOURCE_LIMITS,
19902 UNBOUNDED_RESOURCE_LIMITS,
19903 HAND_AUTHORED_MID_POSTURE,
19904 HAND_AUTHORED_OTHER_POSTURE,
19905 ];
19906 let slice: [ResourceLimits; 3] = [
19907 HAND_AUTHORED_MID_POSTURE,
19908 HAND_AUTHORED_OTHER_POSTURE,
19909 DEFAULT_RESOURCE_LIMITS,
19910 ];
19911 for c in candidates {
19912 let direct = slice.iter().all(|p| c.leq(*p));
19913 assert_eq!(
19914 c.is_lower_bound_of(&slice),
19915 direct,
19916 "is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
19917 );
19918 }
19919 }
19920
19921 #[test]
19922 fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
19923 // Dual method-vs-scaffold cross-check.
19924 let candidates: [ResourceLimits; 5] = [
19925 EMPTY_RESOURCE_LIMITS,
19926 DEFAULT_RESOURCE_LIMITS,
19927 UNBOUNDED_RESOURCE_LIMITS,
19928 HAND_AUTHORED_MID_POSTURE,
19929 HAND_AUTHORED_OTHER_POSTURE,
19930 ];
19931 let slice: [ResourceLimits; 3] = [
19932 HAND_AUTHORED_MID_POSTURE,
19933 HAND_AUTHORED_OTHER_POSTURE,
19934 DEFAULT_RESOURCE_LIMITS,
19935 ];
19936 for c in candidates {
19937 let direct = slice.iter().all(|p| p.leq(c));
19938 assert_eq!(
19939 c.is_upper_bound_of(&slice),
19940 direct,
19941 "is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
19942 );
19943 }
19944 }
19945
19946 #[test]
19947 fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
19948 // Const-fn pin — the N-ary lower-bound predicate is evaluable
19949 // in const context, so a caller can pin an N-ary-bound-
19950 // membership identity at compile time. Sibling of the const-fn
19951 // evaluability pins on `leq` and `within` one PRIMITIVE-KIND
19952 // axis over, AND sibling of `strictest_of`'s const-fn pin one
19953 // COMBINATOR-KIND axis over.
19954 //
19955 // A regression to a runtime `fn` here would fail the `const _:
19956 // () = assert!(...)` bindings below at compile time.
19957 const _: () = assert!(EMPTY_RESOURCE_LIMITS
19958 .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
19959 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19960 const _: () =
19961 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19962 }
19963
19964 #[test]
19965 fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
19966 // Const-fn dual pin.
19967 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
19968 .is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
19969 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19970 const _: () =
19971 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19972 }
19973
19974 /// The full canonical preset roster the strict-order pins sweep
19975 /// through — the (bottom, middle, top) bounded-lattice preset
19976 /// triple plus the two hand-authored incomparable asymmetric
19977 /// postures. Every strict-order law below quantifies over this
19978 /// slice, so a regression that breaks strict `<` on any preset
19979 /// pair discriminates at ONE named fixture rather than at a
19980 /// per-test ad-hoc literal.
19981 const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
19982 EMPTY_RESOURCE_LIMITS,
19983 DEFAULT_RESOURCE_LIMITS,
19984 UNBOUNDED_RESOURCE_LIMITS,
19985 HAND_AUTHORED_MID_POSTURE,
19986 HAND_AUTHORED_OTHER_POSTURE,
19987 ];
19988
19989 #[test]
19990 fn resource_limits_lt_is_irreflexive() {
19991 // Strict partial-order law — `a < a == false` for every
19992 // posture. Irreflexivity is the strict-relation axiom peer of
19993 // reflexivity on `leq` one STRICTNESS axis over on the
19994 // partial-order axiom surface; a regression that dropped the
19995 // antisymmetric leg (`!other.leq(self)`) — leaving the body at
19996 // `self.leq(other)` — would fire here on every preset since
19997 // `leq` is reflexive.
19998 for &a in STRICT_ORDER_ROSTER {
19999 assert!(!a.lt(a), "irreflexivity failed on {a:?}");
20000 }
20001 }
20002
20003 #[test]
20004 fn resource_limits_lt_is_asymmetric() {
20005 // Strict partial-order law — `a < b ⇒ ¬(b < a)` for every
20006 // pair. The two directions of the strict relation cannot both
20007 // hold, since `a.lt(b)` implies `!b.leq(a)` and `b.lt(a)`
20008 // requires `b.leq(a)`. Sweeps every ordered pair in the
20009 // canonical preset roster; discriminates a regression that
20010 // dropped the antisymmetric leg in either direction.
20011 for &a in STRICT_ORDER_ROSTER {
20012 for &b in STRICT_ORDER_ROSTER {
20013 if a.lt(b) {
20014 assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
20015 }
20016 }
20017 }
20018 }
20019
20020 #[test]
20021 fn resource_limits_lt_is_transitive() {
20022 // Strict partial-order law — `a < b ∧ b < c ⇒ a < c`. Sweeps
20023 // every ordered triple in the canonical preset roster; each
20024 // consequence is checked whenever both antecedents hold. On
20025 // the (bottom, middle, top) diagonal the canonical witness
20026 // `EMPTY < DEFAULT < UNBOUNDED` composes to
20027 // `EMPTY < UNBOUNDED`, exercising the transitivity chain on
20028 // the bounded-lattice diagonal explicitly; the sweep pins the
20029 // same law on every other transitively-related triple in the
20030 // roster.
20031 for &a in STRICT_ORDER_ROSTER {
20032 for &b in STRICT_ORDER_ROSTER {
20033 for &c in STRICT_ORDER_ROSTER {
20034 if a.lt(b) && b.lt(c) {
20035 assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20036 }
20037 }
20038 }
20039 }
20040 }
20041
20042 #[test]
20043 fn resource_limits_lt_refines_leq() {
20044 // Refinement axiom — `a < b ⇒ a ≤ b`. The strict relation is
20045 // a refinement of the non-strict one; a witness of strict `<`
20046 // is also a witness of `≤`. Sweeps every ordered pair in the
20047 // canonical preset roster.
20048 for &a in STRICT_ORDER_ROSTER {
20049 for &b in STRICT_ORDER_ROSTER {
20050 if a.lt(b) {
20051 assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
20052 }
20053 }
20054 }
20055 }
20056
20057 #[test]
20058 fn resource_limits_lt_agrees_with_leq_minus_equality() {
20059 // Antisymmetry consequence — on ANY antisymmetric partial
20060 // order (which the pointwise `≤` on `ResourceLimits` is),
20061 // `a < b ⇔ a ≤ b ∧ a ≠ b`. Cross-checks the shipped
20062 // antisymmetric-leq encoding against the leq-and-not-equal
20063 // encoding across the full 5×5 preset matrix; a regression to
20064 // a non-antisymmetric preorder encoding would fire here.
20065 for &a in STRICT_ORDER_ROSTER {
20066 for &b in STRICT_ORDER_ROSTER {
20067 let shipped = a.lt(b);
20068 let alt = a.leq(b) && a != b;
20069 assert_eq!(
20070 shipped, alt,
20071 "encoding cross-check failed on ({a:?}, {b:?})",
20072 );
20073 }
20074 }
20075 }
20076
20077 #[test]
20078 fn resource_limits_lt_of_bottom_diagonal_pinned() {
20079 // Concrete-preset pin — the strict chain across the (bottom,
20080 // middle, top) preset triple on the bounded-lattice diagonal.
20081 // Every `DEFAULT_MAX_*` module constant is a concrete positive
20082 // value strictly less than [`usize::MAX`] and strictly greater
20083 // than `0`, so on every axis the three endpoints are strictly
20084 // ordered; the six-axis pointwise conjunction lifts the
20085 // per-axis strict chain to the posture-level strict chain.
20086 //
20087 // Peer of `resource_limits_leq_of_default_and_unbounded_is_a_
20088 // strict_order` one STRICTNESS axis over — where the non-
20089 // strict pin asserted "leq holds one way and not the other"
20090 // via two `leq` calls, this pin asserts the strict-chain
20091 // conclusion via one `lt` call per link and pinpoints the
20092 // three-preset chain on the bounded-lattice diagonal.
20093 assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20094 assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20095 assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20096 }
20097
20098 #[test]
20099 fn resource_limits_lt_rejects_incomparable_postures() {
20100 // Partial-order non-total pin — the two hand-authored
20101 // asymmetric postures have MID smaller on three axes and OTHER
20102 // smaller on the other three, so neither `leq` direction
20103 // holds; strict `<` therefore fails in BOTH directions. The
20104 // pin discriminates a regression that promoted strict `<`
20105 // from partial to total (e.g. treating incomparable pairs as
20106 // strictly `<` in some canonical direction), and pairs with
20107 // `resource_limits_leq_is_not_total_on_asymmetric_postures`
20108 // one STRICTNESS axis over.
20109 assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
20110 assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
20111 }
20112
20113 #[test]
20114 fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
20115 // Direct-encoding cross-check — `a.lt(b) == (a.leq(b) &&
20116 // !b.leq(a))` at every pair by definition. Sibling of the
20117 // `leq_minus_equality` pin one ENCODING axis over: that pin
20118 // routes through the equality-based encoding (an
20119 // antisymmetry consequence), this pin routes through the
20120 // definitional antisymmetric-leq encoding. A regression that
20121 // shipped a different (e.g. axis-wise strict) encoding of
20122 // `lt` diverges from this cross-check on the incomparable
20123 // asymmetric-preset pair.
20124 for &a in STRICT_ORDER_ROSTER {
20125 for &b in STRICT_ORDER_ROSTER {
20126 let shipped = a.lt(b);
20127 let direct = a.leq(b) && !b.leq(a);
20128 assert_eq!(
20129 shipped, direct,
20130 "direct-encoding cross-check failed on ({a:?}, {b:?})",
20131 );
20132 }
20133 }
20134 }
20135
20136 #[test]
20137 fn resource_limits_gt_is_dual_of_lt() {
20138 // Direction-flip pin — `a.gt(b) == b.lt(a)` at every pair. The
20139 // shipped `gt` body delegates to `lt` verbatim so this
20140 // agreement holds definitionally, but the pin discriminates a
20141 // regression that re-implemented `gt` independently and
20142 // dropped the direction flip. Sweeps the full 5×5 preset
20143 // matrix so incomparable + strictly-ordered + equal pairs
20144 // are all exercised.
20145 for &a in STRICT_ORDER_ROSTER {
20146 for &b in STRICT_ORDER_ROSTER {
20147 assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
20148 }
20149 }
20150 }
20151
20152 #[test]
20153 fn resource_limits_gt_of_top_diagonal_pinned() {
20154 // Concrete-preset pin — the strict chain across the (top,
20155 // middle, bottom) preset triple on the bounded-lattice
20156 // diagonal, walked in the "above" direction. Peer of
20157 // `resource_limits_lt_of_bottom_diagonal_pinned` one
20158 // DIRECTION axis over.
20159 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20160 assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20161 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20162 }
20163
20164 #[test]
20165 fn resource_limits_gt_is_irreflexive() {
20166 // Strict partial-order law dual — `a > a == false` for every
20167 // posture. Inherits from the irreflexivity of `lt` via the
20168 // shipped `gt` body's delegation, but pinned explicitly so a
20169 // future non-delegating re-implementation of `gt` still has
20170 // its irreflexivity gated at the type-level test cohort.
20171 for &a in STRICT_ORDER_ROSTER {
20172 assert!(!a.gt(a), "irreflexivity failed on {a:?}");
20173 }
20174 }
20175
20176 #[test]
20177 fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
20178 // Const-fn pin — the strict `<` relation is evaluable in
20179 // const context, so a caller can pin a preset-strict-order
20180 // identity at compile time. Sibling of the const-fn
20181 // evaluability pin on `leq` one STRICTNESS axis over.
20182 //
20183 // A regression to a runtime `fn` here would fail the `const
20184 // _: () = assert!(...)` bindings below at compile time.
20185 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20186 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20187 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20188 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20189 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
20190 }
20191
20192 #[test]
20193 fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
20194 // Const-fn dual pin — sibling of `lt`'s const-fn pin one
20195 // DIRECTION axis over.
20196 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20197 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20198 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20199 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20200 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
20201 }
20202
20203 #[test]
20204 fn resource_limits_geq_is_dual_of_leq() {
20205 // Direction-flip pin — `a.geq(b) == b.leq(a)` at every pair.
20206 // The shipped `geq` body delegates to `leq` verbatim so this
20207 // agreement holds definitionally, but the pin discriminates a
20208 // regression that re-implemented `geq` independently and
20209 // dropped the direction flip. Sweeps the full 5×5 preset
20210 // matrix so incomparable + strictly-ordered + equal pairs are
20211 // all exercised. Peer of `resource_limits_gt_is_dual_of_lt`
20212 // one STRICTNESS axis over on the (below, above) × (non-strict,
20213 // strict) 2×2 pairwise partial-order face.
20214 for &a in STRICT_ORDER_ROSTER {
20215 for &b in STRICT_ORDER_ROSTER {
20216 assert_eq!(
20217 a.geq(b),
20218 b.leq(a),
20219 "geq/leq duality failed on ({a:?}, {b:?})",
20220 );
20221 }
20222 }
20223 }
20224
20225 #[test]
20226 fn resource_limits_geq_is_reflexive() {
20227 // Non-strict partial-order law — `a ≥ a == true` for every
20228 // posture. Reflexivity is the non-strict-relation axiom peer of
20229 // irreflexivity on `gt` one STRICTNESS axis over on the
20230 // partial-order axiom surface; a regression that hardened the
20231 // delegation into a strict `!other.leq(self)` would fire here
20232 // on every preset since the reflexive pair collapses.
20233 for &a in STRICT_ORDER_ROSTER {
20234 assert!(a.geq(a), "reflexivity failed on {a:?}");
20235 }
20236 }
20237
20238 #[test]
20239 fn resource_limits_geq_is_antisymmetric() {
20240 // Non-strict partial-order law — `a ≥ b ∧ b ≥ a ⇒ a == b`.
20241 // Antisymmetry is inherited from `leq` via the shipped `geq`
20242 // body's `other.leq(self)` delegation (which combined with
20243 // `b.geq(a)` — i.e. `a.leq(b)` — gives `leq` antisymmetry on
20244 // the pair). Sweeps the full 5×5 preset matrix; the pin fires
20245 // on any regression that let two DISTINCT postures satisfy
20246 // `geq` in both directions.
20247 for &a in STRICT_ORDER_ROSTER {
20248 for &b in STRICT_ORDER_ROSTER {
20249 if a.geq(b) && b.geq(a) {
20250 assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
20251 }
20252 }
20253 }
20254 }
20255
20256 #[test]
20257 fn resource_limits_geq_is_transitive() {
20258 // Non-strict partial-order law — `a ≥ b ∧ b ≥ c ⇒ a ≥ c`.
20259 // Inherits from the transitivity of `leq` via the shipped
20260 // delegation; sweeps every ordered triple in the canonical
20261 // preset roster. Peer of `resource_limits_lt_is_transitive`
20262 // one STRICTNESS axis over and `resource_limits_leq_is_
20263 // transitive` one DIRECTION axis over.
20264 for &a in STRICT_ORDER_ROSTER {
20265 for &b in STRICT_ORDER_ROSTER {
20266 for &c in STRICT_ORDER_ROSTER {
20267 if a.geq(b) && b.geq(c) {
20268 assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20269 }
20270 }
20271 }
20272 }
20273 }
20274
20275 #[test]
20276 fn resource_limits_geq_refines_gt() {
20277 // Refinement axiom peer — `a > b ⇒ a ≥ b`. The strict
20278 // relation is a refinement of the non-strict one in the
20279 // "above" direction, exactly as `lt` refines `leq` in the
20280 // "below" direction (pinned by
20281 // `resource_limits_lt_refines_leq` one DIRECTION axis over).
20282 // A witness of strict `>` is also a witness of `≥`.
20283 for &a in STRICT_ORDER_ROSTER {
20284 for &b in STRICT_ORDER_ROSTER {
20285 if a.gt(b) {
20286 assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
20287 }
20288 }
20289 }
20290 }
20291
20292 #[test]
20293 fn resource_limits_geq_agrees_with_gt_plus_equality() {
20294 // Antisymmetry consequence peer — on ANY antisymmetric partial
20295 // order, `a ≥ b ⇔ a > b ∨ a == b`. Cross-checks the shipped
20296 // delegation against the strict-or-equal encoding across the
20297 // full 5×5 preset matrix; a regression that shipped a non-
20298 // antisymmetric preorder encoding for `geq` fires here. Peer
20299 // of `resource_limits_lt_agrees_with_leq_minus_equality` one
20300 // DIRECTION-AND-STRICTNESS axis over.
20301 for &a in STRICT_ORDER_ROSTER {
20302 for &b in STRICT_ORDER_ROSTER {
20303 let shipped = a.geq(b);
20304 let alt = a.gt(b) || a == b;
20305 assert_eq!(
20306 shipped, alt,
20307 "encoding cross-check failed on ({a:?}, {b:?})",
20308 );
20309 }
20310 }
20311 }
20312
20313 #[test]
20314 fn resource_limits_geq_of_top_diagonal_pinned() {
20315 // Concrete-preset pin — the non-strict chain across the (top,
20316 // middle, bottom) preset triple on the bounded-lattice
20317 // diagonal, walked in the "above" direction. Peer of
20318 // `resource_limits_gt_of_top_diagonal_pinned` one STRICTNESS
20319 // axis over: the non-strict pin also holds at the reflexive
20320 // diagonal, so every posture sits above ITSELF via `geq` in
20321 // addition to sitting above the strictly-tighter presets.
20322 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20323 assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20324 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20325 assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20326 assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20327 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20328 }
20329
20330 #[test]
20331 fn resource_limits_geq_rejects_incomparable_postures() {
20332 // Partial-order non-total pin — the two hand-authored
20333 // asymmetric postures have MID smaller on three axes and OTHER
20334 // smaller on the other three, so neither `leq` direction holds
20335 // and therefore neither `geq` direction holds. The pin
20336 // discriminates a regression that promoted `geq` from partial
20337 // to total, and pairs with
20338 // `resource_limits_lt_rejects_incomparable_postures` one
20339 // STRICTNESS-AND-DIRECTION axis over.
20340 assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
20341 assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
20342 }
20343
20344 #[test]
20345 fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
20346 // Arity-reduction pin — `a.geq(b) == a.is_upper_bound_of(&[b])`
20347 // at every pair. The pairwise "above" relation is exactly the
20348 // 1-input specialization of the N-ary upper-bound predicate;
20349 // the pin cross-checks that the two named entries agree on
20350 // the single-element case, which discriminates a regression
20351 // that drifted one from the other. Peer of the single-element
20352 // identity docstring on `is_upper_bound_of` one ARITY axis
20353 // over.
20354 for &a in STRICT_ORDER_ROSTER {
20355 for &b in STRICT_ORDER_ROSTER {
20356 assert_eq!(
20357 a.geq(b),
20358 a.is_upper_bound_of(&[b]),
20359 "arity-reduction failed on ({a:?}, {b:?})",
20360 );
20361 }
20362 }
20363 }
20364
20365 #[test]
20366 fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
20367 // Const-fn pin — the non-strict `≥` relation is evaluable in
20368 // const context, so a caller can pin a preset-non-strict-order
20369 // identity at compile time. Sibling of the const-fn evaluability
20370 // pins on `leq` one DIRECTION axis over AND on `gt` one
20371 // STRICTNESS axis over.
20372 //
20373 // A regression to a runtime `fn` here would fail the `const _:
20374 // () = assert!(...)` bindings below at compile time.
20375 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20376 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20377 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20378 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20379 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20380 }
20381
20382 #[test]
20383 fn resource_limits_is_incomparable_is_irreflexive() {
20384 // Antichain-corner reflexivity pin — `a.is_incomparable(a) ==
20385 // false` on every posture. Every posture is COMPARABLE to
20386 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so the
20387 // conjunction's `!self.leq(other)` leg forces the verdict to
20388 // `false` at every diagonal input. Discriminates a regression
20389 // that dropped the `!` on `self.leq(other)` (leaving the body
20390 // at `self.leq(other) && !self.geq(other)`) — that would fold
20391 // the diagonal to `false` on the reflexive `leq` arm but
20392 // silently flip the antichain corner on non-diagonal cells.
20393 for &a in STRICT_ORDER_ROSTER {
20394 assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
20395 }
20396 }
20397
20398 #[test]
20399 fn resource_limits_is_incomparable_is_symmetric() {
20400 // Antichain-corner symmetry pin — `a.is_incomparable(b) ==
20401 // b.is_incomparable(a)` on every pair. The composition
20402 // `!self.leq(other) && !self.geq(other)` folds through the
20403 // `self.leq(other) == other.geq(self)` dual identity on `geq`
20404 // and the conjunction's commutativity into an order-
20405 // independent projection on its two inputs. Sweeps every
20406 // ordered pair in the canonical preset roster;
20407 // discriminates a regression that broke either leg's dual
20408 // identity — one direction would fold to `false` on cells the
20409 // other still folded to `true`.
20410 for &a in STRICT_ORDER_ROSTER {
20411 for &b in STRICT_ORDER_ROSTER {
20412 assert_eq!(
20413 a.is_incomparable(b),
20414 b.is_incomparable(a),
20415 "symmetry failed on ({a:?}, {b:?})",
20416 );
20417 }
20418 }
20419 }
20420
20421 #[test]
20422 fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
20423 // De Morgan dual pin — `a.is_incomparable(b) == !(a.leq(b) ||
20424 // a.geq(b))` on every pair. The antichain corner is the SET-
20425 // COMPLEMENT of the union of the two pointwise-domination
20426 // directions on the (comparable, incomparable) two-cell
20427 // partition of the ordered pair × verdict surface. Sweeps
20428 // every ordered pair in the canonical preset roster;
20429 // discriminates a regression that drifted the antichain
20430 // characterization off the substrate's `!leq && !geq`
20431 // composition (e.g., a shortcut through `!self.leq(other) ||
20432 // !self.geq(other)` — the OR-form falsely reports
20433 // incomparability on non-equal comparable pairs).
20434 for &a in STRICT_ORDER_ROSTER {
20435 for &b in STRICT_ORDER_ROSTER {
20436 let via_named = a.is_incomparable(b);
20437 let via_composition = !(a.leq(b) || a.geq(b));
20438 assert_eq!(
20439 via_named, via_composition,
20440 "De Morgan dual failed on ({a:?}, {b:?})",
20441 );
20442 }
20443 }
20444 }
20445
20446 #[test]
20447 fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
20448 // Two-cell partition pin — on every ordered pair EITHER
20449 // (a.leq(b) || a.geq(b)) OR a.is_incomparable(b), and NEVER
20450 // both. The (comparable, incomparable) partition of the
20451 // ordered pair × verdict surface is exhaustive-and-disjoint —
20452 // a substrate-level EXCLUSIVITY THEOREM the projection pins
20453 // once. Sweeps every ordered pair in the canonical preset
20454 // roster; discriminates a regression that let the two cells
20455 // overlap (e.g., an implementation that reported the antichain
20456 // corner as `true` on the comparable diagonal).
20457 for &a in STRICT_ORDER_ROSTER {
20458 for &b in STRICT_ORDER_ROSTER {
20459 let comparable = a.leq(b) || a.geq(b);
20460 let incomparable = a.is_incomparable(b);
20461 assert!(
20462 comparable != incomparable,
20463 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20464 );
20465 }
20466 }
20467 }
20468
20469 #[test]
20470 fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
20471 // Bottom-pole absorption pin — `a.is_incomparable(
20472 // EMPTY_RESOURCE_LIMITS) == false` on every posture `a`.
20473 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20474 // `EMPTY.leq(a)` holds on every axis (usize zero is the
20475 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20476 // dual identity, which falsifies the `!self.geq(other)` leg.
20477 // Sweeps the canonical preset roster; pairs with the top-
20478 // pole absorption sibling one POLE axis over.
20479 for &a in STRICT_ORDER_ROSTER {
20480 assert!(
20481 !a.is_incomparable(EMPTY_RESOURCE_LIMITS),
20482 "bottom-pole absorption failed on {a:?}",
20483 );
20484 assert!(
20485 !EMPTY_RESOURCE_LIMITS.is_incomparable(a),
20486 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20487 );
20488 }
20489 }
20490
20491 #[test]
20492 fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
20493 // Top-pole absorption pin — `a.is_incomparable(
20494 // UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a`.
20495 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20496 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20497 // usize::MAX` unconditionally), which falsifies the
20498 // `!self.leq(other)` leg. Sweeps the canonical preset
20499 // roster; pairs with the bottom-pole absorption sibling one
20500 // POLE axis over.
20501 for &a in STRICT_ORDER_ROSTER {
20502 assert!(
20503 !a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
20504 "top-pole absorption failed on {a:?}",
20505 );
20506 assert!(
20507 !UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
20508 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20509 );
20510 }
20511 }
20512
20513 #[test]
20514 fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
20515 // Antichain load-bearing arm — the two hand-authored
20516 // asymmetric postures sit on distinct branches (each is
20517 // smaller on three axes and larger on the other three), so
20518 // neither pointwise-domination direction closes and the
20519 // antichain verdict HOLDS in both orderings. Pairs with
20520 // `resource_limits_geq_rejects_incomparable_postures` +
20521 // `resource_limits_lt_rejects_incomparable_postures` one COVER
20522 // axis over: those pin the two comparable-direction predicates
20523 // FALSIFY on this pair; THIS pin confirms the antichain
20524 // verdict HOLDS on the SAME pair — the two are the negation-
20525 // paired sides of the (comparable, incomparable) two-cell
20526 // partition.
20527 assert!(
20528 HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
20529 "antichain arm failed on (MID, OTHER)",
20530 );
20531 assert!(
20532 HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
20533 "antichain arm failed on flipped (OTHER, MID)",
20534 );
20535 }
20536
20537 #[test]
20538 fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
20539 // Shipped preset comparability pin — every ordered pair drawn
20540 // from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset triangle
20541 // is COMPARABLE (in both directions the pairwise partial-order
20542 // relation gives a verdict). The three shipped presets form a
20543 // TOTALLY ORDERED chain on the pointwise partial-order (EMPTY
20544 // <= DEFAULT <= UNBOUNDED on every field), so the antichain
20545 // corner MUST fold to `false` on every ordered pair drawn from
20546 // them. Discriminates a regression that promoted a shipped
20547 // preset off the total-order chain by drifting ONE field
20548 // silently.
20549 const TRIANGLE: &[ResourceLimits] = &[
20550 EMPTY_RESOURCE_LIMITS,
20551 DEFAULT_RESOURCE_LIMITS,
20552 UNBOUNDED_RESOURCE_LIMITS,
20553 ];
20554 for &a in TRIANGLE {
20555 for &b in TRIANGLE {
20556 assert!(
20557 !a.is_incomparable(b),
20558 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20559 );
20560 }
20561 }
20562 }
20563
20564 #[test]
20565 fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
20566 // Const-fn pin — the antichain characterization is evaluable
20567 // in const context, so a caller can pin an antichain-
20568 // disagreement identity at compile time. Sibling of the
20569 // const-fn evaluability pins on `leq` / `geq` / `lt` / `gt`
20570 // one COVER axis over on the pairwise-relation surface.
20571 //
20572 // A regression to a runtime `fn` here would fail the `const _:
20573 // () = assert!(...)` bindings below at compile time.
20574 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20575 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20576 const _: () =
20577 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20578 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20579 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20580 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20581 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20582 }
20583
20584 // ── ResourceLimits::is_comparable ─────────────────────────────────
20585 //
20586 // The COMPARABLE CORNER on the pairwise-relation surface — the
20587 // DIRECT POSITIVE DUAL of `is_incomparable` one COVER-COMPLEMENT
20588 // axis over on the (comparable, incomparable) two-cell partition
20589 // of the ordered pair × verdict surface. See `ResourceLimits::is_comparable`
20590 // for the algebra; the pins below fix each documented property
20591 // exactly once so a regression on any leg fires on a NAMED test
20592 // rather than an ad-hoc downstream break.
20593
20594 #[test]
20595 fn resource_limits_is_comparable_is_reflexive() {
20596 // Comparable-corner reflexivity pin — `a.is_comparable(a) ==
20597 // true` on every posture. Every posture is COMPARABLE to
20598 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so
20599 // the disjunction's `self.leq(other)` leg holds at every
20600 // diagonal input. Sibling of `is_incomparable`'s
20601 // irreflexivity one CELL axis over on the (comparable,
20602 // incomparable) partition — the diagonal MUST fold to `true`
20603 // here iff it folds to `false` there.
20604 for &a in STRICT_ORDER_ROSTER {
20605 assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
20606 }
20607 }
20608
20609 #[test]
20610 fn resource_limits_is_comparable_is_symmetric() {
20611 // Comparable-corner symmetry pin — `a.is_comparable(b) ==
20612 // b.is_comparable(a)` on every pair. The composition
20613 // `self.leq(other) || self.geq(other)` folds through the
20614 // `self.leq(other) == other.geq(self)` dual identity on
20615 // `geq` and the disjunction's commutativity into an order-
20616 // independent projection on its two inputs. Both cells of an
20617 // EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface
20618 // must themselves be symmetric — the two verdicts flip in
20619 // lockstep on argument swap.
20620 for &a in STRICT_ORDER_ROSTER {
20621 for &b in STRICT_ORDER_ROSTER {
20622 assert_eq!(
20623 a.is_comparable(b),
20624 b.is_comparable(a),
20625 "symmetry failed on ({a:?}, {b:?})",
20626 );
20627 }
20628 }
20629 }
20630
20631 #[test]
20632 fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
20633 // De Morgan dual pin — `a.is_comparable(b) ==
20634 // !a.is_incomparable(b)` on every pair. The comparable corner
20635 // is the SET-COMPLEMENT of the antichain corner on the
20636 // (comparable, incomparable) two-cell partition of the
20637 // ordered pair × verdict surface. The DIRECT dual of
20638 // `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
20639 // one CELL axis over: that test pins the antichain corner as
20640 // the negation of the `leq || geq` disjunction; THIS test
20641 // pins the comparability corner as the negation of the
20642 // antichain projection. Together the two pin BOTH SIDES of
20643 // the partition, closing it as a substrate-level
20644 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM.
20645 for &a in STRICT_ORDER_ROSTER {
20646 for &b in STRICT_ORDER_ROSTER {
20647 let via_named = a.is_comparable(b);
20648 let via_negated_dual = !a.is_incomparable(b);
20649 assert_eq!(
20650 via_named, via_negated_dual,
20651 "De Morgan dual failed on ({a:?}, {b:?})",
20652 );
20653 }
20654 }
20655 }
20656
20657 #[test]
20658 fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
20659 // Direct composition pin — `a.is_comparable(b) == a.leq(b) ||
20660 // a.geq(b)` on every pair. Discriminates a regression that
20661 // drifted the comparability body off the substrate's `leq ||
20662 // geq` composition (e.g., a shortcut through `a.leq(b) &&
20663 // a.geq(b)` — the AND-form falsely reports comparability
20664 // only on the equality diagonal, dropping the strict-order
20665 // cells). Anchors the two-primitive delegation as a
20666 // substrate-level identity.
20667 for &a in STRICT_ORDER_ROSTER {
20668 for &b in STRICT_ORDER_ROSTER {
20669 let via_named = a.is_comparable(b);
20670 let via_composition = a.leq(b) || a.geq(b);
20671 assert_eq!(
20672 via_named, via_composition,
20673 "leq||geq composition failed on ({a:?}, {b:?})",
20674 );
20675 }
20676 }
20677 }
20678
20679 #[test]
20680 fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
20681 // Bottom-pole absorption pin — `a.is_comparable(
20682 // EMPTY_RESOURCE_LIMITS) == true` on every posture `a`.
20683 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20684 // `EMPTY.leq(a)` holds on every axis (`usize` zero is the
20685 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20686 // dual identity, which satisfies the `self.geq(other)` leg.
20687 // The DIRECT COMPLEMENT of
20688 // `resource_limits_is_incomparable_folds_false_at_the_bottom_pole`
20689 // one CELL axis over on the SAME preset roster — where the
20690 // antichain verdict FOLDS FALSE at the bottom pole, the
20691 // comparability verdict FOLDS TRUE.
20692 for &a in STRICT_ORDER_ROSTER {
20693 assert!(
20694 a.is_comparable(EMPTY_RESOURCE_LIMITS),
20695 "bottom-pole absorption failed on {a:?}",
20696 );
20697 assert!(
20698 EMPTY_RESOURCE_LIMITS.is_comparable(a),
20699 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20700 );
20701 }
20702 }
20703
20704 #[test]
20705 fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
20706 // Top-pole absorption pin — `a.is_comparable(
20707 // UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a`.
20708 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20709 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20710 // usize::MAX` unconditionally), which satisfies the
20711 // `self.leq(other)` leg. The DIRECT COMPLEMENT of
20712 // `resource_limits_is_incomparable_folds_false_at_the_top_pole`
20713 // one CELL axis over on the SAME preset roster.
20714 for &a in STRICT_ORDER_ROSTER {
20715 assert!(
20716 a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
20717 "top-pole absorption failed on {a:?}",
20718 );
20719 assert!(
20720 UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
20721 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20722 );
20723 }
20724 }
20725
20726 #[test]
20727 fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
20728 // Antichain load-bearing arm — the two hand-authored
20729 // asymmetric postures sit on distinct branches (each is
20730 // smaller on three axes and larger on the other three), so
20731 // NEITHER pointwise-domination direction closes and the
20732 // comparability verdict FALSIFIES in both orderings. The
20733 // DIRECT NEGATION of
20734 // `resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair`
20735 // one CELL axis over on the SAME hand-authored pair — the
20736 // two are the negation-paired sides of the (comparable,
20737 // incomparable) two-cell partition, and pinning BOTH cells'
20738 // verdicts on the SAME load-bearing antichain pair closes
20739 // the partition as a substrate-level EXCLUSIVITY THEOREM.
20740 assert!(
20741 !HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
20742 "antichain arm failed on (MID, OTHER)",
20743 );
20744 assert!(
20745 !HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
20746 "antichain arm failed on flipped (OTHER, MID)",
20747 );
20748 }
20749
20750 #[test]
20751 fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
20752 // Shipped preset comparability pin — every ordered pair
20753 // drawn from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset
20754 // triangle is COMPARABLE (in at least one direction the
20755 // pairwise partial-order relation gives a verdict). The
20756 // three shipped presets form a TOTALLY ORDERED chain on the
20757 // pointwise partial-order (EMPTY <= DEFAULT <= UNBOUNDED on
20758 // every field), so the comparability corner MUST fold to
20759 // `true` on every ordered pair drawn from them. The DIRECT
20760 // COMPLEMENT of
20761 // `resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle`
20762 // one CELL axis over on the SAME triangle. Discriminates a
20763 // regression that promoted a shipped preset off the total-
20764 // order chain by drifting ONE field silently.
20765 const TRIANGLE: &[ResourceLimits] = &[
20766 EMPTY_RESOURCE_LIMITS,
20767 DEFAULT_RESOURCE_LIMITS,
20768 UNBOUNDED_RESOURCE_LIMITS,
20769 ];
20770 for &a in TRIANGLE {
20771 for &b in TRIANGLE {
20772 assert!(
20773 a.is_comparable(b),
20774 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20775 );
20776 }
20777 }
20778 }
20779
20780 #[test]
20781 fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
20782 // Exhaustive-and-disjoint partition pin — on every ordered
20783 // pair EXACTLY ONE of `a.is_comparable(b)` OR
20784 // `a.is_incomparable(b)` holds. The (comparable,
20785 // incomparable) partition of the ordered pair × verdict
20786 // surface is exhaustive-and-disjoint — a substrate-level
20787 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM the two projections
20788 // together pin. Complements
20789 // `resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable`
20790 // one CELL axis over: that test pins the partition against
20791 // the raw `a.leq(b) || a.geq(b)` composition; THIS test pins
20792 // the partition against the NAMED comparability projection —
20793 // a regression that drifted `is_comparable` off the
20794 // composition without breaking the composition-based test
20795 // would still fire here.
20796 for &a in STRICT_ORDER_ROSTER {
20797 for &b in STRICT_ORDER_ROSTER {
20798 let comparable = a.is_comparable(b);
20799 let incomparable = a.is_incomparable(b);
20800 assert!(
20801 comparable != incomparable,
20802 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20803 );
20804 }
20805 }
20806 }
20807
20808 #[test]
20809 fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
20810 // Const-fn pin — the comparability characterization is
20811 // evaluable in const context, so a caller can pin a
20812 // comparability-agreement identity at compile time. Sibling
20813 // of the const-fn evaluability pins on `leq` / `geq` / `lt`
20814 // / `gt` / `is_incomparable` one COVER axis over on the
20815 // pairwise-relation surface.
20816 //
20817 // A regression to a runtime `fn` here would fail the `const _:
20818 // () = assert!(...)` bindings below at compile time.
20819 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20820 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20821 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20822 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20823 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20824 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20825 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20826 }
20827
20828 // ── ResourceLimits::partial_cmp ───────────────────────────────────
20829 //
20830 // The `Option<Ordering>` FULL-VERDICT corner on the pairwise-
20831 // relation surface — the JOINT LIFT of the (comparable, incomparable)
20832 // two-cell partition together with the ordering-direction refinement
20833 // on the comparable arm. See `ResourceLimits::partial_cmp` for the
20834 // algebra; the pins below fix each documented property exactly once
20835 // so a regression on any leg fires on a NAMED test rather than an
20836 // ad-hoc downstream break.
20837
20838 #[test]
20839 fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
20840 // Diagonal-equality pin — `a.partial_cmp(a) ==
20841 // Some(Ordering::Equal)` on every posture. The `leq`
20842 // reflexivity closes both arms of the equality-guarding
20843 // conjunction, so the diagonal cell of the (Less, Equal,
20844 // Greater, None) 4-tile partition pins to the Equal tile.
20845 // Sibling of `is_comparable_is_reflexive` one PROJECTION
20846 // axis over — where the projection folds TRUE at the
20847 // diagonal, THIS method refines the verdict to `Equal`.
20848 for &a in STRICT_ORDER_ROSTER {
20849 assert_eq!(
20850 a.partial_cmp(a),
20851 Some(Ordering::Equal),
20852 "diagonal-equality failed on {a:?}",
20853 );
20854 }
20855 }
20856
20857 #[test]
20858 fn resource_limits_partial_cmp_reverses_under_argument_swap() {
20859 // Antisymmetric-swap pin — `a.partial_cmp(b) ==
20860 // b.partial_cmp(a).map(Ordering::reverse)` on every pair.
20861 // Swapping the argument pair FLIPS the ordering-direction
20862 // verdict (`Less <-> Greater`), fixes the diagonal-equality
20863 // arm, and fixes the antichain arm. Discriminates
20864 // `is_incomparable` / `is_comparable` (both SYMMETRIC on
20865 // argument swap) one POSTURE axis over — the FULL verdict
20866 // carries directional content that inverts under swap where
20867 // the projections do not.
20868 for &a in STRICT_ORDER_ROSTER {
20869 for &b in STRICT_ORDER_ROSTER {
20870 let forward = a.partial_cmp(b);
20871 let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
20872 assert_eq!(
20873 forward, swapped_reversed,
20874 "swap-reverse failed on ({a:?}, {b:?})",
20875 );
20876 }
20877 }
20878 }
20879
20880 #[test]
20881 fn resource_limits_partial_cmp_none_iff_incomparable() {
20882 // Antichain-arm identity pin — `a.partial_cmp(b).is_none()
20883 // == a.is_incomparable(b)` on every pair. The `None` arm of
20884 // the return coincides EXACTLY with the antichain-corner
20885 // projection. Anchors the joint-lift claim: THIS method
20886 // subsumes `is_incomparable` at its `is_none()` projection
20887 // without drifting the antichain characterization.
20888 for &a in STRICT_ORDER_ROSTER {
20889 for &b in STRICT_ORDER_ROSTER {
20890 let via_partial_cmp = a.partial_cmp(b).is_none();
20891 let via_is_incomparable = a.is_incomparable(b);
20892 assert_eq!(
20893 via_partial_cmp, via_is_incomparable,
20894 "none-iff-incomparable failed on ({a:?}, {b:?})",
20895 );
20896 }
20897 }
20898 }
20899
20900 #[test]
20901 fn resource_limits_partial_cmp_some_iff_comparable() {
20902 // Comparability-arm identity pin — `a.partial_cmp(b).is_some()
20903 // == a.is_comparable(b)` on every pair. The `Some(_)` arm of
20904 // the return coincides EXACTLY with the comparability-corner
20905 // projection. The DUAL of the antichain-arm pin one CELL axis
20906 // over on the (comparable, incomparable) partition — together
20907 // the two pins close the JOINT LIFT of both cells at THIS
20908 // method.
20909 for &a in STRICT_ORDER_ROSTER {
20910 for &b in STRICT_ORDER_ROSTER {
20911 let via_partial_cmp = a.partial_cmp(b).is_some();
20912 let via_is_comparable = a.is_comparable(b);
20913 assert_eq!(
20914 via_partial_cmp, via_is_comparable,
20915 "some-iff-comparable failed on ({a:?}, {b:?})",
20916 );
20917 }
20918 }
20919 }
20920
20921 #[test]
20922 fn resource_limits_partial_cmp_less_iff_lt() {
20923 // Strict-less agreement pin — `a.partial_cmp(b) ==
20924 // Some(Ordering::Less)` iff `a.lt(b)`. The strict-less arm of
20925 // the (Less, Equal, Greater, None) 4-tile partition binds to
20926 // the named `lt` companion primitive at exactly one dispatch
20927 // site. A regression that drifted the arm off `lt` (e.g., to
20928 // `leq` — including the equality diagonal) would fire here.
20929 for &a in STRICT_ORDER_ROSTER {
20930 for &b in STRICT_ORDER_ROSTER {
20931 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
20932 let via_lt = a.lt(b);
20933 assert_eq!(
20934 via_partial_cmp, via_lt,
20935 "less-iff-lt failed on ({a:?}, {b:?})",
20936 );
20937 }
20938 }
20939 }
20940
20941 #[test]
20942 fn resource_limits_partial_cmp_greater_iff_gt() {
20943 // Strict-greater agreement pin — `a.partial_cmp(b) ==
20944 // Some(Ordering::Greater)` iff `a.gt(b)`. Dual of the strict-
20945 // less agreement pin one DIRECTION axis over on the (Less,
20946 // Greater) strict-order pair — a regression that dropped the
20947 // asymmetry between the two arms (e.g., collapsed both to
20948 // `Less`) would fire here.
20949 for &a in STRICT_ORDER_ROSTER {
20950 for &b in STRICT_ORDER_ROSTER {
20951 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
20952 let via_gt = a.gt(b);
20953 assert_eq!(
20954 via_partial_cmp, via_gt,
20955 "greater-iff-gt failed on ({a:?}, {b:?})",
20956 );
20957 }
20958 }
20959 }
20960
20961 #[test]
20962 fn resource_limits_partial_cmp_equal_iff_eq() {
20963 // Equality agreement pin — `a.partial_cmp(b) ==
20964 // Some(Ordering::Equal)` iff `a == b`. The equality arm of the
20965 // 4-tile partition binds through the pointwise antisymmetric-
20966 // equality characterization (`a.leq(b) && b.leq(a) → a == b`,
20967 // pinned by `resource_limits_leq_is_antisymmetric`) to the
20968 // `PartialEq::eq` companion. Discriminates a regression that
20969 // drifted the Equal arm off the antisymmetric conjunction.
20970 for &a in STRICT_ORDER_ROSTER {
20971 for &b in STRICT_ORDER_ROSTER {
20972 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
20973 let via_eq = a == b;
20974 assert_eq!(
20975 via_partial_cmp, via_eq,
20976 "equal-iff-eq failed on ({a:?}, {b:?})",
20977 );
20978 }
20979 }
20980 }
20981
20982 #[test]
20983 fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
20984 // Bottom-to-top-pole ordering pin — every strictly-ascending
20985 // pair on the shipped preset chain (EMPTY <= DEFAULT <=
20986 // UNBOUNDED) folds to `Some(Ordering::Less)`. The three
20987 // shipped presets form a TOTALLY ORDERED chain on the
20988 // pointwise partial-order (every DEFAULT_MAX_* is a concrete
20989 // positive value strictly less than usize::MAX), so the
20990 // strict-less arm fires on every ordered pair drawn in
20991 // ascending direction.
20992 assert_eq!(
20993 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20994 Some(Ordering::Less),
20995 );
20996 assert_eq!(
20997 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20998 Some(Ordering::Less),
20999 );
21000 assert_eq!(
21001 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21002 Some(Ordering::Less),
21003 );
21004 }
21005
21006 #[test]
21007 fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
21008 // Top-to-bottom-pole ordering pin — every strictly-descending
21009 // pair on the shipped preset chain folds to
21010 // `Some(Ordering::Greater)`. The DUAL of the ascending pin
21011 // one DIRECTION axis over — together the two pins close the
21012 // (ascending, descending) direction pair on the shipped
21013 // preset chain at the ordering-direction refinement.
21014 assert_eq!(
21015 DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21016 Some(Ordering::Greater),
21017 );
21018 assert_eq!(
21019 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21020 Some(Ordering::Greater),
21021 );
21022 assert_eq!(
21023 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21024 Some(Ordering::Greater),
21025 );
21026 }
21027
21028 #[test]
21029 fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
21030 // Antichain load-bearing arm — the two hand-authored
21031 // asymmetric postures sit on distinct branches (each is
21032 // smaller on three axes and larger on the other three), so
21033 // NEITHER pointwise-domination direction closes and the
21034 // fall-through `None` arm fires in both orderings. The DIRECT
21035 // LIFT of `is_incomparable_holds_on_the_hand_authored_antichain_pair`
21036 // one PROJECTION axis over on the SAME hand-authored pair —
21037 // where the projection folds TRUE, THIS method folds to
21038 // `None`. The two are the negation-paired sides of the
21039 // (Some(_), None) two-cell partition of `Option<Ordering>`.
21040 assert_eq!(
21041 HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
21042 None,
21043 );
21044 assert_eq!(
21045 HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
21046 None,
21047 );
21048 }
21049
21050 #[test]
21051 fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
21052 // Const-fn pin — the full-verdict projection is evaluable in
21053 // const context, so a caller can pin an ordering-direction
21054 // identity at compile time. Sibling of the const-fn
21055 // evaluability pins on `leq` / `geq` / `lt` / `gt` /
21056 // `is_incomparable` / `is_comparable` one COVER axis over on
21057 // the pairwise-relation surface.
21058 //
21059 // `matches!` in const context requires the pattern's variants
21060 // to be const-constructible — `Ordering::Less` / `Equal` /
21061 // `Greater` are plain enum constructors, so the assertions
21062 // below stay inside stable const-fn scope. A regression to a
21063 // runtime `fn` here would fail the `const _: () = assert!(
21064 // matches!(...))` bindings below at compile time.
21065 const _: () = assert!(matches!(
21066 EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21067 Some(Ordering::Equal),
21068 ));
21069 const _: () = assert!(matches!(
21070 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21071 Some(Ordering::Less),
21072 ));
21073 const _: () = assert!(matches!(
21074 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21075 Some(Ordering::Greater),
21076 ));
21077 const _: () = assert!(matches!(
21078 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21079 Some(Ordering::Less),
21080 ));
21081 const _: () = assert!(matches!(
21082 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21083 Some(Ordering::Less),
21084 ));
21085 }
21086
21087 #[test]
21088 fn resource_limits_is_chain_empty_slice_is_vacuously_true() {
21089 // Empty-slice vacuous truth — the empty conjunction is vacuously
21090 // true; the empty slice contains no distinct pairs to reject.
21091 // Peer of `is_lower_bound_of(&[]) == true`'s empty-slice identity
21092 // one PRIMITIVE-KIND axis over on the (leq, comparable) ×
21093 // (pairwise, N-ary) primitive surface.
21094 assert!(ResourceLimits::is_chain(&[]));
21095 }
21096
21097 #[test]
21098 fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
21099 // Empty-slice vacuous truth — the empty conjunction is vacuously
21100 // true; the empty slice contains no distinct pairs to reject.
21101 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21102 // COVER-COMPLEMENT axis over: both cells of the (chain, antichain)
21103 // set-level pair AGREE at the empty slice on the vacuous-truth
21104 // verdict — the two cells coincide at the empty face of the
21105 // set-level verdict surface.
21106 assert!(ResourceLimits::is_antichain(&[]));
21107 }
21108
21109 #[test]
21110 fn resource_limits_is_chain_singleton_is_vacuously_true() {
21111 // Singleton vacuous truth — a one-element slice contains no
21112 // distinct pairs, so the outer-and-inner-index conjunction never
21113 // enters the inner loop and returns `true` vacuously. Pinned on
21114 // every canonical preset AND on both hand-authored asymmetric
21115 // postures so the vacuous-truth verdict holds regardless of the
21116 // singleton element's lattice position.
21117 assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21118 assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
21119 assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
21120 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
21121 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
21122 }
21123
21124 #[test]
21125 fn resource_limits_is_antichain_singleton_is_vacuously_true() {
21126 // Singleton vacuous truth — a one-element slice contains no
21127 // distinct pairs, so the outer-and-inner-index conjunction never
21128 // enters the inner loop and returns `true` vacuously. Peer of
21129 // `is_chain`'s singleton-vacuous-truth identity: both cells AGREE
21130 // at singleton slices — the (chain, antichain) verdict surface
21131 // coincides at every slice with strictly fewer than two distinct
21132 // pairs.
21133 assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21134 assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
21135 assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
21136 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
21137 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
21138 }
21139
21140 #[test]
21141 fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
21142 // Diagonal-duplicate identity — `is_comparable` is REFLEXIVE
21143 // (`a.is_comparable(a) == true`), so the single distinct index
21144 // pair `(0, 1)` binds `a.is_comparable(a) == true` and the
21145 // conjunction holds at every diagonal-duplicate slice.
21146 // Distinguishes it from `is_antichain` one COVER-COMPLEMENT axis
21147 // over, whose diagonal-duplicate verdict is `false` via
21148 // `is_incomparable`'s IRREFLEXIVITY.
21149 assert!(ResourceLimits::is_chain(&[
21150 DEFAULT_RESOURCE_LIMITS,
21151 DEFAULT_RESOURCE_LIMITS,
21152 ]));
21153 assert!(ResourceLimits::is_chain(&[
21154 HAND_AUTHORED_MID_POSTURE,
21155 HAND_AUTHORED_MID_POSTURE,
21156 ]));
21157 }
21158
21159 #[test]
21160 fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
21161 // Diagonal-duplicate rejection — `is_incomparable` is
21162 // IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
21163 // distinct index pair `(0, 1)` binds `a.is_incomparable(a) ==
21164 // false` and the conjunction rejects at every diagonal-duplicate
21165 // slice. The reflexivity/irreflexivity divergence between the
21166 // two pair-level projections PROPAGATES to the set-level
21167 // projections at any slice with a duplicated element — the two
21168 // set-level cells DIVERGE at every diagonal-duplicate slice, the
21169 // mirror of their AGREEMENT at every empty-or-singleton slice
21170 // one CARDINALITY axis over.
21171 assert!(!ResourceLimits::is_antichain(&[
21172 DEFAULT_RESOURCE_LIMITS,
21173 DEFAULT_RESOURCE_LIMITS,
21174 ]));
21175 assert!(!ResourceLimits::is_antichain(&[
21176 HAND_AUTHORED_MID_POSTURE,
21177 HAND_AUTHORED_MID_POSTURE,
21178 ]));
21179 }
21180
21181 #[test]
21182 fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
21183 // Ordered-chain closure — the shipped-preset triple on the
21184 // bounded-lattice diagonal is an ascending chain (`EMPTY <=
21185 // DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among
21186 // the three is comparable. Pinned in every permutation of the
21187 // triple so the doubly-indexed all-pairs walk closes on every
21188 // enumeration of the same chain.
21189 assert!(ResourceLimits::is_chain(&[
21190 EMPTY_RESOURCE_LIMITS,
21191 DEFAULT_RESOURCE_LIMITS,
21192 UNBOUNDED_RESOURCE_LIMITS,
21193 ]));
21194 assert!(ResourceLimits::is_chain(&[
21195 UNBOUNDED_RESOURCE_LIMITS,
21196 DEFAULT_RESOURCE_LIMITS,
21197 EMPTY_RESOURCE_LIMITS,
21198 ]));
21199 assert!(ResourceLimits::is_chain(&[
21200 DEFAULT_RESOURCE_LIMITS,
21201 EMPTY_RESOURCE_LIMITS,
21202 UNBOUNDED_RESOURCE_LIMITS,
21203 ]));
21204 }
21205
21206 #[test]
21207 fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
21208 // Chain rejection — the shipped-preset pair on the bounded-
21209 // lattice diagonal is a strict chain (`EMPTY.lt(DEFAULT)`), so
21210 // the single distinct pair is COMPARABLE and the antichain
21211 // conjunction rejects. The DIRECT FALSIFICATION of
21212 // `is_chain_holds_on_the_shipped_preset_triple` one CELL axis
21213 // over on the SAME lattice diagonal — the two set-level cells
21214 // carry the negation-paired verdict at every strictly-chained
21215 // slice, the mirror of their AGREEMENT at every empty-or-
21216 // singleton slice one CARDINALITY axis over.
21217 assert!(!ResourceLimits::is_antichain(&[
21218 EMPTY_RESOURCE_LIMITS,
21219 DEFAULT_RESOURCE_LIMITS,
21220 ]));
21221 assert!(!ResourceLimits::is_antichain(&[
21222 DEFAULT_RESOURCE_LIMITS,
21223 UNBOUNDED_RESOURCE_LIMITS,
21224 ]));
21225 assert!(!ResourceLimits::is_antichain(&[
21226 EMPTY_RESOURCE_LIMITS,
21227 DEFAULT_RESOURCE_LIMITS,
21228 UNBOUNDED_RESOURCE_LIMITS,
21229 ]));
21230 }
21231
21232 #[test]
21233 fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
21234 // Antichain rejection — the two hand-authored asymmetric
21235 // postures sit on distinct branches (each is smaller on three
21236 // axes and larger on the other three), so neither pointwise-
21237 // domination direction closes and the pair is INCOMPARABLE. The
21238 // chain conjunction rejects at the first incomparable pair.
21239 // Pinned in both orderings so the doubly-indexed pair walk
21240 // rejects regardless of index order (the diagonal-adjacent
21241 // pair-level rejection is symmetric on argument swap).
21242 assert!(!ResourceLimits::is_chain(&[
21243 HAND_AUTHORED_MID_POSTURE,
21244 HAND_AUTHORED_OTHER_POSTURE,
21245 ]));
21246 assert!(!ResourceLimits::is_chain(&[
21247 HAND_AUTHORED_OTHER_POSTURE,
21248 HAND_AUTHORED_MID_POSTURE,
21249 ]));
21250 }
21251
21252 #[test]
21253 fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
21254 // Hand-authored antichain closure — the two hand-authored
21255 // asymmetric postures sit on distinct branches (each is smaller
21256 // on three axes and larger on the other three), so neither
21257 // pointwise-domination direction closes and the pair is
21258 // INCOMPARABLE. The antichain conjunction accepts every distinct
21259 // pair of the slice. DIRECT LIFT of `is_chain`'s antichain
21260 // rejection one CELL axis over on the SAME hand-authored pair —
21261 // pinning BOTH cells' verdicts on the SAME load-bearing
21262 // antichain pair closes the (chain, antichain) two-cell face on
21263 // arity-2 slices as a substrate-level EXCLUSIVITY THEOREM.
21264 assert!(ResourceLimits::is_antichain(&[
21265 HAND_AUTHORED_MID_POSTURE,
21266 HAND_AUTHORED_OTHER_POSTURE,
21267 ]));
21268 assert!(ResourceLimits::is_antichain(&[
21269 HAND_AUTHORED_OTHER_POSTURE,
21270 HAND_AUTHORED_MID_POSTURE,
21271 ]));
21272 }
21273
21274 #[test]
21275 fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
21276 // Mixed-set rejection — both `(DEFAULT, MID)` and `(DEFAULT,
21277 // OTHER)` are comparable pairs (DEFAULT's shipped `DEFAULT_MAX_*`
21278 // per-axis values are all much larger than either hand-authored
21279 // posture's, so DEFAULT dominates both pointwise), but the
21280 // `(MID, OTHER)` pair is the hand-authored INCOMPARABLE pair.
21281 // The set-level chain verdict is stricter than the union of its
21282 // pair-level verdicts — one antichain pair anywhere in the
21283 // slice falsifies the whole projection.
21284 assert!(!ResourceLimits::is_chain(&[
21285 DEFAULT_RESOURCE_LIMITS,
21286 HAND_AUTHORED_MID_POSTURE,
21287 HAND_AUTHORED_OTHER_POSTURE,
21288 ]));
21289 }
21290
21291 #[test]
21292 fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
21293 // Mixed-set rejection — even though the second-and-third pair
21294 // is incomparable (the hand-authored antichain pair), the first
21295 // pair (`DEFAULT`-and-`MID`) is comparable, so the whole-set
21296 // conjunction rejects. Pins the three-cell (chain, antichain,
21297 // mixed) set-level face at the mixed-set cell: the same slice
21298 // both `is_chain` AND `is_antichain` reject at, and the third
21299 // cell of the three-cell face the two set-level projections
21300 // carry the ends of.
21301 assert!(!ResourceLimits::is_antichain(&[
21302 DEFAULT_RESOURCE_LIMITS,
21303 HAND_AUTHORED_MID_POSTURE,
21304 HAND_AUTHORED_OTHER_POSTURE,
21305 ]));
21306 }
21307
21308 #[test]
21309 fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
21310 // De Morgan mirror at arity 2 — on any two-element slice `[a,
21311 // b]` with `a != b`, the set-level (chain, antichain) two-cell
21312 // face reduces to the pair-level (comparable, incomparable)
21313 // two-cell partition and the two cells become MUTUALLY
21314 // EXCLUSIVE (the mixed cell requires ≥3 elements to open). The
21315 // set-level De Morgan mirror of the pair-level identity
21316 // `a.is_comparable(b) == !a.is_incomparable(b)` `is_comparable`
21317 // pins one CARDINALITY axis over.
21318 let presets: [ResourceLimits; 5] = [
21319 EMPTY_RESOURCE_LIMITS,
21320 DEFAULT_RESOURCE_LIMITS,
21321 UNBOUNDED_RESOURCE_LIMITS,
21322 HAND_AUTHORED_MID_POSTURE,
21323 HAND_AUTHORED_OTHER_POSTURE,
21324 ];
21325 for a in presets {
21326 for b in presets {
21327 if a == b {
21328 continue;
21329 }
21330 let pair = [a, b];
21331 let chain = ResourceLimits::is_chain(&pair);
21332 let antichain = ResourceLimits::is_antichain(&pair);
21333 assert_ne!(
21334 chain, antichain,
21335 "distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
21336 );
21337 }
21338 }
21339 }
21340
21341 #[test]
21342 fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
21343 // Empty-and-singleton agreement — the two set-level projections
21344 // carry the SAME vacuous-truth verdict at every slice with
21345 // strictly fewer than two distinct pairs (empty AND singleton).
21346 // Pinned as a substrate-level identity so a future rewrite of
21347 // either projection cannot silently drift from the shared
21348 // vacuous-truth verdict.
21349 assert_eq!(
21350 ResourceLimits::is_chain(&[]),
21351 ResourceLimits::is_antichain(&[]),
21352 );
21353 let presets: [ResourceLimits; 5] = [
21354 EMPTY_RESOURCE_LIMITS,
21355 DEFAULT_RESOURCE_LIMITS,
21356 UNBOUNDED_RESOURCE_LIMITS,
21357 HAND_AUTHORED_MID_POSTURE,
21358 HAND_AUTHORED_OTHER_POSTURE,
21359 ];
21360 for a in presets {
21361 let single = [a];
21362 assert_eq!(
21363 ResourceLimits::is_chain(&single),
21364 ResourceLimits::is_antichain(&single),
21365 "singleton agreement failed on {a:?}",
21366 );
21367 }
21368 }
21369
21370 #[test]
21371 fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
21372 // Const-fn pin — the set-level chain projection is evaluable in
21373 // const context, so a caller can pin a chain-membership
21374 // identity at compile time. Sibling of the const-fn
21375 // evaluability pins on `is_comparable` one CARDINALITY axis
21376 // over and on `is_lower_bound_of` / `is_upper_bound_of` one
21377 // PRIMITIVE-KIND axis over on the N-ary aggregation surface.
21378 const _: () = assert!(ResourceLimits::is_chain(&[]));
21379 const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21380 const _: () = assert!(ResourceLimits::is_chain(&[
21381 EMPTY_RESOURCE_LIMITS,
21382 DEFAULT_RESOURCE_LIMITS,
21383 UNBOUNDED_RESOURCE_LIMITS,
21384 ]));
21385 }
21386
21387 #[test]
21388 fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
21389 // Const-fn pin — the set-level antichain projection is
21390 // evaluable in const context, so a caller can pin an antichain-
21391 // membership identity at compile time. Sibling of the const-fn
21392 // evaluability pins on `is_incomparable` one CARDINALITY axis
21393 // over and on `is_chain` one COVER-COMPLEMENT axis over.
21394 const _: () = assert!(ResourceLimits::is_antichain(&[]));
21395 const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21396 const _: () = assert!(ResourceLimits::is_antichain(&[
21397 HAND_AUTHORED_MID_POSTURE,
21398 HAND_AUTHORED_OTHER_POSTURE,
21399 ]));
21400 }
21401
21402 #[test]
21403 fn resource_limits_is_mixed_empty_slice_is_false() {
21404 // Empty-slice contract — is_chain and is_antichain BOTH return
21405 // `true` vacuously, so `!true && !true == false`. The empty
21406 // slice is NOT mixed. Sibling posture to the vacuous-truth
21407 // verdicts of is_chain + is_antichain one CELL axis over on the
21408 // (chain, antichain, mixed) three-cell face.
21409 assert!(!ResourceLimits::is_mixed(&[]));
21410 }
21411
21412 #[test]
21413 fn resource_limits_is_mixed_singleton_is_false() {
21414 // Singleton contract — a one-element slice contains no distinct
21415 // pairs; is_chain + is_antichain BOTH return `true` vacuously.
21416 // Pinned on every canonical preset AND on both hand-authored
21417 // asymmetric postures so the false-at-singleton verdict holds
21418 // regardless of the singleton element's lattice position.
21419 assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21420 assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
21421 assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
21422 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
21423 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
21424 }
21425
21426 #[test]
21427 fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
21428 // Diagonal-duplicate contract — is_chain returns `true` via
21429 // is_comparable's REFLEXIVITY; is_antichain returns `false` via
21430 // is_incomparable's IRREFLEXIVITY; the conjunction
21431 // `!true && !false == false`. The diagonal-duplicate slice is a
21432 // CHAIN (per the reflexivity arm), not mixed.
21433 assert!(!ResourceLimits::is_mixed(&[
21434 DEFAULT_RESOURCE_LIMITS,
21435 DEFAULT_RESOURCE_LIMITS,
21436 ]));
21437 assert!(!ResourceLimits::is_mixed(&[
21438 HAND_AUTHORED_MID_POSTURE,
21439 HAND_AUTHORED_MID_POSTURE,
21440 ]));
21441 }
21442
21443 #[test]
21444 fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
21445 // Chain rejection — the shipped-preset triple on the bounded-
21446 // lattice diagonal is an ascending chain, is_chain returns
21447 // `true`, and `!true && …` short-circuits to `false`. A PURE
21448 // chain is not mixed. Pinned in every permutation of the triple
21449 // so the projection factors through the ordering-agnostic
21450 // sibling is_chain verdict on the same chain.
21451 assert!(!ResourceLimits::is_mixed(&[
21452 EMPTY_RESOURCE_LIMITS,
21453 DEFAULT_RESOURCE_LIMITS,
21454 UNBOUNDED_RESOURCE_LIMITS,
21455 ]));
21456 assert!(!ResourceLimits::is_mixed(&[
21457 UNBOUNDED_RESOURCE_LIMITS,
21458 DEFAULT_RESOURCE_LIMITS,
21459 EMPTY_RESOURCE_LIMITS,
21460 ]));
21461 }
21462
21463 #[test]
21464 fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
21465 // Antichain rejection — the hand-authored antichain pair binds
21466 // is_antichain to `true`, and `!… && !true == false`. A PURE
21467 // antichain is not mixed. Pinned in both orderings so the
21468 // projection factors through the argument-swap-symmetric
21469 // sibling is_antichain verdict on the same antichain.
21470 assert!(!ResourceLimits::is_mixed(&[
21471 HAND_AUTHORED_MID_POSTURE,
21472 HAND_AUTHORED_OTHER_POSTURE,
21473 ]));
21474 assert!(!ResourceLimits::is_mixed(&[
21475 HAND_AUTHORED_OTHER_POSTURE,
21476 HAND_AUTHORED_MID_POSTURE,
21477 ]));
21478 }
21479
21480 #[test]
21481 fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
21482 ) {
21483 // Mixed-set closure — LOAD-BEARING `true`-arm catch. The
21484 // (DEFAULT, MID) pair is comparable (falsifying is_antichain)
21485 // while the (MID, OTHER) pair is incomparable (falsifying
21486 // is_chain); the conjunction `!false && !false == true`. This
21487 // is the SAME fixture the sibling is_chain + is_antichain BOTH
21488 // reject at — the DISCRIMINATING positive-arm catch for the
21489 // (mixed) cell of the three-cell face the two set-level
21490 // projections carry the ends of.
21491 assert!(ResourceLimits::is_mixed(&[
21492 DEFAULT_RESOURCE_LIMITS,
21493 HAND_AUTHORED_MID_POSTURE,
21494 HAND_AUTHORED_OTHER_POSTURE,
21495 ]));
21496 }
21497
21498 #[test]
21499 fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
21500 ) {
21501 // Trichotomy exhaustiveness at arity ≥2 with pairwise-distinct
21502 // elements — EXACTLY ONE of is_chain, is_antichain, is_mixed
21503 // returns `true` on every pair OR triple drawn from the shipped
21504 // presets whose elements are pairwise distinct. The three cells
21505 // partition the set-level verdict surface at every non-
21506 // degenerate slice. Substrate-level identity: pinning it here
21507 // catches a future rewrite of any of the three projections that
21508 // silently drifts from the partition contract.
21509 let presets: [ResourceLimits; 5] = [
21510 EMPTY_RESOURCE_LIMITS,
21511 DEFAULT_RESOURCE_LIMITS,
21512 UNBOUNDED_RESOURCE_LIMITS,
21513 HAND_AUTHORED_MID_POSTURE,
21514 HAND_AUTHORED_OTHER_POSTURE,
21515 ];
21516 // Arity-2 sweep — the mixed cell CANNOT open at arity 2 (only
21517 // one distinct pair), so is_mixed is always false and the
21518 // partition reduces to the (chain, antichain) two-cell face
21519 // that is_chain + is_antichain already pinned MUTUALLY
21520 // EXCLUSIVE. Cross-check that the three-cell projection
21521 // AGREES with the two-cell projection at arity 2.
21522 for a in presets {
21523 for b in presets {
21524 if a == b {
21525 continue;
21526 }
21527 let pair = [a, b];
21528 let chain = ResourceLimits::is_chain(&pair);
21529 let antichain = ResourceLimits::is_antichain(&pair);
21530 let mixed = ResourceLimits::is_mixed(&pair);
21531 let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21532 assert_eq!(
21533 true_count, 1,
21534 "trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21535 );
21536 assert!(
21537 !mixed,
21538 "is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
21539 );
21540 }
21541 }
21542 // Arity-3 sweep on distinct triples — the mixed cell opens
21543 // whenever the triple carries both a comparable and an
21544 // incomparable pair (e.g. DEFAULT + MID + OTHER above).
21545 for a in presets {
21546 for b in presets {
21547 for c in presets {
21548 if a == b || b == c || a == c {
21549 continue;
21550 }
21551 let triple = [a, b, c];
21552 let chain = ResourceLimits::is_chain(&triple);
21553 let antichain = ResourceLimits::is_antichain(&triple);
21554 let mixed = ResourceLimits::is_mixed(&triple);
21555 let true_count =
21556 usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21557 assert_eq!(
21558 true_count, 1,
21559 "trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21560 );
21561 }
21562 }
21563 }
21564 }
21565
21566 #[test]
21567 fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
21568 // Const-fn pin — the set-level mixed projection is evaluable
21569 // in const context, so a caller can pin a mixed-set identity
21570 // at compile time. Sibling of the const-fn evaluability pins
21571 // on is_chain + is_antichain one CELL axis over.
21572 const _: () = assert!(!ResourceLimits::is_mixed(&[]));
21573 const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21574 const _: () = assert!(ResourceLimits::is_mixed(&[
21575 DEFAULT_RESOURCE_LIMITS,
21576 HAND_AUTHORED_MID_POSTURE,
21577 HAND_AUTHORED_OTHER_POSTURE,
21578 ]));
21579 }
21580
21581 // ── ResourceLimits::is_ascending / ::is_descending — sequence-level ──
21582
21583 #[test]
21584 fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
21585 // Empty-slice vacuous truth — the empty conjunction is vacuously
21586 // true; the empty slice contains no consecutive pairs to reject.
21587 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21588 // CONSECUTIVE-VS-ALL-PAIRS axis over: both cells of the (set-
21589 // level, sequence-level) projection pair AGREE at the empty slice
21590 // on the vacuous-truth verdict.
21591 assert!(ResourceLimits::is_ascending(&[]));
21592 }
21593
21594 #[test]
21595 fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
21596 // Peer of is_ascending's empty-slice identity one PAIR-LEVEL-
21597 // PRIMITIVE axis over: both cells of the (ascending, descending)
21598 // sequence-level pair AGREE at the empty slice.
21599 assert!(ResourceLimits::is_descending(&[]));
21600 }
21601
21602 #[test]
21603 fn resource_limits_is_ascending_singleton_is_vacuously_true() {
21604 // Singleton vacuous truth — a one-element slice contains no
21605 // consecutive pairs, so the walk never enters the loop and
21606 // returns `true` vacuously. Pinned on every canonical preset AND
21607 // on both hand-authored asymmetric postures.
21608 assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21609 assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
21610 assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
21611 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
21612 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
21613 }
21614
21615 #[test]
21616 fn resource_limits_is_descending_singleton_is_vacuously_true() {
21617 assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21618 assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
21619 assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
21620 assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
21621 assert!(ResourceLimits::is_descending(&[
21622 HAND_AUTHORED_OTHER_POSTURE
21623 ]));
21624 }
21625
21626 #[test]
21627 fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
21628 // Diagonal-duplicate identity — `leq` is REFLEXIVE
21629 // (`a.leq(a) == true` via each axis's `<=` reflexivity), so the
21630 // single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
21631 // the conjunction holds at every diagonal-duplicate slice.
21632 // AGREES with is_chain's diagonal-duplicate verdict — the two
21633 // projections diverge only where the ORDER matters, and
21634 // duplicated elements carry no ordering information.
21635 assert!(ResourceLimits::is_ascending(&[
21636 DEFAULT_RESOURCE_LIMITS,
21637 DEFAULT_RESOURCE_LIMITS,
21638 ]));
21639 assert!(ResourceLimits::is_ascending(&[
21640 HAND_AUTHORED_MID_POSTURE,
21641 HAND_AUTHORED_MID_POSTURE,
21642 ]));
21643 }
21644
21645 #[test]
21646 fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
21647 // Diagonal-duplicate identity — `geq` is REFLEXIVE, so the
21648 // single consecutive pair `(0, 1)` binds `a.geq(a) == true`.
21649 // AGREES with is_ascending's diagonal-duplicate verdict — both
21650 // leq and geq carry reflexivity, so the sequence-level (T, T)
21651 // corner opens at every diagonal-duplicate slice, distinct from
21652 // the set-level (T, F) verdict is_chain / is_antichain carry on
21653 // the same slice.
21654 assert!(ResourceLimits::is_descending(&[
21655 DEFAULT_RESOURCE_LIMITS,
21656 DEFAULT_RESOURCE_LIMITS,
21657 ]));
21658 assert!(ResourceLimits::is_descending(&[
21659 HAND_AUTHORED_MID_POSTURE,
21660 HAND_AUTHORED_MID_POSTURE,
21661 ]));
21662 }
21663
21664 #[test]
21665 fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
21666 // Ordered ascending closure — the shipped-preset triple on the
21667 // bounded-lattice diagonal enumerated leq-ascending has
21668 // `EMPTY.leq(DEFAULT) == true` and `DEFAULT.leq(UNBOUNDED) ==
21669 // true`, so every consecutive pair passes the conjunction.
21670 // Pinned on the arity-2 prefix AND on the full arity-3 chain so
21671 // the walk closes at every non-degenerate ascending enumeration
21672 // length.
21673 assert!(ResourceLimits::is_ascending(&[
21674 EMPTY_RESOURCE_LIMITS,
21675 DEFAULT_RESOURCE_LIMITS,
21676 ]));
21677 assert!(ResourceLimits::is_ascending(&[
21678 DEFAULT_RESOURCE_LIMITS,
21679 UNBOUNDED_RESOURCE_LIMITS,
21680 ]));
21681 assert!(ResourceLimits::is_ascending(&[
21682 EMPTY_RESOURCE_LIMITS,
21683 DEFAULT_RESOURCE_LIMITS,
21684 UNBOUNDED_RESOURCE_LIMITS,
21685 ]));
21686 }
21687
21688 #[test]
21689 fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
21690 // Ordered descending closure — the reversed enumeration is a
21691 // geq-monotone descending sequence. The DIRECT MIRROR of
21692 // is_ascending's ascending closure one PERMUTATION axis over.
21693 assert!(ResourceLimits::is_descending(&[
21694 DEFAULT_RESOURCE_LIMITS,
21695 EMPTY_RESOURCE_LIMITS,
21696 ]));
21697 assert!(ResourceLimits::is_descending(&[
21698 UNBOUNDED_RESOURCE_LIMITS,
21699 DEFAULT_RESOURCE_LIMITS,
21700 ]));
21701 assert!(ResourceLimits::is_descending(&[
21702 UNBOUNDED_RESOURCE_LIMITS,
21703 DEFAULT_RESOURCE_LIMITS,
21704 EMPTY_RESOURCE_LIMITS,
21705 ]));
21706 }
21707
21708 #[test]
21709 fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
21710 // Order-sensitivity — the reversed enumeration of the same
21711 // shipped-preset chain has `UNBOUNDED.leq(DEFAULT) == false` at
21712 // the first consecutive pair. is_chain accepts BOTH orderings
21713 // (order-INSENSITIVE); this projection accepts only the leq-
21714 // sorted one — the DISCRIMINATING arm between the sequence-level
21715 // and set-level projections.
21716 assert!(!ResourceLimits::is_ascending(&[
21717 UNBOUNDED_RESOURCE_LIMITS,
21718 DEFAULT_RESOURCE_LIMITS,
21719 EMPTY_RESOURCE_LIMITS,
21720 ]));
21721 }
21722
21723 #[test]
21724 fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
21725 // The direct falsification of is_ascending's ascending closure
21726 // one CELL axis over on the SAME slice.
21727 assert!(!ResourceLimits::is_descending(&[
21728 EMPTY_RESOURCE_LIMITS,
21729 DEFAULT_RESOURCE_LIMITS,
21730 UNBOUNDED_RESOURCE_LIMITS,
21731 ]));
21732 }
21733
21734 #[test]
21735 fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
21736 // Non-monotone chain permutation — [DEFAULT, EMPTY, UNBOUNDED]
21737 // is a permutation of the shipped-preset chain, so it IS a chain
21738 // (is_chain accepts it), but the first consecutive pair binds
21739 // `DEFAULT.leq(EMPTY) == false`, so the sequence-level ascending
21740 // walk rejects. Also is_descending rejects — the third
21741 // consecutive pair binds `EMPTY.geq(UNBOUNDED) == false`. Pins
21742 // the DISCRIMINATING arm between the sequence-level and set-
21743 // level projections at a slice that is a chain but neither
21744 // ascending nor descending.
21745 let slice = [
21746 DEFAULT_RESOURCE_LIMITS,
21747 EMPTY_RESOURCE_LIMITS,
21748 UNBOUNDED_RESOURCE_LIMITS,
21749 ];
21750 assert!(ResourceLimits::is_chain(&slice));
21751 assert!(!ResourceLimits::is_ascending(&slice));
21752 assert!(!ResourceLimits::is_descending(&slice));
21753 }
21754
21755 #[test]
21756 fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
21757 // The two hand-authored asymmetric postures are incomparable
21758 // (neither MID.leq(OTHER) nor OTHER.leq(MID)), so the single
21759 // consecutive pair rejects. Pinned in both orderings — the pair-
21760 // level leq verdict rejects on the antichain pair regardless of
21761 // which side is `self`.
21762 assert!(!ResourceLimits::is_ascending(&[
21763 HAND_AUTHORED_MID_POSTURE,
21764 HAND_AUTHORED_OTHER_POSTURE,
21765 ]));
21766 assert!(!ResourceLimits::is_ascending(&[
21767 HAND_AUTHORED_OTHER_POSTURE,
21768 HAND_AUTHORED_MID_POSTURE,
21769 ]));
21770 }
21771
21772 #[test]
21773 fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
21774 assert!(!ResourceLimits::is_descending(&[
21775 HAND_AUTHORED_MID_POSTURE,
21776 HAND_AUTHORED_OTHER_POSTURE,
21777 ]));
21778 assert!(!ResourceLimits::is_descending(&[
21779 HAND_AUTHORED_OTHER_POSTURE,
21780 HAND_AUTHORED_MID_POSTURE,
21781 ]));
21782 }
21783
21784 #[test]
21785 fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
21786 // Transitivity theorem — for every slice `postures`,
21787 // `is_ascending(postures) == true` implies `is_chain(postures)
21788 // == true`. The pointwise partial order is TRANSITIVE, so a
21789 // consecutive-pair leq-chain closes under transitive composition
21790 // into the all-pairs leq-chain. Substrate-level THEOREM: the
21791 // sequence-level ascending verdict is strictly STRONGER than the
21792 // set-level chain verdict. Same identity for is_descending —
21793 // both sequence-level projections imply the set-level chain
21794 // projection.
21795 let presets: [ResourceLimits; 5] = [
21796 EMPTY_RESOURCE_LIMITS,
21797 DEFAULT_RESOURCE_LIMITS,
21798 UNBOUNDED_RESOURCE_LIMITS,
21799 HAND_AUTHORED_MID_POSTURE,
21800 HAND_AUTHORED_OTHER_POSTURE,
21801 ];
21802 // Every arity-2 slice from the 5×5 preset matrix witnesses the
21803 // implication in both directions of the (ascending, descending)
21804 // pair.
21805 for a in presets {
21806 for b in presets {
21807 let slice = [a, b];
21808 if ResourceLimits::is_ascending(&slice) {
21809 assert!(
21810 ResourceLimits::is_chain(&slice),
21811 "is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
21812 );
21813 }
21814 if ResourceLimits::is_descending(&slice) {
21815 assert!(
21816 ResourceLimits::is_chain(&slice),
21817 "is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
21818 );
21819 }
21820 }
21821 }
21822 // Load-bearing arity-3 witness — the shipped ascending chain
21823 // triple satisfies is_ascending AND is_chain; the descending
21824 // reversal satisfies is_descending AND is_chain.
21825 let asc = [
21826 EMPTY_RESOURCE_LIMITS,
21827 DEFAULT_RESOURCE_LIMITS,
21828 UNBOUNDED_RESOURCE_LIMITS,
21829 ];
21830 assert!(ResourceLimits::is_ascending(&asc));
21831 assert!(ResourceLimits::is_chain(&asc));
21832 let desc = [
21833 UNBOUNDED_RESOURCE_LIMITS,
21834 DEFAULT_RESOURCE_LIMITS,
21835 EMPTY_RESOURCE_LIMITS,
21836 ];
21837 assert!(ResourceLimits::is_descending(&desc));
21838 assert!(ResourceLimits::is_chain(&desc));
21839 }
21840
21841 #[test]
21842 fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
21843 // Empty-and-singleton agreement — the two sequence-level
21844 // projections carry the SAME vacuous-truth verdict at every
21845 // slice with fewer than two consecutive pairs. Pinned as a
21846 // substrate-level identity so a future rewrite of either
21847 // projection cannot silently drift from the shared vacuous-
21848 // truth verdict.
21849 assert_eq!(
21850 ResourceLimits::is_ascending(&[]),
21851 ResourceLimits::is_descending(&[]),
21852 );
21853 let presets: [ResourceLimits; 5] = [
21854 EMPTY_RESOURCE_LIMITS,
21855 DEFAULT_RESOURCE_LIMITS,
21856 UNBOUNDED_RESOURCE_LIMITS,
21857 HAND_AUTHORED_MID_POSTURE,
21858 HAND_AUTHORED_OTHER_POSTURE,
21859 ];
21860 for a in presets {
21861 let single = [a];
21862 assert_eq!(
21863 ResourceLimits::is_ascending(&single),
21864 ResourceLimits::is_descending(&single),
21865 "empty-and-singleton agreement failed on singleton {a:?}",
21866 );
21867 let dup = [a, a];
21868 assert_eq!(
21869 ResourceLimits::is_ascending(&dup),
21870 ResourceLimits::is_descending(&dup),
21871 "diagonal-duplicate agreement failed on {a:?}",
21872 );
21873 }
21874 }
21875
21876 #[test]
21877 fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
21878 // Const-fn pin — the sequence-level ascending projection is
21879 // evaluable in const context, so a caller can pin an ascending-
21880 // sequence identity at compile time.
21881 const _: () = assert!(ResourceLimits::is_ascending(&[]));
21882 const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21883 const _: () = assert!(ResourceLimits::is_ascending(&[
21884 EMPTY_RESOURCE_LIMITS,
21885 DEFAULT_RESOURCE_LIMITS,
21886 UNBOUNDED_RESOURCE_LIMITS,
21887 ]));
21888 const _: () = assert!(!ResourceLimits::is_ascending(&[
21889 UNBOUNDED_RESOURCE_LIMITS,
21890 EMPTY_RESOURCE_LIMITS,
21891 ]));
21892 }
21893
21894 #[test]
21895 fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
21896 // Const-fn pin — sibling of is_ascending one PAIR-LEVEL-
21897 // PRIMITIVE axis over.
21898 const _: () = assert!(ResourceLimits::is_descending(&[]));
21899 const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21900 const _: () = assert!(ResourceLimits::is_descending(&[
21901 UNBOUNDED_RESOURCE_LIMITS,
21902 DEFAULT_RESOURCE_LIMITS,
21903 EMPTY_RESOURCE_LIMITS,
21904 ]));
21905 const _: () = assert!(!ResourceLimits::is_descending(&[
21906 EMPTY_RESOURCE_LIMITS,
21907 UNBOUNDED_RESOURCE_LIMITS,
21908 ]));
21909 }
21910}