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 (see above). The
7392 /// bound that is actually true and worth holding is that the memo must
7393 /// never *cost* more than it saves.
7394 const MAX_OVERHEAD_VS_BYTECODE: f64 = 1.05;
7395 const SAMPLES: usize = 5;
7396
7397 let forms = read(&cache_friendly_workload(10_000)).unwrap();
7398
7399 let best = |mut make: Box<dyn FnMut() -> Expander>| -> Duration {
7400 (0..SAMPLES)
7401 .map(|_| {
7402 let mut e = make();
7403 let f = forms.clone();
7404 let t = Instant::now();
7405 let out = e.expand_program(f).unwrap();
7406 let d = t.elapsed();
7407 std::hint::black_box(out);
7408 d
7409 })
7410 .min()
7411 .expect("SAMPLES > 0")
7412 };
7413
7414 let t_subst = best(Box::new(Expander::new_substitute_only));
7415 let t_byte = best(Box::new(Expander::new_bytecode_no_cache));
7416 let t_cached = best(Box::new(Expander::new));
7417
7418 let vs_subst = t_subst.as_secs_f64() / t_cached.as_secs_f64();
7419 let vs_byte = t_byte.as_secs_f64() / t_cached.as_secs_f64();
7420 eprintln!(
7421 "\n=== macroexpand: 10k calls × 10 unique (macro, args) pairs, \
7422 best of {SAMPLES} ===\n\
7423 substitute only : {t_subst:?}\n\
7424 bytecode no cache : {t_byte:?}\n\
7425 bytecode + cache : {t_cached:?}\n\
7426 speedup : {vs_subst:.2}× vs substitute, {vs_byte:.2}× vs bytecode\n"
7427 );
7428
7429 assert!(
7430 vs_subst >= MARGIN_VS_SUBSTITUTE,
7431 "the bytecode+cache path should beat substitution by \
7432 {MARGIN_VS_SUBSTITUTE}×, got {vs_subst:.2}× \
7433 ({t_cached:?} vs {t_subst:?})"
7434 );
7435 assert!(
7436 vs_byte >= 1.0 / MAX_OVERHEAD_VS_BYTECODE,
7437 "the memo must not cost more than it saves: {vs_byte:.2}× vs \
7438 bytecode-no-cache ({t_cached:?} vs {t_byte:?}). A ratio below 1 \
7439 means maintaining the cache is now pure overhead on this workload."
7440 );
7441 }
7442
7443 #[test]
7444 fn cache_respects_arg_changes() {
7445 // Cache must not return stale results when args differ.
7446 let src = "
7447 (defmacro wrap (x) `(list ,x ,x))
7448 (wrap a)
7449 (wrap b)
7450 (wrap a) ;; same as first — cached hit
7451 ";
7452 let mut e = Expander::new();
7453 let out = e.expand_program(read(src).unwrap()).unwrap();
7454 assert_eq!(out.len(), 3);
7455 assert_eq!(out[0], parse("(list a a)"));
7456 assert_eq!(out[1], parse("(list b b)"));
7457 assert_eq!(out[2], parse("(list a a)"));
7458 // Two distinct args → 2 cache entries.
7459 assert_eq!(e.cache_size(), 2);
7460 }
7461
7462 #[test]
7463 fn clear_cache_empties_memo() {
7464 let mut e = Expander::new();
7465 let out = e
7466 .expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
7467 .unwrap();
7468 assert_eq!(out.len(), 2);
7469 assert_eq!(e.cache_size(), 2);
7470 e.clear_cache();
7471 assert_eq!(e.cache_size(), 0);
7472 }
7473
7474 // ── Unbound template-var: structural variant + did-you-mean hint ──
7475
7476 /// Helper for the unbound-template-var tests — pins the variant shape
7477 /// and carries any error context up to the assert site for legibility.
7478 fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
7479 match err {
7480 LispError::UnboundTemplateVar { prefix, name, hint } => {
7481 (*prefix, name.as_str(), hint.as_deref())
7482 }
7483 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7484 }
7485 }
7486
7487 #[test]
7488 fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
7489 // `,xs` against macro params `[x]` — distance 1, bound 1 — hints `,x`.
7490 // Path: compile_node Unquote (the bytecode-template compile, default
7491 // expander).
7492 let mut e = Expander::new();
7493 let err = e
7494 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7495 .expect_err("unbound template var must error");
7496 let (prefix, name, hint) = unbound_var(&err);
7497 assert_eq!(prefix, UnquoteForm::Unquote);
7498 assert_eq!(name, "xs");
7499 assert_eq!(hint, Some("x"));
7500 }
7501
7502 #[test]
7503 fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
7504 // `,@argz` against macro params `[args]` — distance 1, bound 2 —
7505 // hints `,@args`. Path: compile_node UnquoteSplice.
7506 let mut e = Expander::new();
7507 let err = e
7508 .expand_program(
7509 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7510 )
7511 .expect_err("unbound splice must error");
7512 let (prefix, name, hint) = unbound_var(&err);
7513 assert_eq!(prefix, UnquoteForm::Splice);
7514 assert_eq!(name, "argz");
7515 assert_eq!(hint, Some("args"));
7516 }
7517
7518 #[test]
7519 fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
7520 // Same shape but routed through the substitute-only expander — proves
7521 // the substitute path emits the same variant as the bytecode path.
7522 let mut e = Expander::new_substitute_only();
7523 let err = e
7524 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7525 .expect_err("substitute unbound must error");
7526 let (prefix, name, hint) = unbound_var(&err);
7527 assert_eq!(prefix, UnquoteForm::Unquote);
7528 assert_eq!(name, "xs");
7529 assert_eq!(hint, Some("x"));
7530 }
7531
7532 #[test]
7533 fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
7534 // The substitute path's UnquoteSplice branch fires for splices that
7535 // appear inside a list during the recursive walk. `,@argz` against
7536 // `[args]` hints `,@args`.
7537 let mut e = Expander::new_substitute_only();
7538 let err = e
7539 .expand_program(
7540 read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
7541 )
7542 .expect_err("substitute splice unbound must error");
7543 let (prefix, name, hint) = unbound_var(&err);
7544 assert_eq!(prefix, UnquoteForm::Splice);
7545 assert_eq!(name, "argz");
7546 assert_eq!(hint, Some("args"));
7547 }
7548
7549 #[test]
7550 fn unbound_template_var_omits_hint_when_no_close_match() {
7551 // `,wholly-unrelated` against `[x]` — far past the bound, so no
7552 // hint. Negative control: a wrong hint is worse than no hint, so
7553 // the slot must stay empty when the substrate isn't confident.
7554 let mut e = Expander::new();
7555 let err = e
7556 .expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
7557 .expect_err("unrelated unbound must error");
7558 let (prefix, name, hint) = unbound_var(&err);
7559 assert_eq!(prefix, UnquoteForm::Unquote);
7560 assert_eq!(name, "wholly-unrelated");
7561 assert_eq!(hint, None);
7562 }
7563
7564 #[test]
7565 fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
7566 // End-to-end through the Display impl — pins the rendered diagnostic
7567 // a downstream tool sees today (REPL, tatara-check). Hint stays
7568 // additive: the legacy `"unbound"` substring still appears, so any
7569 // assertion that pattern-matches on it keeps passing.
7570 let mut e = Expander::new();
7571 let err = e
7572 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7573 .expect_err("unbound must error");
7574 let msg = format!("{err}");
7575 assert!(
7576 msg.contains("did you mean ,x?"),
7577 "expected hint suffix in message, got: {msg}"
7578 );
7579 assert!(
7580 msg.contains("unbound"),
7581 "expected legacy `unbound` substring in message, got: {msg}"
7582 );
7583 assert!(
7584 msg.contains(",xs"),
7585 "expected the offending form in message, got: {msg}"
7586 );
7587 }
7588
7589 #[test]
7590 fn unbound_template_var_position_is_none_today() {
7591 // Negative control for the future-spans move: until `Sexp` carries
7592 // source positions, `position()` returns `None` for this variant.
7593 let mut e = Expander::new();
7594 let err = e
7595 .expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
7596 .expect_err("unbound must error");
7597 assert_eq!(err.position(), None);
7598 }
7599
7600 // ── Non-symbol unquote target: structural variant ─────────────────
7601
7602 /// Helper for the non-symbol-unquote-target tests — pins the variant
7603 /// shape and carries any error context up to the assert site for
7604 /// legibility. Sibling of `unbound_var` and `splice_outside_list_got`;
7605 /// returns the `display` projection of the typed `SexpWitness` so the
7606 /// existing call sites stay byte-for-byte comparable to the legacy
7607 /// `got: String` shape.
7608 fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
7609 match err {
7610 LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
7611 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7612 }
7613 }
7614
7615 #[test]
7616 fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
7617 // `,(list 1 2)` — the inner is a list, not a symbol. Path:
7618 // compile_node Unquote (the bytecode-template compile, default
7619 // expander). Pins variant identity AND prefix AND the offending
7620 // literal so a regression that re-inlines the legacy
7621 // `LispError::Compile` shape fails-loudly here.
7622 let mut e = Expander::new();
7623 let err = e
7624 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7625 .expect_err("non-symbol unquote target must error");
7626 let (prefix, got) = non_symbol_target(&err);
7627 assert_eq!(prefix, UnquoteForm::Unquote);
7628 assert_eq!(got, "(list 1 2)");
7629 }
7630
7631 #[test]
7632 fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
7633 // `,@5` — the inner is an int atom, not a symbol. Path:
7634 // compile_node UnquoteSplice. The integer literal round-trips
7635 // through the variant's `got` slot via `Sexp::Display`.
7636 let mut e = Expander::new();
7637 let err = e
7638 .expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
7639 .expect_err("non-symbol splice target must error");
7640 let (prefix, got) = non_symbol_target(&err);
7641 assert_eq!(prefix, UnquoteForm::Splice);
7642 assert_eq!(got, "5");
7643 }
7644
7645 #[test]
7646 fn non_symbol_unquote_in_substitute_emits_structural_variant() {
7647 // Same shape as the bytecode path but routed through the
7648 // substitute-only expander — proves the substitute path emits the
7649 // same variant as the compile_node path. Pins that the lift is
7650 // path-uniform.
7651 let mut e = Expander::new_substitute_only();
7652 let err = e
7653 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7654 .expect_err("substitute non-symbol target must error");
7655 let (prefix, got) = non_symbol_target(&err);
7656 assert_eq!(prefix, UnquoteForm::Unquote);
7657 assert_eq!(got, "(list 1 2)");
7658 }
7659
7660 #[test]
7661 fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
7662 // The substitute path's UnquoteSplice-inside-list branch fires for
7663 // splices that appear inside a list during the recursive walk.
7664 // `,@(list 1 2)` inside the body — the inner is a literal list, not
7665 // a symbol — emits the same variant as the compile_node path.
7666 let mut e = Expander::new_substitute_only();
7667 let err = e
7668 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7669 .expect_err("substitute non-symbol splice must error");
7670 let (prefix, got) = non_symbol_target(&err);
7671 assert_eq!(prefix, UnquoteForm::Splice);
7672 assert_eq!(got, "(list 1 2)");
7673 }
7674
7675 #[test]
7676 fn non_symbol_unquote_target_position_is_none_today() {
7677 // Negative control for the future-spans move: until `Sexp` carries
7678 // source positions, `position()` returns `None` for this variant.
7679 // A future run that gives `Sexp` source spans adds `pos:
7680 // Option<usize>` to ONE place; this test gives that change a
7681 // deliberate fail-before/pass-after delta.
7682 let mut e = Expander::new();
7683 let err = e
7684 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7685 .expect_err("non-symbol target must error");
7686 assert_eq!(err.position(), None);
7687 }
7688
7689 // ── unquote_target_symbol: typed gate-1 primitive for ,X / ,@X ──────
7690 //
7691 // The `unquote_target_symbol(inner, form)?` primitive lifts the
7692 // inline `inner.as_symbol().ok_or_else(|| non_symbol_unquote_target(
7693 // form, inner))?` pattern that previously appeared at four call
7694 // sites (`compile_node` Unquote/UnquoteSplice + `substitute` Unquote
7695 // + `substitute` list-inner UnquoteSplice) behind ONE named
7696 // primitive. The tests below pin: (a) the Ok-arm borrows the
7697 // symbol name from `inner` for both UnquoteForm variants; (b) the
7698 // Err-arm routes through `non_symbol_unquote_target` and emits the
7699 // structural `LispError::NonSymbolUnquoteTarget` variant carrying
7700 // the typed `SexpWitness` (joint shape + display identity) for the
7701 // closed set of reachable non-symbol shapes (int / keyword / list /
7702 // nil); (c) the helper is path-uniform — the same Ok / Err
7703 // contracts hold regardless of which call site invokes it. A
7704 // regression that re-inlines the gate-1 projection at any of the
7705 // four call sites can no longer drift independent of the others —
7706 // the helper IS the gate.
7707
7708 #[test]
7709 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
7710 // Positive control for the Ok-arm: `inner = Sexp::Symbol("xs")`
7711 // under `UnquoteForm::Unquote` projects through `as_symbol()`
7712 // to the borrowed `&str`. The returned slice's lifetime is
7713 // tied to `inner` so the caller can feed it directly into
7714 // `params.iter().position(...)` (`compile_node`) or
7715 // `bindings.get(...)` (`substitute`) without an intermediate
7716 // allocation. Fail-before/pass-after: this assert is meaningless
7717 // pre-lift because the helper does not exist; post-lift it
7718 // pins the typed gate-1 contract at the named primitive.
7719 let inner = Sexp::symbol("xs");
7720 let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7721 .expect("symbol inner must project to Ok");
7722 assert_eq!(name, "xs");
7723 }
7724
7725 #[test]
7726 fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
7727 // Sibling positive control: `UnquoteForm::Splice` shares the
7728 // gate-1 contract with `Unquote`. The helper is path-uniform
7729 // across both syntactic markers — a regression that bifurcates
7730 // the two arms (e.g., accepting non-symbols for `,@X` but not
7731 // `,X`) fails-loudly here. Pins that the closed-set
7732 // `UnquoteForm` enum's two variants share ONE projection
7733 // posture across the gate-1 boundary.
7734 let inner = Sexp::symbol("rest");
7735 let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
7736 .expect("symbol inner must project to Ok under Splice");
7737 assert_eq!(name, "rest");
7738 }
7739
7740 #[test]
7741 fn unquote_target_symbol_rejects_int_inner_under_unquote() {
7742 // Negative control for the Err-arm: `inner = Sexp::Int(5)` is
7743 // NOT a symbol — the gate-1 projection fires and routes through
7744 // `non_symbol_unquote_target` to the structural
7745 // `LispError::NonSymbolUnquoteTarget` variant. Pin the variant
7746 // identity AND the typed `SexpWitness` joint identity (shape +
7747 // display literal): a regression that drops the witness shape
7748 // or display fails-loudly here.
7749 let inner = Sexp::int(5);
7750 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7751 .expect_err("int inner must error at gate-1");
7752 match err {
7753 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7754 assert_eq!(prefix, UnquoteForm::Unquote);
7755 assert_eq!(got.shape, crate::error::SexpShape::Int);
7756 assert_eq!(got.display, "5");
7757 }
7758 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7759 }
7760 }
7761
7762 #[test]
7763 fn unquote_target_symbol_rejects_list_inner_under_splice() {
7764 // Sibling negative control: `inner = (list 1 2)` is a list, not
7765 // a symbol — the gate-1 projection fires AND routes through
7766 // `non_symbol_unquote_target(UnquoteForm::Splice, inner)`. Pins
7767 // both the variant identity AND the typed witness's joint
7768 // shape (`SexpShape::List`) + display (`"(list 1 2)"`) so a
7769 // future shape drift fails-loudly. Sibling of the Int / Unquote
7770 // pin: closes the gate-1 contract across the closed-set
7771 // product of {Int, List, Keyword, …} × {Unquote, Splice}.
7772 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
7773 let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
7774 .expect_err("list inner must error at gate-1");
7775 match err {
7776 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7777 assert_eq!(prefix, UnquoteForm::Splice);
7778 assert_eq!(got.shape, crate::error::SexpShape::List);
7779 assert_eq!(got.display, "(list 1 2)");
7780 }
7781 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7782 }
7783 }
7784
7785 #[test]
7786 fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
7787 // Pin a third reachable non-symbol shape: `Sexp::Keyword(":foo")`.
7788 // The gate-1 projection rejects keywords AS WELL as ints and
7789 // lists — closes the closed-set of "non-symbol shapes the gate
7790 // rejects" across one more reachable variant. The typed witness
7791 // carries `SexpShape::Keyword` + display `:foo` jointly so
7792 // authoring tools (REPL, LSP) bind on the structural shape
7793 // directly.
7794 let inner = Sexp::keyword("foo");
7795 let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
7796 .expect_err("keyword inner must error at gate-1");
7797 match err {
7798 LispError::NonSymbolUnquoteTarget { prefix, got } => {
7799 assert_eq!(prefix, UnquoteForm::Unquote);
7800 assert_eq!(got.shape, crate::error::SexpShape::Keyword);
7801 assert_eq!(got.display, ":foo");
7802 }
7803 other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
7804 }
7805 }
7806
7807 #[test]
7808 fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
7809 // Path-uniformity pin: end-to-end through ALL FOUR call sites
7810 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
7811 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
7812 // every non-symbol unquote target now routes through the SAME
7813 // `unquote_target_symbol(inner, form)?` helper. The four
7814 // end-to-end expansions below all reject with the SAME variant
7815 // (`NonSymbolUnquoteTarget`) — pins that the lift preserves the
7816 // path-uniform rejection contract `non_symbol_unquote_target`'s
7817 // prior lift established (and that drove the bytecode-vs-
7818 // substitute reunification in 0e9c… and successors). A
7819 // regression that re-inlines the gate-1 projection at one of
7820 // the four sites can drift the four call sites independent of
7821 // each other — this test would catch that drift.
7822 let cases: &[(&str, UnquoteForm)] = &[
7823 // compile_node Unquote (bytecode-path)
7824 ("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
7825 // compile_node UnquoteSplice (bytecode-path)
7826 ("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
7827 ];
7828 for (src, expected_form) in cases {
7829 let mut e = Expander::new();
7830 let err = e
7831 .expand_program(read(src).unwrap())
7832 .expect_err("non-symbol unquote target must error end-to-end");
7833 match err {
7834 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
7835 assert_eq!(prefix, *expected_form, "for src: {src}");
7836 }
7837 other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
7838 }
7839 }
7840 // substitute Unquote (substitute-only path) — sibling pin to
7841 // `non_symbol_unquote_in_substitute_emits_structural_variant`.
7842 let mut e_subst = Expander::new_substitute_only();
7843 let err = e_subst
7844 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
7845 .expect_err("substitute Unquote must error end-to-end");
7846 assert!(
7847 matches!(
7848 err,
7849 LispError::NonSymbolUnquoteTarget {
7850 prefix: UnquoteForm::Unquote,
7851 ..
7852 }
7853 ),
7854 "expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
7855 );
7856 // substitute list-inner UnquoteSplice (substitute-only path) —
7857 // sibling pin to
7858 // `non_symbol_unquote_splice_inside_list_in_substitute_emits_…`.
7859 let mut e_subst2 = Expander::new_substitute_only();
7860 let err = e_subst2
7861 .expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
7862 .expect_err("substitute UnquoteSplice-in-list must error end-to-end");
7863 assert!(
7864 matches!(
7865 err,
7866 LispError::NonSymbolUnquoteTarget {
7867 prefix: UnquoteForm::Splice,
7868 ..
7869 }
7870 ),
7871 "expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
7872 );
7873 }
7874
7875 // ── Gate-2 (must-be-bound-in-scope) typed primitives ──────────────
7876 // Pins the contract of the two gate-2 helpers — `resolve_param_index`
7877 // (bytecode-template compile path) and `resolve_binding`
7878 // (substitute path) — that the four inline `<lookup>.ok_or_else(||
7879 // unbound_template_var(FORM, name, candidates))` projections at
7880 // `compile_node` Unquote/UnquoteSplice AND `substitute` Unquote/
7881 // UnquoteSplice-inside-list collapse behind. Tests pin: (a) Ok-arm
7882 // projection under both `UnquoteForm` variants — the helper returns
7883 // the resolved `usize` (compile path) or `&Sexp` (substitute path)
7884 // for in-scope names; (b) Err-arm projection routes through
7885 // `unbound_template_var` to the typed `LispError::UnboundTemplateVar`
7886 // variant with the correct `prefix` AND the suggest-driven `hint`;
7887 // (c) the helpers are path-uniform — both compile-path arms share
7888 // ONE `resolve_param_index`; both substitute-path arms share ONE
7889 // `resolve_binding`. A regression that re-inlines the gate-2
7890 // projection at any of the four call sites can no longer drift
7891 // independent of the others — the two helpers ARE the gate.
7892
7893 #[test]
7894 fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
7895 // Positive control for the Ok-arm: `name = "x"` against
7896 // `params = ["a", "x", "rest"]` projects through
7897 // `params.iter().position(|p| *p == name)` to `Some(1)`, which
7898 // the helper unwraps to `Ok(1)`. The returned index feeds
7899 // directly into `TemplateOp::Subst(idx)` at the compile site.
7900 let params = ["a", "x", "rest"];
7901 let idx = resolve_param_index("x", ¶ms, UnquoteForm::Unquote)
7902 .expect("bound name must project to Ok at gate-2");
7903 assert_eq!(idx, 1);
7904 }
7905
7906 #[test]
7907 fn resolve_param_index_returns_position_for_bound_name_under_splice() {
7908 // Sibling positive control: `UnquoteForm::Splice` shares the
7909 // gate-2 contract with `Unquote`. The helper is path-uniform
7910 // across both syntactic markers on the compile path — a
7911 // regression that bifurcates the two arms fails-loudly here.
7912 let params = ["a", "x", "rest"];
7913 let idx = resolve_param_index("rest", ¶ms, UnquoteForm::Splice)
7914 .expect("bound name must project to Ok at gate-2 under Splice");
7915 assert_eq!(idx, 2);
7916 }
7917
7918 #[test]
7919 fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
7920 // Negative control for the Err-arm: `name = "xs"` against
7921 // `params = ["x"]` — distance 1, bound 1 — routes through
7922 // `unbound_template_var` to the structural
7923 // `LispError::UnboundTemplateVar` variant with `hint = Some("x")`.
7924 // Pin the variant identity AND the prefix AND the suggest-driven
7925 // hint: a regression that drops the suggestion fails-loudly here.
7926 let params = ["x"];
7927 let err = resolve_param_index("xs", ¶ms, UnquoteForm::Unquote)
7928 .expect_err("unbound name must error at gate-2");
7929 match err {
7930 LispError::UnboundTemplateVar { prefix, name, hint } => {
7931 assert_eq!(prefix, UnquoteForm::Unquote);
7932 assert_eq!(name, "xs");
7933 assert_eq!(hint.as_deref(), Some("x"));
7934 }
7935 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7936 }
7937 }
7938
7939 #[test]
7940 fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
7941 // Sibling negative control: `name = "wholly-unrelated"` against
7942 // `params = ["x"]` — past the bounded edit distance, so no hint.
7943 // Pin that the suggest-driven hint stays empty under Splice when
7944 // the substrate isn't confident — a wrong hint is worse than no
7945 // hint. Closes the closed-set product of {hint, no-hint} ×
7946 // {Unquote, Splice} on the compile-path gate-2.
7947 let params = ["x"];
7948 let err = resolve_param_index("wholly-unrelated", ¶ms, UnquoteForm::Splice)
7949 .expect_err("unrelated unbound must error at gate-2");
7950 match err {
7951 LispError::UnboundTemplateVar { prefix, name, hint } => {
7952 assert_eq!(prefix, UnquoteForm::Splice);
7953 assert_eq!(name, "wholly-unrelated");
7954 assert_eq!(hint, None);
7955 }
7956 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
7957 }
7958 }
7959
7960 #[test]
7961 fn resolve_binding_returns_value_for_bound_name_under_unquote() {
7962 // Positive control for the substitute-path Ok-arm: `name = "x"`
7963 // against a bindings map `{x: 42, y: "hi"}` projects through
7964 // `bindings.get(name)` to `Some(&Sexp::Int(42))`, which the
7965 // helper unwraps to `Ok(&Sexp::Int(42))`. The returned
7966 // `&Sexp` borrows from the bindings map — the top-level
7967 // `Sexp::Unquote(_)` substitute caller adds a single
7968 // `.cloned()` to satisfy its owned-`Sexp` return obligation.
7969 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7970 bindings.insert("x".to_string(), Sexp::int(42));
7971 bindings.insert("y".to_string(), Sexp::string("hi"));
7972 let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
7973 .expect("bound name must project to Ok at gate-2 (substitute)");
7974 assert_eq!(val, &Sexp::int(42));
7975 }
7976
7977 #[test]
7978 fn resolve_binding_returns_value_for_bound_name_under_splice() {
7979 // Sibling positive control: `UnquoteForm::Splice` shares the
7980 // gate-2 contract with `Unquote` on the substitute path too.
7981 // The bound value is a `Sexp::List` because the splice arm's
7982 // caller match expression expects `Sexp::List(items)` — but
7983 // the helper itself doesn't inspect the value's shape; it
7984 // just hands back the borrow. A regression that gate-checks
7985 // the value's shape inside `resolve_binding` (instead of at
7986 // the caller match arm) fails-loudly here.
7987 let mut bindings: HashMap<String, Sexp> = HashMap::new();
7988 bindings.insert(
7989 "args".to_string(),
7990 Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
7991 );
7992 let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
7993 .expect("bound name must project to Ok at gate-2 under Splice");
7994 assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
7995 }
7996
7997 #[test]
7998 fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
7999 // Negative control for the substitute-path Err-arm: `name =
8000 // "xs"` against bindings `{x: 1}` — distance 1, bound 1 —
8001 // routes through `unbound_template_var` to the structural
8002 // `LispError::UnboundTemplateVar` variant with `hint =
8003 // Some("x")`. The candidate set is drawn from
8004 // `bound_names(bindings)` — the live bindings' keys, never a
8005 // stale snapshot.
8006 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8007 bindings.insert("x".to_string(), Sexp::int(1));
8008 let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
8009 .expect_err("unbound name must error at gate-2 (substitute)");
8010 match err {
8011 LispError::UnboundTemplateVar { prefix, name, hint } => {
8012 assert_eq!(prefix, UnquoteForm::Unquote);
8013 assert_eq!(name, "xs");
8014 assert_eq!(hint.as_deref(), Some("x"));
8015 }
8016 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8017 }
8018 }
8019
8020 #[test]
8021 fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
8022 // Sibling negative control on the substitute path: past-bound
8023 // distance → no hint. Closes the closed-set product of
8024 // {hint, no-hint} × {Unquote, Splice} on the substitute-path
8025 // gate-2.
8026 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8027 bindings.insert("args".to_string(), Sexp::Nil);
8028 let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
8029 .expect_err("unrelated unbound must error at gate-2");
8030 match err {
8031 LispError::UnboundTemplateVar { prefix, name, hint } => {
8032 assert_eq!(prefix, UnquoteForm::Splice);
8033 assert_eq!(name, "wholly-unrelated");
8034 assert_eq!(hint, None);
8035 }
8036 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
8037 }
8038 }
8039
8040 #[test]
8041 fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
8042 // Path-uniformity pin: end-to-end through ALL FOUR call sites
8043 // (`compile_node` Unquote, `compile_node` UnquoteSplice,
8044 // `substitute` Unquote, `substitute` list-inner UnquoteSplice)
8045 // every unbound-template-var rejection now routes through one
8046 // of the TWO `resolve_param_index` / `resolve_binding` helpers
8047 // — `Expander::new()` runs the compile path, so its two arms
8048 // share `resolve_param_index`; `Expander::new_substitute_only()`
8049 // runs the substitute path, so its two arms share
8050 // `resolve_binding`. The four end-to-end expansions below all
8051 // reject with the SAME variant (`UnboundTemplateVar`) with the
8052 // expected `prefix` — pins that the lift preserves the
8053 // path-uniform rejection contract `unbound_template_var`'s
8054 // prior naming established. A regression that re-inlines the
8055 // gate-2 projection at one of the four sites can drift the
8056 // four call sites independent of each other — this test would
8057 // catch that drift.
8058 struct Case {
8059 src: &'static str,
8060 expander: fn() -> Expander,
8061 expected_form: UnquoteForm,
8062 }
8063 let cases: &[Case] = &[
8064 // compile_node Unquote (bytecode path) — uses resolve_param_index
8065 Case {
8066 src: "(defmacro w (x) `(list ,xs)) (w 1)",
8067 expander: Expander::new,
8068 expected_form: UnquoteForm::Unquote,
8069 },
8070 // compile_node UnquoteSplice (bytecode path) — uses resolve_param_index
8071 Case {
8072 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8073 expander: Expander::new,
8074 expected_form: UnquoteForm::Splice,
8075 },
8076 // substitute Unquote (substitute-only path) — uses resolve_binding
8077 Case {
8078 src: "(defmacro w (x) `(list ,xs)) (w 1)",
8079 expander: Expander::new_substitute_only,
8080 expected_form: UnquoteForm::Unquote,
8081 },
8082 // substitute UnquoteSplice-in-list (substitute-only path) — uses resolve_binding
8083 Case {
8084 src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
8085 expander: Expander::new_substitute_only,
8086 expected_form: UnquoteForm::Splice,
8087 },
8088 ];
8089 for case in cases {
8090 let mut e = (case.expander)();
8091 let err = e
8092 .expand_program(read(case.src).unwrap())
8093 .expect_err("unbound template var must error end-to-end");
8094 match err {
8095 LispError::UnboundTemplateVar { prefix, .. } => {
8096 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8097 }
8098 other => panic!(
8099 "expected UnboundTemplateVar for {}, got: {other:?}",
8100 case.src
8101 ),
8102 }
8103 }
8104 }
8105
8106 // ── resolve_unquote_in_params / _in_bindings: gate-1+gate-2 composition ─
8107
8108 #[test]
8109 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
8110 // Ok-arm composition under `UnquoteForm::Unquote`: gate-1 projects
8111 // the symbol-inner to "x"; gate-2 looks "x" up in `params` and
8112 // returns its index. The combined helper returns the gate-2
8113 // result directly — pins that gate-1's Ok-arm threads into
8114 // gate-2's input without intermediate state.
8115 let inner = Sexp::symbol("x");
8116 let params = ["x", "y"];
8117 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8118 .expect("symbol-inner bound at index 0 must resolve");
8119 assert_eq!(idx, 0);
8120 }
8121
8122 #[test]
8123 fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
8124 // Sibling Ok-arm under `UnquoteForm::Splice`: pins that the
8125 // marker doesn't change the projection — only the rejection
8126 // path's `prefix` slot.
8127 let inner = Sexp::symbol("args");
8128 let params = ["f", "args"];
8129 let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8130 .expect("symbol-inner bound at index 1 must resolve");
8131 assert_eq!(idx, 1);
8132 }
8133
8134 #[test]
8135 fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
8136 // Err-arm at gate-1 (must-be-a-symbol): the inner is a list, not
8137 // a symbol, so gate-1 rejects via `non_symbol_unquote_target`
8138 // BEFORE gate-2's param lookup runs. Pins that the composition's
8139 // sequencing is gate-1-then-gate-2: a regression that runs
8140 // gate-2 first would attempt to look up "(list 1 2)" as a param
8141 // name and emit `LispError::UnboundTemplateVar { name: "(list 1
8142 // 2)", ... }` — a confusing diagnostic that would substring-grep
8143 // "unbound" instead of "expected symbol". This test pins the
8144 // structural floor: a non-symbol inner is rejected as a non-
8145 // symbol, never re-treated as a bound-name lookup key.
8146 let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
8147 let params = ["x"];
8148 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
8149 .expect_err("non-symbol inner must reject at gate-1");
8150 match err {
8151 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8152 assert_eq!(prefix, UnquoteForm::Unquote);
8153 assert_eq!(got.display, "(list 1 2)");
8154 }
8155 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8156 }
8157 }
8158
8159 #[test]
8160 fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
8161 // Err-arm at gate-2 (must-be-bound-in-scope): the inner IS a
8162 // symbol (gate-1 passes) but the name isn't in `params`, so
8163 // gate-2 rejects via `unbound_template_var`. Pins that gate-1
8164 // forwards its Ok-arm `&str` borrow into gate-2's lookup, and
8165 // that the marker `prefix` is threaded into gate-2's rejection
8166 // unchanged (a regression that hard-codes `UnquoteForm::Unquote`
8167 // at the composition boundary would fail this Splice-marker
8168 // assertion).
8169 let inner = Sexp::symbol("missing");
8170 let params = ["x", "y"];
8171 let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
8172 .expect_err("unbound symbol must reject at gate-2");
8173 match err {
8174 LispError::UnboundTemplateVar { prefix, name, .. } => {
8175 assert_eq!(prefix, UnquoteForm::Splice);
8176 assert_eq!(name, "missing");
8177 }
8178 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8179 }
8180 }
8181
8182 #[test]
8183 fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
8184 // Substitute-path sibling of `resolve_unquote_in_params_returns_
8185 // index_for_symbol_inner_under_unquote`. The combined helper
8186 // composes gate-1 (project inner to symbol) THEN gate-2 (look
8187 // up name in bindings). The returned `&Sexp` borrows from
8188 // `bindings` so the list-inner caller threads it straight into
8189 // the splice-expansion match without an intermediate allocation.
8190 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8191 bindings.insert("v".to_string(), Sexp::int(42));
8192 let inner = Sexp::symbol("v");
8193 let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8194 .expect("symbol-inner bound to 42 must resolve");
8195 assert_eq!(val, &Sexp::int(42));
8196 }
8197
8198 #[test]
8199 fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
8200 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8201 // non_symbol_inner_at_gate_1`. Pins the gate-1-then-gate-2
8202 // sequencing on the substitute path: a non-symbol inner is
8203 // rejected as a non-symbol BEFORE the bindings map is consulted.
8204 let bindings: HashMap<String, Sexp> = HashMap::new();
8205 let inner = Sexp::int(5);
8206 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
8207 .expect_err("non-symbol inner must reject at gate-1");
8208 match err {
8209 LispError::NonSymbolUnquoteTarget { prefix, got } => {
8210 assert_eq!(prefix, UnquoteForm::Splice);
8211 assert_eq!(got.display, "5");
8212 }
8213 other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
8214 }
8215 }
8216
8217 #[test]
8218 fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
8219 // Substitute-path sibling of `resolve_unquote_in_params_rejects_
8220 // unbound_symbol_at_gate_2`. Pins the gate-2 rejection on the
8221 // substitute path with the marker threaded into the rejection's
8222 // `prefix` slot.
8223 let mut bindings: HashMap<String, Sexp> = HashMap::new();
8224 bindings.insert("known".to_string(), Sexp::Nil);
8225 let inner = Sexp::symbol("missing");
8226 let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
8227 .expect_err("unbound symbol must reject at gate-2");
8228 match err {
8229 LispError::UnboundTemplateVar { prefix, name, .. } => {
8230 assert_eq!(prefix, UnquoteForm::Unquote);
8231 assert_eq!(name, "missing");
8232 }
8233 other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
8234 }
8235 }
8236
8237 #[test]
8238 fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
8239 // End-to-end pin: all FOUR call sites of the gate-1+gate-2
8240 // composition (compile_node Unquote, compile_node UnquoteSplice,
8241 // substitute Unquote, substitute list-inner UnquoteSplice) now
8242 // share TWO composed primitives — `resolve_unquote_in_params`
8243 // on the bytecode path, `resolve_unquote_in_bindings` on the
8244 // substitute path — and ALL four reject gate-1 failures (non-
8245 // symbol inner) with the SAME `LispError::NonSymbolUnquoteTarget`
8246 // variant carrying the expected `prefix` slot. Before the lift,
8247 // each site threaded `form` twice through two helper calls; this
8248 // test pins that the lift preserves the gate's rejection-shape
8249 // identity across all four sites for a non-symbol inner — i.e.
8250 // gate-1 fires identically across both expansion strategies.
8251 struct Case {
8252 src: &'static str,
8253 expander: fn() -> Expander,
8254 expected_form: UnquoteForm,
8255 }
8256 let cases: &[Case] = &[
8257 Case {
8258 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8259 expander: Expander::new,
8260 expected_form: UnquoteForm::Unquote,
8261 },
8262 Case {
8263 src: "(defmacro w (x) `(outer ,@5)) (w 1)",
8264 expander: Expander::new,
8265 expected_form: UnquoteForm::Splice,
8266 },
8267 Case {
8268 src: "(defmacro w (x) `,(list 1 2)) (w 1)",
8269 expander: Expander::new_substitute_only,
8270 expected_form: UnquoteForm::Unquote,
8271 },
8272 Case {
8273 src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
8274 expander: Expander::new_substitute_only,
8275 expected_form: UnquoteForm::Splice,
8276 },
8277 ];
8278 for case in cases {
8279 let mut e = (case.expander)();
8280 let err = e
8281 .expand_program(read(case.src).unwrap())
8282 .expect_err("non-symbol inner must error end-to-end");
8283 match err {
8284 LispError::NonSymbolUnquoteTarget { prefix, .. } => {
8285 assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
8286 }
8287 other => panic!(
8288 "expected NonSymbolUnquoteTarget for {}, got: {other:?}",
8289 case.src
8290 ),
8291 }
8292 }
8293 }
8294
8295 // ── Splice outside list: structural variant + path-uniform rejection ─
8296
8297 /// Helper for the splice-outside-list tests — pins the variant shape
8298 /// and carries the offending `got` field up to the assert site for
8299 /// legibility. Sibling of `unbound_var` and `non_symbol_target`.
8300 fn splice_outside_list_got(err: &LispError) -> &str {
8301 match err {
8302 LispError::SpliceOutsideList { got } => got.display.as_str(),
8303 other => panic!("expected SpliceOutsideList, got: {other:?}"),
8304 }
8305 }
8306
8307 #[test]
8308 fn splice_outside_list_in_substitute_emits_structural_variant() {
8309 // `,@xs` at the body's top level — there is no containing list to
8310 // splice into. Path: substitute (the `Expander::new_substitute_only`
8311 // path's top-level `Sexp::UnquoteSplice(_)` arm). Pins variant
8312 // identity AND the offending inner so a regression that re-inlines
8313 // the legacy `LispError::Compile` shape fails-loudly here.
8314 let mut e = Expander::new_substitute_only();
8315 let err = e
8316 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8317 .expect_err("splice outside list must error");
8318 assert_eq!(splice_outside_list_got(&err), "xs");
8319 }
8320
8321 #[test]
8322 fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
8323 // `,@(list 1 2)` at the body's top level — the inner is a literal
8324 // list, not a symbol. The structural variant carries the inner's
8325 // Sexp::Display projection so the operator sees the literal value
8326 // they wrote in the parenthetical.
8327 let mut e = Expander::new_substitute_only();
8328 let err = e
8329 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8330 .expect_err("splice outside list must error");
8331 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8332 }
8333
8334 #[test]
8335 fn splice_outside_list_in_compile_template_emits_structural_variant() {
8336 // The bytecode path's `compile_template` gate now rejects top-level
8337 // `,@X` bodies BEFORE walking — closing the prior silent-divergence
8338 // where the bytecode interpreter's outermost stack frame absorbed
8339 // the splice. Pins that the bytecode path emits the SAME structural
8340 // variant the substitute path emits — `,@-outside-list` is rejected
8341 // path-uniformly. Path: `Expander::new()` (compile_templates = true)
8342 // → `compile_template` gate.
8343 let mut e = Expander::new();
8344 let err = e
8345 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8346 .expect_err("compile-template splice outside list must error");
8347 assert_eq!(splice_outside_list_got(&err), "xs");
8348 }
8349
8350 #[test]
8351 fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
8352 // Same shape as the substitute test but routed through the bytecode
8353 // path's `compile_template` gate. Proves the gate fires on a
8354 // non-symbol inner too — the slot's contents are irrelevant; only
8355 // the syntactic position matters.
8356 let mut e = Expander::new();
8357 let err = e
8358 .expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
8359 .expect_err("compile-template splice outside list must error");
8360 assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
8361 }
8362
8363 #[test]
8364 fn splice_outside_list_substitute_and_bytecode_paths_agree() {
8365 // Path-uniform rejection: the SAME source emits the SAME structural
8366 // variant (`SpliceOutsideList { got: "xs" }`) under both expansion
8367 // strategies. Before the `compile_template` gate, the bytecode path
8368 // silently produced a list while the substitute path errored —
8369 // expansion strategy was observable. After the gate, the gate is
8370 // strategy-uniform, so a macro that registers under one strategy
8371 // registers under the other.
8372 let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
8373 let mut subst = Expander::new_substitute_only();
8374 let mut bytecode = Expander::new();
8375 let err_subst = subst
8376 .expand_program(read(src).unwrap())
8377 .expect_err("substitute must error");
8378 let err_byte = bytecode
8379 .expand_program(read(src).unwrap())
8380 .expect_err("bytecode must error");
8381 assert_eq!(splice_outside_list_got(&err_subst), "xs");
8382 assert_eq!(splice_outside_list_got(&err_byte), "xs");
8383 }
8384
8385 #[test]
8386 fn splice_outside_list_position_is_none_today() {
8387 // Negative control for the future-spans move: until `Sexp` carries
8388 // source positions, `position()` returns `None` for this variant.
8389 // A future run that gives `Sexp` source spans adds `pos:
8390 // Option<usize>` to ONE place; this test gives that change a
8391 // deliberate fail-before/pass-after delta.
8392 let mut e = Expander::new_substitute_only();
8393 let err = e
8394 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8395 .expect_err("splice outside list must error");
8396 assert_eq!(err.position(), None);
8397 }
8398
8399 #[test]
8400 fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
8401 // End-to-end through the Display impl — pins the rendered diagnostic
8402 // a downstream tool sees today (REPL, tatara-check). The legacy
8403 // substring `"\`,@\` may only appear inside a list"` is preserved
8404 // verbatim AND the parenthetical `(got ,@xs)` names the offending
8405 // form; tools that pattern-match on the variant gain structural
8406 // binding to `got`.
8407 let mut e = Expander::new_substitute_only();
8408 let err = e
8409 .expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
8410 .expect_err("splice outside list must error");
8411 let msg = format!("{err}");
8412 assert_eq!(
8413 msg,
8414 "compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
8415 );
8416 }
8417
8418 #[test]
8419 fn splice_inside_list_still_succeeds_under_both_paths() {
8420 // Negative control: a well-positioned splice (`,@xs` INSIDE a list)
8421 // continues to succeed under both paths — the new gate only fires
8422 // when the splice is the entire body. Pins that the gate is scoped
8423 // to top-level only, not all `,@` occurrences. Uses a `&rest`-bound
8424 // list so `xs` is unambiguously a Sexp::List `(1 2)` rather than a
8425 // bare list-literal whose first symbol would also splice through.
8426 let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
8427 let mut subst = Expander::new_substitute_only();
8428 let mut bytecode = Expander::new();
8429 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8430 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8431 assert_eq!(out_subst, out_byte);
8432 assert_eq!(out_subst[0], parse("(outer 1 2)"));
8433 }
8434
8435 // ── splice_value_into: the shared splice-result coercion ──
8436
8437 #[test]
8438 fn splice_value_into_list_flattens_elements_into_builder() {
8439 // The canonical splice arm: a bound LIST contributes its elements
8440 // in order, preserving anything already in the builder.
8441 let mut builder = vec![Sexp::symbol("outer")];
8442 splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
8443 assert_eq!(
8444 builder,
8445 vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
8446 );
8447 }
8448
8449 #[test]
8450 fn splice_value_into_nil_is_a_noop() {
8451 // Splicing the empty list (`Sexp::Nil`) contributes nothing —
8452 // the builder is unchanged.
8453 let mut builder = vec![Sexp::symbol("outer")];
8454 splice_value_into(&mut builder, &Sexp::Nil);
8455 assert_eq!(builder, vec![Sexp::symbol("outer")]);
8456 }
8457
8458 #[test]
8459 fn splice_value_into_scalar_pushes_single_element() {
8460 // A non-list, non-nil bound value degrades `,@x` to `,x`: it
8461 // splices as exactly one element. Pins the "free middle" coercion
8462 // every scalar shape (int, keyword, …) shares.
8463 let mut builder = vec![Sexp::symbol("outer")];
8464 splice_value_into(&mut builder, &Sexp::int(5));
8465 assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
8466 let mut other: Vec<Sexp> = vec![];
8467 splice_value_into(&mut other, &Sexp::keyword("k"));
8468 assert_eq!(other, vec![Sexp::keyword("k")]);
8469 }
8470
8471 #[test]
8472 fn splice_of_non_list_value_coerces_identically_under_both_paths() {
8473 // The point of the lift: the NON-list splice arms (scalar → single
8474 // element, nil → nothing) coerce identically under the substitute
8475 // AND bytecode strategies. Before the coercion was lifted to ONE
8476 // primitive these two arms lived inline at two sites; this test
8477 // pins that the two strategies cannot drift on the non-list arms.
8478 let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
8479 let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
8480 for src in [scalar, empty] {
8481 let mut subst = Expander::new_substitute_only();
8482 let mut bytecode = Expander::new();
8483 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8484 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8485 assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
8486 }
8487 let mut e = Expander::new();
8488 assert_eq!(
8489 e.expand_program(read(scalar).unwrap()).unwrap()[0],
8490 parse("(outer 5)")
8491 );
8492 let mut e2 = Expander::new();
8493 assert_eq!(
8494 e2.expand_program(read(empty).unwrap()).unwrap()[0],
8495 parse("(outer)")
8496 );
8497 }
8498
8499 // ── Missing macro arg: structural variant + path-uniform rejection ──
8500
8501 /// Helper for the missing-macro-arg tests — pins the variant shape
8502 /// and carries the failing macro's name + un-bound param up to the
8503 /// assert site for legibility. Sibling of `unbound_var`,
8504 /// `non_symbol_target`, and `splice_outside_list_got`.
8505 fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
8506 match err {
8507 LispError::MissingMacroArg { macro_name, param } => {
8508 (macro_name.as_str(), param.as_str())
8509 }
8510 other => panic!("expected MissingMacroArg, got: {other:?}"),
8511 }
8512 }
8513
8514 #[test]
8515 fn missing_macro_arg_in_compile_template_emits_structural_variant() {
8516 // `(need-two 1)` against `(need-two a b)` — `b` has no arg. Path:
8517 // `apply_compiled` (the bytecode-template path, default expander).
8518 // Pins variant identity AND macro_name AND the un-bound param so a
8519 // regression that re-inlines the legacy `LispError::Compile` shape
8520 // fails-loudly here.
8521 let mut e = Expander::new();
8522 let err = e
8523 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8524 .expect_err("missing required macro arg must error");
8525 let (macro_name, param) = missing_macro_arg_fields(&err);
8526 assert_eq!(macro_name, "need-two");
8527 assert_eq!(param, "b");
8528 }
8529
8530 #[test]
8531 fn missing_macro_arg_in_substitute_emits_structural_variant() {
8532 // Same shape as the bytecode test but routed through the
8533 // substitute-only expander → `bind_args` is the failing site.
8534 // Proves the substitute path emits the SAME structural variant the
8535 // bytecode path emits — `missing required arg` rejection is
8536 // path-uniform across both expansion strategies.
8537 let mut e = Expander::new_substitute_only();
8538 let err = e
8539 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8540 .expect_err("missing required macro arg must error");
8541 let (macro_name, param) = missing_macro_arg_fields(&err);
8542 assert_eq!(macro_name, "need-two");
8543 assert_eq!(param, "b");
8544 }
8545
8546 #[test]
8547 fn missing_macro_arg_first_position_is_named() {
8548 // `(f)` against `(f a b)` — `a` (the FIRST required param) has no
8549 // arg. The variant names `a`, not `b` — naming the LEFTMOST
8550 // un-bound param is the shape `bind_args` / `apply_compiled` both
8551 // emit (each iterates positionally and bails on the first missing
8552 // slot). Pins the leftmost-bail contract so a regression that
8553 // names the rightmost (or a surplus) param fails-loudly.
8554 let mut e = Expander::new();
8555 let err = e
8556 .expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
8557 .expect_err("missing first required arg must error");
8558 let (macro_name, param) = missing_macro_arg_fields(&err);
8559 assert_eq!(macro_name, "f");
8560 assert_eq!(param, "a");
8561 }
8562
8563 #[test]
8564 fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
8565 // Path-uniform rejection: the SAME source emits the SAME structural
8566 // variant under both expansion strategies. Negative control for
8567 // the divergence-closing posture: a future refactor that drifts
8568 // either path's rejection shape (or drops one path's rejection
8569 // entirely) fails-loudly here. Sibling of
8570 // `splice_outside_list_substitute_and_bytecode_paths_agree` —
8571 // both close `THEORY.md §II.1 invariant 2 — free middle` for one
8572 // failure mode each.
8573 let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
8574 let mut subst = Expander::new_substitute_only();
8575 let mut bytecode = Expander::new();
8576 let err_subst = subst
8577 .expand_program(read(src).unwrap())
8578 .expect_err("substitute must error");
8579 let err_byte = bytecode
8580 .expand_program(read(src).unwrap())
8581 .expect_err("bytecode must error");
8582 assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
8583 assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
8584 }
8585
8586 #[test]
8587 fn missing_macro_arg_position_is_none_today() {
8588 // Negative control for the future-spans move: until `Sexp` carries
8589 // source positions, `position()` returns `None` for this variant.
8590 // A future run that gives `Sexp` source spans adds `pos:
8591 // Option<usize>` to ONE place; this test gives that change a
8592 // deliberate fail-before/pass-after delta. Parallel to
8593 // `splice_outside_list_position_is_none_today`.
8594 let mut e = Expander::new();
8595 let err = e
8596 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8597 .expect_err("missing required macro arg must error");
8598 assert_eq!(err.position(), None);
8599 }
8600
8601 #[test]
8602 fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
8603 // End-to-end through the Display impl — pins the rendered diagnostic
8604 // a downstream tool sees today (REPL, tatara-check). The legacy
8605 // substring `"missing required arg: {param}"` is preserved verbatim
8606 // AND the head clause names the failing macro via `"call to
8607 // {macro_name}"`; tools that pattern-match on the variant gain
8608 // structural binding to `macro_name` / `param`.
8609 let mut e = Expander::new();
8610 let err = e
8611 .expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
8612 .expect_err("missing required macro arg must error");
8613 assert_eq!(
8614 format!("{err}"),
8615 "compile error in call to need-two: missing required arg: b"
8616 );
8617 }
8618
8619 #[test]
8620 fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
8621 // Both `macro_name` (`wrap-twice`) and `param` (`notify-ref`)
8622 // round-trip through the variant unchanged. Pinning this contract
8623 // means a regression that camelCases or lowercases either side
8624 // fails-loudly here. Parallel to the
8625 // `unknown_kwarg_display_carries_kebab_case_keys_unchanged`
8626 // assertion for the kwarg-gate's symmetric surface.
8627 let mut e = Expander::new();
8628 let err = e
8629 .expand_program(
8630 read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
8631 .unwrap(),
8632 )
8633 .expect_err("missing required macro arg must error");
8634 let (macro_name, param) = missing_macro_arg_fields(&err);
8635 assert_eq!(macro_name, "wrap-twice");
8636 assert_eq!(param, "body");
8637 }
8638
8639 #[test]
8640 fn rest_param_only_macro_with_no_args_still_succeeds() {
8641 // Negative control: a macro whose only param is `&rest` must NOT
8642 // error when called with zero args — the rest-param binds to the
8643 // empty list. The new structural variant fires only on REQUIRED
8644 // params; the `Param::Rest` arm in both `bind_args` and
8645 // `apply_compiled` continues to bind the empty tail. Pins that the
8646 // helper is scoped to required-param failure, not all
8647 // arity-mismatch shapes.
8648 let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
8649 let mut subst = Expander::new_substitute_only();
8650 let mut bytecode = Expander::new();
8651 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8652 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8653 assert_eq!(out_subst, out_byte);
8654 assert_eq!(out_subst[0], parse("(list)"));
8655 }
8656
8657 // ── TooManyMacroArgs: call-site mirror of RestParamTrailingTokens ──
8658 //
8659 // A rest-less param list has a FIXED maximum arity equal to
8660 // `required.len() + optional.len()`. Surplus call args have nowhere to
8661 // bind. Before this gate the surplus was silently truncated to the
8662 // slice the binder could consume — the typed-entry macro-call-gate
8663 // rejected too-few-args loudly (`MissingMacroArg`) but accepted
8664 // too-many silently, an asymmetry the definition-side `&rest <name>
8665 // extra` rejection (`RestParamTrailingTokens`) had no call-side dual.
8666 // After this gate the call-site arity surface is structurally
8667 // complete in both directions; the substitute + bytecode paths share
8668 // `MacroParams::bind`, so both inherit the rejection without drift.
8669
8670 /// Helper for the too-many-args tests — projects to (macro_name,
8671 /// expected, got) for legibility. Sibling of `missing_macro_arg_fields`.
8672 fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
8673 match err {
8674 LispError::TooManyMacroArgs {
8675 macro_name,
8676 expected,
8677 got,
8678 } => (macro_name.as_str(), *expected, *got),
8679 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
8680 }
8681 }
8682
8683 #[test]
8684 fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
8685 // `(defmacro f (a b) ...)` called as `(f 1 2 3)` — `3` has
8686 // nowhere to bind. The rest-less binder rejects via
8687 // `TooManyMacroArgs { macro_name: "f", expected: 2, got: 3 }`,
8688 // NOT silently drops `3`. Pins both the variant identity AND the
8689 // structural fields the typed gate exposes for authoring-tool
8690 // quick-fixes ("you supplied 3 args; the macro takes at most
8691 // 2").
8692 let mut e = Expander::new();
8693 let err = e
8694 .expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8695 .expect_err("surplus arg on rest-less call must error");
8696 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8697 assert_eq!(macro_name, "f");
8698 assert_eq!(expected, 2);
8699 assert_eq!(got, 3);
8700 }
8701
8702 #[test]
8703 fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
8704 // The rest-less binder's fixed maximum arity is `required.len() +
8705 // optional.len()` — the optional section CONTRIBUTES to capacity.
8706 // `(defmacro f (a &optional b) ...)` accepts 1 OR 2 args; 3
8707 // args rejects with `expected: 2` (required + optional, NOT just
8708 // required). Pins the optional-counts-in-capacity contract so a
8709 // regression that omits optionals from the expected calculation
8710 // (and erroneously rejects 2-arg calls) fails-loudly here.
8711 let mut e = Expander::new();
8712 let err = e
8713 .expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
8714 .expect_err("surplus arg beyond required+optional must error");
8715 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8716 assert_eq!(macro_name, "f");
8717 assert_eq!(expected, 2);
8718 assert_eq!(got, 3);
8719 }
8720
8721 #[test]
8722 fn too_many_macro_args_required_plus_two_optionals_arity_three() {
8723 // Larger optional section (capacity 1 + 2 = 3). 4 args rejects
8724 // with `expected: 3`. Pins that the capacity calculation scales
8725 // with optional.len(), not just at-most-one. Mixes a bare
8726 // optional with an optional carrying a default form — both shapes
8727 // contribute identically to capacity (the typed `OptionalParam`
8728 // entry's `default: Option<Sexp>` is irrelevant to the arity gate).
8729 let mut e = Expander::new();
8730 let err = e
8731 .expand_program(
8732 read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
8733 )
8734 .expect_err("surplus arg beyond required+two-optional must error");
8735 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8736 assert_eq!(macro_name, "f");
8737 assert_eq!(expected, 3);
8738 assert_eq!(got, 4);
8739 }
8740
8741 #[test]
8742 fn too_many_macro_args_does_not_fire_when_rest_is_present() {
8743 // Negative control: a rest-PRESENT param list has no maximum
8744 // arity — the `&rest` slot collects every trailing arg into a
8745 // `Sexp::List`. `(defmacro f (a &rest xs) ...)` called as
8746 // `(f 1 2 3 4)` MUST succeed; the new gate fires ONLY when
8747 // `MacroParams.rest` is `None`. Pins the rest-present-path
8748 // remains permissive — a regression that wrongly fires the
8749 // too-many gate for any surplus (including the rest-collecting
8750 // path) would break every `&rest`-using macro.
8751 let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
8752 let mut subst = Expander::new_substitute_only();
8753 let mut bytecode = Expander::new();
8754 let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
8755 let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
8756 assert_eq!(out_subst, out_byte);
8757 assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
8758 }
8759
8760 #[test]
8761 fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
8762 // Negative control: the rest-less gate fires STRICTLY when
8763 // `args.len() > expected` — at exact arity the binder accepts.
8764 // `(defmacro f (a &optional b) ...)` called as `(f 1 2)` binds
8765 // a=1, b=2 successfully (the optional takes its supplied arg,
8766 // not the default). Pins the boundary condition so a regression
8767 // that flips the comparison to `>=` (rejecting exact-arity
8768 // calls) fails-loudly here.
8769 let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
8770 let mut e = Expander::new();
8771 let out = e.expand_program(read(src).unwrap()).unwrap();
8772 assert_eq!(out[0], parse("(list 1 2)"));
8773 }
8774
8775 #[test]
8776 fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
8777 // Path-uniform rejection: the SAME source emits the SAME
8778 // structural variant under both expansion strategies. The
8779 // shared `MacroParams::bind` makes the rejection lands once and
8780 // both paths inherit it. Mirror of
8781 // `missing_macro_arg_substitute_and_bytecode_paths_agree` —
8782 // both close `THEORY.md §II.1 invariant 2 — free middle` for
8783 // one failure mode each.
8784 let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
8785 let mut subst = Expander::new_substitute_only();
8786 let mut bytecode = Expander::new();
8787 let err_subst = subst
8788 .expand_program(read(src).unwrap())
8789 .expect_err("substitute must error");
8790 let err_byte = bytecode
8791 .expand_program(read(src).unwrap())
8792 .expect_err("bytecode must error");
8793 assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
8794 assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
8795 }
8796
8797 #[test]
8798 fn too_many_macro_args_fires_after_missing_required_priority_held() {
8799 // Priority discipline: the required walk fires
8800 // `MissingMacroArg` BEFORE the rest-less surplus gate is
8801 // reached. `(defmacro f (a b c) …) (f 1)` is `MissingMacroArg
8802 // { param: "b" }`, NOT `TooManyMacroArgs` (and certainly not a
8803 // collision). The two failure modes are structurally disjoint:
8804 // too-few-required vs. too-many-with-no-rest. Pins the bail-on-
8805 // first-missing-required contract so a regression that swaps
8806 // the two gates' order would emit the wrong variant.
8807 let mut e = Expander::new();
8808 let err = e
8809 .expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
8810 .expect_err("missing required must error");
8811 assert!(
8812 matches!(err, LispError::MissingMacroArg { .. }),
8813 "expected MissingMacroArg (priority), got: {err:?}"
8814 );
8815 }
8816
8817 #[test]
8818 fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
8819 // Degenerate case: a nullary macro `(defmacro f () ...)` has
8820 // capacity 0; ANY supplied arg rejects with `expected: 0`. Pins
8821 // the gate fires even when the rest-less max-arity is zero —
8822 // i.e. the rejection is structural, not conditional on a
8823 // non-empty required+optional.
8824 let mut e = Expander::new();
8825 let err = e
8826 .expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
8827 .expect_err("nullary macro called with arg must error");
8828 let (macro_name, expected, got) = too_many_macro_args_fields(&err);
8829 assert_eq!(macro_name, "f");
8830 assert_eq!(expected, 0);
8831 assert_eq!(got, 1);
8832 }
8833
8834 #[test]
8835 fn too_many_macro_args_display_renders_legacy_compile_substring() {
8836 // The rendered Display matches the legacy `Compile`-shaped
8837 // diagnostic style — `"compile error in call to {macro_name}:
8838 // too many args: expected at most {expected}, got {got}"` — so
8839 // the existing `"compile error in call to"` substring authoring
8840 // tools' assertions key on stays unchanged. Pins the byte-level
8841 // rendered shape so a regression that drifts the prefix /
8842 // separator / labels fails-loudly here.
8843 let err = LispError::TooManyMacroArgs {
8844 macro_name: "pair".into(),
8845 expected: 2,
8846 got: 5,
8847 };
8848 assert_eq!(
8849 err.to_string(),
8850 "compile error in call to pair: too many args: expected at most 2, got 5"
8851 );
8852 }
8853
8854 #[test]
8855 fn too_many_macro_args_position_is_none_today() {
8856 // Negative control for the future-spans move: until `Sexp`
8857 // carries source positions, `position()` returns `None` for
8858 // this variant. A future run that gives `Sexp` source spans
8859 // adds `pos: Option<usize>` to ONE place; this test gives that
8860 // change a deliberate fail-before/pass-after delta. Parallel to
8861 // `missing_macro_arg_position_is_none_today`.
8862 let err = LispError::TooManyMacroArgs {
8863 macro_name: "pair".into(),
8864 expected: 2,
8865 got: 3,
8866 };
8867 assert_eq!(err.position(), None);
8868 }
8869
8870 /// Helper for the non-symbol-param tests — pins the variant shape and
8871 /// carries the failing position + offending element up to the assert
8872 /// site for legibility. Sibling of `missing_macro_arg_fields`.
8873 fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
8874 match err {
8875 LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
8876 other => panic!("expected NonSymbolParam, got: {other:?}"),
8877 }
8878 }
8879
8880 #[test]
8881 fn non_symbol_param_at_first_position_emits_structural_variant() {
8882 // `(defmacro f (5) ...)` — the first element of the param list is
8883 // an integer literal, not a symbol. Pins variant identity AND
8884 // that `position` is the loop index inside `parse_params` (0 for
8885 // the first slot) AND that `got` is the offending element via
8886 // `Sexp::Display` (`5`). A regression that re-inlines the legacy
8887 // `LispError::Compile` shape (which named neither the position
8888 // nor the offending element) fails-loudly here.
8889 let mut e = Expander::new();
8890 let err = e
8891 .expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
8892 .expect_err("non-symbol param must error");
8893 let (position, got) = non_symbol_param_fields(&err);
8894 assert_eq!(position, 0);
8895 assert_eq!(got, "5");
8896 }
8897
8898 #[test]
8899 fn non_symbol_param_at_second_position_emits_structural_variant() {
8900 // `(defmacro f (a 5) ...)` — `a` parses fine, `5` at position 1
8901 // misfires. Pins that `position` advances with the loop index, so
8902 // an LSP quick-fix that wants to point at "the second element of
8903 // your param list" gains the index as data, no source re-parse
8904 // required.
8905 let mut e = Expander::new();
8906 let err = e
8907 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
8908 .expect_err("non-symbol param must error");
8909 let (position, got) = non_symbol_param_fields(&err);
8910 assert_eq!(position, 1);
8911 assert_eq!(got, "5");
8912 }
8913
8914 #[test]
8915 fn non_symbol_param_carries_keyword_value_unchanged() {
8916 // `:k` at a param-list position. `Sexp::Display` for
8917 // `Atom::Keyword(s)` writes `:s`; pins that the variant's `got`
8918 // field round-trips the keyword form unchanged so an LSP that
8919 // surfaces "you wrote `:k` where a symbol was expected" gains
8920 // the literal keyword value as data, no re-parsing required.
8921 let mut e = Expander::new();
8922 let err = e
8923 .expand_program(read("(defmacro f (:k) `(list))").unwrap())
8924 .expect_err("non-symbol param must error");
8925 let (position, got) = non_symbol_param_fields(&err);
8926 assert_eq!(position, 0);
8927 assert_eq!(got, ":k");
8928 }
8929
8930 #[test]
8931 fn non_symbol_param_carries_nested_list_value_unchanged() {
8932 // A nested list at a param-list position. `Sexp::Display` for
8933 // `List(xs)` writes `(<x1> <x2> ...)`; pins that the variant's
8934 // `got` field carries the nested form's full Display projection
8935 // unchanged so the operator sees what they wrote.
8936 let mut e = Expander::new();
8937 let err = e
8938 .expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
8939 .expect_err("non-symbol param must error");
8940 let (position, got) = non_symbol_param_fields(&err);
8941 assert_eq!(position, 0);
8942 assert_eq!(got, "(nested)");
8943 }
8944
8945 #[test]
8946 fn non_symbol_param_in_defpoint_template_emits_same_variant() {
8947 // `defpoint-template` shares `parse_params` with `defmacro` (all
8948 // three head keywords route through `macro_def_from`). Pins that
8949 // the lift fires path-uniformly across the three head keywords
8950 // — `defmacro`, `defpoint-template`, `defcheck` — so the
8951 // structural-completeness floor holds for every defmacro-shaped
8952 // form, not just the one with the `defmacro` head literal.
8953 let mut e = Expander::new();
8954 let err = e
8955 .expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
8956 .expect_err("non-symbol param must error");
8957 let (position, got) = non_symbol_param_fields(&err);
8958 assert_eq!(position, 0);
8959 assert_eq!(got, "5");
8960 }
8961
8962 #[test]
8963 fn non_symbol_param_in_defcheck_emits_same_variant() {
8964 // Sibling of the defpoint-template test — `defcheck` is the
8965 // third head keyword `macro_def_from` recognizes. All three
8966 // route through the same `parse_params` and now reject
8967 // non-symbol params with the same structural variant.
8968 let mut e = Expander::new();
8969 let err = e
8970 .expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
8971 .expect_err("non-symbol param must error");
8972 let (position, got) = non_symbol_param_fields(&err);
8973 assert_eq!(position, 1);
8974 assert_eq!(got, "5");
8975 }
8976
8977 #[test]
8978 fn non_symbol_param_position_is_none_today() {
8979 // Negative control for the future-spans move: until `Sexp`
8980 // carries source positions, `position()` on `LispError` returns
8981 // `None` for this variant. A future run that gives `Sexp`
8982 // source spans adds `pos: Option<usize>` to ONE place; this
8983 // test gives that change a deliberate fail-before/pass-after
8984 // delta. Parallel to `missing_macro_arg_position_is_none_today`.
8985 let mut e = Expander::new();
8986 let err = e
8987 .expand_program(read("(defmacro f (5) `(list))").unwrap())
8988 .expect_err("non-symbol param must error");
8989 assert_eq!(err.position(), None);
8990 }
8991
8992 #[test]
8993 fn non_symbol_param_message_renders_legacy_substring_with_position() {
8994 // End-to-end through Display — pins the rendered diagnostic that
8995 // downstream tools (REPL, `tatara-check`) see today. Legacy
8996 // substrings `"defmacro params"` AND `"expected symbol"` are
8997 // preserved verbatim; the appended `at position {position}, got
8998 // {got}` clause is the new structural detail. Tools that
8999 // pattern-match on the variant gain structural binding to
9000 // `position` / `got`.
9001 let mut e = Expander::new();
9002 let err = e
9003 .expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
9004 .expect_err("non-symbol param must error");
9005 assert_eq!(
9006 format!("{err}"),
9007 "compile error in defmacro params: \
9008 expected symbol at position 1, got 5"
9009 );
9010 }
9011
9012 #[test]
9013 fn non_symbol_param_substitute_and_bytecode_paths_agree() {
9014 // Path-uniform rejection: the SAME source emits the SAME
9015 // structural variant under both expansion strategies. The
9016 // defmacro-syntax-gate fires inside `macro_def_from` →
9017 // `parse_params`, BEFORE either strategy's expansion path runs;
9018 // so both `Expander::new()` (bytecode) and
9019 // `Expander::new_substitute_only()` (substitute) reject the
9020 // SAME malformed defmacro at the SAME gate. Sibling of
9021 // `missing_macro_arg_substitute_and_bytecode_paths_agree`.
9022 let src = "(defmacro f (a 5) `(,a))";
9023 let mut subst = Expander::new_substitute_only();
9024 let mut bytecode = Expander::new();
9025 let err_subst = subst
9026 .expand_program(read(src).unwrap())
9027 .expect_err("substitute must error");
9028 let err_byte = bytecode
9029 .expand_program(read(src).unwrap())
9030 .expect_err("bytecode must error");
9031 assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
9032 assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
9033 }
9034
9035 /// Helper for the rest-param-missing-name tests — pins the variant
9036 /// shape and carries the marker position + offending follower (or
9037 /// its absence) up to the assert site for legibility. Sibling of
9038 /// `non_symbol_param_fields`.
9039 fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
9040 match err {
9041 LispError::RestParamMissingName { rest_position, got } => {
9042 (*rest_position, got.as_ref().map(|w| w.display.as_str()))
9043 }
9044 other => panic!("expected RestParamMissingName, got: {other:?}"),
9045 }
9046 }
9047
9048 #[test]
9049 fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
9050 // `(defmacro f (&rest))` — the marker is the only param-list
9051 // element; nothing follows. Pins variant identity AND that
9052 // `rest_position == 0` (the first slot) AND that `got == None`
9053 // (no follower exists). A regression that re-inlines the legacy
9054 // `LispError::Compile` shape (which named neither field) fails-
9055 // loudly here.
9056 let mut e = Expander::new();
9057 let err = e
9058 .expand_program(read("(defmacro f (&rest) `(list))").unwrap())
9059 .expect_err("&rest with no follower must error");
9060 let (rest_position, got) = rest_param_missing_name_fields(&err);
9061 assert_eq!(rest_position, 0);
9062 assert_eq!(got, None);
9063 }
9064
9065 #[test]
9066 fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
9067 // `(defmacro f (a &rest))` — `a` parses fine, `&rest` at param-list
9068 // position 1 has no follower at all. Pins that `rest_position`
9069 // advances with the loop index, so an LSP quick-fix that wants to
9070 // point at "your `&rest` at position 1 has no name" gains the
9071 // marker position as data, no source re-parse required.
9072 let mut e = Expander::new();
9073 let err = e
9074 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9075 .expect_err("&rest with no follower must error");
9076 let (rest_position, got) = rest_param_missing_name_fields(&err);
9077 assert_eq!(rest_position, 1);
9078 assert_eq!(got, None);
9079 }
9080
9081 #[test]
9082 fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
9083 // `(defmacro f (&rest 5))` — `&rest` at position 0 followed by
9084 // `5` (an integer literal, not a symbol). Pins that the variant's
9085 // `got` field is `Some` and carries the offending follower's
9086 // `Sexp::Display` projection; the bifurcation between "missing
9087 // entirely" and "present but non-symbol" is in the renderable
9088 // detail, not in what the gate rejects.
9089 let mut e = Expander::new();
9090 let err = e
9091 .expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
9092 .expect_err("&rest followed by non-symbol must error");
9093 let (rest_position, got) = rest_param_missing_name_fields(&err);
9094 assert_eq!(rest_position, 0);
9095 assert_eq!(got, Some("5"));
9096 }
9097
9098 #[test]
9099 fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
9100 // `(defmacro f (a &rest :foo))` — keyword follower at the rest-name
9101 // slot. `Sexp::Display` for `Atom::Keyword(s)` writes `:s`; pins
9102 // that the variant's `got` field round-trips the keyword form
9103 // unchanged so an LSP that surfaces "you wrote `:foo` where a
9104 // rest-name was expected" gains the literal keyword value as
9105 // data, no re-parsing required.
9106 let mut e = Expander::new();
9107 let err = e
9108 .expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
9109 .expect_err("&rest followed by keyword must error");
9110 let (rest_position, got) = rest_param_missing_name_fields(&err);
9111 assert_eq!(rest_position, 1);
9112 assert_eq!(got, Some(":foo"));
9113 }
9114
9115 #[test]
9116 fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
9117 // `(defmacro f (&rest (nested)))` — nested-list follower at the
9118 // rest-name slot. `Sexp::Display` for `List(xs)` writes
9119 // `(<x1> <x2> ...)`; pins that the variant's `got` field carries
9120 // the nested form's full Display projection unchanged so the
9121 // operator sees what they wrote.
9122 let mut e = Expander::new();
9123 let err = e
9124 .expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
9125 .expect_err("&rest followed by list must error");
9126 let (rest_position, got) = rest_param_missing_name_fields(&err);
9127 assert_eq!(rest_position, 0);
9128 assert_eq!(got, Some("(nested)"));
9129 }
9130
9131 #[test]
9132 fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
9133 // `defpoint-template` shares `parse_params` with `defmacro` (all
9134 // three head keywords route through `macro_def_from`). Pins that
9135 // the lift fires path-uniformly across the three head keywords —
9136 // a regression that handles `defpoint-template`'s param list
9137 // differently from `defmacro`'s would fail-loudly here.
9138 let mut e = Expander::new();
9139 let err = e
9140 .expand_program(read("(defpoint-template t (a &rest) `(,a))").unwrap())
9141 .expect_err("&rest with no follower must error");
9142 let (rest_position, got) = rest_param_missing_name_fields(&err);
9143 assert_eq!(rest_position, 1);
9144 assert_eq!(got, None);
9145 }
9146
9147 #[test]
9148 fn rest_param_missing_name_in_defcheck_emits_same_variant() {
9149 // Sibling for the `defcheck` head; rounds out the three-head-
9150 // keyword coverage so the lift is path-uniform across
9151 // `defmacro` / `defpoint-template` / `defcheck`. After this
9152 // test the defmacro-syntax-gate rejects `&rest`-without-name
9153 // identically across all three head keywords — the
9154 // typed-entry surface is single-shape across the cluster.
9155 let mut e = Expander::new();
9156 let err = e
9157 .expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
9158 .expect_err("&rest followed by non-symbol must error");
9159 let (rest_position, got) = rest_param_missing_name_fields(&err);
9160 assert_eq!(rest_position, 0);
9161 assert_eq!(got, Some("5"));
9162 }
9163
9164 #[test]
9165 fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
9166 // Path-uniform rejection: the SAME source emits the SAME
9167 // structural variant under both expansion strategies. The
9168 // defmacro-syntax-gate fires inside `macro_def_from` →
9169 // `parse_params`, BEFORE either strategy's expansion path
9170 // runs; so both `Expander::new()` (bytecode) and
9171 // `Expander::new_substitute_only()` (substitute) reject the
9172 // SAME malformed defmacro at the SAME gate. Sibling of
9173 // `non_symbol_param_substitute_and_bytecode_paths_agree`.
9174 let src = "(defmacro f (a &rest 5) `(,a))";
9175 let mut subst = Expander::new_substitute_only();
9176 let mut bytecode = Expander::new();
9177 let err_subst = subst
9178 .expand_program(read(src).unwrap())
9179 .expect_err("substitute must error");
9180 let err_byte = bytecode
9181 .expand_program(read(src).unwrap())
9182 .expect_err("bytecode must error");
9183 assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
9184 assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
9185 }
9186
9187 #[test]
9188 fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
9189 // End-to-end through Display — pins the rendered diagnostic
9190 // consumers see today (REPL, tatara-check) AND the new `(rest
9191 // marker at position {rest_position}, got {got})` clause. The
9192 // legacy `"&rest needs a name"` substring rides through
9193 // verbatim.
9194 let mut e = Expander::new();
9195 let err = e
9196 .expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
9197 .expect_err("&rest followed by non-symbol must error");
9198 assert_eq!(
9199 format!("{err}"),
9200 "compile error in defmacro params: &rest needs a name \
9201 (rest marker at position 1, got 5)"
9202 );
9203 }
9204
9205 #[test]
9206 fn rest_param_missing_name_message_renders_none_provided_when_follower_absent() {
9207 // Same as the prior test but for the "missing entirely" branch.
9208 // The renderable detail is `(rest marker at position
9209 // {rest_position}, none provided)` — naming the absence
9210 // structurally instead of an empty / partial parenthetical.
9211 let mut e = Expander::new();
9212 let err = e
9213 .expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
9214 .expect_err("&rest with no follower must error");
9215 assert_eq!(
9216 format!("{err}"),
9217 "compile error in defmacro params: &rest needs a name \
9218 (rest marker at position 1, none provided)"
9219 );
9220 }
9221
9222 #[test]
9223 fn rest_param_missing_name_position_is_none_today() {
9224 // Pins that `position()` returns `None` so the future `pos:
9225 // Option<usize>` add (once `Sexp` carries source spans) lands
9226 // as a deliberate fail-before/pass-after delta rather than a
9227 // silent default. Parallel to
9228 // `non_symbol_param_position_is_none_today` and
9229 // `missing_macro_arg_position_is_none_today`.
9230 let err_missing = LispError::RestParamMissingName {
9231 rest_position: 1,
9232 got: None,
9233 };
9234 assert_eq!(err_missing.position(), None);
9235 let err_got = LispError::RestParamMissingName {
9236 rest_position: 0,
9237 got: Some(crate::error::SexpWitness::new(
9238 crate::error::SexpShape::Int,
9239 "5",
9240 )),
9241 };
9242 assert_eq!(err_got.position(), None);
9243 }
9244
9245 // --- RestParamTrailingTokens: the parse_params gate's third (and
9246 // final) definition-site failure mode ---
9247 //
9248 // A `&rest <name>` absorbs every remaining call arg, so it is the LAST
9249 // thing a param list can name. Before this variant `parse_params`
9250 // returned the moment it bound the rest name, SILENTLY DROPPING any
9251 // trailing tokens — `(a &rest xs extra)` parsed as if `extra` weren't
9252 // there. These tests pin the loud rejection that replaces the silent
9253 // drop; the symbol `RestParamTrailingTokens` exists only after this
9254 // change, so the whole block is fail-before/pass-after by construction
9255 // (compile-time edge) and the end-to-end regression guard below pins
9256 // that the malformed defmacro no longer expands cleanly.
9257
9258 /// Helper mirroring `rest_param_missing_name_fields` — pins the variant
9259 /// shape and lifts the marker position, trailing count, and first
9260 /// offender's display up to the assert site.
9261 fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
9262 match err {
9263 LispError::RestParamTrailingTokens {
9264 rest_position,
9265 extra,
9266 first,
9267 } => (*rest_position, *extra, first.display.as_str()),
9268 other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
9269 }
9270 }
9271
9272 #[test]
9273 fn parse_params_rejects_single_trailing_token_after_rest_name() {
9274 // `(a &rest c extra)` — `&rest c` is well-formed, but `extra`
9275 // follows. The rest name is bound at position 2, the marker at 1;
9276 // the lone trailing token `extra` is reported (extra == 1, first ==
9277 // "extra"). Before this variant `parse_params` returned at the rest
9278 // name and `extra` vanished.
9279 let err = parse_params(&read("a &rest c extra").unwrap())
9280 .expect_err("a trailing token after the rest name must error");
9281 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9282 }
9283
9284 #[test]
9285 fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
9286 // `(&rest c x y z)` — three tokens follow the rest name. `extra`
9287 // counts ALL of them (3), `first` is the first (`x`), and the
9288 // marker is at position 0. A regression that reports only the
9289 // first trailing token's presence (extra hard-coded to 1) fails
9290 // loudly here.
9291 let err = parse_params(&read("&rest c x y z").unwrap())
9292 .expect_err("multiple trailing tokens must error");
9293 assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
9294 }
9295
9296 #[test]
9297 fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
9298 // `(a &rest c 5)` — the rest NAME `c` is a valid symbol, so this is
9299 // NOT a `RestParamMissingName`; the integer `5` is a trailing token
9300 // AFTER a well-formed `&rest c`. Pins that the two sibling failure
9301 // modes don't collide: a malformed rest-name is `RestParamMissingName`,
9302 // a well-formed rest-name followed by junk is
9303 // `RestParamTrailingTokens`. `first` round-trips `5` via the typed
9304 // witness's `Sexp::Display` projection.
9305 let err = parse_params(&read("a &rest c 5").unwrap())
9306 .expect_err("a trailing non-symbol after the rest name must error");
9307 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
9308 }
9309
9310 #[test]
9311 fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
9312 // The fidelity fix, end-to-end through `expand_program`: a defmacro
9313 // whose param list carries a stray token after `&rest <name>` now
9314 // ERRORS at the typed-entry gate instead of expanding as though the
9315 // stray token weren't there. This is the regression guard for the
9316 // silent-drop bug — before this change the same source expanded
9317 // cleanly and `extra` was discarded with no signal.
9318 let mut e = Expander::new();
9319 let err = e
9320 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9321 .expect_err("trailing token after &rest name must error");
9322 assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
9323 }
9324
9325 #[test]
9326 fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
9327 // Path-uniform rejection: the gate fires inside `macro_def_from` →
9328 // `parse_params`, BEFORE either expansion strategy runs, so both
9329 // `Expander::new()` (bytecode) and `Expander::new_substitute_only()`
9330 // (substitute) reject the SAME malformed defmacro at the SAME gate.
9331 // Sibling of `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9332 let src = "(defmacro f (a &rest xs extra) `(,a))";
9333 let mut subst = Expander::new_substitute_only();
9334 let mut bytecode = Expander::new();
9335 let err_subst = subst
9336 .expand_program(read(src).unwrap())
9337 .expect_err("substitute must error");
9338 let err_byte = bytecode
9339 .expand_program(read(src).unwrap())
9340 .expect_err("bytecode must error");
9341 assert_eq!(
9342 rest_param_trailing_tokens_fields(&err_subst),
9343 (1, 1, "extra")
9344 );
9345 assert_eq!(
9346 rest_param_trailing_tokens_fields(&err_byte),
9347 (1, 1, "extra")
9348 );
9349 }
9350
9351 #[test]
9352 fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
9353 // End-to-end through Display — pins the rendered diagnostic AND the
9354 // new `(rest marker at position {n}, {extra} trailing after name,
9355 // first: {first})` clause. The `compile error in defmacro params:`
9356 // prefix matches the sibling `&rest needs a name` rendering's shape.
9357 let mut e = Expander::new();
9358 let err = e
9359 .expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
9360 .expect_err("trailing token after &rest name must error");
9361 assert_eq!(
9362 format!("{err}"),
9363 "compile error in defmacro params: &rest name must be last \
9364 (rest marker at position 1, 1 trailing after name, first: extra)"
9365 );
9366 }
9367
9368 #[test]
9369 fn rest_param_trailing_tokens_position_is_none_today() {
9370 // Pins `position() == None` so the future `pos: Option<usize>` add
9371 // (once `Sexp` carries source spans) lands as a deliberate
9372 // fail-before/pass-after delta. Parallel to
9373 // `rest_param_missing_name_position_is_none_today`.
9374 let err = LispError::RestParamTrailingTokens {
9375 rest_position: 1,
9376 extra: 1,
9377 first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
9378 };
9379 assert_eq!(err.position(), None);
9380 }
9381
9382 // --- MacroDefHead enum (the closed-set lift) ---
9383 //
9384 // The next nine tests pin the typed-enum lift that closes the
9385 // three-times rule on the `head: &str → &'static str` projection
9386 // idiom previously inlined at FOUR sites (the `matches!` gate at
9387 // the top of `macro_def_from` plus the projection match inside
9388 // each of `defmacro_arity`, `defmacro_non_symbol_name`,
9389 // `defmacro_non_list_params`). Every test in this block names
9390 // `MacroDefHead` directly — the symbol exists only after the
9391 // lift, so the entire block is fail-before/pass-after by
9392 // construction (compile-time edge). Theory anchor: THEORY.md
9393 // §VI.1 — three-times rule; THEORY.md §V.1 — the closed set is
9394 // a TYPE rather than a `matches!` literal.
9395
9396 #[test]
9397 fn macro_def_head_from_keyword_recognizes_defmacro() {
9398 // Pins that `MacroDefHead::from_keyword("defmacro")` returns
9399 // `Some(MacroDefHead::Defmacro)` — the first of the three
9400 // canonical macro-definition head keywords. A regression that
9401 // re-inlines a `matches!`-only gate (without the typed-enum
9402 // projection) deletes `from_keyword` and fails-loudly here.
9403 assert_eq!(
9404 MacroDefHead::from_keyword("defmacro"),
9405 Some(MacroDefHead::Defmacro)
9406 );
9407 }
9408
9409 #[test]
9410 fn macro_def_head_from_keyword_recognizes_defpoint_template() {
9411 // Pins that `MacroDefHead::from_keyword("defpoint-template")`
9412 // returns `Some(MacroDefHead::DefpointTemplate)` — the second
9413 // of the three canonical head keywords. The `defpoint-template`
9414 // form is the K8s-as-processes authoring surface (see
9415 // tatara-process); `macro_def_from` must recognize it
9416 // identically to `defmacro` so the `(defpoint-template …)`
9417 // form's macro-style binding works the same way.
9418 assert_eq!(
9419 MacroDefHead::from_keyword("defpoint-template"),
9420 Some(MacroDefHead::DefpointTemplate)
9421 );
9422 }
9423
9424 #[test]
9425 fn macro_def_head_from_keyword_recognizes_defcheck() {
9426 // Pins that `MacroDefHead::from_keyword("defcheck")` returns
9427 // `Some(MacroDefHead::Defcheck)` — the third and final
9428 // canonical head keyword. The `defcheck` form is the
9429 // workspace-coherence authoring surface (see
9430 // tatara-reconciler/checks.lisp); `macro_def_from` must
9431 // recognize it identically to `defmacro` so user-defined
9432 // checks inherit the macro-style binding semantics.
9433 assert_eq!(
9434 MacroDefHead::from_keyword("defcheck"),
9435 Some(MacroDefHead::Defcheck)
9436 );
9437 }
9438
9439 #[test]
9440 fn macro_def_head_from_keyword_rejects_unknown() {
9441 // Pins that `MacroDefHead::from_keyword` returns `None` for
9442 // anything outside the closed set — a non-symbol keyword
9443 // (`"if"`), a near-miss spelling (`"defmacroo"`,
9444 // `"defcheckk"`), and the empty string. `macro_def_from`
9445 // depends on this `None` projection to mean "this form is
9446 // not a defmacro form" and walk past — a regression that
9447 // accidentally accepts a near-miss head (e.g. via a
9448 // lower-cased `EqualFold` match) would route `(defmacroo …)`
9449 // through the arity gate, which is wrong. Pins all four
9450 // canonical near-miss / non-canonical inputs.
9451 assert_eq!(MacroDefHead::from_keyword("if"), None);
9452 assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
9453 assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
9454 assert_eq!(MacroDefHead::from_keyword(""), None);
9455 }
9456
9457 #[test]
9458 fn macro_def_head_keyword_round_trips_each_variant() {
9459 // Pins that `MacroDefHead::keyword` returns the canonical
9460 // `&'static str` literal for each variant. Together with
9461 // `from_keyword` this closes the bidirectional projection:
9462 // for every canonical head keyword `s`, `MacroDefHead::
9463 // from_keyword(s).unwrap().keyword() == s`. The `&'static
9464 // str` lifetime on the return type is load-bearing — it's
9465 // what lets the `LispError::Defmacro*` variants carry
9466 // `head: &'static str` slots without an arbitrary owned
9467 // `String`. Pinning the `: &'static str` binding here
9468 // makes the lifetime requirement load-bearing in the test.
9469 let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
9470 let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
9471 let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
9472 assert_eq!(s_defmacro, "defmacro");
9473 assert_eq!(s_defpoint, "defpoint-template");
9474 assert_eq!(s_defcheck, "defcheck");
9475 }
9476
9477 #[test]
9478 fn macro_def_head_keyword_round_trips_through_from_keyword() {
9479 // Pins that the two halves of the projection compose to the
9480 // identity on the closed set: for every canonical head
9481 // keyword, projecting `&str → MacroDefHead → &'static str`
9482 // returns the original literal. Sibling of
9483 // `macro_def_head_keyword_round_trips_each_variant` —
9484 // together they pin both directions of the bidirection.
9485 for kw in ["defmacro", "defpoint-template", "defcheck"] {
9486 let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
9487 assert_eq!(head.keyword(), kw);
9488 }
9489 }
9490
9491 #[test]
9492 fn macro_def_head_threads_through_defmacro_arity_helper() {
9493 // Pins that `defmacro_arity` accepts a typed `MacroDefHead`
9494 // and threads it through to the variant's typed `head` slot
9495 // unchanged — no `&str` projection at the helper boundary
9496 // (the projection through `MacroDefHead::keyword()` happens at
9497 // Display rendering time inside the `#[error(...)]`
9498 // annotation). A regression that drops the `MacroDefHead`
9499 // parameter type (e.g. by reverting to `head: &str`) breaks
9500 // compilation here. Pinning each of the three variants gives
9501 // the typed-head threading the same path-uniformity edge the
9502 // existing `defmacro_arity_in_*_emits_same_variant` tests pin
9503 // for the call-site path through `macro_def_from`.
9504 for head in [
9505 MacroDefHead::Defmacro,
9506 MacroDefHead::DefpointTemplate,
9507 MacroDefHead::Defcheck,
9508 ] {
9509 let err = defmacro_arity(head, 2);
9510 match err {
9511 LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
9512 other => panic!("expected DefmacroArity, got: {other:?}"),
9513 }
9514 }
9515 }
9516
9517 #[test]
9518 fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
9519 // Sibling of the `defmacro_arity` threading test — pins that
9520 // `defmacro_non_symbol_name` accepts a typed `MacroDefHead`
9521 // and threads it through to the variant's typed `head` slot
9522 // unchanged. The `got: &Sexp` parameter rides through
9523 // `crate::domain::sexp_witness` into the variant's typed
9524 // `got: SexpWitness` slot so BOTH the structural shape AND
9525 // the rendered literal are preserved across the helper
9526 // boundary, parallel to how `non_symbol_param` and
9527 // `non_symbol_unquote_target` project their `&Sexp` arguments
9528 // through the same typed joint primitive.
9529 let got = parse("5");
9530 for head in [
9531 MacroDefHead::Defmacro,
9532 MacroDefHead::DefpointTemplate,
9533 MacroDefHead::Defcheck,
9534 ] {
9535 let err = defmacro_non_symbol_name(head, &got);
9536 match err {
9537 LispError::DefmacroNonSymbolName { head: h, got: g } => {
9538 assert_eq!(h, head);
9539 assert_eq!(g.shape, crate::error::SexpShape::Int);
9540 assert_eq!(g.display, "5");
9541 }
9542 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9543 }
9544 }
9545 }
9546
9547 #[test]
9548 fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
9549 // Sibling of the `defmacro_arity` and
9550 // `defmacro_non_symbol_name` threading tests — pins that
9551 // `defmacro_non_list_params` accepts a typed `MacroDefHead`
9552 // and threads it through to the variant's typed `head` slot
9553 // unchanged. Together the three threading tests close the
9554 // typed-enum lift across all three error helpers — every
9555 // call site that constructs a `LispError::Defmacro*` variant
9556 // takes its `head` from a `MacroDefHead`, never from a `&str`
9557 // match. The `got: &Sexp` parameter rides through
9558 // `crate::domain::sexp_witness` into the variant's typed
9559 // `got: SexpWitness` slot so BOTH the structural shape AND
9560 // the rendered literal are preserved across the helper
9561 // boundary, parallel to how `defmacro_non_symbol_name`,
9562 // `non_symbol_param`, and `non_symbol_unquote_target` project
9563 // their `&Sexp` arguments through the same typed joint
9564 // primitive.
9565 let got = parse("x");
9566 for head in [
9567 MacroDefHead::Defmacro,
9568 MacroDefHead::DefpointTemplate,
9569 MacroDefHead::Defcheck,
9570 ] {
9571 let err = defmacro_non_list_params(head, &got);
9572 match err {
9573 LispError::DefmacroNonListParams { head: h, got: g } => {
9574 assert_eq!(h, head);
9575 assert_eq!(g.shape, crate::error::SexpShape::Symbol);
9576 assert_eq!(g.display, "x");
9577 }
9578 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9579 }
9580 }
9581 }
9582
9583 /// Helper for the defmacro-arity tests — pins the variant shape and
9584 /// carries the head / arity up to the assert site for legibility.
9585 /// Sibling of `non_symbol_param_fields` and
9586 /// `rest_param_missing_name_fields`.
9587 fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
9588 match err {
9589 LispError::DefmacroArity { head, arity } => (*head, *arity),
9590 other => panic!("expected DefmacroArity, got: {other:?}"),
9591 }
9592 }
9593
9594 #[test]
9595 fn defmacro_arity_with_head_only_emits_structural_variant() {
9596 // `(defmacro)` — only the head, no name / params / body. Pins
9597 // variant identity AND that `arity == 1` (just the head
9598 // element) AND that `head == "defmacro"`. A regression that
9599 // re-inlines the legacy `LispError::Compile` shape (which
9600 // named neither field) fails-loudly here.
9601 let mut e = Expander::new();
9602 let err = e
9603 .expand_program(read("(defmacro)").unwrap())
9604 .expect_err("defmacro arity gate must error");
9605 let (head, arity) = defmacro_arity_fields(&err);
9606 assert_eq!(head, MacroDefHead::Defmacro);
9607 assert_eq!(arity, 1);
9608 }
9609
9610 #[test]
9611 fn defmacro_arity_with_head_and_name_emits_structural_variant() {
9612 // `(defmacro f)` — head + name, missing params + body. Pins
9613 // that `arity` advances with the actual form length (2 for
9614 // this case) so an LSP quick-fix that wants to surface "you
9615 // wrote 2 elements; need 4" gains the count as data, no
9616 // source re-parse required.
9617 let mut e = Expander::new();
9618 let err = e
9619 .expand_program(read("(defmacro f)").unwrap())
9620 .expect_err("defmacro arity gate must error");
9621 let (head, arity) = defmacro_arity_fields(&err);
9622 assert_eq!(head, MacroDefHead::Defmacro);
9623 assert_eq!(arity, 2);
9624 }
9625
9626 #[test]
9627 fn defmacro_arity_with_head_name_params_emits_structural_variant() {
9628 // `(defmacro f ())` — head + name + params, missing body
9629 // (the most-complete partial defmacro that still trips the
9630 // arity gate). Pins that `arity == 3` exactly so an LSP
9631 // quick-fix that wants to surface "your defmacro is one
9632 // element short — body is missing" gains the count as data.
9633 let mut e = Expander::new();
9634 let err = e
9635 .expand_program(read("(defmacro f ())").unwrap())
9636 .expect_err("defmacro arity gate must error");
9637 let (head, arity) = defmacro_arity_fields(&err);
9638 assert_eq!(head, MacroDefHead::Defmacro);
9639 assert_eq!(arity, 3);
9640 }
9641
9642 #[test]
9643 fn defmacro_arity_in_defpoint_template_emits_same_variant() {
9644 // `defpoint-template` shares `macro_def_from` with `defmacro`
9645 // (all three head keywords route through the same gate). Pins
9646 // that the lift fires path-uniformly across the three head
9647 // keywords AND that the variant's `head` slot carries the
9648 // actual head literal — `defpoint-template`, not `defmacro`
9649 // — so an LSP that wants to point at "your defpoint-template
9650 // form is missing elements" gains the head as data.
9651 let mut e = Expander::new();
9652 let err = e
9653 .expand_program(read("(defpoint-template t)").unwrap())
9654 .expect_err("defpoint-template arity gate must error");
9655 let (head, arity) = defmacro_arity_fields(&err);
9656 assert_eq!(head, MacroDefHead::DefpointTemplate);
9657 assert_eq!(arity, 2);
9658 }
9659
9660 #[test]
9661 fn defmacro_arity_in_defcheck_emits_same_variant() {
9662 // Sibling of the defpoint-template test — `defcheck` is the
9663 // third head keyword `macro_def_from` recognizes. All three
9664 // route through the same arity gate and now reject too-short
9665 // forms with the same structural variant.
9666 let mut e = Expander::new();
9667 let err = e
9668 .expand_program(read("(defcheck)").unwrap())
9669 .expect_err("defcheck arity gate must error");
9670 let (head, arity) = defmacro_arity_fields(&err);
9671 assert_eq!(head, MacroDefHead::Defcheck);
9672 assert_eq!(arity, 1);
9673 }
9674
9675 #[test]
9676 fn defmacro_arity_substitute_and_bytecode_paths_agree() {
9677 // Path-uniform rejection: the SAME source emits the SAME
9678 // structural variant under both expansion strategies. The
9679 // arity gate fires inside `macro_def_from` BEFORE either
9680 // strategy's expansion path runs; so both `Expander::new()`
9681 // (bytecode) and `Expander::new_substitute_only()`
9682 // (substitute) reject the SAME malformed defmacro at the
9683 // SAME gate. Sibling of
9684 // `non_symbol_param_substitute_and_bytecode_paths_agree` and
9685 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9686 let src = "(defmacro f)";
9687 let mut subst = Expander::new_substitute_only();
9688 let mut bytecode = Expander::new();
9689 let err_subst = subst
9690 .expand_program(read(src).unwrap())
9691 .expect_err("substitute must error");
9692 let err_byte = bytecode
9693 .expand_program(read(src).unwrap())
9694 .expect_err("bytecode must error");
9695 assert_eq!(
9696 defmacro_arity_fields(&err_subst),
9697 (MacroDefHead::Defmacro, 2)
9698 );
9699 assert_eq!(
9700 defmacro_arity_fields(&err_byte),
9701 (MacroDefHead::Defmacro, 2)
9702 );
9703 }
9704
9705 #[test]
9706 fn defmacro_arity_message_renders_legacy_substring_with_arity() {
9707 // End-to-end through Display — pins the rendered diagnostic
9708 // consumers see today (REPL, `tatara-check`) AND the new
9709 // `(got {arity} elements, need 4)` clause. The legacy
9710 // `"(defmacro name (params) body) required"` substring
9711 // rides through verbatim. Tools that pattern-match on the
9712 // variant gain structural binding to `head` / `arity`.
9713 let mut e = Expander::new();
9714 let err = e
9715 .expand_program(read("(defmacro f)").unwrap())
9716 .expect_err("defmacro arity gate must error");
9717 assert_eq!(
9718 format!("{err}"),
9719 "compile error in defmacro: (defmacro name (params) body) required \
9720 (got 2 elements, need 4)"
9721 );
9722 }
9723
9724 #[test]
9725 fn defmacro_arity_position_is_none_today() {
9726 // Negative control for the future-spans move: until `Sexp`
9727 // carries source positions, `position()` on `LispError`
9728 // returns `None` for this variant. A future run that gives
9729 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9730 // this test gives that change a deliberate fail-before/pass-
9731 // after delta. Parallel to
9732 // `non_symbol_param_position_is_none_today` and
9733 // `rest_param_missing_name_position_is_none_today`.
9734 let mut e = Expander::new();
9735 let err = e
9736 .expand_program(read("(defmacro)").unwrap())
9737 .expect_err("defmacro arity gate must error");
9738 assert_eq!(err.position(), None);
9739 }
9740
9741 #[test]
9742 fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
9743 // Negative control: a defmacro with exactly 4 elements (head
9744 // + name + params + body) passes the arity gate. Pins that
9745 // the lift is scoped to the arity-deficient case, not to
9746 // every defmacro form. After this test, a regression that
9747 // tightens the arity gate to >= 5 (e.g. spuriously requiring
9748 // a docstring slot) fails-loudly here.
9749 let mut e = Expander::new();
9750 let out = e
9751 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9752 .expect("well-formed defmacro must succeed");
9753 assert_eq!(out[0], Sexp::int(42));
9754 }
9755
9756 /// Helper for the defmacro-non-symbol-name tests — pins variant
9757 /// shape and carries the head / got up to the assert site for
9758 /// legibility. Sibling of `defmacro_arity_fields`,
9759 /// `non_symbol_param_fields`, and `rest_param_missing_name_fields`.
9760 fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
9761 match err {
9762 LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
9763 other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
9764 }
9765 }
9766
9767 #[test]
9768 fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
9769 // `(defmacro 5 () body)` — the form passes the arity gate
9770 // (4 elements) but list[1] is `5`, not a symbol. Pins variant
9771 // identity AND that `head == "defmacro"` AND that `got ==
9772 // "5"`. A regression that re-inlines the legacy
9773 // `LispError::Compile { form: "defmacro", message: "expected
9774 // name symbol" }` shape (which named the failure mode but
9775 // not the offending element) fails-loudly here.
9776 let mut e = Expander::new();
9777 let err = e
9778 .expand_program(read("(defmacro 5 () body)").unwrap())
9779 .expect_err("defmacro non-symbol name gate must error");
9780 let (head, got) = defmacro_non_symbol_name_fields(&err);
9781 assert_eq!(head, MacroDefHead::Defmacro);
9782 assert_eq!(got, "5");
9783 }
9784
9785 #[test]
9786 fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
9787 // `(defmacro :foo () body)` — list[1] is the keyword `:foo`,
9788 // not a symbol. Pins that `Sexp::Display` for
9789 // `Atom::Keyword(s)` writes `:s` and the variant's `got` slot
9790 // carries the keyword form unchanged. An LSP that wants to
9791 // surface "you wrote `:foo` where a name symbol was expected"
9792 // gains the literal keyword value as data, no source re-parse
9793 // required.
9794 let mut e = Expander::new();
9795 let err = e
9796 .expand_program(read("(defmacro :foo () body)").unwrap())
9797 .expect_err("defmacro non-symbol name gate must error");
9798 let (head, got) = defmacro_non_symbol_name_fields(&err);
9799 assert_eq!(head, MacroDefHead::Defmacro);
9800 assert_eq!(got, ":foo");
9801 }
9802
9803 #[test]
9804 fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
9805 // `(defmacro "name" () body)` — list[1] is the string
9806 // literal `"name"`, not a symbol. Pins that `Sexp::Display`
9807 // for `Atom::String(s)` writes `"s"` (with quotes) and the
9808 // variant's `got` slot carries the quoted form unchanged.
9809 let mut e = Expander::new();
9810 let err = e
9811 .expand_program(read("(defmacro \"name\" () body)").unwrap())
9812 .expect_err("defmacro non-symbol name gate must error");
9813 let (head, got) = defmacro_non_symbol_name_fields(&err);
9814 assert_eq!(head, MacroDefHead::Defmacro);
9815 assert_eq!(got, "\"name\"");
9816 }
9817
9818 #[test]
9819 fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
9820 // `(defmacro (nested) () body)` — list[1] is a nested list,
9821 // not a symbol. Pins that `Sexp::Display` for a list writes
9822 // `(elements)` and the variant's `got` slot carries the
9823 // parenthesized form unchanged.
9824 let mut e = Expander::new();
9825 let err = e
9826 .expand_program(read("(defmacro (nested) () body)").unwrap())
9827 .expect_err("defmacro non-symbol name gate must error");
9828 let (head, got) = defmacro_non_symbol_name_fields(&err);
9829 assert_eq!(head, MacroDefHead::Defmacro);
9830 assert_eq!(got, "(nested)");
9831 }
9832
9833 #[test]
9834 fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
9835 // `defpoint-template` shares `macro_def_from` with `defmacro`
9836 // (all three head keywords route through the same gate).
9837 // Pins that the lift fires path-uniformly across the three
9838 // head keywords AND that the variant's `head` slot carries
9839 // the actual head literal — `defpoint-template`, not
9840 // `defmacro` — so an LSP that wants to point at "your
9841 // defpoint-template form's name slot isn't a symbol" gains
9842 // the head as data.
9843 let mut e = Expander::new();
9844 let err = e
9845 .expand_program(read("(defpoint-template 7 () body)").unwrap())
9846 .expect_err("defpoint-template non-symbol name gate must error");
9847 let (head, got) = defmacro_non_symbol_name_fields(&err);
9848 assert_eq!(head, MacroDefHead::DefpointTemplate);
9849 assert_eq!(got, "7");
9850 }
9851
9852 #[test]
9853 fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
9854 // Sibling for the `defcheck` head — third head keyword
9855 // `macro_def_from` recognizes. Rounds out the three-head-
9856 // keyword coverage so the lift is path-uniform across
9857 // `defmacro` / `defpoint-template` / `defcheck`.
9858 let mut e = Expander::new();
9859 let err = e
9860 .expand_program(read("(defcheck :k () body)").unwrap())
9861 .expect_err("defcheck non-symbol name gate must error");
9862 let (head, got) = defmacro_non_symbol_name_fields(&err);
9863 assert_eq!(head, MacroDefHead::Defcheck);
9864 assert_eq!(got, ":k");
9865 }
9866
9867 #[test]
9868 fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
9869 // Path-uniform rejection: the SAME source emits the SAME
9870 // structural variant under both expansion strategies. The
9871 // name-symbol gate fires inside `macro_def_from` BEFORE
9872 // either expansion strategy runs, so the gate is naturally
9873 // path-uniform; pinning it gives a regression that drifts
9874 // either strategy's handling of non-symbol-name defmacros (or
9875 // makes one strategy accept what the other rejects) a fail-
9876 // before/pass-after edge. Sibling of
9877 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
9878 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
9879 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
9880 let src = "(defmacro 5 () body)";
9881 let mut subst = Expander::new_substitute_only();
9882 let mut bytecode = Expander::new();
9883 let err_subst = subst
9884 .expand_program(read(src).unwrap())
9885 .expect_err("substitute must error");
9886 let err_byte = bytecode
9887 .expand_program(read(src).unwrap())
9888 .expect_err("bytecode must error");
9889 assert_eq!(
9890 defmacro_non_symbol_name_fields(&err_subst),
9891 (MacroDefHead::Defmacro, "5")
9892 );
9893 assert_eq!(
9894 defmacro_non_symbol_name_fields(&err_byte),
9895 (MacroDefHead::Defmacro, "5")
9896 );
9897 }
9898
9899 #[test]
9900 fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
9901 // End-to-end through Display — pins the rendered diagnostic
9902 // consumers see today (REPL, `tatara-check`) AND the new
9903 // `, got {got}` clause. The legacy `"expected name symbol"`
9904 // substring rides through verbatim; the prefix matches the
9905 // legacy `Compile { form: "defmacro", message: "expected name
9906 // symbol" }` byte-for-byte. Tools that pattern-match on the
9907 // variant gain structural binding to `head` / `got`.
9908 let mut e = Expander::new();
9909 let err = e
9910 .expand_program(read("(defmacro 5 () body)").unwrap())
9911 .expect_err("defmacro non-symbol name gate must error");
9912 assert_eq!(
9913 format!("{err}"),
9914 "compile error in defmacro: expected name symbol, got 5"
9915 );
9916 }
9917
9918 #[test]
9919 fn defmacro_non_symbol_name_position_is_none_today() {
9920 // Negative control for the future-spans move: until `Sexp`
9921 // carries source positions, `position()` on `LispError`
9922 // returns `None` for this variant. A future run that gives
9923 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
9924 // this test gives that change a deliberate fail-before/pass-
9925 // after delta. Parallel to
9926 // `defmacro_arity_position_is_none_today`,
9927 // `non_symbol_param_position_is_none_today`, and
9928 // `rest_param_missing_name_position_is_none_today`.
9929 let mut e = Expander::new();
9930 let err = e
9931 .expand_program(read("(defmacro 5 () body)").unwrap())
9932 .expect_err("defmacro non-symbol name gate must error");
9933 assert_eq!(err.position(), None);
9934 }
9935
9936 #[test]
9937 fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
9938 // Negative control: a defmacro whose name slot IS a symbol
9939 // passes the name-symbol gate. Pins that the lift is scoped
9940 // to the non-symbol-name case, not to every defmacro form.
9941 // After this test, a regression that tightens the gate to
9942 // reject e.g. kebab-cased names fails-loudly here.
9943 let mut e = Expander::new();
9944 let out = e
9945 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
9946 .expect("well-formed defmacro must succeed");
9947 assert_eq!(out[0], Sexp::int(42));
9948 }
9949
9950 #[test]
9951 fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
9952 // Pins the gate ordering: a 4-element defmacro whose name
9953 // slot is non-symbol fires `DefmacroNonSymbolName`, NOT
9954 // `DefmacroArity`. The arity gate (>= 4 elements) admits
9955 // this form; the name-symbol gate is the next checkpoint.
9956 // A regression that swaps the gate ordering (e.g. checks
9957 // name-symbol before arity, so `(defmacro 5)` would emit
9958 // `DefmacroNonSymbolName` instead of `DefmacroArity`) fails-
9959 // loudly here.
9960 let mut e = Expander::new();
9961 let err = e
9962 .expand_program(read("(defmacro 5 () body)").unwrap())
9963 .expect_err("name-symbol gate must error");
9964 assert!(
9965 matches!(err, LispError::DefmacroNonSymbolName { .. }),
9966 "expected DefmacroNonSymbolName, got: {err:?}"
9967 );
9968
9969 let err_arity = e
9970 .expand_program(read("(defmacro 5)").unwrap())
9971 .expect_err("arity gate must error");
9972 assert!(
9973 matches!(err_arity, LispError::DefmacroArity { .. }),
9974 "expected DefmacroArity (arity < 4 short-circuits before name check), \
9975 got: {err_arity:?}"
9976 );
9977 }
9978
9979 /// Helper for the defmacro-non-list-params tests — pins variant
9980 /// shape and carries the head / got up to the assert site for
9981 /// legibility. Sibling of `defmacro_arity_fields`,
9982 /// `defmacro_non_symbol_name_fields`, `non_symbol_param_fields`,
9983 /// and `rest_param_missing_name_fields`.
9984 fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
9985 match err {
9986 LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
9987 other => panic!("expected DefmacroNonListParams, got: {other:?}"),
9988 }
9989 }
9990
9991 #[test]
9992 fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
9993 // `(defmacro f x body)` — the form passes both the arity gate
9994 // (4 elements) AND the name-symbol gate (`f` is a symbol) but
9995 // list[2] is the symbol `x`, not a list. Pins variant identity
9996 // AND that `head == "defmacro"` AND that `got == "x"`. A
9997 // regression that re-inlines the legacy `LispError::Compile {
9998 // form: "defmacro", message: "expected param list" }` shape
9999 // (which named the failure mode but not the offending element)
10000 // fails-loudly here.
10001 let mut e = Expander::new();
10002 let err = e
10003 .expand_program(read("(defmacro f x body)").unwrap())
10004 .expect_err("defmacro non-list params gate must error");
10005 let (head, got) = defmacro_non_list_params_fields(&err);
10006 assert_eq!(head, MacroDefHead::Defmacro);
10007 assert_eq!(got, "x");
10008 }
10009
10010 #[test]
10011 fn defmacro_non_list_params_with_int_emits_structural_variant() {
10012 // `(defmacro f 5 body)` — list[2] is `5`, not a list. Pins
10013 // that `Sexp::Display` for `Atom::Int(n)` writes `n` and the
10014 // variant's `got` slot carries the integer form unchanged. An
10015 // LSP that surfaces "you wrote `5` where a param list was
10016 // expected" gains the literal value as data, no source
10017 // re-parse required.
10018 let mut e = Expander::new();
10019 let err = e
10020 .expand_program(read("(defmacro f 5 body)").unwrap())
10021 .expect_err("defmacro non-list params gate must error");
10022 let (head, got) = defmacro_non_list_params_fields(&err);
10023 assert_eq!(head, MacroDefHead::Defmacro);
10024 assert_eq!(got, "5");
10025 }
10026
10027 #[test]
10028 fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
10029 // `(defmacro f :foo body)` — list[2] is the keyword `:foo`,
10030 // not a list. Pins that `Sexp::Display` for `Atom::Keyword(s)`
10031 // writes `:s` and the variant's `got` slot carries the
10032 // keyword form unchanged.
10033 let mut e = Expander::new();
10034 let err = e
10035 .expand_program(read("(defmacro f :foo body)").unwrap())
10036 .expect_err("defmacro non-list params gate must error");
10037 let (head, got) = defmacro_non_list_params_fields(&err);
10038 assert_eq!(head, MacroDefHead::Defmacro);
10039 assert_eq!(got, ":foo");
10040 }
10041
10042 #[test]
10043 fn defmacro_non_list_params_with_string_emits_structural_variant() {
10044 // `(defmacro f "params" body)` — list[2] is the string literal
10045 // `"params"`, not a list. Pins that `Sexp::Display` for
10046 // `Atom::String(s)` writes `"s"` (with quotes) and the
10047 // variant's `got` slot carries the quoted form unchanged.
10048 let mut e = Expander::new();
10049 let err = e
10050 .expand_program(read("(defmacro f \"params\" body)").unwrap())
10051 .expect_err("defmacro non-list params gate must error");
10052 let (head, got) = defmacro_non_list_params_fields(&err);
10053 assert_eq!(head, MacroDefHead::Defmacro);
10054 assert_eq!(got, "\"params\"");
10055 }
10056
10057 #[test]
10058 fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
10059 // `defpoint-template` shares `macro_def_from` with `defmacro`
10060 // (all three head keywords route through the same gate).
10061 // Pins that the lift fires path-uniformly across the three
10062 // head keywords AND that the variant's `head` slot carries
10063 // the actual head literal — `defpoint-template`, not
10064 // `defmacro` — so an LSP that wants to point at "your
10065 // defpoint-template form's param-list slot isn't a list"
10066 // gains the head as data.
10067 let mut e = Expander::new();
10068 let err = e
10069 .expand_program(read("(defpoint-template t x body)").unwrap())
10070 .expect_err("defpoint-template non-list params gate must error");
10071 let (head, got) = defmacro_non_list_params_fields(&err);
10072 assert_eq!(head, MacroDefHead::DefpointTemplate);
10073 assert_eq!(got, "x");
10074 }
10075
10076 #[test]
10077 fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
10078 // Sibling for the `defcheck` head — third head keyword
10079 // `macro_def_from` recognizes. Rounds out the three-head-
10080 // keyword coverage so the lift is path-uniform across
10081 // `defmacro` / `defpoint-template` / `defcheck`.
10082 let mut e = Expander::new();
10083 let err = e
10084 .expand_program(read("(defcheck c 7 body)").unwrap())
10085 .expect_err("defcheck non-list params gate must error");
10086 let (head, got) = defmacro_non_list_params_fields(&err);
10087 assert_eq!(head, MacroDefHead::Defcheck);
10088 assert_eq!(got, "7");
10089 }
10090
10091 #[test]
10092 fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
10093 // Path-uniform rejection: the SAME source emits the SAME
10094 // structural variant under both expansion strategies. The
10095 // param-list gate fires inside `macro_def_from` BEFORE either
10096 // expansion strategy runs, so the gate is naturally path-
10097 // uniform; pinning it gives a regression that drifts either
10098 // strategy's handling of non-list-params defmacros (or makes
10099 // one strategy accept what the other rejects) a fail-before/
10100 // pass-after edge. Sibling of
10101 // `defmacro_arity_substitute_and_bytecode_paths_agree`,
10102 // `defmacro_non_symbol_name_substitute_and_bytecode_paths_agree`,
10103 // `non_symbol_param_substitute_and_bytecode_paths_agree`, and
10104 // `rest_param_missing_name_substitute_and_bytecode_paths_agree`.
10105 let src = "(defmacro f x body)";
10106 let mut subst = Expander::new_substitute_only();
10107 let mut bytecode = Expander::new();
10108 let err_subst = subst
10109 .expand_program(read(src).unwrap())
10110 .expect_err("substitute must error");
10111 let err_byte = bytecode
10112 .expand_program(read(src).unwrap())
10113 .expect_err("bytecode must error");
10114 assert_eq!(
10115 defmacro_non_list_params_fields(&err_subst),
10116 (MacroDefHead::Defmacro, "x")
10117 );
10118 assert_eq!(
10119 defmacro_non_list_params_fields(&err_byte),
10120 (MacroDefHead::Defmacro, "x")
10121 );
10122 }
10123
10124 #[test]
10125 fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
10126 // End-to-end through Display — pins the rendered diagnostic
10127 // consumers see today (REPL, `tatara-check`) AND the new
10128 // `, got {got}` clause. The legacy `"expected param list"`
10129 // substring rides through verbatim; the prefix matches the
10130 // legacy `Compile { form: "defmacro", message: "expected
10131 // param list" }` byte-for-byte. Tools that pattern-match on
10132 // the variant gain structural binding to `head` / `got`.
10133 let mut e = Expander::new();
10134 let err = e
10135 .expand_program(read("(defmacro f x body)").unwrap())
10136 .expect_err("defmacro non-list params gate must error");
10137 assert_eq!(
10138 format!("{err}"),
10139 "compile error in defmacro: expected param list, got x"
10140 );
10141 }
10142
10143 #[test]
10144 fn defmacro_non_list_params_position_is_none_today() {
10145 // Negative control for the future-spans move: until `Sexp`
10146 // carries source positions, `position()` on `LispError`
10147 // returns `None` for this variant. A future run that gives
10148 // `Sexp` source spans adds `pos: Option<usize>` to ONE place;
10149 // this test gives that change a deliberate fail-before/pass-
10150 // after delta. Parallel to
10151 // `defmacro_arity_position_is_none_today`,
10152 // `defmacro_non_symbol_name_position_is_none_today`,
10153 // `non_symbol_param_position_is_none_today`, and
10154 // `rest_param_missing_name_position_is_none_today`.
10155 let mut e = Expander::new();
10156 let err = e
10157 .expand_program(read("(defmacro f x body)").unwrap())
10158 .expect_err("defmacro non-list params gate must error");
10159 assert_eq!(err.position(), None);
10160 }
10161
10162 #[test]
10163 fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
10164 // Negative control: a defmacro whose param-list slot IS a
10165 // list passes the param-list gate. Pins that the lift is
10166 // scoped to the non-list-params case, not to every defmacro
10167 // form. After this test, a regression that tightens the gate
10168 // to reject e.g. empty param lists fails-loudly here.
10169 let mut e = Expander::new();
10170 let out = e
10171 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10172 .expect("well-formed defmacro must succeed");
10173 assert_eq!(out[0], Sexp::int(42));
10174 }
10175
10176 #[test]
10177 fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
10178 // Pins the gate ordering: a 4-element defmacro whose name
10179 // slot IS a symbol but whose param-list slot is non-list
10180 // fires `DefmacroNonListParams`, NOT `DefmacroNonSymbolName`.
10181 // The name-symbol gate admits this form; the param-list gate
10182 // is the next checkpoint. A regression that swaps the gate
10183 // ordering (e.g. checks param-list before name-symbol, so
10184 // `(defmacro 5 x body)` would emit `DefmacroNonListParams`
10185 // instead of `DefmacroNonSymbolName`) fails-loudly here.
10186 let mut e = Expander::new();
10187 let err = e
10188 .expand_program(read("(defmacro f x body)").unwrap())
10189 .expect_err("param-list gate must error");
10190 assert!(
10191 matches!(err, LispError::DefmacroNonListParams { .. }),
10192 "expected DefmacroNonListParams, got: {err:?}"
10193 );
10194
10195 let err_name = e
10196 .expand_program(read("(defmacro 5 x body)").unwrap())
10197 .expect_err("name-symbol gate must error");
10198 assert!(
10199 matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
10200 "expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
10201 got: {err_name:?}"
10202 );
10203 }
10204
10205 #[test]
10206 fn defmacro_non_list_params_fires_after_arity_gate_passes() {
10207 // Pins the full gate ordering: a 4-element defmacro whose
10208 // first three slots are head/symbol/non-list fires
10209 // `DefmacroNonListParams`, NOT `DefmacroArity`. The arity
10210 // gate (>= 4 elements) admits this form; the name-symbol
10211 // gate admits the symbol; the param-list gate is the third
10212 // checkpoint. A regression that drifts the gate sequence
10213 // (e.g. fires `DefmacroArity` for a 4-element form) fails-
10214 // loudly here. Parallel to
10215 // `defmacro_non_symbol_name_fires_after_arity_gate_passes`
10216 // — together they pin the full
10217 // arity → name-symbol → param-list ordering inside
10218 // `macro_def_from`.
10219 let mut e = Expander::new();
10220 let err = e
10221 .expand_program(read("(defmacro f x body)").unwrap())
10222 .expect_err("param-list gate must error");
10223 assert!(
10224 matches!(err, LispError::DefmacroNonListParams { .. }),
10225 "expected DefmacroNonListParams, got: {err:?}"
10226 );
10227
10228 let err_arity = e
10229 .expand_program(read("(defmacro f x)").unwrap())
10230 .expect_err("arity gate must error");
10231 assert!(
10232 matches!(err_arity, LispError::DefmacroArity { .. }),
10233 "expected DefmacroArity (arity < 4 short-circuits before param-list check), \
10234 got: {err_arity:?}"
10235 );
10236 }
10237
10238 #[test]
10239 fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
10240 // Negative control: `&rest` is a symbol (`Atom::Symbol("&rest")`)
10241 // at the parser level, so `as_symbol()` succeeds for it. The
10242 // `NonSymbolParam` variant does NOT fire on the `&rest` marker
10243 // itself; the dedicated `&rest needs a name` rejection (a
10244 // separate failure mode in this cluster) handles malformed
10245 // rest-param shapes. Pins that the lift is scoped to
10246 // non-symbol elements at param-list positions, not to
10247 // every malformed-param shape.
10248 let mut e = Expander::new();
10249 let out = e
10250 .expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
10251 .expect("&rest with name must succeed");
10252 assert_eq!(out[0], parse("(list 1 2 3)"));
10253 }
10254
10255 #[test]
10256 fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
10257 // End-to-end through the Display impl — pins the rendered diagnostic
10258 // a downstream tool sees today (REPL, tatara-check). The shape is
10259 // parallel to the existing `TypeMismatch` variant: form, expected
10260 // shape, offending literal — all three slots present.
10261 let mut e = Expander::new();
10262 let err = e
10263 .expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
10264 .expect_err("non-symbol target must error");
10265 assert_eq!(
10266 format!("{err}"),
10267 "compile error in ,: expected symbol, got (list 1 2)"
10268 );
10269 }
10270
10271 // ── template_invariant_violation: structural lift ───────────────
10272 //
10273 // The four byte-identical inline `LispError::Compile { form:
10274 // macro_name.into(), message: <invariant> }` triples in `apply_compiled`
10275 // (Subst-bad-index, Splice-bad-index, EndList-empty-stack,
10276 // final-no-value gates) were lifted to `template_invariant_violation`,
10277 // and the helper's emission was promoted from `LispError::Compile`-
10278 // shape to the structural `LispError::TemplateInvariant { macro_name,
10279 // kind: TemplateInvariantKind }` variant. The index payload of the
10280 // Subst / Splice gates lives INSIDE the variant (`SubstBadIndex(usize)`
10281 // / `SpliceBadIndex(usize)`), so the invalid combination "stack-gate
10282 // kind with an op-index" (e.g. `EndListEmptyStack` carrying a `usize`)
10283 // is structurally unrepresentable. Display matches the legacy
10284 // `Compile`-shaped diagnostic byte-for-byte via the closed-set
10285 // `TemplateInvariantKind::message()` projection so authoring-tool
10286 // substring greps see no drift across the lift.
10287 //
10288 // The tests below pin: (a) the helper produces the structural
10289 // `LispError::TemplateInvariant` variant with `macro_name` and `kind`
10290 // first-class; (b) the Subst / Splice gates thread the bad index
10291 // through the typed variants `SubstBadIndex(usize)` / `SpliceBadIndex(usize)`
10292 // unchanged; (c) the two REACHABLE invariant-violation paths through
10293 // `apply_compiled` — Subst with out-of-bounds idx, Splice with
10294 // out-of-bounds idx — route through the helper end-to-end (the
10295 // EndList / no-value paths are guarded by `last_mut().unwrap()`
10296 // ahead of `pop().ok_or_else()` and are not reachable through any
10297 // single CompiledTemplate; they remain defensive against future
10298 // changes to the stack discipline); (d) the legacy Display
10299 // rendering matches byte-for-byte across the lift; (e) positive
10300 // controls: a well-formed CompiledTemplate routes PAST the helper
10301 // cleanly, and unrelated macro errors (missing-required-arg) do
10302 // NOT route through the helper.
10303
10304 #[test]
10305 fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
10306 // Direct unit test of the helper: a fixed macro_name and a
10307 // `TemplateInvariantKind` produce a `LispError::TemplateInvariant`
10308 // variant with the macro_name in the `macro_name` slot and the
10309 // kind passed through verbatim in the `kind` slot. A regression
10310 // that drifts the variant (e.g., back to `LispError::Compile`)
10311 // or swaps the slot positions fails-loudly here.
10312 let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
10313 match err {
10314 LispError::TemplateInvariant { macro_name, kind } => {
10315 assert_eq!(macro_name, "test-macro");
10316 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
10317 }
10318 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10319 }
10320 }
10321
10322 #[test]
10323 fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
10324 // The Subst gate's `usize` idx lives INSIDE the
10325 // `TemplateInvariantKind::SubstBadIndex(usize)` variant rather
10326 // than being substring-rendered into a free-form `message`
10327 // slot. Pin that the helper threads the bad index through the
10328 // typed variant unchanged; a regression that drops the index
10329 // payload (e.g., via a `usize -> ()` projection) fails here.
10330 let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
10331 match err {
10332 LispError::TemplateInvariant { macro_name, kind } => {
10333 assert_eq!(macro_name, "wrap");
10334 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10335 }
10336 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10337 }
10338 }
10339
10340 #[test]
10341 fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
10342 // Hand-crafted CompiledTemplate with a Subst(99) op against
10343 // an empty params list: `args_by_index` has length 0, so
10344 // `.get(99)` returns None and the `ok_or_else` triggers
10345 // through the helper. Fail-before-pass-after: this same input
10346 // pre-lift went through `LispError::Compile { form: macro_name,
10347 // message: format!("compiled template referenced bad param
10348 // index {idx}") }`; post-lift it routes through
10349 // `template_invariant_violation` and emits the structural
10350 // `TemplateInvariant { macro_name, kind: SubstBadIndex(99) }`
10351 // variant with the bad index threaded through as typed data.
10352 let tmpl = CompiledTemplate {
10353 ops: vec![TemplateOp::Subst(99)],
10354 };
10355 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10356 .expect_err("bad idx must error");
10357 match err {
10358 LispError::TemplateInvariant { macro_name, kind } => {
10359 assert_eq!(macro_name, "test-macro");
10360 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10361 }
10362 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10363 }
10364 }
10365
10366 #[test]
10367 fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
10368 // Hand-crafted CompiledTemplate with a Splice(42) op against
10369 // an empty params list. Sibling of the Subst-bad-idx test;
10370 // pins the Splice gate routes through the helper with the
10371 // typed `SpliceBadIndex(42)` kind carrying the bad index.
10372 let tmpl = CompiledTemplate {
10373 ops: vec![TemplateOp::Splice(42)],
10374 };
10375 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10376 .expect_err("bad splice idx must error");
10377 match err {
10378 LispError::TemplateInvariant { macro_name, kind } => {
10379 assert_eq!(macro_name, "call-macro");
10380 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10381 }
10382 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10383 }
10384 }
10385
10386 #[test]
10387 fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
10388 // End-to-end through the `LispError` Display impl — pins the
10389 // rendered diagnostic byte-for-byte: `"compile error in
10390 // test-macro: compiled template referenced bad param index 99"`.
10391 // Authoring tools that substring-grep the rendered diagnostic
10392 // (`tatara-check`'s diagnostic capture, REPL substring-greps)
10393 // see no drift across the lift. Parallel to how
10394 // `compile_named_non_symbol_name_renders_legacy_compile_shape`
10395 // pins the sibling-file (compile.rs) lift's Display contract.
10396 let tmpl = CompiledTemplate {
10397 ops: vec![TemplateOp::Subst(99)],
10398 };
10399 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10400 .expect_err("bad idx must error");
10401 assert_eq!(
10402 format!("{err}"),
10403 "compile error in test-macro: compiled template referenced bad param index 99"
10404 );
10405 }
10406
10407 #[test]
10408 fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
10409 // Sibling Display test for the Splice gate. Pins the message
10410 // byte-for-byte through the `LispError` Display impl: `"compile
10411 // error in call-macro: compiled template referenced bad splice
10412 // index 42"`.
10413 let tmpl = CompiledTemplate {
10414 ops: vec![TemplateOp::Splice(42)],
10415 };
10416 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10417 .expect_err("bad splice idx must error");
10418 assert_eq!(
10419 format!("{err}"),
10420 "compile error in call-macro: compiled template referenced bad splice index 42"
10421 );
10422 }
10423
10424 #[test]
10425 fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
10426 // Positive control: a CompiledTemplate produced by the
10427 // bytecode compiler (`compile_template`) for a well-formed
10428 // macro never references an out-of-bounds index nor
10429 // unbalances the stack, so `apply_compiled` routes PAST the
10430 // helper cleanly. A regression that fires the helper on
10431 // well-formed bytecode (e.g., off-by-one in the index
10432 // resolution) would fail here. End-to-end through the public
10433 // `Expander` surface so the test exercises the same code
10434 // path users see.
10435 let mut e = Expander::new();
10436 let out = e
10437 .expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
10438 .expect("well-formed macro expansion must not fire template-invariant-violation");
10439 assert_eq!(out.len(), 1);
10440 assert_eq!(out[0], Sexp::int(42));
10441 }
10442
10443 #[test]
10444 fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
10445 // Negative control: the `missing_macro_arg` gate in the shared
10446 // positional binder (`MacroParams::bind`) fires BEFORE the bytecode
10447 // loop runs,
10448 // so a missing required arg routes through
10449 // `LispError::MissingMacroArg`, NOT through
10450 // `template_invariant_violation`. Pins the helper is
10451 // precisely scoped to bytecode-runtime invariant violations
10452 // (Subst / Splice / stack gates), not to macro-call arity
10453 // errors (the latter has its own structural variant). A
10454 // regression that conflates the two gate clusters would
10455 // route this case through `Compile { ... }` instead of
10456 // `MissingMacroArg` and fail-loudly here.
10457 let mut e = Expander::new();
10458 let err = e
10459 .expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
10460 .expect_err("missing required arg must error");
10461 assert!(
10462 matches!(err, LispError::MissingMacroArg { .. }),
10463 "expected MissingMacroArg, got: {err:?}"
10464 );
10465 }
10466
10467 // ── resolve_bound_arg: bytecode-runtime bound-arg-by-index lookup ──
10468 //
10469 // `resolve_bound_arg(args_by_index, idx, macro_name, kind)` lifts the
10470 // `args_by_index.get(*idx).ok_or_else(|| template_invariant_violation(
10471 // macro_name, KIND(*idx)))?` projection that recurred at BOTH the
10472 // `TemplateOp::Subst` and `TemplateOp::Splice` arms inside
10473 // `apply_compiled`. The arms differ in the kind constructor
10474 // (`SubstBadIndex` vs. `SpliceBadIndex`) and in their post-lookup
10475 // verb (clone+push vs. splice-coerce), but the lookup-and-reject
10476 // prelude is byte-identical modulo the constructor. These tests
10477 // pin the lifted helper's contract directly; the existing
10478 // `apply_compiled_*_bad_idx_*` tests are the path-uniformity
10479 // guards proving both production arms route through it without
10480 // behavior drift.
10481
10482 #[test]
10483 fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
10484 // For an in-range index, the helper returns `Ok(&args[idx])`
10485 // borrowed VERBATIM — same pointer as `args_by_index.get(idx)`.
10486 // Pins the borrow-not-clone contract: a regression that drifts
10487 // the helper to clone+return (`Result<Sexp>` instead of
10488 // `Result<&Sexp>`) would allocate per lookup at the production
10489 // `Subst`/`Splice` hot path. The kind constructor must NOT
10490 // fire on the success path (`FnOnce`'s lazy semantics) — pin
10491 // that the test passes a constructor that would panic if
10492 // called, asserting the helper short-circuits before invoking
10493 // it on the in-range arm.
10494 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10495 let got = resolve_bound_arg(&args, 1, "m", |_| {
10496 panic!("kind constructor must not fire on the in-range path")
10497 })
10498 .expect("in-range lookup must succeed");
10499 assert!(
10500 std::ptr::eq(got, &args[1]),
10501 "resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
10502 );
10503 assert_eq!(*got, Sexp::int(2));
10504 }
10505
10506 #[test]
10507 fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
10508 // For an out-of-range index, the helper raises the structural
10509 // `LispError::TemplateInvariant` variant with the caller-
10510 // supplied `SubstBadIndex` kind constructor applied to the bad
10511 // index. Pins the post-lift emission shape (variant identity
10512 // + the kind constructor threaded with the actual idx); a
10513 // regression that drops the idx payload (e.g., via a `usize ->
10514 // ()` projection) or hard-codes a different kind at the helper
10515 // boundary fails-loudly here. Fail-before-pass-after: this
10516 // assert is contradicted by the pre-lift code path (which
10517 // never called `resolve_bound_arg` because it didn't exist),
10518 // ratifies the post-lift one.
10519 let args: Vec<Sexp> = Vec::new();
10520 let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
10521 .expect_err("out-of-range lookup must error");
10522 match err {
10523 LispError::TemplateInvariant { macro_name, kind } => {
10524 assert_eq!(macro_name, "test-macro");
10525 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
10526 }
10527 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10528 }
10529 }
10530
10531 #[test]
10532 fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
10533 // Path-uniformity for the per-call-site kind constructor: the
10534 // SAME out-of-range idx via the `SpliceBadIndex` constructor
10535 // emits `kind: SpliceBadIndex(7)` — distinct from the sibling
10536 // `SubstBadIndex(7)` variant. Pins that the constructor is
10537 // chosen per call site (not hard-coded at the helper boundary),
10538 // closing the structural matrix `{Subst, Splice} × {in-range,
10539 // out-of-range}` the two production arms span across the
10540 // bytecode-runtime's bound-arg-by-index reads. A regression
10541 // that hard-codes a single kind at the helper boundary would
10542 // emit the same variant identity for both call sites and
10543 // fail-loudly here.
10544 let args: Vec<Sexp> = Vec::new();
10545 let err = resolve_bound_arg(
10546 &args,
10547 7,
10548 "test-macro",
10549 TemplateInvariantKind::SpliceBadIndex,
10550 )
10551 .expect_err("out-of-range lookup must error");
10552 match err {
10553 LispError::TemplateInvariant { macro_name, kind } => {
10554 assert_eq!(macro_name, "test-macro");
10555 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
10556 }
10557 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10558 }
10559 }
10560
10561 #[test]
10562 fn resolve_bound_arg_threads_macro_name_verbatim() {
10563 // Path-uniformity for the `macro_name` slot: the helper threads
10564 // the caller's borrow into the variant's owned `String` slot
10565 // verbatim. Pin two distinct macro names route through with no
10566 // mutual interference — a regression that hard-codes a single
10567 // macro_name at the helper boundary or swaps the parameter
10568 // ordering fails-loudly here. Same posture as
10569 // `compiler_spec_io_err_threads_each_stage_through_unchanged`
10570 // pins the typed `stage` slot in the disk-persistence sibling
10571 // lift.
10572 let args: Vec<Sexp> = Vec::new();
10573 for name in ["wrap", "call-macro", "obs"] {
10574 let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
10575 .expect_err("out-of-range lookup must error");
10576 match err {
10577 LispError::TemplateInvariant { macro_name, kind } => {
10578 assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
10579 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10580 }
10581 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10582 }
10583 }
10584 }
10585
10586 #[test]
10587 fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
10588 // Edge case: idx 0 with a single-element args_by_index returns
10589 // `Ok(&args[0])`. Pins the lower-bound of the in-range surface
10590 // — a regression that off-by-ones the lookup (e.g., `get(idx +
10591 // 1)` or `get(idx).filter(|_| idx > 0)`) would fail here.
10592 // Sibling to the upper-bound `resolve_bound_arg_out_of_range_
10593 // with_subst_kind_emits_typed_invariant` test.
10594 let args = vec![Sexp::int(42)];
10595 let got = resolve_bound_arg(&args, 0, "m", |_| {
10596 panic!("kind constructor must not fire on the in-range path")
10597 })
10598 .expect("idx-0 lookup must succeed");
10599 assert!(std::ptr::eq(got, &args[0]));
10600 assert_eq!(*got, Sexp::int(42));
10601 }
10602
10603 #[test]
10604 fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
10605 // Edge case: idx `len - 1` is the highest valid index. Pin
10606 // that it routes through the success arm (NOT the error arm),
10607 // closing the in-range surface end-to-end with the lower-
10608 // bound sibling. A regression that off-by-ones the upper
10609 // bound (e.g., `get(idx).filter(|_| idx < args.len() - 1)`)
10610 // would fail here.
10611 let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
10612 let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
10613 panic!("kind constructor must not fire on the in-range path")
10614 })
10615 .expect("last-element lookup must succeed");
10616 assert!(std::ptr::eq(got, args.last().unwrap()));
10617 assert_eq!(*got, Sexp::int(3));
10618 }
10619
10620 #[test]
10621 fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
10622 // Boundary case: idx EQUAL to `args.len()` is out-of-range
10623 // (since `get` is 0-indexed). Pin that this routes through
10624 // the error arm with the kind constructor applied to the
10625 // EXACT idx that was tried. A regression that off-by-ones
10626 // the boundary (e.g., admits `idx == len`) would fail here.
10627 // This is the canonical off-by-one trap; the helper's
10628 // contract pins it at the variant-construction boundary.
10629 let args = vec![Sexp::int(1)];
10630 let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
10631 .expect_err("idx == len must error");
10632 match err {
10633 LispError::TemplateInvariant { kind, .. } => {
10634 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
10635 }
10636 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10637 }
10638 }
10639
10640 #[test]
10641 fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
10642 // Boundary case: an empty `args_by_index` slice rejects every
10643 // idx (including 0). Pin that the helper's emission shape is
10644 // uniform regardless of which out-of-range idx fires the
10645 // rejection — `SubstBadIndex(0)` for an empty slice is the
10646 // bytecode-runtime mirror of a zero-arity macro template
10647 // referencing the 0-th param.
10648 let args: Vec<Sexp> = Vec::new();
10649 let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
10650 .expect_err("empty slice rejects every idx");
10651 match err {
10652 LispError::TemplateInvariant { macro_name, kind } => {
10653 assert_eq!(macro_name, "zero-arity");
10654 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
10655 }
10656 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10657 }
10658 }
10659
10660 #[test]
10661 fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
10662 // End-to-end path-uniformity: a `Subst(99)` op against a
10663 // zero-arity macro routes the bytecode-runtime's bound-arg
10664 // lookup through `resolve_bound_arg` with the
10665 // `SubstBadIndex` constructor, emitting the structural
10666 // variant with `kind: SubstBadIndex(99)`. The pre-lift
10667 // sibling test `apply_compiled_subst_bad_idx_routes_through_
10668 // template_invariant_violation` pins that the same input
10669 // routes through `template_invariant_violation`; this test
10670 // pins that BOTH still hold under the post-lift composition
10671 // — `resolve_bound_arg` calls `template_invariant_violation`
10672 // internally on the rejection arm. A regression that drifts
10673 // ONE arm's projection from the other (e.g., swaps the
10674 // constructor at one call site, or short-circuits the
10675 // composition) would fail here.
10676 let tmpl = CompiledTemplate {
10677 ops: vec![TemplateOp::Subst(99)],
10678 };
10679 let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
10680 .expect_err("bad idx must error");
10681 match err {
10682 LispError::TemplateInvariant { macro_name, kind } => {
10683 assert_eq!(macro_name, "test-macro");
10684 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
10685 }
10686 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10687 }
10688 }
10689
10690 #[test]
10691 fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
10692 // Sibling end-to-end path-uniformity for the `Splice` arm:
10693 // the post-lift composition routes a `Splice(42)` op through
10694 // `resolve_bound_arg` with the `SpliceBadIndex` constructor,
10695 // emitting `kind: SpliceBadIndex(42)`. Together with the
10696 // `Subst` sibling test above, this pins the structural matrix
10697 // `{Subst, Splice} × resolve_bound_arg` end-to-end through
10698 // the public `apply_compiled` surface, so a regression that
10699 // drifts ONE arm's kind constructor (e.g., the `Splice` arm
10700 // accidentally emits `SubstBadIndex` after a copy-paste
10701 // refactor) fails-loudly here.
10702 let tmpl = CompiledTemplate {
10703 ops: vec![TemplateOp::Splice(42)],
10704 };
10705 let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
10706 .expect_err("bad splice idx must error");
10707 match err {
10708 LispError::TemplateInvariant { macro_name, kind } => {
10709 assert_eq!(macro_name, "call-macro");
10710 assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
10711 }
10712 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
10713 }
10714 }
10715
10716 #[test]
10717 fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
10718 // Positive control: a `Subst(0)` op against a one-arg macro
10719 // routes through `resolve_bound_arg`'s success arm and the
10720 // `Subst` post-lookup verb (clone + push) emits the bound
10721 // value verbatim. Pin the post-lift composition's success
10722 // path: the clone-and-push semantics live at the call site
10723 // (NOT in `resolve_bound_arg`, which only borrows), and a
10724 // regression that drifts the borrow contract (e.g., the
10725 // helper clones internally + the call site clones again)
10726 // would still pass observationally but would regress the
10727 // hot-path allocation count.
10728 let params = MacroParams {
10729 required: vec!["x".into()],
10730 optional: Vec::new(),
10731 rest: None,
10732 };
10733 let tmpl = CompiledTemplate {
10734 ops: vec![TemplateOp::Subst(0)],
10735 };
10736 let out = apply_compiled("id", ¶ms, &tmpl, &[Sexp::int(42)])
10737 .expect("in-range Subst must succeed");
10738 assert_eq!(out, Sexp::int(42));
10739 }
10740
10741 // ── current_builder_mut: the bytecode-runtime top-of-stack projection ──
10742 //
10743 // `current_builder_mut(stack)` lifts the `stack.last_mut().unwrap()`
10744 // projection that appeared at FOUR sites inside `apply_compiled`'s
10745 // op-loop (Literal, Subst, Splice, post-EndList parent-fold) into ONE
10746 // named primitive. The expect message names the bytecode-runtime
10747 // invariant ("at least one stack frame during op-loop") so a
10748 // regression that drifts the loop's frame management (a new op that
10749 // pops without pushing, an early-return that bypasses EndList's
10750 // stack-check) surfaces a NAMED panic rather than a silent unwrap.
10751 // These tests pin the projection's contract directly; the existing
10752 // `apply_compiled_*` tests + the cross-strategy `expansion_layers_
10753 // agree_on_output_and_cache_wins` benchmark are the path-uniformity
10754 // guards proving the four sites still emit the canonical bytecode-
10755 // runtime output across the lift.
10756
10757 #[test]
10758 fn current_builder_mut_returns_the_top_frame_reference() {
10759 // The simplest projection: on a single-frame stack, the helper
10760 // returns a `&mut Vec<Sexp>` pointing at THAT frame. Pin the
10761 // projection's identity end-to-end via a push that mutates
10762 // through the borrow and observe the original frame carries the
10763 // pushed value back.
10764 let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
10765 current_builder_mut(&mut stack).push(Sexp::int(42));
10766 assert_eq!(stack.len(), 1);
10767 assert_eq!(stack[0], vec![Sexp::int(42)]);
10768 }
10769
10770 #[test]
10771 fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
10772 // The projection MUST target the topmost frame, not the bottom
10773 // one — every `TemplateOp::BeginList` pushes a fresh frame the
10774 // subsequent ops emit into, and a regression that flipped the
10775 // projection to `first_mut` (or to a fixed bottom-frame
10776 // reference) would silently smear all op output into the
10777 // outermost result. Pin path-uniformity with the bytecode-
10778 // runtime's mid-list emission posture: with three frames on
10779 // the stack (one outer + two pending lists), the helper
10780 // returns a borrow into the third frame, leaving frames 0 and
10781 // 1 untouched.
10782 let mut stack: Vec<Vec<Sexp>> = vec![
10783 vec![Sexp::symbol("outer")],
10784 vec![Sexp::symbol("inner-a")],
10785 vec![Sexp::symbol("inner-b")],
10786 ];
10787 current_builder_mut(&mut stack).push(Sexp::int(99));
10788 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
10789 assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
10790 assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
10791 }
10792
10793 #[test]
10794 fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
10795 // Structural identity binding the lift to its pre-lift inline
10796 // shape: `current_builder_mut(&mut stack)` IS
10797 // `stack.last_mut().unwrap()` — the same `&mut Vec<Sexp>`,
10798 // pointing at the same allocation. Pin pointer equality via
10799 // `std::ptr::eq` on the projected slice's `as_ptr()` to rule
10800 // out any allocation-shape drift across the lift.
10801 let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
10802 let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
10803 let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
10804 assert!(
10805 std::ptr::eq(via_lift_ptr, via_inline_ptr),
10806 "current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
10807 );
10808 }
10809
10810 #[test]
10811 #[should_panic(
10812 expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
10813 )]
10814 fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
10815 // The bytecode-runtime invariant is encoded in the expect
10816 // message: an empty stack at the projection boundary is
10817 // structurally unreachable inside `apply_compiled`'s op-loop
10818 // (the outermost frame is seeded at entry and every BeginList
10819 // / EndList pair preserves the count >= 1). Pin that the
10820 // NAMED invariant fires on the failure path so a regression
10821 // that drifts the loop's frame management surfaces a
10822 // diagnostic-grade panic rather than a silent unwrap over
10823 // `None`. Authoring tools / future debug-mode hooks can
10824 // pattern-match on the named invariant string instead of
10825 // tracking down an unnamed unwrap site.
10826 let mut empty: Vec<Vec<Sexp>> = Vec::new();
10827 let _ = current_builder_mut(&mut empty);
10828 }
10829
10830 #[test]
10831 fn current_builder_mut_routes_apply_compiled_literal_emit() {
10832 // End-to-end path-uniformity guard: a single-op program
10833 // `TemplateOp::Literal(s)` routes its push through
10834 // `current_builder_mut(&mut stack)` and the literal lands in
10835 // the outermost frame. After the op-loop completes the outer
10836 // `stack.pop().FinalNoValue` gate sees a non-empty top frame
10837 // containing exactly one element, which `apply_compiled`'s
10838 // tail (`top.len() == 1 { top.remove(0) }`) projects back as
10839 // the bound value. Pre-lift the same emission ran through
10840 // `stack.last_mut().unwrap().push(s.clone())`; post-lift it
10841 // runs through `current_builder_mut(&mut stack).push(s.clone())`
10842 // — the byte-identical outcome pins that the Literal arm's
10843 // routing through the new projection preserves the bytecode-
10844 // runtime's emission shape.
10845 let tmpl = CompiledTemplate {
10846 ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
10847 };
10848 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10849 .expect("literal-only template must succeed");
10850 assert_eq!(out, Sexp::symbol("hello"));
10851 }
10852
10853 #[test]
10854 fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
10855 // End-to-end path-uniformity guard for the post-EndList
10856 // parent-fold push: `(BeginList, Literal(a), Literal(b),
10857 // EndList)` builds an inner frame `[a, b]`, pops it on
10858 // EndList, then pushes `Sexp::List([a, b])` into the parent
10859 // (outer) frame via `current_builder_mut`. The outermost
10860 // `stack.pop()` then surfaces that list as the bound result.
10861 // Pre-lift the parent-fold push ran through
10862 // `stack.last_mut().unwrap().push(Sexp::List(items))`; post-
10863 // lift it runs through `current_builder_mut(&mut stack).
10864 // push(Sexp::List(items))` — pin the byte-identical outcome
10865 // so a regression that drifts the parent-fold target (e.g.,
10866 // pushes onto the just-popped frame's pointer instead of the
10867 // new top) fails loudly here.
10868 let tmpl = CompiledTemplate {
10869 ops: vec![
10870 TemplateOp::BeginList,
10871 TemplateOp::Literal(Sexp::symbol("a")),
10872 TemplateOp::Literal(Sexp::symbol("b")),
10873 TemplateOp::EndList,
10874 ],
10875 };
10876 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
10877 .expect("BeginList/EndList template must succeed");
10878 assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
10879 }
10880
10881 #[test]
10882 fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
10883 // End-to-end path-uniformity guard for BOTH index-reading
10884 // arms routing through the lifted projection: a one-required +
10885 // one-rest macro `(call f &rest args)` with template
10886 // `(BeginList, Subst(0), Splice(1), EndList)` exercises both
10887 // Subst's clone-and-push AND Splice's splice-value-into
10888 // emit-paths against the current builder via
10889 // `current_builder_mut(&mut stack)`. The composed result is
10890 // `(foo 1 2 3)` — Subst lands the bound `f = foo` and
10891 // Splice flattens `args = (1 2 3)` — and the byte-identical
10892 // outcome pins that BOTH Subst and Splice arms' emits route
10893 // through the SHARED projection. Sibling to
10894 // `apply_compiled_splice_in_range_routes_past_resolve_bound
10895 // _arg_into_splice_value_into` which already exercises this
10896 // shape end-to-end; the addition here is the path-uniformity
10897 // anchor for the `current_builder_mut` lift specifically.
10898 let params = MacroParams {
10899 required: vec!["f".into()],
10900 optional: Vec::new(),
10901 rest: Some("args".into()),
10902 };
10903 let tmpl = CompiledTemplate {
10904 ops: vec![
10905 TemplateOp::BeginList,
10906 TemplateOp::Subst(0),
10907 TemplateOp::Splice(1),
10908 TemplateOp::EndList,
10909 ],
10910 };
10911 let out = apply_compiled(
10912 "call",
10913 ¶ms,
10914 &tmpl,
10915 &[
10916 Sexp::symbol("foo"),
10917 Sexp::int(1),
10918 Sexp::int(2),
10919 Sexp::int(3),
10920 ],
10921 )
10922 .expect("Subst + Splice template must succeed");
10923 assert_eq!(
10924 out,
10925 Sexp::List(vec![
10926 Sexp::symbol("foo"),
10927 Sexp::int(1),
10928 Sexp::int(2),
10929 Sexp::int(3),
10930 ])
10931 );
10932 }
10933
10934 #[test]
10935 fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
10936 // Positive control for the `Splice` arm: a `&rest` macro that
10937 // splices a bound list routes through `resolve_bound_arg`'s
10938 // success arm and the `Splice` post-lookup verb
10939 // (`splice_value_into`) flattens the bound list into the
10940 // builder. Pin the composition's success path end-to-end:
10941 // the bound `Sexp::List([1, 2, 3])` at idx 1 flattens into
10942 // the outer builder's `(call 1 2 3)` shape — the same output
10943 // `rest_param_splices_with_at` pins through the public
10944 // surface, here pinned with the bytecode-runtime composition
10945 // exposed directly.
10946 let params = MacroParams {
10947 required: vec!["f".into()],
10948 optional: Vec::new(),
10949 rest: Some("args".into()),
10950 };
10951 let tmpl = CompiledTemplate {
10952 ops: vec![
10953 TemplateOp::BeginList,
10954 TemplateOp::Subst(0),
10955 TemplateOp::Splice(1),
10956 TemplateOp::EndList,
10957 ],
10958 };
10959 let out = apply_compiled(
10960 "call",
10961 ¶ms,
10962 &tmpl,
10963 &[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
10964 )
10965 .expect("in-range Splice must succeed");
10966 assert_eq!(
10967 out,
10968 Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
10969 );
10970 }
10971
10972 // ── pop_builder_frame: the bytecode-runtime stack-frame consume ─────
10973 //
10974 // `pop_builder_frame(stack, macro_name, kind)` lifts the
10975 // `stack.pop().ok_or_else(|| template_invariant_violation(macro_name,
10976 // kind))?` chain that recurred at two sites inside `apply_compiled`
10977 // (the `EndList` arm + the post-loop final pop) into ONE named
10978 // primitive on the bytecode-runtime's stack-frame algebra. Sibling of
10979 // `current_builder_mut` (the top-frame-borrow projection — the same
10980 // `&mut Vec<Vec<Sexp>>` consumed at the borrow face) and
10981 // `resolve_bound_arg` (the bound-arg-by-index lookup primitive that
10982 // also routes through `TemplateInvariantKind` as the per-call-site
10983 // rejection identity). These tests pin the primitive's contract
10984 // directly; the existing `apply_compiled_*` tests are the path-
10985 // uniformity guards proving the two sites route through it without
10986 // behavior drift.
10987
10988 #[test]
10989 fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
10990 // Happy path: a two-frame stack pops the topmost frame off and
10991 // returns it AS-IS while shrinking the stack by exactly one
10992 // element. Pin both the return value (the popped frame's
10993 // contents are byte-identical to what was pushed) AND the
10994 // mutation (`stack.len()` drops from 2 to 1).
10995 let mut stack: Vec<Vec<Sexp>> = vec![
10996 vec![Sexp::symbol("outer")],
10997 vec![Sexp::int(1), Sexp::int(2)],
10998 ];
10999 let popped =
11000 pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
11001 .expect("non-empty stack must pop cleanly");
11002 assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
11003 assert_eq!(stack.len(), 1);
11004 assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
11005 }
11006
11007 #[test]
11008 fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
11009 // Empty-stack rejection: an empty stack flows through the
11010 // `EndListEmptyStack` kind constructor into a structural
11011 // `LispError::TemplateInvariant { macro_name, kind:
11012 // EndListEmptyStack }` variant. Fail-before-pass-after: pre-lift
11013 // the same input would route through the inline
11014 // `stack.pop().ok_or_else(|| template_invariant_violation(_,
11015 // EndListEmptyStack))?` chain at the `EndList` arm; post-lift
11016 // it routes through ONE named primitive both pop-emitting
11017 // sites share, and a regression that drops the kind threading
11018 // (e.g. unifies both kinds into one constant) fails here.
11019 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11020 let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
11021 .expect_err("empty stack must reject");
11022 match err {
11023 LispError::TemplateInvariant { macro_name, kind } => {
11024 assert_eq!(macro_name, "wrap");
11025 assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
11026 }
11027 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11028 }
11029 }
11030
11031 #[test]
11032 fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
11033 // Path-uniformity across the closed-set of pop-emitting kinds:
11034 // the same primitive threads `FinalNoValue` verbatim through
11035 // its `TemplateInvariantKind` slot, with the macro_name
11036 // identity preserved across the call boundary. Sibling of the
11037 // `EndListEmptyStack` test above — together they pin the
11038 // primitive's closed-set posture (BOTH reachable
11039 // pop-emitting kinds route through the same primitive, neither
11040 // is hard-coded), so a future `TemplateInvariantKind` variant
11041 // added for a new pop-emitting op (e.g. a hypothetical
11042 // `EndManyEmptyStack`) extends the primitive's reachability
11043 // mechanically by passing the new kind through the same slot.
11044 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11045 let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
11046 .expect_err("empty stack must reject");
11047 match err {
11048 LispError::TemplateInvariant { macro_name, kind } => {
11049 assert_eq!(macro_name, "id");
11050 assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
11051 }
11052 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11053 }
11054 }
11055
11056 #[test]
11057 fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
11058 // Closed-set posture check: even kinds the production
11059 // `apply_compiled` op-loop ROUTES through `resolve_bound_arg`
11060 // rather than `pop_builder_frame` (the indexed `SubstBadIndex(_)`
11061 // / `SpliceBadIndex(_)` siblings) MUST compose correctly with
11062 // this primitive's typed slot — they are NOT reachable here
11063 // from the production loop, but `TemplateInvariantKind` does
11064 // not distinguish "indexed" kinds from "stack-gate" kinds at
11065 // the helper's signature, so a regression that special-cases
11066 // one kind family would be a silent type-narrowing the closed-
11067 // set typed enum was lifted to prevent. Pin the universal
11068 // routing: ANY kind variant feeds through the primitive's
11069 // `kind` slot identically. Sibling assertion to
11070 // `template_invariant_violation_threads_subst_idx_through_typed_variant`
11071 // — same compose-the-kind-into-the-variant contract, one
11072 // composition step further down the substrate stack.
11073 let mut empty: Vec<Vec<Sexp>> = Vec::new();
11074 let err = pop_builder_frame(
11075 &mut empty,
11076 "compose",
11077 TemplateInvariantKind::SubstBadIndex(42),
11078 )
11079 .expect_err("empty stack must reject regardless of kind family");
11080 match err {
11081 LispError::TemplateInvariant { macro_name, kind } => {
11082 assert_eq!(macro_name, "compose");
11083 assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
11084 }
11085 other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
11086 }
11087 }
11088
11089 #[test]
11090 fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
11091 // Structural-identity binding the lift to its pre-lift inline
11092 // shape: `pop_builder_frame(stack, macro_name, kind)` IS
11093 // `stack.pop().ok_or_else(|| template_invariant_violation(
11094 // macro_name, kind))?` — both reachable arms (success +
11095 // failure) must produce byte-identical outcomes. The success
11096 // arm checks the popped Vec contents AND the post-pop stack
11097 // length match the inline path; the failure arm checks the
11098 // emitted variant's identity AND its `macro_name` / `kind`
11099 // slots match the inline path. A regression that drifts the
11100 // primitive's projection (e.g. a `stack.swap_remove(_)` typo,
11101 // or a kind-rewrite at the helper boundary) fails on at least
11102 // one of the two arms.
11103 // Success arm:
11104 let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11105 let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
11106 let via_lift = pop_builder_frame(
11107 &mut stack_lift,
11108 "macro",
11109 TemplateInvariantKind::EndListEmptyStack,
11110 )
11111 .expect("non-empty stack pops cleanly through lift");
11112 let via_inline = stack_inline.pop().ok_or_else(|| {
11113 template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
11114 });
11115 assert_eq!(
11116 via_lift,
11117 via_inline.unwrap(),
11118 "popped frame must be byte-identical across lift vs inline"
11119 );
11120 assert_eq!(
11121 stack_lift.len(),
11122 stack_inline.len(),
11123 "post-pop stack length must be byte-identical across lift vs inline"
11124 );
11125 // Failure arm:
11126 let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
11127 let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
11128 let err_lift = pop_builder_frame(
11129 &mut empty_lift,
11130 "macro",
11131 TemplateInvariantKind::FinalNoValue,
11132 )
11133 .expect_err("empty stack rejects through lift");
11134 let err_inline = empty_inline
11135 .pop()
11136 .ok_or_else(|| {
11137 template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
11138 })
11139 .expect_err("empty stack rejects through inline");
11140 match (err_lift, err_inline) {
11141 (
11142 LispError::TemplateInvariant {
11143 macro_name: m_lift,
11144 kind: k_lift,
11145 },
11146 LispError::TemplateInvariant {
11147 macro_name: m_inline,
11148 kind: k_inline,
11149 },
11150 ) => {
11151 assert_eq!(m_lift, m_inline);
11152 assert_eq!(k_lift, k_inline);
11153 }
11154 (l, i) => panic!(
11155 "expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
11156 ),
11157 }
11158 }
11159
11160 #[test]
11161 fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
11162 // End-to-end path-uniformity guard for the `EndList` arm: a
11163 // `(BeginList, Literal(x), EndList)` program pushes a child
11164 // frame, populates it with one literal, then routes the child
11165 // frame OUT of the stack via `pop_builder_frame` (kind
11166 // `EndListEmptyStack` — unreachable on this valid input, but
11167 // the kind threads through the primitive identically). The
11168 // post-pop verb (`Sexp::List(items)` push into the parent via
11169 // `current_builder_mut`) yields the same `Sexp::List([x])`
11170 // shape the consumer projected pre-lift. Pin the byte-
11171 // identical outcome so a regression that drifts the EndList
11172 // arm's routing (e.g. swaps the kind constructor, or routes
11173 // through a different stack-mutating primitive) fails loudly
11174 // here. Sibling of
11175 // `current_builder_mut_routes_apply_compiled_end_list_parent_fold`
11176 // — that test pins the parent-fold PUSH; this test pins the
11177 // child-frame POP that immediately precedes it. Together the
11178 // two close the EndList arm's path-uniformity across the
11179 // pop-then-push composition.
11180 let tmpl = CompiledTemplate {
11181 ops: vec![
11182 TemplateOp::BeginList,
11183 TemplateOp::Literal(Sexp::symbol("only")),
11184 TemplateOp::EndList,
11185 ],
11186 };
11187 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11188 .expect("BeginList/EndList one-literal template must succeed");
11189 assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
11190 }
11191
11192 #[test]
11193 fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
11194 // End-to-end path-uniformity guard for the post-loop final
11195 // pop: a single-op `Literal(s)` program emits `s` into the
11196 // outermost (seed) frame, then the post-loop tail routes the
11197 // seed frame OUT via `pop_builder_frame` (kind `FinalNoValue`
11198 // — unreachable on this valid input). The post-pop arity gate
11199 // (`top.len() == 1 { top.remove(0) }`) projects the literal
11200 // back as the bound value. Pre-lift the same emission ran
11201 // through the inline `stack.pop().ok_or_else(|| template_
11202 // invariant_violation(_, FinalNoValue))?` chain at the
11203 // post-loop tail; post-lift it routes through ONE named
11204 // primitive the EndList arm ALSO routes through, and the
11205 // single-literal outcome is byte-identical across both code
11206 // paths. Sibling of
11207 // `current_builder_mut_routes_apply_compiled_literal_emit` —
11208 // that test pins the EMIT into the seed frame; this test pins
11209 // the POP of the same seed frame at the post-loop tail.
11210 // Together the two close the Literal-only program's path-
11211 // uniformity across the emit-then-consume composition.
11212 let tmpl = CompiledTemplate {
11213 ops: vec![TemplateOp::Literal(Sexp::int(123))],
11214 };
11215 let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
11216 .expect("literal-only template must succeed");
11217 assert_eq!(out, Sexp::int(123));
11218 }
11219
11220 // ── MacroParams: the typed param-list primitive ─────────────────────
11221 //
11222 // `parse_params` now yields a `MacroParams { required, optional, rest }`
11223 // whose shape makes the canonical lambda-list ordering (required →
11224 // optional → rest, "&rest is last + at-most-one", "&optional at most
11225 // once") structural rather than a construction discipline a `Vec<Param>`
11226 // only happened to uphold. These tests pin the parser's mapping into the
11227 // typed shape, the flat-index contract `names()` exposes to the template
11228 // bytecode, and the single positional binder `bind()` both expansion
11229 // strategies now route through. The end-to-end `rest_param_splices_with_at`
11230 // and `compiled_template_matches_substitute_path` tests above are the
11231 // path-uniformity guards proving both strategies still agree.
11232
11233 #[test]
11234 fn parse_params_maps_required_then_rest_into_typed_shape() {
11235 // `(a b &rest c)` — two required, one rest. The rest name lands in
11236 // the `Option`, never in `required`.
11237 let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
11238 assert_eq!(
11239 params,
11240 MacroParams {
11241 required: vec!["a".into(), "b".into()],
11242 optional: Vec::new(),
11243 rest: Some("c".into()),
11244 }
11245 );
11246 }
11247
11248 #[test]
11249 fn parse_params_rest_absent_leaves_none() {
11250 // `(x y)` — no `&rest`, so `rest` is structurally `None`. There is
11251 // no representation in which a rest-less list carries a stray rest.
11252 let params = parse_params(&read("x y").unwrap()).unwrap();
11253 assert_eq!(
11254 params,
11255 MacroParams {
11256 required: vec!["x".into(), "y".into()],
11257 optional: Vec::new(),
11258 rest: None,
11259 }
11260 );
11261 }
11262
11263 #[test]
11264 fn parse_params_maps_optional_section_between_required_and_rest() {
11265 // `(a &optional b c &rest d)` — the canonical lambda-list order. `a`
11266 // is required, `b`/`c` are optional, `d` is rest. The `&optional`
11267 // marker switches collection from `required` to `optional`; `&rest`
11268 // remains terminal.
11269 let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
11270 assert_eq!(
11271 params,
11272 MacroParams {
11273 required: vec!["a".into()],
11274 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11275 rest: Some("d".into()),
11276 }
11277 );
11278 }
11279
11280 #[test]
11281 fn parse_params_optional_with_no_rest_leaves_rest_none() {
11282 // `(&optional x)` — a leading `&optional` (zero required) with no
11283 // rest. `required` is empty, `x` is the sole optional, `rest` None.
11284 let params = parse_params(&read("&optional x").unwrap()).unwrap();
11285 assert_eq!(
11286 params,
11287 MacroParams {
11288 required: Vec::new(),
11289 optional: vec![OptionalParam::bare("x")],
11290 rest: None,
11291 }
11292 );
11293 }
11294
11295 #[test]
11296 fn parse_params_rejects_repeated_optional_marker() {
11297 // `(a &optional b &optional c)` — a second `&optional` is
11298 // unrepresentable (one flat optional section), so the gate REJECTS
11299 // rather than binding args to a marker symbol named `&optional`. The
11300 // two marker positions (1 and 3) are named.
11301 let err = parse_params(&read("a &optional b &optional c").unwrap())
11302 .expect_err("repeated &optional must error");
11303 assert!(
11304 matches!(
11305 err,
11306 LispError::OptionalMarkerRepeated {
11307 first_position: 1,
11308 second_position: 3,
11309 }
11310 ),
11311 "expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
11312 );
11313 }
11314
11315 #[test]
11316 fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
11317 // `(&rest xs &optional y)` — `&rest <name>` is terminal, so the
11318 // `&optional y` tail is REJECTED as trailing tokens (not silently
11319 // dropped, and not a repeated-optional error: the rest gate fires
11320 // first). Pins the interaction the prior run (3627426) signposted.
11321 let err = parse_params(&read("&rest xs &optional y").unwrap())
11322 .expect_err("tokens after &rest <name> must error");
11323 assert!(
11324 matches!(err, LispError::RestParamTrailingTokens { .. }),
11325 "expected RestParamTrailingTokens, got: {err:?}"
11326 );
11327 }
11328
11329 #[test]
11330 fn names_are_required_then_optional_then_rest_in_flat_index_order() {
11331 // The flat-index contract the bytecode `Subst(idx)`/`Splice(idx)`
11332 // depends on: required names at 0.., then optional names, then the
11333 // rest name last.
11334 let params = MacroParams {
11335 required: vec!["a".into(), "b".into()],
11336 optional: vec![OptionalParam::bare("c")],
11337 rest: Some("d".into()),
11338 };
11339 assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
11340 // Optional names occupy the indices immediately after the required run.
11341 assert_eq!(params.names()[params.required.len()], "c");
11342 // The rest name is last, after required + optional — i.e. at the
11343 // structural `fixed_arity()` boundary the typed primitive names.
11344 assert_eq!(params.names()[params.fixed_arity()], "d");
11345 }
11346
11347 // ── `MacroParams::{REST_MARKER, OPTIONAL_MARKER,
11348 // LAMBDA_LIST_KEYWORD_LEAD}` — the typed CL lambda-list-keyword
11349 // marker algebra ────────────────────────────────────────────────
11350 //
11351 // The three `pub const`s close the Common-Lisp lambda-list keyword
11352 // family at the typed [`MacroParams`] algebra: `REST_MARKER` and
11353 // `OPTIONAL_MARKER` are the two `&'static str` markers the parser's
11354 // typed dispatch specialises on; `LAMBDA_LIST_KEYWORD_LEAD` is the
11355 // canonical `'&'` char shared as the LEAD byte of both markers.
11356 // Pre-lift the same two `&'static str` markers lived as two inline
11357 // `s == "..."` comparisons at `parse_params` — post-lift both
11358 // comparisons route through the typed constants so a delimiter swap
11359 // (e.g. a Racket-compat `#!rest` port, a Clojure-compat `&` port)
11360 // lands at ONE constant on the typed algebra.
11361 //
11362 // These pins cover: (a) the exact `&'static str` / `char` values,
11363 // (b) the structural round-trip law binding each `&'static str`
11364 // marker to its shared `char` LEAD byte, (c) the pairwise
11365 // disjointness of the two `&'static str` markers, (d) the cross-
11366 // axis disjointness of the `char` LEAD byte against every sibling
11367 // outer-marker `char` the substrate's other closed-set algebras
11368 // specialise on, and (e) the path-uniformity of `parse_params`
11369 // through the typed constants.
11370
11371 #[test]
11372 fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
11373 // Pins the constant's exact `&'static str` bytes so a typo
11374 // (`"&res"`, `"&Rest"`, `"&rst"`) or an accidental redefinition
11375 // surfaces immediately. Sibling-shape pin to
11376 // `macro_params_optional_marker_projects_canonical_ampersand_optional_str`
11377 // on the peer `&optional` axis.
11378 assert_eq!(
11379 MacroParams::REST_MARKER,
11380 "&rest",
11381 "MacroParams::REST_MARKER drifted from the substrate- \
11382 canonical CL lambda-list `&rest` marker — the parser's \
11383 `parse_params` rest-slot dispatch AND every downstream \
11384 authoring / rendering surface binds to this ONE typed \
11385 constant.",
11386 );
11387 }
11388
11389 #[test]
11390 fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
11391 // Pins the constant's exact `&'static str` bytes so a typo
11392 // (`"&opt"`, `"&Optional"`, `"&option"`) or an accidental
11393 // redefinition surfaces immediately. Sibling-shape pin to
11394 // `macro_params_rest_marker_projects_canonical_ampersand_rest_str`
11395 // on the peer `&rest` axis.
11396 assert_eq!(
11397 MacroParams::OPTIONAL_MARKER,
11398 "&optional",
11399 "MacroParams::OPTIONAL_MARKER drifted from the substrate- \
11400 canonical CL lambda-list `&optional` marker — the parser's \
11401 `parse_params` optional-section dispatch AND every \
11402 downstream authoring / rendering surface binds to this \
11403 ONE typed constant.",
11404 );
11405 }
11406
11407 #[test]
11408 fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
11409 // Pins the constant's exact `char` value so a typo (`'#'`,
11410 // `'@'`, `'!'`) or an accidental redefinition surfaces
11411 // immediately. Sibling-shape pin to
11412 // `atom_keyword_marker_lead_projects_canonical_colon_char`,
11413 // `atom_bool_literal_lead_projects_canonical_hash_char`,
11414 // `atom_str_delimiter_projects_canonical_double_quote_char`
11415 // on the peer per-role LEAD-byte axes across the substrate's
11416 // closed-set outer algebras.
11417 assert_eq!(
11418 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11419 '&',
11420 "LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
11421 canonical `&` LEAD byte — the CL lambda-list-keyword \
11422 family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
11423 typed constant as their common LEAD byte.",
11424 );
11425 }
11426
11427 #[test]
11428 fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
11429 // STRUCTURAL ROUND-TRIP CONTRACT: the `&'static str` marker
11430 // `MacroParams::REST_MARKER` starts with the `char` LEAD byte
11431 // `MacroParams::LAMBDA_LIST_KEYWORD_LEAD` — the projection law
11432 // binding the two typed constants on the [`MacroParams`]
11433 // algebra. A regression that renames the `&'static str` (e.g.
11434 // to Racket's `"#!rest"` keyword-args) OR the `char` (e.g. to
11435 // `'#'` for the `#!` shebang lead) without updating the other
11436 // fails HERE. Sibling-shape pin to
11437 // `atom_keyword_marker_lead_prefixes_keyword_marker` on the
11438 // peer `Atom`-algebra Keyword-prefix LEAD-byte axis.
11439 assert!(
11440 MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11441 "MacroParams::REST_MARKER `{}` does NOT start with \
11442 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11443 typed constants have drifted apart on the [`MacroParams`] \
11444 algebra; the CL lambda-list-keyword family disjointness \
11445 contract can no longer bind to ONE shared LEAD byte.",
11446 MacroParams::REST_MARKER,
11447 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11448 );
11449 }
11450
11451 #[test]
11452 fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
11453 // STRUCTURAL ROUND-TRIP CONTRACT: peer to
11454 // `macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead`
11455 // on the `&optional` axis. Both `&'static str` markers of the
11456 // CL lambda-list-keyword family MUST share the canonical `char`
11457 // LEAD byte so the typed-marker disjointness contract can bind
11458 // to ONE shared LEAD byte on the [`MacroParams`] algebra.
11459 assert!(
11460 MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11461 "MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
11462 MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
11463 typed constants have drifted apart on the [`MacroParams`] \
11464 algebra; the CL lambda-list-keyword family disjointness \
11465 contract can no longer bind to ONE shared LEAD byte.",
11466 MacroParams::OPTIONAL_MARKER,
11467 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11468 );
11469 }
11470
11471 #[test]
11472 fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
11473 // PAIRWISE DISJOINTNESS PIN: the two `&'static str` markers on
11474 // the CL lambda-list-keyword algebra MUST differ so the
11475 // parser's typed dispatch cascade at `parse_params` (which
11476 // tests `REST_MARKER` FIRST, then `OPTIONAL_MARKER`) cannot
11477 // silently route both dispatches through the same arm. A
11478 // regression that aliases the two markers (e.g. both to
11479 // `"&rest"` after a typo) would silently drop the optional
11480 // section's structural distinction — every `&optional` name
11481 // would misclassify as a rest-slot marker.
11482 assert_ne!(
11483 MacroParams::REST_MARKER,
11484 MacroParams::OPTIONAL_MARKER,
11485 "REST_MARKER and OPTIONAL_MARKER collide — the parser's \
11486 typed dispatch cascade at `parse_params` can no longer \
11487 distinguish the rest-slot boundary from the optional- \
11488 section boundary.",
11489 );
11490 }
11491
11492 #[test]
11493 fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
11494 // CROSS-AXIS DISJOINTNESS PIN: `MacroParams::LAMBDA_LIST_KEYWORD_LEAD`
11495 // MUST NOT alias any sibling outer-marker `char` on the
11496 // substrate's other closed-set algebras — the Atom-payload
11497 // markers (`STR_DELIMITER`, `STR_ESCAPE_LEAD`,
11498 // `KEYWORD_MARKER_LEAD`, `BOOL_LITERAL_LEAD`), the paired list
11499 // delimiters (`Sexp::LIST_OPEN` / `Sexp::LIST_CLOSE`), the
11500 // paired line-comment delimiters (`Sexp::COMMENT_LEAD` /
11501 // `Sexp::COMMENT_TERM`), every `QuoteForm::lead_char`
11502 // projection, AND `QuoteForm::SPLICE_DISCRIMINATOR`. A
11503 // collision would silently break the reader's outer dispatch:
11504 // an `&`-prefixed bare atom `&rest` / `&optional` would collide
11505 // with whichever marker it aliased. Sibling-shape pin to
11506 // `atom_keyword_marker_lead_distinct_from_every_other_algebra_marker`
11507 // on the peer `Atom`-algebra Keyword-prefix LEAD-byte axis —
11508 // pins the SAME shape on the CL lambda-list-keyword LEAD-byte
11509 // axis. A future outer-marker extension that collided with
11510 // `'&'` fails HERE at the cross-axis enumeration.
11511 use crate::ast::{Atom, QuoteForm, Sexp};
11512
11513 assert_ne!(
11514 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11515 Atom::STR_DELIMITER,
11516 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
11517 bare `&rest` at a param-list position would ambiguously \
11518 begin a lambda-list keyword AND open a string.",
11519 );
11520 assert_ne!(
11521 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11522 Atom::STR_ESCAPE_LEAD,
11523 "LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
11524 the reader's Str-escape lead byte would alias the CL \
11525 lambda-list-keyword LEAD byte.",
11526 );
11527 assert_ne!(
11528 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11529 Atom::KEYWORD_MARKER_LEAD,
11530 "LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
11531 — a bare `&rest` at a param-list position would \
11532 ambiguously begin a lambda-list keyword AND begin an \
11533 `:foo` keyword.",
11534 );
11535 assert_ne!(
11536 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11537 Atom::BOOL_LITERAL_LEAD,
11538 "LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
11539 a bare `&rest` at a param-list position would ambiguously \
11540 begin a lambda-list keyword AND classify as a Bool prefix.",
11541 );
11542 assert_ne!(
11543 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11544 Sexp::LIST_OPEN,
11545 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
11546 `&rest` at a param-list position would ambiguously begin \
11547 a lambda-list keyword AND open a list.",
11548 );
11549 assert_ne!(
11550 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11551 Sexp::LIST_CLOSE,
11552 "LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
11553 `&rest` at a param-list position would ambiguously begin \
11554 a lambda-list keyword AND close a list.",
11555 );
11556 assert_ne!(
11557 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11558 Sexp::COMMENT_LEAD,
11559 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
11560 bare `&rest` at a param-list position would ambiguously \
11561 begin a lambda-list keyword AND begin a comment.",
11562 );
11563 assert_ne!(
11564 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11565 Sexp::COMMENT_TERM,
11566 "LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
11567 reader's line-comment discard loop would terminate on the \
11568 SAME byte the parser's lambda-list-keyword LEAD dispatch \
11569 binds to.",
11570 );
11571 for qf in QuoteForm::ALL {
11572 assert_ne!(
11573 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11574 qf.lead_char(),
11575 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11576 QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
11577 param-list position would ambiguously begin a lambda- \
11578 list keyword AND begin a quote-family prefix.",
11579 );
11580 }
11581 assert_ne!(
11582 MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11583 QuoteForm::SPLICE_DISCRIMINATOR,
11584 "LAMBDA_LIST_KEYWORD_LEAD collides with \
11585 SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
11586 peek byte would alias the CL lambda-list-keyword LEAD \
11587 byte.",
11588 );
11589 }
11590
11591 #[test]
11592 fn parse_params_recognizes_rest_marker_via_typed_constant() {
11593 // PATH-UNIFORMITY PIN: the parser's rest-slot dispatch at
11594 // `parse_params` MUST classify the typed constant
11595 // `MacroParams::REST_MARKER` as the rest-slot boundary —
11596 // authoring surfaces that assemble a param list through the
11597 // typed constant (rather than through an inline string literal
11598 // read from user source) must reach the SAME rest-slot binding
11599 // the reader-driven path does.
11600 //
11601 // The `read(REST_MARKER)` composition here is the load-bearing
11602 // structural link: we build the param-list source through the
11603 // typed constant, then parse it through the reader, then the
11604 // parser MUST bind `xs` at the `rest` slot. A regression that
11605 // drifts the parser's dispatch away from the typed constant
11606 // (e.g. re-inlines a string literal that goes stale against
11607 // the constant) fails HERE.
11608 let src = format!("a {} xs", MacroParams::REST_MARKER);
11609 let params = parse_params(&read(&src).unwrap()).unwrap();
11610 assert_eq!(
11611 params,
11612 MacroParams {
11613 required: vec!["a".into()],
11614 optional: Vec::new(),
11615 rest: Some("xs".into()),
11616 },
11617 "parse_params dispatch drifted away from REST_MARKER — the \
11618 typed constant no longer routes to the rest-slot arm.",
11619 );
11620 }
11621
11622 #[test]
11623 fn parse_params_recognizes_optional_marker_via_typed_constant() {
11624 // PATH-UNIFORMITY PIN: peer to
11625 // `parse_params_recognizes_rest_marker_via_typed_constant` on
11626 // the `&optional` axis. The parser's optional-section dispatch
11627 // at `parse_params` MUST classify the typed constant
11628 // `MacroParams::OPTIONAL_MARKER` as the section-switch
11629 // boundary that reroutes subsequent bare-symbol names from the
11630 // `required` bin to the `optional` bin.
11631 let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
11632 let params = parse_params(&read(&src).unwrap()).unwrap();
11633 assert_eq!(
11634 params,
11635 MacroParams {
11636 required: vec!["a".into()],
11637 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
11638 rest: None,
11639 },
11640 "parse_params dispatch drifted away from OPTIONAL_MARKER — \
11641 the typed constant no longer routes to the optional- \
11642 section arm.",
11643 );
11644 }
11645
11646 // ── `MacroParams::LAMBDA_LIST_KEYWORDS` + `is_lambda_list_keyword` —
11647 // the closed-set forced-arity ALL array + membership gate that closes
11648 // the CL lambda-list-keyword family at the typed [`MacroParams`]
11649 // algebra. Sibling posture to the closed set of `pub const ALL: [Self;
11650 // N]` forced-arity arrays across the substrate's other closed-set
11651 // outer algebras (`AtomKind::ALL`, `QuoteForm::ALL`, `SexpShape::ALL`,
11652 // `UnquoteForm::ALL`, `MacroDefHead::ALL`); a future third marker
11653 // (`&key`, `&aux`, `&body`) extends the array's arity + adds ONE `pub
11654 // const` for the marker and every downstream family-wide contract
11655 // sweep (LEAD-byte round-trip, pairwise disjointness, membership
11656 // gate) picks up the extension at ONE structural site.
11657 //
11658 // These pins cover: (a) the cardinality contract (the ALL array's
11659 // length matches the pre-lift per-role marker count), (b) the
11660 // ordering contract (each ALL entry matches its corresponding per-
11661 // role `pub const` by-index), (c) the family-wide structural round-
11662 // trip (every ALL element starts_with LEAD), (d) the family-wide
11663 // pairwise disjointness (every distinct ALL index pair yields
11664 // distinct markers), (e) the membership-gate acceptance side (every
11665 // ALL element classifies as `true`), and (f) the membership-gate
11666 // rejection side (the bare LEAD byte + unrecognised `&`-prefixed
11667 // names classify as `false`).
11668 //
11669 // Fail-before/pass-after: every test below references
11670 // `MacroParams::LAMBDA_LIST_KEYWORDS` or
11671 // `MacroParams::is_lambda_list_keyword`, which simply did not exist
11672 // on `MacroParams` before this lift — every assertion was a
11673 // compile-time error against the prior surface.
11674
11675 #[test]
11676 fn macro_params_lambda_list_keywords_has_expected_cardinality() {
11677 // CARDINALITY PIN: the ALL array closes the family at exactly TWO
11678 // entries — the two `&'static str` markers the parser's typed
11679 // dispatch specialises on today (`&rest` + `&optional`). A future
11680 // third marker (`&key`, `&aux`, `&body`) extends this arity to
11681 // 3 AND every downstream family-wide contract sweep picks up the
11682 // extension at ONE structural site. A regression that drops a
11683 // marker from the ALL array (silently narrowing the family) OR
11684 // aliases two markers to the same index (silently narrowing the
11685 // typed dispatch) fails HERE at the cardinality contract before
11686 // the structural round-trip / pairwise-disjointness contracts
11687 // even fire.
11688 assert_eq!(
11689 MacroParams::LAMBDA_LIST_KEYWORDS.len(),
11690 2,
11691 "LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
11692 lambda-list-keyword family closure now names a different \
11693 number of markers than the two the parser's typed dispatch \
11694 specialises on.",
11695 );
11696 }
11697
11698 #[test]
11699 fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
11700 // ORDERING PIN: each ALL entry matches its corresponding per-role
11701 // `pub const` by-index. A regression that swaps the ordering
11702 // (`[OPTIONAL_MARKER, REST_MARKER]` after a rebase) would keep
11703 // the cardinality + membership contracts intact but silently
11704 // reorder downstream consumers that iterate `LAMBDA_LIST_KEYWORDS`
11705 // in canonical order (a future authoring surface that renders
11706 // "supported CL lambda-list keywords: &rest, &optional" would
11707 // silently render "supported CL lambda-list keywords: &optional,
11708 // &rest" instead). Pins the array's declaration-order binding
11709 // structurally so a reorder fails HERE at each element rather
11710 // than only at consumer sites downstream.
11711 assert_eq!(
11712 MacroParams::LAMBDA_LIST_KEYWORDS[0],
11713 MacroParams::REST_MARKER,
11714 "LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
11715 array's declaration-order binding to the per-role `pub \
11716 const` broke at the rest-slot marker slot.",
11717 );
11718 assert_eq!(
11719 MacroParams::LAMBDA_LIST_KEYWORDS[1],
11720 MacroParams::OPTIONAL_MARKER,
11721 "LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
11722 ALL array's declaration-order binding to the per-role \
11723 `pub const` broke at the optional-section marker slot.",
11724 );
11725 }
11726
11727 #[test]
11728 fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
11729 // FAMILY-WIDE STRUCTURAL ROUND-TRIP PIN: every element of the ALL
11730 // array MUST start with the canonical LEAD byte. Where the two
11731 // per-marker `_prefixed_by_lambda_list_keyword_lead` pins each
11732 // named ONE marker inline as a duplicate 3-line
11733 // `assert!(A.starts_with(LEAD), ...)` shape, this family-wide
11734 // sweep routes the SAME contract through the closed-set ALL
11735 // array — a future third marker (`&key`, `&aux`, `&body`)
11736 // extends the array AND is automatically covered by this sweep
11737 // without adding a third `_prefixed_by_...` per-marker pin. The
11738 // per-marker pins stay as fine-grained fail-loud sites; this
11739 // pin is the algebra-wide closure the family-extending refactor
11740 // routes through.
11741 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11742 assert!(
11743 m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
11744 "LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
11745 LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
11746 list-keyword family's structural round-trip contract \
11747 no longer binds every marker to its shared LEAD byte.",
11748 lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
11749 );
11750 }
11751 }
11752
11753 #[test]
11754 fn macro_params_lambda_list_keywords_pairwise_distinct() {
11755 // FAMILY-WIDE PAIRWISE DISJOINTNESS PIN: every distinct index pair
11756 // `(i, j)` in the ALL array yields distinct markers. Where the
11757 // pre-lift `_rest_and_optional_markers_pairwise_disjoint` pin
11758 // named the TWO markers as a hand-rolled `assert_ne!` pair, this
11759 // family-wide sweep routes the SAME contract through the closed-
11760 // set ALL array so a future third marker automatically extends
11761 // the sweep to `(3 * 2) / 2 == 3` distinct pairs without re-
11762 // deriving per-marker `assert_ne!` calls. The pre-lift pin
11763 // stays as the fine-grained fail-loud site; this pin is the
11764 // family-wide closure the algebra-extending refactor routes
11765 // through.
11766 for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11767 for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
11768 if i == j {
11769 continue;
11770 }
11771 assert_ne!(
11772 a, b,
11773 "LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
11774 LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
11775 list-keyword family's pairwise disjointness \
11776 contract no longer binds distinct index pairs \
11777 to distinct markers.",
11778 );
11779 }
11780 }
11781 }
11782
11783 #[test]
11784 fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
11785 // ACCEPTANCE-SIDE MEMBERSHIP PIN: every element of the ALL array
11786 // MUST classify as `true` through the typed membership gate. The
11787 // gate is defined as `LAMBDA_LIST_KEYWORDS.contains(&s)`, so this
11788 // pin is the structural closure binding the gate's return value
11789 // to the ALL array's element set. A regression that specialises
11790 // the gate to a subset of the array (e.g. a `matches!(s,
11791 // "&rest")` inline that silently drops the `&optional` branch)
11792 // fails HERE at the ALL sweep. Sibling-shape acceptance sweep to
11793 // the closed-set `ClosedSet::parse_label` roundtrip pin — every
11794 // element of `Self::ALL` decodes through the projection back to
11795 // itself.
11796 for m in MacroParams::LAMBDA_LIST_KEYWORDS {
11797 assert!(
11798 MacroParams::is_lambda_list_keyword(m),
11799 "is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
11800 element `{m}` — the closed-set membership gate's \
11801 acceptance side drifted from the ALL array.",
11802 );
11803 }
11804 }
11805
11806 #[test]
11807 fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
11808 // REJECTION-SIDE MEMBERSHIP PIN (LEAD byte alone): the bare LEAD
11809 // byte `"&"` MUST classify as `false`. A future Clojure-compat
11810 // port might land a bare `&` marker — but until such an
11811 // extension explicitly lands on the ALL array, the substrate
11812 // MUST NOT silently classify the LEAD byte alone as a
11813 // recognised CL lambda-list keyword. Pins the gate's
11814 // rejection-side contract against a plausible future
11815 // near-miss extension.
11816 let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
11817 assert!(
11818 !MacroParams::is_lambda_list_keyword(&bare_lead),
11819 "is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
11820 the closed-set membership gate silently classifies the bare `&` \
11821 LEAD byte as a recognised CL lambda-list keyword despite the ALL \
11822 array containing only the two suffixed markers.",
11823 );
11824 }
11825
11826 #[test]
11827 fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
11828 // REJECTION-SIDE MEMBERSHIP PIN (unrecognised near-misses): the
11829 // three plausible future CL lambda-list keywords (`&key` for
11830 // keyword-args, `&aux` for auxiliaries, `&body` for the
11831 // docstring-carrying tail of `defmacro` bodies) MUST classify as
11832 // `false` UNTIL they explicitly land on the ALL array. Pins the
11833 // gate's conservative rejection contract — the closed-set
11834 // membership gate is not a "starts with `&`" heuristic; it is a
11835 // typed enumeration over the currently-recognised markers. A
11836 // regression that silently loosens the gate to a `starts_with`
11837 // check (dropping the closed-set discipline) fails HERE.
11838 for candidate in ["&key", "&aux", "&body"] {
11839 assert!(
11840 !MacroParams::is_lambda_list_keyword(candidate),
11841 "is_lambda_list_keyword accepted the unrecognised \
11842 `&`-prefixed name `{candidate}` — the closed-set \
11843 membership gate silently loosened its acceptance beyond \
11844 the two markers the ALL array names today.",
11845 );
11846 }
11847 }
11848
11849 #[test]
11850 fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
11851 // REJECTION-SIDE MEMBERSHIP PIN (bare identifiers + empty): a
11852 // bare identifier (`"a"`, `"xs"`, `"foo"`) and the empty string
11853 // MUST classify as `false`. These are the shapes the parser's
11854 // bare-symbol dispatch cascade sees at `parse_params` when
11855 // walking a param list, so pinning them here binds the
11856 // membership gate's rejection contract to the parser's most
11857 // common non-marker input shapes.
11858 for candidate in ["a", "xs", "foo", ""] {
11859 assert!(
11860 !MacroParams::is_lambda_list_keyword(candidate),
11861 "is_lambda_list_keyword accepted the bare non-marker \
11862 input `{candidate}` — the closed-set membership gate's \
11863 rejection side no longer excludes bare identifiers \
11864 the parser routes through the fall-through cascade.",
11865 );
11866 }
11867 }
11868
11869 // ── fixed_arity: the rest-start / rest-less max-arity primitive ─────
11870 //
11871 // `fixed_arity()` lifts the `self.required.len() + self.optional.len()`
11872 // arithmetic that recurred three times inside `MacroParams::bind` — at
11873 // the `Vec::with_capacity` site (where it adds `usize::from(rest.is_some())`
11874 // to get the bound-values count), at the `rest_start` site (inside the
11875 // `if let Some(rest)` branch), and at the `expected` site (inside the
11876 // rest-less `else`). The latter two sites live in mutually-exclusive
11877 // branches yet name ONE structural concept; lifting them collapses the
11878 // arithmetic to one named primitive. These tests pin the primitive's
11879 // contract directly; the existing `bind_*` tests are the path-uniformity
11880 // guards proving `bind`'s sites route through the same value without
11881 // behavior drift.
11882 //
11883 // Fail-before/pass-after: every test below references
11884 // `params.fixed_arity()`, which simply did not exist on `MacroParams`
11885 // before this lift — every assertion's `expect: ___ == params.fixed_arity()`
11886 // line was a compile-time error against the prior surface.
11887
11888 #[test]
11889 fn fixed_arity_is_zero_for_the_empty_param_list() {
11890 // `()` — a nullary macro has fixed arity 0, the rest-less binder
11891 // boundary at which the FIRST surplus arg already rejects.
11892 let params = MacroParams::default();
11893 assert_eq!(params.fixed_arity(), 0);
11894 }
11895
11896 #[test]
11897 fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
11898 // `(a b c)` — three required, no optional, no rest. fixed_arity is
11899 // exactly the required length.
11900 let params = MacroParams {
11901 required: vec!["a".into(), "b".into(), "c".into()],
11902 optional: Vec::new(),
11903 rest: None,
11904 };
11905 assert_eq!(params.fixed_arity(), 3);
11906 }
11907
11908 #[test]
11909 fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
11910 // `(&optional x y)` — two optional, no required. fixed_arity is the
11911 // optional length; the optional section participates in the fixed
11912 // run because supplied positional args bind to it.
11913 let params = MacroParams {
11914 required: Vec::new(),
11915 optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
11916 rest: None,
11917 };
11918 assert_eq!(params.fixed_arity(), 2);
11919 }
11920
11921 #[test]
11922 fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
11923 // `(a b &optional c d e)` — two required + three optional, no rest.
11924 // fixed_arity is 5: the maximum arity a rest-less call can supply.
11925 let params = MacroParams {
11926 required: vec!["a".into(), "b".into()],
11927 optional: vec![
11928 OptionalParam::bare("c"),
11929 OptionalParam::bare("d"),
11930 OptionalParam::bare("e"),
11931 ],
11932 rest: None,
11933 };
11934 assert_eq!(params.fixed_arity(), 5);
11935 }
11936
11937 #[test]
11938 fn fixed_arity_ignores_rest_slot_by_construction() {
11939 // `(a &optional b &rest r)` and `(a &optional b)` — identical fixed
11940 // arity (2). The `&rest` slot has NO maximum and is structurally
11941 // excluded from `fixed_arity`. Naming this invariant pins that a
11942 // regression that drifts the primitive to "required + optional +
11943 // rest.is_some() as usize" fails loudly here — that drift would
11944 // collapse `fixed_arity` into `names().len()`, losing the rest-start
11945 // vs total-bound-values distinction the typed shape relies on.
11946 let with_rest = MacroParams {
11947 required: vec!["a".into()],
11948 optional: vec![OptionalParam::bare("b")],
11949 rest: Some("r".into()),
11950 };
11951 let without_rest = MacroParams {
11952 required: vec!["a".into()],
11953 optional: vec![OptionalParam::bare("b")],
11954 rest: None,
11955 };
11956 assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
11957 assert_eq!(with_rest.fixed_arity(), 2);
11958 }
11959
11960 #[test]
11961 fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
11962 // When `rest` is `Some`, `names()[fixed_arity()]` IS the rest name
11963 // — the rest-start reading of the primitive. Same arithmetic the
11964 // bytecode index would hit (`Subst(fixed_arity())` resolves to the
11965 // rest-bound `Sexp::List`).
11966 let params = MacroParams {
11967 required: vec!["a".into(), "b".into()],
11968 optional: vec![OptionalParam::bare("c")],
11969 rest: Some("r".into()),
11970 };
11971 assert_eq!(params.fixed_arity(), 3);
11972 assert_eq!(params.names()[params.fixed_arity()], "r");
11973 }
11974
11975 #[test]
11976 fn fixed_arity_equals_names_length_when_rest_is_absent() {
11977 // When `rest` is `None`, `names().len() == fixed_arity()` — there
11978 // is no rest-name slot to extend the flat run past the fixed
11979 // boundary. Pins the structural identity
11980 // `names().len() == fixed_arity() + usize::from(rest.is_some())`
11981 // for the rest-less case; the rest-present case is pinned by the
11982 // sibling test above (where the boundary is the rest-name index,
11983 // i.e. one short of `names().len()`).
11984 let params = MacroParams {
11985 required: vec!["a".into(), "b".into()],
11986 optional: vec![OptionalParam::bare("c")],
11987 rest: None,
11988 };
11989 assert_eq!(params.names().len(), params.fixed_arity());
11990 assert_eq!(params.names().len(), 3);
11991 }
11992
11993 #[test]
11994 fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
11995 // The `expected` field of `TooManyMacroArgs` IS `fixed_arity()` —
11996 // the rest-less binder rejects iff `args.len() > fixed_arity()`.
11997 // This pin is the path-uniformity guard binding the typed primitive
11998 // to the binder's rejection contract: a regression that drifts
11999 // `bind`'s `expected` arithmetic from `fixed_arity()` would silently
12000 // surface a different boundary in the diagnostic without touching
12001 // the primitive — and this assertion fails loudly. Mirror of the
12002 // sibling rest-less surplus pin (`bind_rest_less_params_reject_
12003 // surplus_args`); this test pins WHAT the `expected` slot's value
12004 // structurally IS, that pin checks the variant SHAPE.
12005 let params = MacroParams {
12006 required: vec!["a".into(), "b".into()],
12007 optional: vec![OptionalParam::bare("c")],
12008 rest: None,
12009 };
12010 assert_eq!(params.fixed_arity(), 3);
12011 let err = params
12012 .bind(
12013 "m",
12014 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12015 )
12016 .expect_err("4 args against fixed_arity 3 must reject");
12017 match err {
12018 LispError::TooManyMacroArgs {
12019 expected,
12020 got,
12021 macro_name,
12022 } => {
12023 assert_eq!(expected, params.fixed_arity());
12024 assert_eq!(got, 4);
12025 assert_eq!(macro_name, "m");
12026 }
12027 other => panic!("expected TooManyMacroArgs, got {other:?}"),
12028 }
12029 }
12030
12031 #[test]
12032 fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
12033 // When `rest` is `Some`, `bind` collects `args[fixed_arity()..]`
12034 // into the rest's `Sexp::List`. Pin that the rest list contents
12035 // are exactly the suffix beginning at `fixed_arity()` — the
12036 // primitive's rest-start reading IS the slice index the binder
12037 // consumes. A regression that drifts `bind`'s rest-collection
12038 // slice from `fixed_arity()` would surface as a misaligned rest
12039 // list (off-by-one in either direction) and this assertion fails
12040 // loudly. Sibling of `fixed_arity_is_the_rest_less_surplus_
12041 // rejection_boundary` on the rest-PRESENT branch.
12042 let params = MacroParams {
12043 required: vec!["a".into()],
12044 optional: vec![OptionalParam::bare("b")],
12045 rest: Some("r".into()),
12046 };
12047 assert_eq!(params.fixed_arity(), 2);
12048 let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
12049 let vals = params.bind("m", &args).unwrap();
12050 // Bound vec: [a=1, b=2, r=(3 4)] — the rest list IS args[fixed_arity()..].
12051 let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
12052 assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
12053 }
12054
12055 #[test]
12056 fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
12057 // Exactly-saturated rest-present call: `args.len() == fixed_arity()`.
12058 // The rest slot collects the empty slice; bind succeeds, never
12059 // misaligning to an off-by-one underflow. Pin the boundary on the
12060 // rest-PRESENT path — the rest-less mirror is
12061 // `too_many_macro_args_does_not_fire_at_exact_max_arity` above.
12062 let params = MacroParams {
12063 required: vec!["a".into()],
12064 optional: vec![OptionalParam::bare("b")],
12065 rest: Some("r".into()),
12066 };
12067 assert_eq!(params.fixed_arity(), 2);
12068 let vals = params
12069 .bind("m", &[Sexp::int(1), Sexp::int(2)])
12070 .expect("rest-present at exact fixed_arity must bind cleanly");
12071 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
12072 }
12073
12074 #[test]
12075 fn bind_threads_required_positionally_and_collects_rest_as_list() {
12076 // `(a b &rest c)` bound to `1 2 3 4`: a=1, b=2, c=(3 4). The bound
12077 // vec is parallel to `names()`, so the rest list sits at the rest's
12078 // flat index.
12079 let params = MacroParams {
12080 required: vec!["a".into(), "b".into()],
12081 optional: Vec::new(),
12082 rest: Some("c".into()),
12083 };
12084 let vals = params
12085 .bind(
12086 "m",
12087 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12088 )
12089 .unwrap();
12090 assert_eq!(
12091 vals,
12092 vec![
12093 Sexp::int(1),
12094 Sexp::int(2),
12095 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12096 ]
12097 );
12098 }
12099
12100 #[test]
12101 fn bind_supplied_optional_takes_its_positional_arg() {
12102 // `(a &optional b)` bound to `1 2`: a=1, b=2. A supplied optional
12103 // behaves exactly like a positional — only its ABSENCE differs.
12104 let params = MacroParams {
12105 required: vec!["a".into()],
12106 optional: vec![OptionalParam::bare("b")],
12107 rest: None,
12108 };
12109 let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
12110 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
12111 }
12112
12113 #[test]
12114 fn bind_unsupplied_optional_defaults_to_nil() {
12115 // `(a &optional b c)` bound to just `1`: a=1, then b and c run out of
12116 // args and bind to `Sexp::Nil` — CL's default for an `&optional` with
12117 // no supplied default-form. The bound vec is still parallel to
12118 // `names()`, so the template's `,b` / `,c` resolve to nil, not a
12119 // missing-arg error.
12120 let params = MacroParams {
12121 required: vec!["a".into()],
12122 optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
12123 rest: None,
12124 };
12125 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12126 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
12127 }
12128
12129 #[test]
12130 fn bind_rest_collects_args_beyond_required_and_optional() {
12131 // `(a &optional b &rest c)` bound to `1 2 3 4`: a=1, b=2 (supplied),
12132 // c=(3 4). The rest starts AFTER the required+optional run, so the
12133 // optional's supplied arg is not swept into the rest.
12134 let params = MacroParams {
12135 required: vec!["a".into()],
12136 optional: vec![OptionalParam::bare("b")],
12137 rest: Some("c".into()),
12138 };
12139 let vals = params
12140 .bind(
12141 "m",
12142 &[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
12143 )
12144 .unwrap();
12145 assert_eq!(
12146 vals,
12147 vec![
12148 Sexp::int(1),
12149 Sexp::int(2),
12150 Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
12151 ]
12152 );
12153 }
12154
12155 #[test]
12156 fn bind_unsupplied_optional_then_empty_rest() {
12157 // `(a &optional b &rest c)` bound to just `1`: a=1, b=nil (absent),
12158 // c=() (nothing left). Both the optional default AND the empty-rest
12159 // contract hold in the same bind.
12160 let params = MacroParams {
12161 required: vec!["a".into()],
12162 optional: vec![OptionalParam::bare("b")],
12163 rest: Some("c".into()),
12164 };
12165 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12166 assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
12167 }
12168
12169 #[test]
12170 fn bind_rest_with_no_remaining_args_is_the_empty_list() {
12171 // Exactly-saturated required args + a rest that captures nothing →
12172 // the rest binds to the empty list, never errors. Mirrors the
12173 // splice contract `,@()` contributes nothing.
12174 let params = MacroParams {
12175 required: vec!["a".into()],
12176 optional: Vec::new(),
12177 rest: Some("c".into()),
12178 };
12179 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12180 assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
12181 }
12182
12183 #[test]
12184 fn bind_missing_required_errors_before_any_rest_collection() {
12185 // A required name with no arg at its position is a
12186 // `MissingMacroArg` — the gate fires during the required walk,
12187 // before the rest is ever collected.
12188 let params = MacroParams {
12189 required: vec!["a".into(), "b".into()],
12190 optional: Vec::new(),
12191 rest: Some("c".into()),
12192 };
12193 let err = params
12194 .bind("m", &[Sexp::int(1)])
12195 .expect_err("missing required `b` must error");
12196 assert!(
12197 matches!(err, LispError::MissingMacroArg { .. }),
12198 "expected MissingMacroArg, got: {err:?}"
12199 );
12200 }
12201
12202 #[test]
12203 fn bind_missing_required_errors_even_with_optional_present() {
12204 // An absent REQUIRED arg errors even when the param list has an
12205 // optional section: the required walk fires `MissingMacroArg` before
12206 // the optional arm (which would otherwise default to nil) is reached.
12207 // Required absence is an error; optional absence is a nil default.
12208 let params = MacroParams {
12209 required: vec!["a".into(), "b".into()],
12210 optional: vec![OptionalParam::bare("c")],
12211 rest: None,
12212 };
12213 let err = params
12214 .bind("m", &[Sexp::int(1)])
12215 .expect_err("missing required `b` must error before optional defaulting");
12216 assert!(
12217 matches!(err, LispError::MissingMacroArg { .. }),
12218 "expected MissingMacroArg, got: {err:?}"
12219 );
12220 }
12221
12222 #[test]
12223 fn bind_rest_less_params_reject_surplus_args() {
12224 // The rest-less binder REJECTS surplus call args via the
12225 // structural `TooManyMacroArgs { macro_name, expected, got }`
12226 // rejection — the call-site mirror of `RestParamTrailingTokens`
12227 // (the definition-site rejection lifted at the parse_params
12228 // boundary). Closes the asymmetry where the typed-entry
12229 // macro-call-gate rejected too-few-args loudly
12230 // (`MissingMacroArg`) but silently truncated too-many. `expected`
12231 // is the rest-less binder's fixed maximum arity
12232 // (`required.len() + optional.len()`); `got` is the actual
12233 // call-site arg count.
12234 let params = MacroParams {
12235 required: vec!["a".into()],
12236 optional: Vec::new(),
12237 rest: None,
12238 };
12239 let err = params
12240 .bind("m", &[Sexp::int(1), Sexp::int(2)])
12241 .expect_err("rest-less surplus must error");
12242 match err {
12243 LispError::TooManyMacroArgs {
12244 macro_name,
12245 expected,
12246 got,
12247 } => {
12248 assert_eq!(macro_name, "m");
12249 assert_eq!(expected, 1);
12250 assert_eq!(got, 2);
12251 }
12252 other => panic!("expected TooManyMacroArgs, got: {other:?}"),
12253 }
12254 }
12255
12256 // ── OptionalParam: per-param default forms — `&optional (x DEFAULT)` ──
12257 //
12258 // The `&optional` section now admits both bare-symbol entries (`x`) AND
12259 // list-form entries (`(x DEFAULT)`). The typed `OptionalParam.default:
12260 // Option<Sexp>` slot makes the per-param default a FIELD on each
12261 // optional entry, not a discipline a sibling `Vec<Sexp>` would have had
12262 // to maintain in lock-step with `Vec<String>`. These tests pin: the
12263 // parser admits both shapes side-by-side; the four malformed list-spec
12264 // shapes (empty / missing-default / extra-elements / non-symbol-name)
12265 // are rejected via `OptionalParamMalformed` with the typed
12266 // `OptionalParamMalformedReason`; the binder consults the default form
12267 // when the arg is absent and ignores it when supplied; and the end-to-
12268 // end expansion agrees between the bytecode and substitute strategies
12269 // (invariant 2 — free middle).
12270
12271 #[test]
12272 fn parse_params_admits_optional_list_spec_with_default() {
12273 // `(a &optional (b 5))` — one bare optional becomes
12274 // `OptionalParam { name: "b", default: Some(Int(5)) }`. The
12275 // surrounding `MacroParams` shape is otherwise identical.
12276 let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
12277 assert_eq!(
12278 params,
12279 MacroParams {
12280 required: vec!["a".into()],
12281 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12282 rest: None,
12283 }
12284 );
12285 }
12286
12287 #[test]
12288 fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
12289 // `(a &optional b (c "x") d (e 9) &rest r)` — the optional section
12290 // interleaves bare and list-form specs. Each lands in its own
12291 // `OptionalParam` entry; `names()` still yields the flat
12292 // required-then-optional-then-rest order.
12293 let params =
12294 parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
12295 assert_eq!(
12296 params,
12297 MacroParams {
12298 required: vec!["a".into()],
12299 optional: vec![
12300 OptionalParam::bare("b"),
12301 OptionalParam::with_default("c", Sexp::string("x")),
12302 OptionalParam::bare("d"),
12303 OptionalParam::with_default("e", Sexp::int(9)),
12304 ],
12305 rest: Some("r".into()),
12306 }
12307 );
12308 assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
12309 }
12310
12311 #[test]
12312 fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
12313 // `(&optional (x (list 1 2)))` — the default form is itself a list.
12314 // Without an evaluator, the literal Sexp is parked verbatim into
12315 // `default`; the binder produces it for any absent call.
12316 let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
12317 let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12318 assert_eq!(
12319 params,
12320 MacroParams {
12321 required: Vec::new(),
12322 optional: vec![OptionalParam::with_default("x", want_default)],
12323 rest: None,
12324 }
12325 );
12326 }
12327
12328 #[test]
12329 fn parse_params_rejects_empty_list_optional_spec() {
12330 // `(&optional ())` — a zero-element list is the empty-list rejection.
12331 // Without the gate the loop would `as_symbol()` on a `Sexp::List` and
12332 // fall through to `NonSymbolParam`, which mis-classifies the failure
12333 // (this is a malformed DEFAULT-FORM spec, not a "param must be a
12334 // symbol" rejection).
12335 let err = parse_params(&read("&optional ()").unwrap())
12336 .expect_err("empty list optional spec must error");
12337 assert!(
12338 matches!(
12339 err,
12340 LispError::OptionalParamMalformed {
12341 position: 1,
12342 reason: crate::error::OptionalParamMalformedReason::EmptyList,
12343 ..
12344 }
12345 ),
12346 "expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
12347 );
12348 }
12349
12350 #[test]
12351 fn parse_params_rejects_one_element_optional_list_as_missing_default() {
12352 // `(&optional (x))` — a one-element list. REJECTED with reason
12353 // `MissingDefault` rather than reinterpreted as `&optional x`, because
12354 // a parenthesized single-element spec is structurally ambiguous and
12355 // the bare-symbol form `x` IS the canonical "no default" shape.
12356 let err = parse_params(&read("&optional (x)").unwrap())
12357 .expect_err("one-element list optional spec must error");
12358 assert!(
12359 matches!(
12360 err,
12361 LispError::OptionalParamMalformed {
12362 position: 1,
12363 reason: crate::error::OptionalParamMalformedReason::MissingDefault,
12364 ..
12365 }
12366 ),
12367 "expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
12368 );
12369 }
12370
12371 #[test]
12372 fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
12373 // `(&optional (x 5 6))` — a three-element list. CL's `(name default
12374 // supplied-p)` shape is not yet supported (no evaluator → no
12375 // supplied-p variable binding), so the third element is structurally
12376 // surplus. REJECTED with reason `ExtraElements{length: 3}`.
12377 let err = parse_params(&read("&optional (x 5 6)").unwrap())
12378 .expect_err("three-element list optional spec must error");
12379 assert!(
12380 matches!(
12381 err,
12382 LispError::OptionalParamMalformed {
12383 position: 1,
12384 reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
12385 ..
12386 }
12387 ),
12388 "expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
12389 );
12390 }
12391
12392 #[test]
12393 fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
12394 // `(&optional (5 default))` — the name slot must be a symbol; a
12395 // numeric literal is REJECTED with reason `NonSymbolName`. Without
12396 // this branch the gate would silently populate
12397 // `OptionalParam.name` from a stringified non-symbol value (`"5"`),
12398 // breaking the invariant that param names are symbols.
12399 let err = parse_params(&read("&optional (5 default)").unwrap())
12400 .expect_err("non-symbol-name optional spec must error");
12401 assert!(
12402 matches!(
12403 err,
12404 LispError::OptionalParamMalformed {
12405 position: 1,
12406 reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
12407 ..
12408 }
12409 ),
12410 "expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
12411 );
12412 }
12413
12414 #[test]
12415 fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
12416 // `((a 5))` — a list in the REQUIRED section is NOT a default-form
12417 // spec; default forms are an optional-section affordance. The gate
12418 // must fall through to `NonSymbolParam` (parity with the prior
12419 // behavior on lists in the required section), not silently admit
12420 // the list as a default-form spec.
12421 let err =
12422 parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
12423 assert!(
12424 matches!(err, LispError::NonSymbolParam { position: 0, .. }),
12425 "expected NonSymbolParam{{position: 0}}, got: {err:?}"
12426 );
12427 }
12428
12429 #[test]
12430 fn bind_unsupplied_optional_with_default_takes_the_default() {
12431 // `(a &optional (b 5))` bound to just `1`: a=1, b=5 (the declared
12432 // default), not nil. The default form is consulted ONLY when the
12433 // call ran out of args.
12434 let params = MacroParams {
12435 required: vec!["a".into()],
12436 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12437 rest: None,
12438 };
12439 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12440 assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
12441 }
12442
12443 #[test]
12444 fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
12445 // `(&optional (b 5))` bound to `42`: b=42, NOT the default. A
12446 // supplied optional ALWAYS takes its arg; the default is the
12447 // absence-only fallback. Pins that the default form does not
12448 // shadow a supplied call arg.
12449 let params = MacroParams {
12450 required: Vec::new(),
12451 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12452 rest: None,
12453 };
12454 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12455 assert_eq!(vals, vec![Sexp::int(42)]);
12456 }
12457
12458 #[test]
12459 fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
12460 // `(a &optional (b 5) c (d "z"))` bound to just `1`: a=1, b=5
12461 // (default), c=nil (bare floor), d="z" (default). The three
12462 // absence cases coexist in one bind: per-default fill, nil floor,
12463 // and a tail with a literal-string default.
12464 let params = MacroParams {
12465 required: vec!["a".into()],
12466 optional: vec![
12467 OptionalParam::with_default("b", Sexp::int(5)),
12468 OptionalParam::bare("c"),
12469 OptionalParam::with_default("d", Sexp::string("z")),
12470 ],
12471 rest: None,
12472 };
12473 let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
12474 assert_eq!(
12475 vals,
12476 vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
12477 );
12478 }
12479
12480 // ── OptionalParam::resolved_default: the absent-call binder accessor ──
12481 //
12482 // `resolved_default` lifts the `param.default.clone().unwrap_or(Sexp::Nil)`
12483 // two-arm fallback that previously inlined at `MacroParams::bind`'s
12484 // optional arm into ONE named accessor on the typed `OptionalParam`.
12485 // The constructor pair `bare` / `with_default` defines the typed
12486 // shapes of the `default` slot; this accessor names the symmetric
12487 // bound-value projection both shapes yield at the absence boundary.
12488 // Tests pin: (a) `bare(name).resolved_default()` is the `Sexp::Nil`
12489 // no-default floor; (b) `with_default(name, d).resolved_default()` is
12490 // `d.clone()`; (c) the projection is `Clone`-stable across repeated
12491 // calls (the typed `default` field is not consumed); (d) path-
12492 // uniformity at the binder — `bind`'s optional arm routes through
12493 // `resolved_default` for both shapes; (e) end-to-end through both
12494 // expansion strategies, the absent-call binding agrees.
12495
12496 #[test]
12497 fn resolved_default_is_nil_for_bare_optional() {
12498 // `OptionalParam::bare(name).default` is `None`, so
12499 // `resolved_default()` projects to `Sexp::Nil` — the CL
12500 // `&optional` no-default-form floor. Fail-before/pass-after: this
12501 // assert is meaningless pre-lift because the helper does not
12502 // exist; post-lift it pins the typed accessor's `Sexp::Nil` arm
12503 // at the named primitive. Sibling of `bare` itself: the
12504 // constructor defines the shape (`default: None`); the accessor
12505 // names the bound-value projection of that shape.
12506 let p = OptionalParam::bare("x");
12507 assert_eq!(p.resolved_default(), Sexp::Nil);
12508 }
12509
12510 #[test]
12511 fn resolved_default_clones_declared_default_for_with_default_optional() {
12512 // `OptionalParam::with_default(name, d).default` is `Some(d)`, so
12513 // `resolved_default()` projects to `d.clone()` — the declared
12514 // default form. Sibling of the bare-floor pin: the closed-set
12515 // `default: Option<Sexp>` slot's two shapes correspond 1:1 with
12516 // the two arms of `resolved_default`. Pins the closed-set
12517 // exhaustive coverage of `Option<Sexp>` × `{Some, None}`.
12518 let p = OptionalParam::with_default("x", Sexp::int(5));
12519 assert_eq!(p.resolved_default(), Sexp::int(5));
12520 }
12521
12522 #[test]
12523 fn resolved_default_clones_arbitrary_sexp_default_form() {
12524 // The declared default can be any `Sexp` — a literal list, a
12525 // keyword, a string, a quasi-quoted form — because v0 has no
12526 // evaluator and the typed slot parks the literal verbatim. Pin
12527 // that `resolved_default()` is faithful to the parked literal
12528 // regardless of shape: a regression that special-cases an arm
12529 // (e.g., projecting `Sexp::List(_)` to `Sexp::Nil`, or "normalizing"
12530 // a `Sexp::Quote`) fails here. The accessor is exactly
12531 // `default.clone()` for the `Some` arm — no shape rewriting.
12532 let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
12533 let p = OptionalParam::with_default("x", arbitrary.clone());
12534 assert_eq!(p.resolved_default(), arbitrary);
12535 }
12536
12537 #[test]
12538 fn resolved_default_is_clone_stable_across_repeated_calls() {
12539 // The accessor takes `&self` and projects through `Clone`, so
12540 // repeated calls yield IDENTICAL values — the typed `default`
12541 // field is not consumed. Pins that the accessor is idempotent
12542 // for the same `OptionalParam`, which is the contract the binder
12543 // relies on across multiple `bind` invocations of the same
12544 // macro: every call that leaves the optional unfilled yields
12545 // the SAME bound value, never a partially-consumed shape. A
12546 // regression that converted the accessor to `self.default.take()`
12547 // (consuming the field) would still type-check at the call site
12548 // but would silently desync repeated absent-call bindings; this
12549 // test catches that drift.
12550 let p = OptionalParam::with_default("x", Sexp::string("hi"));
12551 let first = p.resolved_default();
12552 let second = p.resolved_default();
12553 assert_eq!(first, second);
12554 assert_eq!(first, Sexp::string("hi"));
12555 }
12556
12557 #[test]
12558 fn resolved_default_is_the_binders_absent_optional_projection() {
12559 // Path-uniformity pin at the binder boundary: `MacroParams::bind`'s
12560 // optional arm consults `param.resolved_default()` for any
12561 // absent slot. Two-arm coverage: a bare optional (`b`) binds to
12562 // `Sexp::Nil` via the `None` arm; a with-default optional
12563 // (`(c 5)`) binds to `Sexp::int(5)` via the `Some` arm. The
12564 // single bind call exercises both arms in one walk, and the
12565 // bound values vec is parallel to `names()` so position
12566 // checking pins the arm-to-slot mapping. A regression that
12567 // re-inlines the two-arm fallback at the binder, drifting it
12568 // independently from the accessor, would still type-check but
12569 // a future shape change to `resolved_default` (e.g., adding a
12570 // typed `&supplied-p` companion slot) would silently desync
12571 // the binder from the accessor — this test catches that drift.
12572 let params = MacroParams {
12573 required: Vec::new(),
12574 optional: vec![
12575 OptionalParam::bare("b"),
12576 OptionalParam::with_default("c", Sexp::int(5)),
12577 ],
12578 rest: None,
12579 };
12580 // Empty args → both optionals are absent → both arms fire.
12581 let vals = params.bind("m", &[]).unwrap();
12582 assert_eq!(vals.len(), 2);
12583 assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
12584 assert_eq!(
12585 vals[1],
12586 OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
12587 );
12588 // And the absolute identities pin the projection's arms:
12589 assert_eq!(vals[0], Sexp::Nil);
12590 assert_eq!(vals[1], Sexp::int(5));
12591 }
12592
12593 #[test]
12594 fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
12595 // End-to-end path-uniformity: a macro with both an `&optional
12596 // (g "hi")` (with-default) and an `&optional h` (bare) param
12597 // expands the same way under bytecode AND substitute
12598 // strategies, because both strategies route through the SHARED
12599 // `MacroParams::bind` which now consults `resolved_default`
12600 // for absent slots. Pins that the accessor's contract is
12601 // structurally observable at the strategy boundary — a
12602 // regression that bifurcated the accessor's behavior between
12603 // the two paths (impossible, since they share `bind`) would
12604 // surface here.
12605 let src = r#"
12606 (defmacro greet (n &optional (g "hi") h)
12607 `(list ,g ,n ,h))
12608 (greet world)
12609 "#;
12610 let expected = vec![Sexp::List(vec![
12611 Sexp::symbol("list"),
12612 Sexp::string("hi"),
12613 Sexp::symbol("world"),
12614 Sexp::Nil,
12615 ])];
12616 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12617 let substitute = Expander::new_substitute_only()
12618 .expand_program(read(src).unwrap())
12619 .unwrap();
12620 assert_eq!(
12621 bytecode, expected,
12622 "bytecode resolved_default expansion drifted"
12623 );
12624 assert_eq!(
12625 substitute, expected,
12626 "substitute resolved_default expansion drifted"
12627 );
12628 assert_eq!(
12629 bytecode, substitute,
12630 "the two strategies disagree on resolved_default expansion"
12631 );
12632 }
12633
12634 #[test]
12635 fn resolved_default_supplied_optional_does_not_consult_accessor() {
12636 // A SUPPLIED optional binds to its CALL ARG, never to the
12637 // accessor's projection. Pins the contract: `resolved_default`
12638 // is the absence-only fallback; a present arg shadows the
12639 // accessor at the binder. Sibling negative control to
12640 // `resolved_default_is_the_binders_absent_optional_projection`:
12641 // that test exercises the absence arm at every slot; this test
12642 // exercises the presence arm at every slot and proves the
12643 // accessor's `Sexp::int(5)` projection is NOT consulted when
12644 // the optional is supplied with `Sexp::int(42)`. A regression
12645 // that wired the binder to always consult the accessor (the
12646 // wrong direction — the default would shadow supplied args) is
12647 // caught here.
12648 let params = MacroParams {
12649 required: Vec::new(),
12650 optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
12651 rest: None,
12652 };
12653 let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
12654 assert_eq!(vals, vec![Sexp::int(42)]);
12655 // And the accessor's would-be projection IS NOT the bound value:
12656 let p = OptionalParam::with_default("b", Sexp::int(5));
12657 assert_ne!(vals[0], p.resolved_default());
12658 }
12659
12660 #[test]
12661 fn optional_default_macro_expands_end_to_end_under_both_strategies() {
12662 // The end-to-end path: a macro with `&optional (g "hi")` expands to
12663 // the default literal when unsupplied, and to the supplied arg when
12664 // present. Both the bytecode and substitute strategies must agree
12665 // (invariant 2 — free middle); they share `MacroParams::bind`, so
12666 // the default arm lands once in `bind` and both strategies inherit
12667 // it unable to drift. This is the test the prior run (611a682)
12668 // signposted as the next-change-that-benefits.
12669 let src = r#"
12670 (defmacro greet (n &optional (g "hi"))
12671 `(list ,g ,n))
12672 (greet world)
12673 (greet world there)
12674 "#;
12675 let expected = vec![
12676 Sexp::List(vec![
12677 Sexp::symbol("list"),
12678 Sexp::string("hi"),
12679 Sexp::symbol("world"),
12680 ]),
12681 Sexp::List(vec![
12682 Sexp::symbol("list"),
12683 Sexp::symbol("there"),
12684 Sexp::symbol("world"),
12685 ]),
12686 ];
12687 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12688 let substitute = Expander::new_substitute_only()
12689 .expand_program(read(src).unwrap())
12690 .unwrap();
12691 assert_eq!(
12692 bytecode, expected,
12693 "bytecode optional-default expansion drifted"
12694 );
12695 assert_eq!(
12696 substitute, expected,
12697 "substitute optional-default expansion drifted"
12698 );
12699 assert_eq!(
12700 bytecode, substitute,
12701 "the two strategies disagree on optional-default expansion"
12702 );
12703 }
12704
12705 #[test]
12706 fn optional_macro_expands_end_to_end_under_both_strategies() {
12707 // The end-to-end path: a macro with an `&optional` param expands
12708 // correctly whether the optional is supplied or defaulted, and the
12709 // bytecode and substitute strategies agree (invariant 2 — free
12710 // middle). `,b` resolves to the supplied arg when present, to
12711 // `Sexp::Nil` when absent (CL's `&optional` default).
12712 let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
12713 // (cons 1 2) — optional supplied; (cons 3 <Nil>) — optional defaulted.
12714 // The defaulted slot is the canonical `Sexp::Nil`, distinct in the AST
12715 // from a reader-produced empty list `()` even though both Display as
12716 // `()`.
12717 let expected = vec![
12718 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
12719 Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
12720 ];
12721 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12722 let substitute = Expander::new_substitute_only()
12723 .expand_program(read(src).unwrap())
12724 .unwrap();
12725 assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
12726 assert_eq!(
12727 substitute, expected,
12728 "substitute optional expansion drifted"
12729 );
12730 assert_eq!(
12731 bytecode, substitute,
12732 "the two strategies disagree on optional expansion"
12733 );
12734 }
12735
12736 // ── MacroDef::template_body: the shared body-projection primitive ──
12737 //
12738 // `template_body` lifts the `match &def.body { Sexp::Quasiquote(inner)
12739 // => inner.as_ref(), other => other }` inline peel — present
12740 // byte-identically at the bytecode (`compile_template`) AND substitute
12741 // (`apply`'s fallback) path entries — into ONE named projection both
12742 // strategies share. The existing `compiled_template_matches_substitute
12743 // _path` and `expansion_layers_agree_on_output_and_cache_wins` tests
12744 // are the path-uniformity guards covering the SHARED-PROJECTION shape;
12745 // the four tests below pin the projection's contract DIRECTLY:
12746 // (a) a quasi-quoted body unwraps to the inner; (b) a non-quasi-quoted
12747 // body returns the body verbatim; (c) the borrow is rooted in the
12748 // body field (single-level peel); (d) the projection is the same
12749 // `&Sexp` both strategies route on (so a regression that drifts the
12750 // body-peel from one strategy to the other becomes a type-level
12751 // change at this helper, not a silent two-site divergence).
12752
12753 #[test]
12754 fn template_body_unwraps_outer_quasiquote_to_inner() {
12755 // The canonical authoring shape: `(defmacro f (a) `(list ,a))` —
12756 // the reader wraps the `` ` `` form into `Sexp::Quasiquote(inner)`,
12757 // and `template_body` peels the outer marker. The returned `&Sexp`
12758 // is the inner walker-payload both expansion strategies consume.
12759 let inner = Sexp::List(vec![
12760 Sexp::symbol("list"),
12761 Sexp::Unquote(Box::new(Sexp::symbol("a"))),
12762 ]);
12763 let def = MacroDef {
12764 name: "f".into(),
12765 params: MacroParams::default(),
12766 body: Sexp::Quasiquote(Box::new(inner.clone())),
12767 };
12768 assert_eq!(def.template_body(), &inner);
12769 }
12770
12771 #[test]
12772 fn template_body_returns_non_quasiquote_body_verbatim() {
12773 // A body authored WITHOUT the outer `` ` `` affordance — a bare
12774 // `Sexp::List` body — returns verbatim. The "other" arm of the
12775 // legacy match. Pin parity with the pre-lift code path so a
12776 // regression that drifts the body-peel into "always peel
12777 // something" (which would break literal-body macros) fails here.
12778 let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
12779 let def = MacroDef {
12780 name: "f".into(),
12781 params: MacroParams::default(),
12782 body: body.clone(),
12783 };
12784 assert_eq!(def.template_body(), &body);
12785 // Atom bodies too — the projection is a single-arm match, not a
12786 // recursive descent. A `Sexp::Atom` body is its own template payload.
12787 let atom_def = MacroDef {
12788 name: "g".into(),
12789 params: MacroParams::default(),
12790 body: Sexp::symbol("nil-template"),
12791 };
12792 assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
12793 }
12794
12795 #[test]
12796 fn template_body_peels_single_level_only() {
12797 // A nested `` ``form `` body — `Sexp::Quasiquote(Box::new(
12798 // Sexp::Quasiquote(...)))` — unwraps ONE outer quasi-quote and
12799 // returns the inner `Sexp::Quasiquote(...)` as-is. The v0 module
12800 // preamble declares "Nested quasi-quotes: Not yet supported"; the
12801 // single-level peel matches the legacy inline match's posture
12802 // (which only matched ONE outer `Sexp::Quasiquote(_)` arm, not a
12803 // recursive loop). A regression that drifts to a recursive peel
12804 // would project too far and the inner `Sexp::Quasiquote` marker —
12805 // which the substitute walker treats as an atomic leaf returned
12806 // verbatim (line ~830, `Sexp::Quote(_) | Sexp::Quasiquote(_)
12807 // => Ok(form.clone())`) — would silently disappear from the
12808 // expansion's emitted form. Pin the single-level contract here.
12809 let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
12810 let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
12811 let def = MacroDef {
12812 name: "nested".into(),
12813 params: MacroParams::default(),
12814 body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
12815 };
12816 // Outer peel returns the INNER quasi-quote, NOT its inner payload.
12817 assert_eq!(def.template_body(), &inner_qq);
12818 assert_ne!(def.template_body(), &inner_payload);
12819 }
12820
12821 #[test]
12822 fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
12823 // A `Sexp::Quote(_)` body — not a `Sexp::Quasiquote(_)` — returns
12824 // verbatim through the "other" arm. The two close-cousin shapes
12825 // share an outer-marker character (`'` vs `` ` ``) at the reader
12826 // boundary but differ semantically: a `Quote` body is a literal
12827 // template (no substitution semantics), a `Quasiquote` body is a
12828 // substitution-walker entry. A regression that conflated the two
12829 // — peeling Quote as if it were Quasiquote — would silently turn
12830 // every quoted-body macro into a template-walked macro. Pin the
12831 // discrimination.
12832 let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
12833 let body = Sexp::Quote(Box::new(inner.clone()));
12834 let def = MacroDef {
12835 name: "quoted".into(),
12836 params: MacroParams::default(),
12837 body: body.clone(),
12838 };
12839 assert_eq!(def.template_body(), &body);
12840 // The inner is NOT what comes back — only Quasiquote-bodied macros
12841 // would peel to the inner.
12842 assert_ne!(def.template_body(), &inner);
12843 }
12844
12845 #[test]
12846 fn template_body_is_the_shared_projection_both_strategies_walk() {
12847 // End-to-end path-uniformity at the projection boundary: a macro
12848 // authored with the canonical quasi-quoted body expands
12849 // IDENTICALLY under bytecode and substitute strategies because
12850 // both route their walker's body through `template_body()` — the
12851 // SAME `&Sexp` projection. Sibling of
12852 // `compiled_template_matches_substitute_path` (which observes
12853 // agreement on the EMITTED form); this test pins agreement on
12854 // the projection ENTRY: `compile_template` and the substitute
12855 // fallback now consume the same `&Sexp` (`def.template_body()`),
12856 // never a divergent inline match the two paths could regress
12857 // independently.
12858 let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
12859 let expected = vec![Sexp::List(vec![
12860 Sexp::symbol("list"),
12861 Sexp::int(5),
12862 Sexp::int(5),
12863 ])];
12864 let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
12865 let substitute = Expander::new_substitute_only()
12866 .expand_program(read(src).unwrap())
12867 .unwrap();
12868 assert_eq!(bytecode, expected, "bytecode body-projection drifted");
12869 assert_eq!(substitute, expected, "substitute body-projection drifted");
12870 assert_eq!(
12871 bytecode, substitute,
12872 "the two strategies disagree on the body-projection's emission"
12873 );
12874 }
12875
12876 // ── Expander::expand: macro-call dispatch routes through `as_call_to_any` ──
12877 //
12878 // `expand` lifts its macro-call recognition to route through the
12879 // substrate's typed-decoded call decomposition: `as_call_to_any(|h|
12880 // self.macros.get(h))` answers "is this form an invocation of any
12881 // registered macro?" in ONE structural query on the Sexp algebra,
12882 // and a HashMap-backed lookup as its classifier. Sibling consumer to
12883 // `macro_def_from` (the typed-macro-definition dispatcher already
12884 // routing through `as_call_to_any(MacroDefHead::from_keyword)` with
12885 // a closed-set enum classifier). With both in place, BOTH dispatch
12886 // sites in the macro expander project through the SAME family
12887 // primitive — each binding the classifier that fits its candidate
12888 // set. The tests below pin the consumer's path-uniformity contract
12889 // at the new boundary: a hand-rolled `as_call_to_any(|h| macros.get
12890 // (h))` dispatch observes the SAME `(def, args)` decomposition the
12891 // `Expander::expand` consumer routes through.
12892
12893 #[test]
12894 fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
12895 // Structural identity: on a registered-macro call, the consumer's
12896 // expansion is observably equivalent to: classify the form via
12897 // `as_call_to_any(|h| macros.get(h))` → some `(def, args)` →
12898 // apply the def to args. Pin path-uniformity: a hand-rolled
12899 // `as_call_to_any` lookup against the same registry the expander
12900 // walks produces the SAME `MacroDef` reference for the SAME
12901 // input form. A regression that drifts the consumer back to an
12902 // inline `as_list + as_call + macros.get` chain (which would
12903 // fragment the family adoption) is caught structurally — the
12904 // hand-rolled `as_call_to_any` and the consumer's dispatch must
12905 // observe the same decomposition.
12906 let mut e = Expander::new();
12907 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12908 .unwrap();
12909 let call_form = parse("(wrap 42)");
12910
12911 // Hand-rolled family-primitive lookup mirrors the lifted consumer.
12912 let (def_via_family, args_via_family) = call_form
12913 .as_call_to_any(|h| e.macros.get(h))
12914 .expect("registered macro call must decompose via as_call_to_any");
12915 assert_eq!(def_via_family.name, "wrap");
12916 assert_eq!(args_via_family, &[Sexp::int(42)]);
12917
12918 // Consumer's expand observes the SAME decomposition: the expanded
12919 // form is `(list 42 42)`, derived from the SAME def + args the
12920 // hand-rolled lookup found. Path-uniform with the family
12921 // primitive at the dispatch boundary.
12922 let expanded = e.expand(&call_form).unwrap();
12923 assert_eq!(
12924 expanded,
12925 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
12926 );
12927 }
12928
12929 #[test]
12930 fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
12931 // Path-uniformity for the non-registered-head path: `as_call_to_any
12932 // (|h| macros.get(h))` returns `None` for a call whose head ISN'T
12933 // a registered macro, and the consumer falls through to the
12934 // children-walk (which expands any nested macro calls). Pin
12935 // both halves: the hand-rolled lookup returns `None`, AND the
12936 // consumer's expand walks into the children. A regression that
12937 // accidentally short-circuits the children-walk for non-macro
12938 // calls (e.g. by treating `as_call_to_any` = `None` as
12939 // "non-expandable" globally) would fail here.
12940 let mut e = Expander::new();
12941 e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
12942 .unwrap();
12943 let outer = parse("(foo (wrap 5))");
12944
12945 // Hand-rolled family-primitive lookup rejects the outer head.
12946 assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
12947
12948 // Consumer walks children — the inner `(wrap 5)` IS a macro call
12949 // and expands to `(list 5 5)`; the outer `foo` head is preserved.
12950 let expanded = e.expand(&outer).unwrap();
12951 assert_eq!(
12952 expanded,
12953 Sexp::List(vec![
12954 Sexp::symbol("foo"),
12955 Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
12956 ])
12957 );
12958 }
12959
12960 #[test]
12961 fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
12962 // Path-uniformity for the non-call path: every shape `as_call`
12963 // rejects (atoms across all 6 kinds, Nil, Quote-family wrappers)
12964 // ALSO routes past `as_call_to_any` into the `as_list()`
12965 // fallthrough, where the not-a-list arm returns `form.clone()`
12966 // verbatim. Pin both halves: the hand-rolled lookup rejects
12967 // every non-call shape regardless of decoder, AND the consumer
12968 // preserves each shape unchanged. A regression that drifts the
12969 // consumer's dispatch order (e.g. checking `as_list()` BEFORE
12970 // `as_call_to_any` in a way that mis-handles Quote-family
12971 // wrappers) would fail here.
12972 let e = Expander::new();
12973 let shapes = [
12974 Sexp::symbol("foo"),
12975 Sexp::int(5),
12976 Sexp::keyword("k"),
12977 Sexp::string("s"),
12978 Sexp::boolean(true),
12979 Sexp::float(1.5),
12980 Sexp::Nil,
12981 Sexp::Quote(Box::new(Sexp::symbol("x"))),
12982 Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
12983 Sexp::Unquote(Box::new(Sexp::symbol("x"))),
12984 Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
12985 ];
12986 for s in &shapes {
12987 // Hand-rolled family-primitive lookup rejects non-call shapes
12988 // even for a promiscuous decoder — the call-shape gate fires
12989 // BEFORE the decoder runs.
12990 assert!(
12991 s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
12992 "non-call shape must yield None for as_call_to_any: {s}"
12993 );
12994 // Consumer preserves the shape verbatim — the not-a-list
12995 // arm at the fallthrough returns `form.clone()`.
12996 assert_eq!(
12997 e.expand(s).unwrap(),
12998 s.clone(),
12999 "non-call shape must round-trip unchanged through expand: {s}"
13000 );
13001 }
13002 }
13003
13004 #[test]
13005 fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
13006 // The empty list `()` has no operator and no children. Pin that
13007 // `as_call_to_any` rejects it (no head to feed the decoder), the
13008 // consumer falls through to `as_list()` which returns `Some(&[])`,
13009 // and the children-walk emits `Sexp::List(vec![])` (an empty
13010 // list, not `form.clone()` of `Sexp::List(vec![])` — both happen
13011 // to be observationally identical, but the path is the
13012 // children-walk arm, NOT the not-a-list arm). Path-uniformity
13013 // gate for the singleton-list edge case the `compile_named_from_
13014 // forms` rejection chain relies on `as_call_to(KEYWORD)` to yield
13015 // `Some(&[])` for — same posture, different family member.
13016 let e = Expander::new();
13017 let empty = Sexp::List(vec![]);
13018
13019 // Hand-rolled family-primitive lookup rejects the empty list.
13020 assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
13021
13022 // Consumer walks children (zero of them) — output is the empty
13023 // list, same as input.
13024 assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
13025 }
13026
13027 // ── Expander::expand_and_collect_calls_to: program-level walk ───────
13028 //
13029 // `expand_and_collect_calls_to(forms, keyword, project)` composes the
13030 // expander's program-level expansion (`expand_program`) with the
13031 // substrate's slice-side typed-keyword projection (`iter_calls_to`)
13032 // and a caller-supplied per-form mapper into ONE method on the
13033 // `Expander` surface. Both `compile_typed::<T>` and
13034 // `compile_named_from_forms::<T>` (compile.rs) route through it; the
13035 // tests below pin its contract directly. The existing compile.rs
13036 // dispatch tests are the path-uniformity guards proving the two
13037 // production sites route through it without behavior drift.
13038
13039 #[test]
13040 fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
13041 // Three forms — two match `defmonitor`, one matches `defalert`.
13042 // The mapper records `args.len()` from each matching form's tail.
13043 // Pin: only the two `defmonitor` matches flow through `project`,
13044 // in source order (so the recorded lengths are 2, 1).
13045 let forms = vec![
13046 Sexp::List(vec![
13047 Sexp::symbol("defmonitor"),
13048 Sexp::keyword("name"),
13049 Sexp::string("first"),
13050 ]),
13051 Sexp::List(vec![
13052 Sexp::symbol("defalert"),
13053 Sexp::keyword("name"),
13054 Sexp::string("not-a-match"),
13055 ]),
13056 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
13057 ];
13058 let mut e = Expander::new();
13059 let lengths: Vec<usize> = e
13060 .expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
13061 .expect("matching forms must compose");
13062 assert_eq!(lengths, vec![2, 1]);
13063 }
13064
13065 #[test]
13066 fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13067 // Path-uniformity for the soft-projection posture inherited from
13068 // `iter_calls_to`: non-matching forms are skipped silently, and
13069 // `project` is never invoked on them. Pin via a counter the
13070 // closure increments per invocation — the counter is the
13071 // observable that distinguishes "skipped silently" from
13072 // "projected to a no-op".
13073 let forms = vec![
13074 Sexp::symbol("bare-atom"),
13075 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
13076 Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
13077 Sexp::List(vec![]),
13078 Sexp::Nil,
13079 ];
13080 let mut count = 0usize;
13081 let mut e = Expander::new();
13082 let out: Vec<()> = e
13083 .expand_and_collect_calls_to(forms, "defmonitor", |_args| {
13084 count += 1;
13085 Ok(())
13086 })
13087 .expect("non-matching-only slice must collect to empty Vec");
13088 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13089 assert_eq!(count, 0, "project must never run for non-matching forms");
13090 }
13091
13092 #[test]
13093 fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13094 // Three matching forms; the closure errors on the SECOND. Pin: the
13095 // collect's `Result<Vec<_>, _>` short-circuit fires at the second
13096 // form's error, the third form's `project` is NEVER invoked, and
13097 // the returned error is exactly the one the closure raised. This
13098 // mirrors `compile_named_from_forms`'s short-circuit on the first
13099 // `NamedFormMissingName` / `NamedFormNonSymbolName` / typed-entry
13100 // kwargs rejection.
13101 let forms = vec![
13102 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13103 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
13104 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13105 ];
13106 let mut seen = Vec::new();
13107 let mut e = Expander::new();
13108 let err = e
13109 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
13110 let n = args[0].as_int().expect("test args are ints");
13111 seen.push(n);
13112 if n == 2 {
13113 Err(LispError::Compile {
13114 form: "test".to_string(),
13115 message: "stop at two".to_string(),
13116 })
13117 } else {
13118 Ok(())
13119 }
13120 })
13121 .expect_err("project's error must short-circuit collect");
13122 // Only the first two forms were ever projected — the third
13123 // never reached `project`.
13124 assert_eq!(seen, vec![1, 2]);
13125 assert!(
13126 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13127 "short-circuit must propagate the project's error verbatim, got {err:?}"
13128 );
13129 }
13130
13131 #[test]
13132 fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
13133 // The `expand_program` step runs BEFORE `iter_calls_to` walks
13134 // anything — so an `expand_program` error (e.g., a malformed
13135 // `defmacro` head) must short-circuit the entire composition
13136 // and `project` must NEVER run. Pin via the closure-running
13137 // counter: an error from the FIRST step never invokes `project`.
13138 // `(defmacro 5 (x) `,x)` — the macro NAME slot is an int, not a
13139 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13140 // fires during `expand_program`.
13141 let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
13142 let mut count = 0usize;
13143 let mut e = Expander::new();
13144 let err = e
13145 .expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
13146 count += 1;
13147 Ok(())
13148 })
13149 .expect_err("expand_program error must short-circuit before project");
13150 assert_eq!(
13151 count, 0,
13152 "project must never run when expand_program errors"
13153 );
13154 // Sanity: the error IS the expand_program-stage rejection, NOT
13155 // a project-stage error (path-uniformity for the ordering).
13156 let rendered = format!("{err}");
13157 assert!(
13158 rendered.contains("NAME") || rendered.contains("symbol"),
13159 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13160 );
13161 }
13162
13163 #[test]
13164 fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
13165 // Boundary: empty forms slice yields zero items regardless of
13166 // keyword. Pin the degenerate boundary — empty in, empty out —
13167 // and the closure is never invoked. Sibling of
13168 // `iter_calls_to_yields_nothing_for_empty_slice` in ast.rs,
13169 // one level up the composition: the program-level walk is
13170 // fused-empty when `expand_program` yields an empty `Vec<Sexp>`.
13171 let mut count = 0usize;
13172 let mut e = Expander::new();
13173 let out: Vec<()> = e
13174 .expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
13175 count += 1;
13176 Ok(())
13177 })
13178 .expect("empty forms is not an error");
13179 assert!(out.is_empty());
13180 assert_eq!(count, 0);
13181 }
13182
13183 #[test]
13184 fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13185 // Critical ordering: `expand_program` runs BEFORE the keyword
13186 // filter, so a `defmacro` whose expansion produces a form with
13187 // a matching head IS visible to `project`. Pin the pipeline
13188 // order: `(defmacro emit-monitor (n) `(defmonitor :name ,n))
13189 // (emit-monitor "alpha") (defmonitor :name "beta")`
13190 // expands the macro call to `(defmonitor :name "alpha")` and
13191 // the keyword walk then sees TWO `defmonitor` forms (the
13192 // macro-emitted one AND the directly-authored one). A
13193 // pre-expansion filter would miss the macro-emitted form.
13194 let forms = read(
13195 "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13196 (emit-monitor \"alpha\")
13197 (defmonitor :name \"beta\")",
13198 )
13199 .unwrap();
13200 let mut e = Expander::new();
13201 let names: Vec<String> = e
13202 .expand_and_collect_calls_to(forms, "defmonitor", |args| {
13203 // Each defmonitor form's tail is `(:name "X")`; project
13204 // the second element's string to capture the name.
13205 Ok(args[1].as_string().unwrap().to_string())
13206 })
13207 .expect("macroexpanded + directly-authored forms must both flow");
13208 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13209 }
13210
13211 #[test]
13212 fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13213 // The `keyword` argument flows straight into `iter_calls_to`'s
13214 // head-comparison gate — a regression that drifts the keyword
13215 // (e.g., uses `T::KEYWORD` from a different `T` at the helper,
13216 // or lowercases / trims the keyword en route) fails here. Pin
13217 // by passing TWO different keywords against the SAME forms
13218 // input and asserting each picks up only its matching forms.
13219 let forms = vec![
13220 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
13221 Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
13222 Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
13223 Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
13224 ];
13225
13226 let mut e = Expander::new();
13227 let monitors: Vec<i64> = e
13228 .expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
13229 Ok(args[0].as_int().unwrap())
13230 })
13231 .unwrap();
13232 assert_eq!(monitors, vec![1, 3]);
13233
13234 let mut e2 = Expander::new();
13235 let alerts: Vec<i64> = e2
13236 .expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
13237 Ok(args[0].as_int().unwrap())
13238 })
13239 .unwrap();
13240 assert_eq!(alerts, vec![2]);
13241
13242 // A keyword nothing matches collects to the empty Vec — same
13243 // shape as the empty-forms case at the slice level.
13244 let mut e3 = Expander::new();
13245 let none: Vec<i64> = e3
13246 .expand_and_collect_calls_to(forms, "missing-keyword", |args| {
13247 Ok(args[0].as_int().unwrap())
13248 })
13249 .unwrap();
13250 assert!(none.is_empty());
13251 }
13252
13253 #[test]
13254 fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
13255 // Structural identity: for any (forms, keyword, project) triple
13256 // whose pieces succeed, the method's result equals the inlined
13257 // pre-lift pipeline `expand_program + iter_calls_to + map +
13258 // collect`. Pin shape AND ordering on a mixed forms input
13259 // (matching + non-matching + macroexpand-emitted matches) so a
13260 // regression that drifts the method's pipeline from the inlined
13261 // closed-form fails here. This is the lift's load-bearing
13262 // path-uniformity gate — the SAME assertion that holds for
13263 // both production consumers (compile.rs) holds at the
13264 // primitive's contract level.
13265 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13266 (foo :idx 1)
13267 (emit-foo 2)
13268 (bar :idx 99)
13269 (foo :idx 3)";
13270 let forms = read(src).unwrap();
13271
13272 // Inlined pre-lift pipeline.
13273 let mut exp_inline = Expander::new();
13274 let expanded = exp_inline.expand_program(forms.clone()).unwrap();
13275 let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
13276 .map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
13277 .collect::<Result<Vec<_>>>()
13278 .unwrap();
13279
13280 // Method pipeline.
13281 let mut exp_method = Expander::new();
13282 let via_method: Vec<i64> = exp_method
13283 .expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13284 .unwrap();
13285
13286 assert_eq!(via_inline, via_method);
13287 assert_eq!(via_inline, vec![1, 2, 3]);
13288 }
13289
13290 // ── Expander::expand_and_collect_calls_to_any: typed-decoded sibling ──
13291 //
13292 // The typed-decoded classifier sibling of `expand_and_collect_calls_to`:
13293 // composes `expand_program` with `iter_calls_to_any` and a per-form
13294 // mapper that receives BOTH the typed witness AND the args tail.
13295 // Closes the (keyword, classifier) 2×2 of expander-surface
13296 // compose-on-iter projections at the typed-decoded corner the prior
13297 // runs' slice-side `iter_calls_to_any` lift left open. The keyword
13298 // sibling now ROUTES through this primitive via a constant-classifier
13299 // composition — the tests below pin both the typed-decoded primitive's
13300 // contract directly AND the routing identity binding the keyword
13301 // sibling to it.
13302 //
13303 // The existing `expand_and_collect_calls_to_*` tests above remain
13304 // load-bearing — they pin the keyword sibling's contract through
13305 // its ROUTED implementation, so a regression that drifts the routing
13306 // composition surfaces at the existing keyword tests too.
13307
13308 #[test]
13309 fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
13310 {
13311 // The typed-decoded primitive's happy path: a closed-set
13312 // classifier decodes head symbols to a typed enum, and the
13313 // per-form projection receives `(decoded, args)` for every
13314 // matched form in source order. Sweep across THREE distinct
13315 // classifier outcomes — `Foo` / `Bar` / `Baz` — interleaved with
13316 // a non-matching form to pin both the typed-witness threading
13317 // AND the source-order yield AND the rejection of non-classifier
13318 // forms in ONE assertion. The decoder runs once per call form
13319 // (`FnMut`); the projection runs once per matched form.
13320 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
13321 enum Op {
13322 Foo,
13323 Bar,
13324 Baz,
13325 }
13326 let src = "(foo 1)
13327 (bar 2)
13328 (other 99)
13329 (baz 3)
13330 (foo 4)";
13331 let forms = read(src).unwrap();
13332 let mut e = Expander::new();
13333 let pairs: Vec<(Op, i64)> = e
13334 .expand_and_collect_calls_to_any(
13335 forms,
13336 |h| match h {
13337 "foo" => Some(Op::Foo),
13338 "bar" => Some(Op::Bar),
13339 "baz" => Some(Op::Baz),
13340 _ => None,
13341 },
13342 |op, args| Ok((op, args[0].as_int().unwrap())),
13343 )
13344 .unwrap();
13345 assert_eq!(
13346 pairs,
13347 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
13348 );
13349 }
13350
13351 #[test]
13352 fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13353 // The soft-projection contract — every shape `iter_calls_to_any`
13354 // rejects (non-list, empty list, list whose head is not a
13355 // symbol, list whose head decodes through the classifier to
13356 // `None`) skips the projection silently. Pin a deliberately-
13357 // panicking projection so the assertion fires loudly if a
13358 // non-matching form reaches it. The decoder accepts only
13359 // `"defmonitor"`; every other shape (atom, list whose head is a
13360 // keyword, list whose head is a symbol the decoder rejects)
13361 // short-circuits before the projection runs.
13362 let src = r#":bare-keyword
13363 "bare-string"
13364 42
13365 ()
13366 (foo bar)
13367 (defmonitor :name "matches")"#;
13368 let forms = read(src).unwrap();
13369 let mut e = Expander::new();
13370 let lengths: Vec<usize> = e
13371 .expand_and_collect_calls_to_any(
13372 forms,
13373 |h| (h == "defmonitor").then_some(()),
13374 |(), args| {
13375 // The projection must run EXACTLY once — for the
13376 // single matched form. Any non-matching form that
13377 // reaches the projection panics here.
13378 assert_eq!(args.len(), 2, "projection ran on non-matching form");
13379 Ok(args.len())
13380 },
13381 )
13382 .unwrap();
13383 assert_eq!(lengths, vec![2]);
13384 }
13385
13386 #[test]
13387 fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
13388 // The Result projection's short-circuit contract — when the
13389 // projection returns `Err` on a matched form, the walk
13390 // short-circuits and subsequent matched forms are NOT projected.
13391 // Pin the source-order short-circuit via a counter the
13392 // projection increments before deciding to fail: the counter
13393 // sits at exactly the index of the failing form (the second
13394 // match), proving the third match never reached the projection.
13395 let src = "(foo 1) (foo 2) (foo 3)";
13396 let forms = read(src).unwrap();
13397 let mut count = 0usize;
13398 let mut e = Expander::new();
13399 let err = e
13400 .expand_and_collect_calls_to_any::<i64, _, _, ()>(
13401 forms,
13402 |h| (h == "foo").then_some(()),
13403 |(), args| {
13404 count += 1;
13405 let v = args[0].as_int().unwrap();
13406 if v == 2 {
13407 return Err(crate::error::LispError::Missing("test-failure"));
13408 }
13409 Ok(v)
13410 },
13411 )
13412 .expect_err("projection must short-circuit on first Err");
13413 assert_eq!(
13414 count, 2,
13415 "projection must have run on first AND failing form, then stopped",
13416 );
13417 // Sanity: the error is the project-stage rejection — the same
13418 // typed `LispError` the projection returned, propagated verbatim.
13419 assert!(
13420 matches!(err, crate::error::LispError::Missing("test-failure")),
13421 "expected the projection's typed Err verbatim, got {err:?}",
13422 );
13423 }
13424
13425 #[test]
13426 fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
13427 // Ordering contract — `expand_program` runs BEFORE
13428 // `iter_calls_to_any` walks the slice. A `(defmacro …)` form
13429 // that emits a classifier-decoded body must have its expanded
13430 // body decoded by the classifier (not the unexpanded macro-call
13431 // head). Pin the ordering with a macro `(emit-foo n)` that
13432 // expands to `(foo :idx n)` — the classifier sees the expanded
13433 // `foo` head, not `emit-foo`. Mirrors the parallel keyword test
13434 // `expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword`
13435 // — the SAME composition contract holds on the typed-decoded
13436 // sibling.
13437 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13438 (foo :idx 1)
13439 (emit-foo 2)
13440 (bar :idx 99)
13441 (foo :idx 3)";
13442 let forms = read(src).unwrap();
13443 let mut e = Expander::new();
13444 // Classifier returns `()` for "foo" only; the keyword sibling's
13445 // routing identity is the same composition we exercise here
13446 // directly.
13447 let idxs: Vec<i64> = e
13448 .expand_and_collect_calls_to_any(
13449 forms,
13450 |h| (h == "foo").then_some(()),
13451 |(), args| Ok(args[1].as_int().unwrap()),
13452 )
13453 .unwrap();
13454 // 1 from the literal call, 2 from the macro-emitted call, 3 from
13455 // the final literal call. The macro-emitted `(foo :idx 2)` is
13456 // present BECAUSE `expand_program` ran first.
13457 assert_eq!(idxs, vec![1, 2, 3]);
13458 }
13459
13460 #[test]
13461 fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
13462 // The `FnMut` constraint — the slice walk calls the classifier
13463 // once per call form, and a decoder that captures mutable state
13464 // (a counter, a registry cache, a visited-set) maintains that
13465 // state across the walk. Pin the `FnMut` contract with a counter
13466 // closure that increments per shape-gate-passing form (calls
13467 // whose head is a symbol), asserting the counter equals the
13468 // total call count regardless of whether the classifier accepts
13469 // each form. Mirrors the parallel slice-side test
13470 // `iter_calls_to_any_admits_fnmut_classifier_maintaining_state_across_batch_walk`
13471 // — the SAME `FnMut` contract holds on the expander surface.
13472 let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
13473 let forms = read(src).unwrap();
13474 let mut e = Expander::new();
13475 let mut classifier_calls = 0usize;
13476 let projected: Vec<i64> = e
13477 .expand_and_collect_calls_to_any(
13478 forms,
13479 |h| {
13480 classifier_calls += 1;
13481 (h == "foo").then_some(())
13482 },
13483 |(), args| Ok(args[0].as_int().unwrap()),
13484 )
13485 .unwrap();
13486 assert_eq!(
13487 classifier_calls, 5,
13488 "classifier must run once per call form (5 forms, all calls with symbol heads)",
13489 );
13490 assert_eq!(
13491 projected,
13492 vec![1, 3, 5],
13493 "projection must run only for classifier-accepted forms in source order",
13494 );
13495 }
13496
13497 #[test]
13498 fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
13499 ) {
13500 // Structural identity: the keyword sibling
13501 // `expand_and_collect_calls_to` ROUTES through the typed-decoded
13502 // primitive via a constant-classifier composition — the
13503 // post-lift composition law binding the two siblings:
13504 //
13505 // expand_and_collect_calls_to(forms, k, p) ==
13506 // expand_and_collect_calls_to_any(forms,
13507 // |h| (h == k).then_some(()),
13508 // |(), args| p(args))
13509 //
13510 // Pin shape AND ordering across THREE representative keyword
13511 // values: one that matches multiple forms, one that matches
13512 // none, one that matches exactly one form. Same mixed input
13513 // (literal + macroexpand-emitted + non-matching) the keyword
13514 // path-uniformity test exercises, so the routing identity holds
13515 // on the SAME input shape the keyword sibling's contract pins.
13516 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13517 (foo :idx 1)
13518 (emit-foo 2)
13519 (bar :idx 99)
13520 (foo :idx 3)";
13521 let forms = read(src).unwrap();
13522
13523 for keyword in ["foo", "bar", "absent"] {
13524 let mut exp_keyword = Expander::new();
13525 let via_keyword: Vec<i64> = exp_keyword
13526 .expand_and_collect_calls_to(forms.clone(), keyword, |args| {
13527 Ok(args[1].as_int().unwrap())
13528 })
13529 .unwrap();
13530 let mut exp_classifier = Expander::new();
13531 let via_classifier: Vec<i64> = exp_classifier
13532 .expand_and_collect_calls_to_any(
13533 forms.clone(),
13534 |h| (h == keyword).then_some(()),
13535 |(), args| Ok(args[1].as_int().unwrap()),
13536 )
13537 .unwrap();
13538 assert_eq!(
13539 via_keyword, via_classifier,
13540 "routing identity drifted for keyword {keyword:?}",
13541 );
13542 }
13543 }
13544
13545 #[test]
13546 fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
13547 {
13548 // The expand_program-stage short-circuit contract — when
13549 // `expand_program` rejects (e.g. a `(defmacro …)` form whose
13550 // param list is malformed), the walk short-circuits BEFORE the
13551 // classifier or projection runs. Pin the ordering with a
13552 // deliberately-panicking classifier AND projection: any
13553 // post-expand-program execution fires the panic. Mirrors the
13554 // parallel keyword test
13555 // `expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs`
13556 // — the SAME composition contract holds on the typed-decoded
13557 // sibling.
13558 // `(defmacro 5 (x) `,x)` — macro NAME slot is an int, not a
13559 // symbol; `macro_def_from`'s `defmacro_non_symbol_name` rejection
13560 // fires during `expand_program`. Same rejection shape the
13561 // parallel keyword test uses, so the ordering contract observed
13562 // here is structurally identical to the keyword sibling's.
13563 let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
13564 let mut e = Expander::new();
13565 let err = e
13566 .expand_and_collect_calls_to_any::<(), _, _, ()>(
13567 forms,
13568 |_h| -> Option<()> {
13569 panic!("classifier must not run when expand_program errors");
13570 },
13571 |(), _args| {
13572 panic!("project must not run when expand_program errors");
13573 },
13574 )
13575 .expect_err("expand_program error must short-circuit before classifier or project");
13576 // Sanity: the error IS an expand_program-stage rejection — not a
13577 // classifier or projection error — so the ordering is observable.
13578 let rendered = format!("{err}");
13579 assert!(
13580 rendered.contains("NAME") || rendered.contains("symbol"),
13581 "expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
13582 );
13583 }
13584
13585 // ── Expander::expand_source_and_collect_calls_to: from-source walk ──
13586 //
13587 // The from-source sibling of `expand_and_collect_calls_to`: composes
13588 // `crate::reader::read(src)?` with the from-forms primitive in ONE
13589 // method on the `Expander` surface. All four typed-program dispatchers
13590 // (free-function `compile_typed` / `compile_named`, preloaded-expander
13591 // `RealizedCompiler::compile_typed` / `compile_named`) route through
13592 // it; the tests below pin its contract directly. The existing
13593 // compile.rs + compiler_spec.rs dispatch tests are the path-uniformity
13594 // guards proving the four production sites route through it without
13595 // behavior drift.
13596
13597 #[test]
13598 fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
13599 // Happy path: a source string with three top-level forms (two
13600 // matching `defmonitor`, one matching `defalert`) flows through
13601 // `read` then through the from-forms primitive. Pin the SAME
13602 // emission shape the from-forms test pins for the matching
13603 // sub-set, sourced from a `&str` rather than a `Vec<Sexp>`.
13604 let src = r#"(defmonitor :name "first")
13605 (defalert :name "not-a-match")
13606 (defmonitor :solo)"#;
13607 let mut e = Expander::new();
13608 let lengths: Vec<usize> = e
13609 .expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
13610 .expect("matching forms must compose");
13611 assert_eq!(lengths, vec![2, 1]);
13612 }
13613
13614 #[test]
13615 fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
13616 // The reader runs BEFORE `expand_program` — so a reader error (an
13617 // unterminated string, an unbalanced paren, an unknown escape)
13618 // must short-circuit the entire composition and `expand_program`
13619 // / the keyword filter / `project` must NEVER run. Pin the
13620 // ordering: an unbalanced open-paren is rejected by the reader
13621 // at parse time, never reaches the from-forms primitive.
13622 let mut count = 0usize;
13623 let mut e = Expander::new();
13624 let err = e
13625 .expand_source_and_collect_calls_to::<(), _>(
13626 "(defmonitor :name \"unbalanced",
13627 "defmonitor",
13628 |_args| {
13629 count += 1;
13630 Ok(())
13631 },
13632 )
13633 .expect_err("reader error must short-circuit before expand_program");
13634 assert_eq!(
13635 count, 0,
13636 "project must never run when reader errors at parse time"
13637 );
13638 // Sanity: the error IS a reader-stage rejection — the rendered
13639 // diagnostic mentions the lexer's gate, not the expander's or
13640 // projector's (path-uniformity for the read-then-walk ordering).
13641 let rendered = format!("{err}");
13642 assert!(
13643 rendered.to_lowercase().contains("string")
13644 || rendered.to_lowercase().contains("paren")
13645 || rendered.to_lowercase().contains("eof")
13646 || rendered.to_lowercase().contains("unexpected")
13647 || rendered.to_lowercase().contains("unterminated")
13648 || rendered.to_lowercase().contains("unclosed"),
13649 "error must be the reader-stage rejection, got: {rendered}"
13650 );
13651 }
13652
13653 #[test]
13654 fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
13655 ) {
13656 // Reader succeeds, but `expand_program` rejects a malformed
13657 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13658 // ordering: `expand_program` rejects BEFORE the keyword filter
13659 // walks anything, `project` must NEVER run. Sibling of the
13660 // from-forms test of the same name — one level up the
13661 // composition (sourced from `&str` rather than `Vec<Sexp>`).
13662 let mut count = 0usize;
13663 let mut e = Expander::new();
13664 let err = e
13665 .expand_source_and_collect_calls_to::<(), _>(
13666 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13667 "defmonitor",
13668 |_args| {
13669 count += 1;
13670 Ok(())
13671 },
13672 )
13673 .expect_err("expand_program error must short-circuit before project");
13674 assert_eq!(
13675 count, 0,
13676 "project must never run when expand_program errors"
13677 );
13678 let rendered = format!("{err}");
13679 assert!(
13680 rendered.contains("NAME") || rendered.contains("symbol"),
13681 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13682 );
13683 }
13684
13685 #[test]
13686 fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
13687 // Reader + `expand_program` both succeed; the per-form `project`
13688 // errors on the SECOND matched form. Pin: the collect's short-
13689 // circuit fires at the second match's error, the third match's
13690 // `project` is NEVER invoked, and the returned error is exactly
13691 // the one the closure raised. Mirrors `compile_named`'s
13692 // short-circuit on the first `NamedFormMissingName` /
13693 // `NamedFormNonSymbolName` / typed-entry rejection — sourced
13694 // from `&str`.
13695 let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
13696 let mut seen = Vec::new();
13697 let mut e = Expander::new();
13698 let err = e
13699 .expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
13700 let n = args[1].as_int().expect("test args are ints");
13701 seen.push(n);
13702 if n == 2 {
13703 Err(LispError::Compile {
13704 form: "test".to_string(),
13705 message: "stop at two".to_string(),
13706 })
13707 } else {
13708 Ok(())
13709 }
13710 })
13711 .expect_err("project's error must short-circuit collect");
13712 assert_eq!(seen, vec![1, 2]);
13713 assert!(
13714 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
13715 "short-circuit must propagate the project's error verbatim, got {err:?}"
13716 );
13717 }
13718
13719 #[test]
13720 fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
13721 // Boundary: empty source string yields zero items regardless of
13722 // keyword. Pin the degenerate boundary — empty in, empty out —
13723 // and the closure is never invoked. Sibling of the from-forms
13724 // test of the same name, one level up the composition: the
13725 // from-source posture is fused-empty when `read` yields an
13726 // empty `Vec<Sexp>` (which, fed into `expand_program`, yields
13727 // an empty `Vec<Sexp>`, which iter_calls_to walks to zero
13728 // matches).
13729 let mut count = 0usize;
13730 let mut e = Expander::new();
13731 let out: Vec<()> = e
13732 .expand_source_and_collect_calls_to("", "anything", |_args| {
13733 count += 1;
13734 Ok(())
13735 })
13736 .expect("empty source is not an error");
13737 assert!(out.is_empty());
13738 assert_eq!(count, 0);
13739 }
13740
13741 #[test]
13742 fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
13743 // Critical ordering preserved across the from-source posture:
13744 // `defmacro` source registers in `expand_program`, its call
13745 // sites expand to the matching keyword, AND the keyword walk
13746 // sees both the macro-emitted form and any directly-authored
13747 // matches in source order. Sibling of the from-forms test of
13748 // the same name, sourced from `&str`.
13749 let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
13750 (emit-monitor \"alpha\")
13751 (defmonitor :name \"beta\")";
13752 let mut e = Expander::new();
13753 let names: Vec<String> = e
13754 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13755 Ok(args[1].as_string().unwrap().to_string())
13756 })
13757 .expect("macroexpanded + directly-authored forms must both flow");
13758 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
13759 }
13760
13761 #[test]
13762 fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
13763 // Structural identity: for any (src, keyword, project) triple
13764 // whose pieces succeed, the from-source method's result equals
13765 // the inlined pre-lift pipeline `read + expand_and_collect_
13766 // calls_to`. Pin shape AND ordering on a mixed source
13767 // (matching + non-matching + macroexpand-emitted matches) so a
13768 // regression that drifts the method's pipeline from the
13769 // inlined closed-form fails here. This is the lift's
13770 // load-bearing path-uniformity gate — the SAME assertion that
13771 // holds for all four production consumers (free-function
13772 // `compile_typed`/`compile_named` + `RealizedCompiler::compile_typed`/`compile_named`)
13773 // holds at the primitive's contract level.
13774 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
13775 (foo :idx 1)
13776 (emit-foo 2)
13777 (bar :idx 99)
13778 (foo :idx 3)";
13779
13780 // Inlined pre-lift pipeline.
13781 let mut exp_inline = Expander::new();
13782 let inline_forms = read(src).unwrap();
13783 let via_inline: Vec<i64> = exp_inline
13784 .expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
13785 .unwrap();
13786
13787 // From-source method pipeline.
13788 let mut exp_method = Expander::new();
13789 let via_method: Vec<i64> = exp_method
13790 .expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
13791 .unwrap();
13792
13793 assert_eq!(via_inline, via_method);
13794 assert_eq!(via_inline, vec![1, 2, 3]);
13795 }
13796
13797 #[test]
13798 fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
13799 // The `keyword` argument flows straight into the from-forms
13800 // primitive's head-comparison gate, which inherits from
13801 // `iter_calls_to` — a regression that drifts the keyword (e.g.,
13802 // lowercases / trims it en route through the read step) fails
13803 // here. Pin by passing TWO different keywords against the SAME
13804 // source and asserting each picks up only its matching forms.
13805 let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
13806
13807 let mut e1 = Expander::new();
13808 let monitors: Vec<i64> = e1
13809 .expand_source_and_collect_calls_to(src, "defmonitor", |args| {
13810 Ok(args[1].as_int().unwrap())
13811 })
13812 .unwrap();
13813 assert_eq!(monitors, vec![1, 3]);
13814
13815 let mut e2 = Expander::new();
13816 let alerts: Vec<i64> = e2
13817 .expand_source_and_collect_calls_to(src, "defalert", |args| {
13818 Ok(args[1].as_int().unwrap())
13819 })
13820 .unwrap();
13821 assert_eq!(alerts, vec![2]);
13822
13823 let mut e3 = Expander::new();
13824 let none: Vec<i64> = e3
13825 .expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
13826 Ok(args[1].as_int().unwrap())
13827 })
13828 .unwrap();
13829 assert!(none.is_empty());
13830 }
13831
13832 #[test]
13833 fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
13834 // Path-uniformity for the soft-projection posture inherited
13835 // from `iter_calls_to`: non-matching forms in the source are
13836 // skipped silently, and `project` is never invoked on them.
13837 // Sibling of the from-forms test of the same name, sourced
13838 // from `&str` with a mix of atoms-and-lists that the reader
13839 // emits at the top level.
13840 let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
13841 let mut count = 0usize;
13842 let mut e = Expander::new();
13843 let out: Vec<()> = e
13844 .expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
13845 count += 1;
13846 Ok(())
13847 })
13848 .expect("non-matching-only source must collect to empty Vec");
13849 assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
13850 assert_eq!(count, 0, "project must never run for non-matching forms");
13851 }
13852
13853 // ── Expander::expand_source_and_collect_calls_to_any — from-source × classifier ─
13854 //
13855 // The from-source posture of the typed-decoded classifier sibling on
13856 // the `Expander` surface — closes the (from-forms, from-source) ×
13857 // (keyword, classifier) 2×2 of compose-on-iter projections at the
13858 // from-source classifier corner the prior runs left open. The
13859 // pre-existing `expand_source_and_collect_calls_to` (from-source ×
13860 // keyword) is the constant-classifier projection of this primitive:
13861 // its body composes this function with a `|h| (h == keyword).
13862 // then_some(())` decoder, so the from-source pipeline lives at ONE
13863 // site. These tests pin both the typed-decoded primitive's contract
13864 // directly AND the post-lift routing of
13865 // `expand_source_and_collect_calls_to` through it.
13866
13867 #[test]
13868 fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
13869 ) {
13870 // Happy path: a source string with four top-level forms (two
13871 // matching "foo", one matching "bar", one rejected-head
13872 // "defmonitor") flows through `read` → `expand_program` → the
13873 // typed-decoded classifier walk. The closed-set classifier
13874 // `Op::from_keyword` admits "foo" and "bar" but rejects
13875 // "defmonitor". Pin the typed-decoded yield shape (decoded
13876 // witness alongside args tail) AND source-order preservation
13877 // sourced from `&str` rather than a pre-parsed `Vec<Sexp>`.
13878 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13879 enum Op {
13880 Foo,
13881 Bar,
13882 }
13883 fn op_from_keyword(head: &str) -> Option<Op> {
13884 match head {
13885 "foo" => Some(Op::Foo),
13886 "bar" => Some(Op::Bar),
13887 _ => None,
13888 }
13889 }
13890 let src = r#"(foo :idx 1)
13891 (defmonitor :idx 99)
13892 (bar :idx 2)
13893 (foo :idx 3)"#;
13894 let mut e = Expander::new();
13895 let yielded: Vec<(Op, i64)> = e
13896 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
13897 Ok((op, args[1].as_int().expect("test args are ints")))
13898 })
13899 .expect("matching forms must compose through from-source classifier walk");
13900 assert_eq!(
13901 yielded,
13902 vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
13903 "yields must be in source order, decoded witness paired with per-form mapper output"
13904 );
13905 }
13906
13907 #[test]
13908 fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
13909 ) {
13910 // The reader runs BEFORE `expand_program` AND BEFORE the
13911 // classifier — so a reader error (an unterminated string)
13912 // short-circuits the entire composition and both the classifier
13913 // AND `project` must NEVER run. Pin the ordering with BOTH
13914 // decoder and projection explicitly panicking — any post-reader
13915 // execution fires the panic. Sibling of
13916 // `expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program`
13917 // one corner over in the 2×2 (the classifier version of the
13918 // same ordering pin).
13919 let mut e = Expander::new();
13920 let err = e
13921 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13922 "(defmonitor :name \"unbalanced",
13923 |_h: &str| -> Option<()> {
13924 panic!("classifier must not run when reader errors at parse time")
13925 },
13926 |(), _args| -> Result<()> { panic!("project must not run when reader errors") },
13927 )
13928 .expect_err("reader error must short-circuit before classifier");
13929 // Sanity: the error is a reader-stage rejection.
13930 let rendered = format!("{err}");
13931 assert!(
13932 rendered.to_lowercase().contains("string")
13933 || rendered.to_lowercase().contains("paren")
13934 || rendered.to_lowercase().contains("eof")
13935 || rendered.to_lowercase().contains("unexpected")
13936 || rendered.to_lowercase().contains("unterminated")
13937 || rendered.to_lowercase().contains("unclosed"),
13938 "error must be the reader-stage rejection, got: {rendered}"
13939 );
13940 }
13941
13942 #[test]
13943 fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
13944 ) {
13945 // Reader succeeds, but `expand_program` rejects a malformed
13946 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
13947 // ordering: `expand_program` rejects BEFORE the classifier walks
13948 // anything, both decoder AND `project` must NEVER run. Sibling
13949 // of the keyword-from-source variant — one corner over in the
13950 // 2×2 with both classifier and projection explicitly panicking.
13951 let mut e = Expander::new();
13952 let err = e
13953 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13954 "(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
13955 |_h: &str| -> Option<()> {
13956 panic!("classifier must not run when expand_program errors")
13957 },
13958 |(), _args| -> Result<()> {
13959 panic!("project must not run when expand_program errors")
13960 },
13961 )
13962 .expect_err("expand_program error must short-circuit before classifier");
13963 let rendered = format!("{err}");
13964 assert!(
13965 rendered.contains("NAME") || rendered.contains("symbol"),
13966 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
13967 );
13968 }
13969
13970 #[test]
13971 fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
13972 // Path-uniformity for the soft-projection posture inherited
13973 // from `iter_calls_to_any`: forms whose head the classifier
13974 // rejects are skipped silently, and `project` is never invoked
13975 // on them. Pin via an explicitly-panicking projection across a
13976 // mix of forms (atom, list-with-non-symbol-head, list with
13977 // unrecognized-by-classifier symbol head). A regression that
13978 // wrongly invokes `project` on a rejected form fires the panic.
13979 let src = r#"bare-atom
13980 (5 :not-a-symbol-head)
13981 (defmonitor :name "decoder-rejects-me")"#;
13982 let mut e = Expander::new();
13983 // The closed-set classifier admits NOTHING in this source.
13984 let out: Vec<()> = e
13985 .expand_source_and_collect_calls_to_any::<(), _, _, ()>(
13986 src,
13987 |_h: &str| -> Option<()> { None },
13988 |(), _args| -> Result<()> {
13989 panic!("project must not run for classifier-rejected forms")
13990 },
13991 )
13992 .expect("classifier-rejects-all source must collect to empty Vec");
13993 assert!(
13994 out.is_empty(),
13995 "no classifier-accepted forms — empty Vec, got {out:?}"
13996 );
13997 }
13998
13999 #[test]
14000 fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
14001 // Reader + `expand_program` + classifier all succeed; the
14002 // per-form `project` errors on the SECOND matched form. Pin: the
14003 // collect's short-circuit fires at the second match's error, the
14004 // third match's `project` is NEVER invoked, and the returned
14005 // error is exactly the one the closure raised. Sibling of the
14006 // keyword-from-source variant — one corner over in the 2×2.
14007 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14008 enum Op {
14009 Foo,
14010 }
14011 fn op_from_keyword(head: &str) -> Option<Op> {
14012 (head == "foo").then_some(Op::Foo)
14013 }
14014 let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
14015 let mut seen = Vec::new();
14016 let mut e = Expander::new();
14017 let err = e
14018 .expand_source_and_collect_calls_to_any::<(), _, _, _>(
14019 src,
14020 op_from_keyword,
14021 |_op: Op, args: &[Sexp]| -> Result<()> {
14022 let n = args[1].as_int().expect("test args are ints");
14023 seen.push(n);
14024 if n == 2 {
14025 Err(LispError::Compile {
14026 form: "test".to_string(),
14027 message: "stop at two".to_string(),
14028 })
14029 } else {
14030 Ok(())
14031 }
14032 },
14033 )
14034 .expect_err("project's error must short-circuit collect");
14035 assert_eq!(seen, vec![1, 2], "third match's project must never run");
14036 assert!(
14037 matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
14038 "short-circuit must propagate the project's error verbatim, got {err:?}"
14039 );
14040 }
14041
14042 #[test]
14043 fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
14044 ) {
14045 // The post-lift composition law binding the keyword-from-source
14046 // sibling to the typed-decoded primitive:
14047 // `expand_source_and_collect_calls_to(src, k, project) ==
14048 // expand_source_and_collect_calls_to_any(src,
14049 // |h| (h == k).then_some(()),
14050 // |(), args| project(args))`
14051 // Pin shape AND ordering across THREE representative keywords
14052 // (matching some, matching exactly one, matching none) on the
14053 // SAME mixed source the keyword path-uniformity test exercises.
14054 // A regression that re-implements the keyword-from-source filter
14055 // as a parallel `read + expand_and_collect_calls_to` pipeline
14056 // (rather than routing through the typed-decoded primitive)
14057 // fails the value pin.
14058 let src = "(defmacro emit-foo (n) `(foo :idx ,n))
14059 (foo :idx 1)
14060 (emit-foo 2)
14061 (bar :idx 99)
14062 (foo :idx 3)";
14063 for k in ["foo", "bar", "missing"] {
14064 let mut e_keyword = Expander::new();
14065 let via_keyword: Vec<i64> = e_keyword
14066 .expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
14067 .unwrap();
14068
14069 let mut e_classifier = Expander::new();
14070 let via_classifier: Vec<i64> = e_classifier
14071 .expand_source_and_collect_calls_to_any(
14072 src,
14073 |h: &str| (h == k).then_some(()),
14074 |(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
14075 )
14076 .unwrap();
14077
14078 assert_eq!(
14079 via_keyword, via_classifier,
14080 "keyword from-source path must equal classifier from-source path for {k:?}"
14081 );
14082 }
14083 }
14084
14085 #[test]
14086 fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
14087 // Critical ordering preserved across the typed-decoded
14088 // from-source posture: `defmacro` source registers in
14089 // `expand_program`, its call sites expand to a classifier-
14090 // accepted form, AND the classifier walk sees both the
14091 // macro-emitted form and any directly-authored matches in
14092 // source order. Sibling of the keyword-from-source ordering
14093 // test, one corner over in the 2×2.
14094 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14095 enum Op {
14096 Foo,
14097 }
14098 fn op_from_keyword(head: &str) -> Option<Op> {
14099 (head == "foo").then_some(Op::Foo)
14100 }
14101 let src = "(defmacro emit-foo (n) `(foo :name ,n))
14102 (emit-foo \"alpha\")
14103 (foo :name \"beta\")";
14104 let mut e = Expander::new();
14105 let names: Vec<(Op, String)> = e
14106 .expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
14107 Ok((op, args[1].as_string().unwrap().to_string()))
14108 })
14109 .expect("macroexpanded + directly-authored forms must both flow through classifier");
14110 assert_eq!(
14111 names,
14112 vec![
14113 (Op::Foo, "alpha".to_string()),
14114 (Op::Foo, "beta".to_string()),
14115 ]
14116 );
14117 }
14118
14119 // ── expand_source_program — from-source × yield-all primitive ───
14120 //
14121 // Closes the 2×2 program-level walk family on the `Expander` surface:
14122 // from-forms × yield-all (`expand_program`), from-forms × keyword-
14123 // projected (`expand_and_collect_calls_to`), from-source × keyword-
14124 // projected (`expand_source_and_collect_calls_to`), AND now
14125 // from-source × yield-all (`expand_source_program`). Pins (a) the
14126 // happy path (mixed forms flow through reader then expand_program),
14127 // (b) reader-stage short-circuit (no expand_program side-effects),
14128 // (c) expand_program-stage short-circuit (defmacro-NAME-not-symbol
14129 // is rejected at its offending form), (d) defmacro absorption (the
14130 // primitive's side-effect on `self.macros` matches the from-forms
14131 // posture's side-effect verbatim), (e) the empty-source boundary,
14132 // and (f) structural identity vs. the inlined pre-lift `read +
14133 // expand_program` pipeline (path-uniformity gate).
14134
14135 #[test]
14136 fn expand_source_program_routes_through_reader_then_expand_program() {
14137 // Happy path: a source string with mixed forms (a `defmacro`
14138 // definition, a `defmonitor` form, a plain symbol literal) flows
14139 // through `read` then through the from-forms primitive. Pin the
14140 // SAME emission shape the from-forms test pins, sourced from a
14141 // `&str` rather than a `Vec<Sexp>`.
14142 let src = r#"(defmacro id (x) `,x)
14143 (defmonitor :name "alpha")
14144 bare-symbol"#;
14145 let mut e = Expander::new();
14146 let out = e
14147 .expand_source_program(src)
14148 .expect("mixed forms must compose");
14149 // `(defmacro id …)` is consumed as a side-effect (registers `id`
14150 // in `e.macros`), so the returned `Vec<Sexp>` contains the
14151 // remaining two top-level forms in source order.
14152 assert_eq!(out.len(), 2);
14153 assert_eq!(
14154 out[0].as_call_to("defmonitor").map(<[_]>::len),
14155 Some(2),
14156 "first surviving form is the defmonitor with two args, got: {:?}",
14157 out[0]
14158 );
14159 assert_eq!(
14160 out[1].as_symbol(),
14161 Some("bare-symbol"),
14162 "second surviving form is the bare symbol literal, got: {:?}",
14163 out[1]
14164 );
14165 // The defmacro side-effect landed in the expander's macro table.
14166 assert!(
14167 e.has("id"),
14168 "defmacro must register `id` in the expander's macro table"
14169 );
14170 }
14171
14172 #[test]
14173 fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
14174 // The reader runs BEFORE `expand_program` — so a reader error (an
14175 // unterminated string, an unbalanced paren) must short-circuit
14176 // the entire composition and `expand_program` must NEVER run.
14177 // Pin the ordering: a `defmacro` BEFORE the unterminated string
14178 // is NOT registered, because the reader fails at parse time
14179 // before `expand_program` sees any form.
14180 let mut e = Expander::new();
14181 let err = e
14182 .expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
14183 .expect_err("reader error must short-circuit before expand_program");
14184 // Sanity: the error IS a reader-stage rejection.
14185 let rendered = format!("{err}").to_lowercase();
14186 assert!(
14187 rendered.contains("string")
14188 || rendered.contains("paren")
14189 || rendered.contains("eof")
14190 || rendered.contains("unexpected")
14191 || rendered.contains("unterminated")
14192 || rendered.contains("unclosed"),
14193 "error must be the reader-stage rejection, got: {err}"
14194 );
14195 // Path-uniformity gate: the `defmacro` lexically BEFORE the
14196 // unterminated string must NOT have been absorbed — `read`'s
14197 // failure short-circuits the entire pipeline before
14198 // `expand_program` walks any forms.
14199 assert!(
14200 !e.has("should-not-register"),
14201 "reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
14202 );
14203 }
14204
14205 #[test]
14206 fn expand_source_program_short_circuits_on_expand_program_error() {
14207 // Reader succeeds, but `expand_program` rejects a malformed
14208 // `defmacro` head (NAME slot is an int, not a symbol). Pin the
14209 // ordering: the expand_program-stage rejection bubbles up
14210 // verbatim, sourced from `&str` rather than `Vec<Sexp>`.
14211 let mut e = Expander::new();
14212 let err = e
14213 .expand_source_program("(defmacro 5 (x) `,x)")
14214 .expect_err("expand_program error must propagate");
14215 let rendered = format!("{err}");
14216 assert!(
14217 rendered.contains("NAME") || rendered.contains("symbol"),
14218 "error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
14219 );
14220 }
14221
14222 #[test]
14223 fn expand_source_program_yields_empty_vec_for_empty_source() {
14224 // Boundary: empty source string yields zero items. Pin the
14225 // degenerate boundary — empty in, empty out — sibling of the
14226 // from-source × keyword-projected test of the same name, one
14227 // level down the composition (no keyword filter).
14228 let mut e = Expander::new();
14229 let out = e
14230 .expand_source_program("")
14231 .expect("empty source is not an error");
14232 assert!(
14233 out.is_empty(),
14234 "empty source must yield empty Vec, got: {out:?}"
14235 );
14236 }
14237
14238 #[test]
14239 fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
14240 // Critical pipeline order: a `defmacro` in the source registers
14241 // AND a subsequent call to it expands AND the expanded form
14242 // surfaces in the returned Vec. Sourced from `&str`, the
14243 // from-source posture preserves the side-effect-then-expansion
14244 // semantics of the from-forms primitive verbatim.
14245 let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
14246 (emit-monitor "alpha")
14247 (emit-monitor "beta")"#;
14248 let mut e = Expander::new();
14249 let out = e
14250 .expand_source_program(src)
14251 .expect("defmacro absorption then expansion must compose");
14252 // Two surviving forms (the `defmacro` itself is consumed as a
14253 // side-effect), each expanded to a `(defmonitor :name "<n>")`.
14254 assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
14255 for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
14256 let args = out[i]
14257 .as_call_to("defmonitor")
14258 .unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
14259 assert_eq!(args[0].as_keyword(), Some("name"));
14260 assert_eq!(args[1].as_string(), Some(expected_name));
14261 }
14262 // The macro is now in the table — a subsequent call would
14263 // expand against this SAME expander.
14264 assert!(e.has("emit-monitor"));
14265 }
14266
14267 #[test]
14268 fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
14269 // Structural identity: for any `src` whose pieces succeed, the
14270 // from-source method's result equals the inlined pre-lift
14271 // pipeline `read + expand_program`. Pin shape AND ordering on a
14272 // mixed source (defmacro definition + macro call + plain form)
14273 // so a regression that drifts the method's pipeline from the
14274 // inlined closed-form fails here. This is the lift's
14275 // load-bearing path-uniformity gate — the SAME assertion holds
14276 // for both production consumers (`RealizedCompiler::compile` +
14277 // `realize_in_memory`'s `:macros` loop) at the primitive's
14278 // contract level.
14279 let src = r#"(defmacro id (x) `,x)
14280 (id (foo 1 2))
14281 (bar)"#;
14282
14283 // Inlined pre-lift pipeline.
14284 let mut e_inline = Expander::new();
14285 let inline_forms = read(src).unwrap();
14286 let via_inline = e_inline
14287 .expand_program(inline_forms)
14288 .expect("inlined pipeline must succeed");
14289
14290 // From-source method pipeline.
14291 let mut e_method = Expander::new();
14292 let via_method = e_method
14293 .expand_source_program(src)
14294 .expect("from-source method pipeline must succeed");
14295
14296 assert_eq!(
14297 via_inline, via_method,
14298 "from-source method must emit byte-identical result to inlined read+expand_program"
14299 );
14300 // Both pipelines registered the same defmacro side-effect.
14301 assert_eq!(e_inline.has("id"), e_method.has("id"));
14302 assert!(e_method.has("id"));
14303 }
14304
14305 #[test]
14306 fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
14307 // Per-`&mut self` absorption posture pin: a `defmacro` registered
14308 // in call 1 SURVIVES into call 2 on the SAME `Expander`. This is
14309 // the load-bearing semantic `realize_in_memory`'s per-spec-macro
14310 // build-up depends on — every iteration's `expand_source_program(
14311 // macro_src)?` lands its `defmacro` heads into the SAME mutable
14312 // `preloaded` expander, so a follow-up `:macros` source can
14313 // invoke macros defined in earlier ones.
14314 let mut e = Expander::new();
14315 let _ = e
14316 .expand_source_program("(defmacro outer (n) `(inner ,n))")
14317 .unwrap();
14318 assert!(e.has("outer"));
14319 // Call 2: defines `inner` AND invokes `outer`, which expands to
14320 // `(inner <n>)`. The expansion proves call 1's `outer` survived
14321 // into call 2's expansion.
14322 let out = e
14323 .expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
14324 .unwrap();
14325 assert_eq!(out.len(), 1, "call 2 yields one expanded form");
14326 // (outer 42) → (inner 42) → (wrapped 42)
14327 let args = out[0]
14328 .as_call_to("wrapped")
14329 .expect("nested expansion must reach `wrapped`");
14330 assert_eq!(args[0].as_int(), Some(42));
14331 }
14332
14333 // ── Expander::register_macro_def: the macro-registration primitive ──
14334 //
14335 // `register_macro_def(&mut self, def: MacroDef) -> Result<()>` lifts
14336 // the byte-identical two-step block —
14337 //
14338 // if self.compile_templates {
14339 // self.templates.insert(def.name.clone(), compile_template(&def)?);
14340 // }
14341 // self.macros.insert(def.name.clone(), def);
14342 //
14343 // — that lived inline at `with_macros` (the bulk-from-iterator
14344 // constructor) AND `expand_program`'s `(defmacro …)`-head arm (the
14345 // program-walk-time registration site) into ONE named method on the
14346 // `Expander` surface. The tests below pin:
14347 //
14348 // (a) the bytecode-default expander (`Expander::new()`) populates
14349 // BOTH `macros` AND `templates` keyed by `def.name`,
14350 // (b) the substitute-only expander (`Expander::new_substitute_only()`)
14351 // populates `macros` but SKIPS `templates` — the
14352 // `compile_templates: false` gate fires structurally,
14353 // (c) a template that fails to compile (`,foo` against `params:
14354 // []`) short-circuits BEFORE `self.macros.insert` runs, so the
14355 // failed registration leaves both tables untouched — no
14356 // partial-write window in which `self.macros.has(name)` is
14357 // true but `self.templates.has(name)` is missing,
14358 // (d) `with_macros([def])` and `register_macro_def(def)` on a fresh
14359 // expander produce structurally identical state (both tables'
14360 // sets of keys + the same `MacroDef` body under each key),
14361 // (e) `expand_program` of one `(defmacro …)` form and
14362 // `register_macro_def` of the parsed `MacroDef` produce
14363 // structurally identical state — closing path-uniformity
14364 // across the lift's two consumers AT the registration site.
14365 //
14366 // The tests bind on `Expander::has(name)` / `Expander::len()` for
14367 // the macros table and on bytecode-vs-substitute output equivalence
14368 // (a registered macro must expand to the SAME result regardless of
14369 // whether the bytecode path's `self.templates` entry exists) for the
14370 // templates table.
14371
14372 fn macro_def_id() -> MacroDef {
14373 // `(defmacro id (x) `,x)` — the simplest well-formed MacroDef:
14374 // one required param, a single-symbol unquote body. Compiles to
14375 // a valid `CompiledTemplate` (no unbound vars), so
14376 // `register_macro_def` succeeds on every Expander posture.
14377 MacroDef {
14378 name: "id".into(),
14379 params: MacroParams {
14380 required: vec!["x".into()],
14381 optional: vec![],
14382 rest: None,
14383 },
14384 // ` ` quasi-quoted `,x` — `Quasiquote(Unquote(Symbol("x")))`.
14385 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14386 }
14387 }
14388
14389 fn macro_def_bad_template() -> MacroDef {
14390 // `(defmacro bad () `,unbound)` — a quasi-quote body with `,unbound`
14391 // against an EMPTY required-params list. `compile_template`
14392 // rejects it with `UnboundTemplateVar` because `unbound` is not
14393 // in `params.names()`. Used to exercise the
14394 // `compile_template`-failed-but-self.macros-still-pristine
14395 // path-uniformity pin.
14396 MacroDef {
14397 name: "bad".into(),
14398 params: MacroParams::default(),
14399 body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
14400 }
14401 }
14402
14403 #[test]
14404 fn register_macro_def_bytecode_default_populates_macros_and_templates() {
14405 // The bytecode-default `Expander::new()` carries
14406 // `compile_templates: true`; `register_macro_def` MUST populate
14407 // BOTH `self.macros` (the substitute path's registry) AND
14408 // `self.templates` (the bytecode path's pre-compiled bytecode
14409 // index) keyed by `def.name`. Fail-before-pass-after: this
14410 // assert requires the new method to exist AND to compose the
14411 // two side-effects in canonical order on a single-call
14412 // registration — pre-lift `register_macro_def` did not exist.
14413 let mut e = Expander::new();
14414 e.register_macro_def(macro_def_id())
14415 .expect("well-formed MacroDef must register");
14416 assert!(
14417 e.has("id"),
14418 "self.macros must carry the registered name after register_macro_def"
14419 );
14420 assert!(
14421 e.templates.contains_key("id"),
14422 "self.templates must carry the compiled bytecode under the bytecode-default posture"
14423 );
14424 // The registered macro must expand correctly through the
14425 // bytecode strategy — proves the inserted template is the right
14426 // bytecode for the body.
14427 let out = e
14428 .expand_program(read("(id 42)").unwrap())
14429 .expect("registered macro must expand");
14430 assert_eq!(out.len(), 1);
14431 assert_eq!(out[0], Sexp::int(42));
14432 }
14433
14434 #[test]
14435 fn register_macro_def_substitute_only_skips_templates() {
14436 // `Expander::new_substitute_only()` carries `compile_templates:
14437 // false`; `register_macro_def` MUST populate `self.macros` for
14438 // the substitute path but SKIP `self.templates` (no bytecode
14439 // pre-compile). Pin the `compile_templates: false` gate fires
14440 // structurally — a regression that drifts the conditional from
14441 // the registration primitive (e.g. a future emitter that
14442 // unconditionally pre-compiles) would silently double the
14443 // benchmark baseline's allocation footprint.
14444 let mut e = Expander::new_substitute_only();
14445 e.register_macro_def(macro_def_id())
14446 .expect("well-formed MacroDef must register under substitute-only");
14447 assert!(
14448 e.has("id"),
14449 "self.macros must carry the registered name even under substitute-only"
14450 );
14451 assert!(
14452 !e.templates.contains_key("id"),
14453 "self.templates MUST be empty under compile_templates: false — the gate fires"
14454 );
14455 // The registered macro must still expand correctly through the
14456 // substitute strategy — proves the inserted MacroDef is the
14457 // right body for substitute-walking.
14458 let out = e
14459 .expand_program(read("(id 42)").unwrap())
14460 .expect("registered macro must expand via substitute path");
14461 assert_eq!(out.len(), 1);
14462 assert_eq!(out[0], Sexp::int(42));
14463 }
14464
14465 #[test]
14466 fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
14467 // The structural ordering pin: when `compile_template(&def)?`
14468 // rejects (e.g. `,unbound` against empty params), the `?` MUST
14469 // short-circuit BEFORE `self.macros.insert(def.name.clone(),
14470 // def)` runs, so a failed registration leaves BOTH tables
14471 // exactly as they were. Without this ordering a future
14472 // bytecode-strategy lookup would resolve `self.macros.get(name)`
14473 // (the body was inserted) AND find `self.templates.get(name)`
14474 // returning `None` (the pre-compile failed), silently coercing
14475 // the macro onto the substitute fallback path despite
14476 // `compile_templates: true`. The lift preserves the pre-lift
14477 // ordering (`compile_template` precedes `self.macros.insert`)
14478 // structurally — the test pins it across the lift.
14479 let mut e = Expander::new();
14480 let err = e
14481 .register_macro_def(macro_def_bad_template())
14482 .expect_err("unbound-template body must reject");
14483 // The rejection is the structural variant the bytecode
14484 // strategy's template-gate emits.
14485 assert!(
14486 matches!(err, LispError::UnboundTemplateVar { .. }),
14487 "expected UnboundTemplateVar, got: {err:?}"
14488 );
14489 // Both tables MUST be pristine — no partial write.
14490 assert!(
14491 !e.has("bad"),
14492 "self.macros must be untouched after compile_template failure"
14493 );
14494 assert!(
14495 !e.templates.contains_key("bad"),
14496 "self.templates must be untouched after compile_template failure"
14497 );
14498 }
14499
14500 #[test]
14501 fn with_macros_routes_through_register_macro_def_path_uniformity() {
14502 // Path-uniformity pin across the bulk-constructor consumer:
14503 // `with_macros([def])` MUST produce the same final state as
14504 // `Expander::new()` + `register_macro_def(def)`. A regression
14505 // that drifts the constructor's per-MacroDef inline block from
14506 // the registration primitive (e.g. a future emitter that
14507 // re-inlines the two-step block at `with_macros` rather than
14508 // delegating) would fail loudly here.
14509 let mut via_register = Expander::new();
14510 via_register
14511 .register_macro_def(macro_def_id())
14512 .expect("register must succeed");
14513 let mut via_with_macros =
14514 Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
14515 assert_eq!(via_register.len(), via_with_macros.len());
14516 assert!(via_register.has("id"));
14517 assert!(via_with_macros.has("id"));
14518 // Both tables key on the same set across both postures.
14519 assert_eq!(
14520 via_register.templates.contains_key("id"),
14521 via_with_macros.templates.contains_key("id"),
14522 "self.templates key-presence must agree across with_macros and register_macro_def"
14523 );
14524 // Both registered expanders expand the registered macro
14525 // identically — the strongest behavioral parity.
14526 let out_a = via_register
14527 .expand_program(read("(id 99)").unwrap())
14528 .unwrap();
14529 let out_b = via_with_macros
14530 .expand_program(read("(id 99)").unwrap())
14531 .unwrap();
14532 assert_eq!(out_a, out_b);
14533 assert_eq!(out_a, vec![Sexp::int(99)]);
14534 }
14535
14536 #[test]
14537 fn expand_program_routes_through_register_macro_def_path_uniformity() {
14538 // Path-uniformity pin across the program-walk consumer:
14539 // `expand_program` of `(defmacro id (x) `,x)` MUST produce the
14540 // same final state as a direct
14541 // `register_macro_def(macro_def_id())`. A regression that
14542 // drifts `expand_program`'s `(defmacro …)`-head arm from the
14543 // registration primitive (e.g. a future emitter that re-inlines
14544 // the two-step block at `expand_program` rather than delegating)
14545 // would fail loudly here.
14546 let mut via_register = Expander::new();
14547 via_register
14548 .register_macro_def(macro_def_id())
14549 .expect("register must succeed");
14550 let mut via_expand_program = Expander::new();
14551 let yielded = via_expand_program
14552 .expand_program(read("(defmacro id (x) `,x)").unwrap())
14553 .expect("expand_program of one defmacro must succeed");
14554 // expand_program drops the (defmacro …) form from its returned
14555 // Vec<Sexp> (defmacro is a definition, not a program form), so
14556 // the yielded list is empty — pin the side-effect-only posture.
14557 assert!(
14558 yielded.is_empty(),
14559 "(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
14560 );
14561 assert!(via_register.has("id"));
14562 assert!(via_expand_program.has("id"));
14563 assert_eq!(via_register.len(), via_expand_program.len());
14564 // Both tables key on the same set across both postures.
14565 assert_eq!(
14566 via_register.templates.contains_key("id"),
14567 via_expand_program.templates.contains_key("id"),
14568 "self.templates key-presence must agree across expand_program and register_macro_def"
14569 );
14570 // Both registered expanders expand the registered macro
14571 // identically — the strongest behavioral parity, closing
14572 // path-uniformity across BOTH consumers (with_macros above,
14573 // expand_program here) at ONE primitive.
14574 let out_a = via_register
14575 .expand_program(read("(id 7)").unwrap())
14576 .unwrap();
14577 let out_b = via_expand_program
14578 .expand_program(read("(id 7)").unwrap())
14579 .unwrap();
14580 assert_eq!(out_a, out_b);
14581 assert_eq!(out_a, vec![Sexp::int(7)]);
14582 }
14583
14584 // ── as_unquote: path-uniformity across the three substitute / compile_node sites ──
14585 //
14586 // The new typed-marker projection `Sexp::as_unquote` lifts the
14587 // (Sexp::Unquote/UnquoteSplice variant, UnquoteForm::Unquote/Splice
14588 // literal) pair into ONE structural query. These tests pin behavioral
14589 // parity end-to-end across BOTH expansion strategies:
14590 // * `compile_node` — the bytecode-template strategy's typed marker
14591 // dispatch routes through as_unquote at the compile step.
14592 // * `substitute` (top-level + list-inner) — the substitute-walker
14593 // fallback strategy's typed marker dispatch routes through
14594 // as_unquote at both per-form sites.
14595 // The structural invariant the prior runs' `expansion_layers_agree_on_
14596 // output_and_cache_wins` benchmark observes — bytecode AND substitute
14597 // produce byte-identical output — is anchored here at the macro-template
14598 // level for every distinct unquote-family shape the substitute and
14599 // bytecode strategies discriminate.
14600
14601 #[test]
14602 fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
14603 // A template body whose only marker is a top-level `,x`. Both
14604 // expansion strategies route through `as_unquote` (compile_node for
14605 // bytecode, substitute for the fallback walker), each pairing
14606 // Sexp::Unquote ↔ UnquoteForm::Unquote at ONE typed projection.
14607 // Pin byte-identical output across both strategies — the
14608 // structural-invariant the new projection's lift was designed to
14609 // make load-bearing structural rather than per-site discipline.
14610 let src = "(defmacro id (x) ,x) (id 42)";
14611 let mut bc = Expander::new();
14612 let mut sub = Expander::new_substitute_only();
14613 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14614 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14615 assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
14616 assert_eq!(out_bc, vec![Sexp::int(42)]);
14617 }
14618
14619 #[test]
14620 fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
14621 // A template body whose marker is a list-inner `,@xs`. The substitute
14622 // strategy's list-inner Splice arm routes through `as_unquote`
14623 // matching Some((UnquoteForm::Splice, inner)); the bytecode strategy's
14624 // compile_node Splice arm routes through `as_unquote` matching the
14625 // same. Pin byte-identical output across both strategies for the
14626 // splice path — the projection's typed-marker pairing
14627 // Sexp::UnquoteSplice ↔ UnquoteForm::Splice is structurally
14628 // identical at every consumer post-lift.
14629 let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
14630 let mut bc = Expander::new();
14631 let mut sub = Expander::new_substitute_only();
14632 let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
14633 let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
14634 assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
14635 let expected = Sexp::List(vec![
14636 Sexp::symbol("list"),
14637 Sexp::int(0),
14638 Sexp::int(1),
14639 Sexp::int(2),
14640 Sexp::int(3),
14641 Sexp::int(99),
14642 ]);
14643 assert_eq!(out_bc, vec![expected]);
14644 }
14645
14646 #[test]
14647 fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
14648 // The substitute strategy's top-level `,@x` arm rejects with
14649 // LispError::SpliceOutsideList (a splice marker with no containing
14650 // list to flatten into). Pre-lift the arm was
14651 // `Sexp::UnquoteSplice(inner) => Err(splice_outside_list(inner))`;
14652 // post-lift the arm routes through `as_unquote` matching
14653 // Some((UnquoteForm::Splice, inner)) and dispatches on the typed
14654 // marker. Pin path-uniformity: the rejection MUST fire identically
14655 // through the new projection. The substitute-only strategy bypasses
14656 // the bytecode path, exposing this gate directly.
14657 //
14658 // `(defmacro bad (xs) ,@xs) (bad (1 2 3))` — the body is a bare
14659 // `,@xs` at top level (NOT wrapped in a containing list), so the
14660 // substitute fallback's top-level Splice arm fires.
14661 let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
14662 let mut sub = Expander::new_substitute_only();
14663 let err = sub.expand_program(read(src).unwrap()).unwrap_err();
14664 assert!(
14665 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14666 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14667 );
14668 }
14669
14670 #[test]
14671 fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
14672 // A `,unbound` template body — the inner symbol isn't a param —
14673 // fires gate-2 (must-be-bound-in-scope). The typed marker
14674 // `UnquoteForm::Unquote` MUST thread through `as_unquote` →
14675 // `resolve_unquote_in_params(inner, params, form)` → gate-2's
14676 // rejection-builder. Pre-lift the marker was bound at the per-arm
14677 // literal `UnquoteForm::Unquote`; post-lift it's bound at the
14678 // typed projection. Pin: the rejection's `prefix` slot is
14679 // UnquoteForm::Unquote, structurally derived from the typed
14680 // projection's typed marker, NOT a literal at the arm body.
14681 let src = "(defmacro bad (x) ,unbound)";
14682 let mut bc = Expander::new();
14683 let err = bc.expand_program(read(src).unwrap()).unwrap_err();
14684 match err {
14685 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14686 assert_eq!(
14687 prefix,
14688 UnquoteForm::Unquote,
14689 "typed marker drifted from UnquoteForm::Unquote at gate-2"
14690 );
14691 }
14692 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14693 }
14694 // Sibling negative control: `,@unbound` inside a containing list
14695 // threads UnquoteForm::Splice through the same projection. The
14696 // splice MUST be inside a list — `compile_template` rejects
14697 // top-level `,@X` bodies with SpliceOutsideList before compile_node
14698 // runs, so the typed-marker dispatch on Splice is observable only
14699 // for well-positioned splice bodies. Closes path-uniformity across
14700 // BOTH typed marker variants at the compile path.
14701 let src_splice = "(defmacro bad (x) (foo ,@unbound))";
14702 let mut bc2 = Expander::new();
14703 let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
14704 match err_splice {
14705 crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
14706 assert_eq!(
14707 prefix,
14708 UnquoteForm::Splice,
14709 "typed marker drifted from UnquoteForm::Splice at gate-2"
14710 );
14711 }
14712 other => panic!("expected UnboundTemplateVar, got: {other:?}"),
14713 }
14714 }
14715
14716 // ── compile_template + contains_unquote path-uniformity through as_unquote ──
14717 //
14718 // The prior `as_unquote` projection lift (eb00684) routed `compile_node`'s
14719 // two arms and `substitute`'s top-level + list-inner arms through the
14720 // typed-marker projection but LEFT `compile_template`'s top-level
14721 // splice-outside-list gate and `contains_unquote`'s family check inline
14722 // matching `Sexp::UnquoteSplice(inner)` / `Sexp::Unquote(_) |
14723 // Sexp::UnquoteSplice(_)`. After this lift both production sites route
14724 // through `as_unquote`: `compile_template` matches `Some((UnquoteForm::
14725 // Splice, inner))` (same shape as `substitute`'s list-inner Splice arm),
14726 // `contains_unquote` uses `as_unquote().is_some()` for the family check.
14727 // Every production-site recognizer of an unquote-family wrapper now
14728 // shares ONE typed-marker projection — the (Sexp variant, UnquoteForm
14729 // variant) pairing for `,@-outside-list` is bound at ONE structural
14730 // query across all FOUR reachable splice-recognition sites, and a future
14731 // regression that drifts the marker pairing at any production site
14732 // becomes a type error at the helper boundary rather than a silent
14733 // per-site divergence.
14734
14735 #[test]
14736 fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
14737 // The bytecode path's `compile_template` gate pre-rejects top-level
14738 // `,@X` bodies via `as_unquote()` matching `Some((UnquoteForm::Splice,
14739 // inner))`. Pre-lift the arm was `if let Sexp::UnquoteSplice(inner) =
14740 // body` — the LAST production-site inline `Sexp::UnquoteSplice(_)`
14741 // match outside the projection itself. Post-lift the (Sexp variant,
14742 // UnquoteForm variant) pairing for the splice-outside-list gate is
14743 // bound at ONE projection function across all three reachable
14744 // emission sites (compile_template top-level, substitute top-level,
14745 // substitute list-inner). Path-uniformity guard at the new boundary:
14746 // the bytecode-path compile-time gate now routes through the same
14747 // shape `substitute`'s list-inner Splice arm uses.
14748 //
14749 // `(defmacro bad (xs) \`,@xs)` — the body's outer quasi-quote is
14750 // peeled by `template_body`, leaving `,@xs` at top level for
14751 // `compile_template` to gate before `compile_node` walks anything.
14752 // The bytecode strategy is default-on (`Expander::new()` sets
14753 // `compile_templates: true`), so the gate fires at
14754 // `register_macro_def` time.
14755 let mut e = Expander::new();
14756 let err = e
14757 .expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
14758 .expect_err("compile_template must reject top-level ,@X via as_unquote");
14759 assert!(
14760 matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
14761 "expected SpliceOutsideList through as_unquote, got: {err:?}"
14762 );
14763 }
14764
14765 #[test]
14766 fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
14767 // Negative control on the typed-marker dispatch at `compile_template`'s
14768 // top-level gate. A top-level `,X` body (Unquote, NOT Splice) is a
14769 // valid template — `compile_node` lowers it to a single `Subst(idx)`
14770 // op. The new `Some((UnquoteForm::Splice, inner))` pattern at the
14771 // gate MUST NOT fire on the Unquote variant — only Splice — so the
14772 // typed marker is observably load-bearing: a regression that drifts
14773 // the pattern from `UnquoteForm::Splice` to the wider
14774 // `Some((_, inner))` would mis-reject `(defmacro id (x) ,x)` here.
14775 //
14776 // `(defmacro id (x) ,x) (id 42)` — `id`'s body is bare `,x` at top
14777 // level; `compile_template` admits it via the typed-marker gate,
14778 // `compile_node` emits `Subst(0)`, and the call expands to `42`.
14779 let src = "(defmacro id (x) ,x) (id 42)";
14780 let mut e = Expander::new();
14781 let expanded = e
14782 .expand_program(read(src).unwrap())
14783 .expect("top-level ,X body must compile through as_unquote-typed gate");
14784 assert_eq!(expanded, vec![Sexp::int(42)]);
14785 }
14786
14787 #[test]
14788 fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
14789 // The fast-path optimizer in `compile_node` short-circuits on
14790 // `contains_unquote(node)` — true iff the subtree carries an
14791 // unquote-family wrapper. Pre-lift the family check inlined
14792 // `Sexp::Unquote(_) | Sexp::UnquoteSplice(_) => true`; post-lift it
14793 // routes through `as_unquote().is_some()`. Pin path-uniformity at
14794 // the family-recognition boundary: every production-site unquote
14795 // recognizer (compile_template's gate, compile_node's per-arm
14796 // dispatch, substitute's top-level + list-inner arms, AND this
14797 // fast-path predicate) now shares ONE typed-marker projection.
14798 //
14799 // Both variants must trigger the fast-path bail — the optimizer's
14800 // gate keys on the family, not the inner. The recursion into
14801 // Quote/Quasiquote/List subtrees ALSO observes the same family
14802 // gate at every level, so a `,@xs` buried under a `\`...` outer
14803 // wrapper still fires it.
14804 let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14805 let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
14806 let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
14807 Sexp::symbol("foo"),
14808 Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
14809 ])));
14810 // The projection's `is_some()` face MUST agree with the pre-lift
14811 // `matches!()` discriminant on every variant — closed-set guarantee
14812 // shared with `as_unquote`'s contract pin in `ast.rs`.
14813 assert!(super::contains_unquote(&bare_unquote));
14814 assert!(super::contains_unquote(&bare_splice));
14815 assert!(super::contains_unquote(&nested));
14816 // Negative control: shapes the projection rejects (Nil, Atom,
14817 // bare List, Quote-family without inner unquote) must NOT trigger
14818 // the fast-path bail — the projection's None face stays observably
14819 // distinct from its Some face.
14820 assert!(!super::contains_unquote(&Sexp::Nil));
14821 assert!(!super::contains_unquote(&Sexp::symbol("plain")));
14822 assert!(!super::contains_unquote(&Sexp::int(5)));
14823 assert!(!super::contains_unquote(&Sexp::List(vec![
14824 Sexp::symbol("plain"),
14825 Sexp::int(1),
14826 ])));
14827 assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
14828 Sexp::symbol("inert")
14829 ))));
14830 }
14831
14832 #[test]
14833 fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
14834 // Pin the lift: `contains_unquote`'s quote-family recognition
14835 // now routes through `Sexp::as_quote_form` (the 4-of-4 wrapper
14836 // projection) AND `QuoteForm::as_unquote_form` (the 2-of-4
14837 // substitution-subset gate) at ONE site — not through the pair
14838 // of `as_unquote().is_some()` + inline
14839 // `Sexp::Quote(inner) | Sexp::Quasiquote(inner)` arm. The
14840 // path-uniformity assertion: for every quote-family wrapper, the
14841 // function's behavior agrees with the manual composition through
14842 // the two typed-marker projections. A regression that re-inlines
14843 // either arm (e.g. drops the `as_unquote_form().is_some()` gate
14844 // and just returns true for any `as_quote_form().is_some()`, or
14845 // restores the per-variant `Quote | Quasiquote` arm) drifts from
14846 // this composition and surfaces here.
14847 use crate::ast::QuoteForm;
14848
14849 // Sweep every QuoteForm variant under both axes:
14850 // * unquote-only-inner (inner = symbol):
14851 // Unquote(s) → true via `as_unquote_form() == Some`
14852 // UnquoteSplice(s) → true via `as_unquote_form() == Some`
14853 // Quote(s) → false (inner has no unquote, recurses to false)
14854 // Quasiquote(s) → false (same)
14855 // * unquote-inner-inside-quote-wrapper:
14856 // Quote(Unquote(s)) → true (Quote arm recurses via `as_quote_form`,
14857 // inner `Unquote` returns true through the
14858 // SAME projection — both arms share the
14859 // ONE typed-marker site post-lift)
14860 let inner_plain = Sexp::symbol("x");
14861 let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
14862
14863 for qf in QuoteForm::ALL {
14864 let wrapped_plain = match qf {
14865 QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
14866 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
14867 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
14868 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
14869 };
14870 // Path-uniformity: behavior derives from the manual two-marker
14871 // composition through `as_quote_form` + `as_unquote_form`.
14872 // The substitution-subset gate (`as_unquote_form().is_some()`)
14873 // returns true for Unquote/UnquoteSplice and false for
14874 // Quote/Quasiquote — directly encoding the 2-of-4 partition.
14875 let via_manual =
14876 qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
14877 assert_eq!(
14878 super::contains_unquote(&wrapped_plain),
14879 via_manual,
14880 "contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
14881 );
14882
14883 let wrapped_unquote = match qf {
14884 QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
14885 QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
14886 QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
14887 QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
14888 };
14889 // EVERY quote-family wrapper around an inner `Unquote` must
14890 // contain unquote — either the outer projects as `Some` on
14891 // the substitution subset (Unquote/UnquoteSplice arms), OR
14892 // the outer is Quote/Quasiquote and the recursion into the
14893 // inner re-fires the projection. The pre-lift Quote arm
14894 // recursed via the inline `Sexp::Quote(inner)` match;
14895 // post-lift the SAME recursion fires via `as_quote_form`'s
14896 // typed projection. Both yield `true`.
14897 assert!(
14898 super::contains_unquote(&wrapped_unquote),
14899 "contains_unquote missed an inner Unquote under {qf:?} — \
14900 the quote-family recursion through as_quote_form drifted"
14901 );
14902 }
14903
14904 // Negative control: a non-quote-family wrapper (List, Atom,
14905 // Nil) must NOT route through `as_quote_form` at all. The
14906 // `if let Some(...) = node.as_quote_form()` gate stays `None`
14907 // and falls through to the List/_ arms. This is the structural
14908 // partition: the projection's `None` face MUST agree with
14909 // `!matches!(node, Sexp::Quote(_) | Sexp::Quasiquote(_) |
14910 // Sexp::Unquote(_) | Sexp::UnquoteSplice(_))`.
14911 let nested_in_list = Sexp::List(vec![
14912 Sexp::symbol("outer"),
14913 Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
14914 ]);
14915 assert!(
14916 super::contains_unquote(&nested_in_list),
14917 "contains_unquote failed to descend into a List subtree containing a \
14918 Quasiquote(Unquote(_)) — list recursion arm drifted"
14919 );
14920 }
14921
14922 // ── expand_and_collect_named_calls_to_any (from-forms × named ────
14923 // × typed-decoded classifier) ─────────────────────────────────────
14924 //
14925 // The (named NAME-then-kwargs × typed-decoded classifier) cell of
14926 // the dispatcher matrix on the `Expander` surface — pre-lift the
14927 // matrix had the bare-kwargs row closed (expand_and_collect_calls_to
14928 // + expand_and_collect_calls_to_any) AND the constant-keyword
14929 // column closed on the named axis (expand_to_named +
14930 // expand_source_to_named) but the (named × classifier) cell was
14931 // open — every consumer that wanted to walk a program by typed-
14932 // decoded classifier AND extract the NAME slot per matched form
14933 // had to compose `expand_and_collect_calls_to_any` with
14934 // `split_name_slot` inline. Post-lift the cell is ONE named primitive
14935 // on the `Expander` surface, and the constant-`T::KEYWORD` named
14936 // sibling routes through it as the (constant-classifier × named)
14937 // specialization.
14938 //
14939 // The tests below pin: (a) the happy path yields the decoded triple
14940 // `(T, &str, &[Sexp])` for every matched form in source order; (b)
14941 // non-matching forms are skipped (soft-projection contract inherited
14942 // from the typed-decoded primitive); (c) the `NamedFormMissingName`
14943 // variant fires for matched forms with no NAME slot, threading the
14944 // classifier-supplied `&'static str` keyword; (d) the
14945 // `NamedFormNonSymbolName` variant fires for matched forms with a
14946 // non-symbol-or-string NAME slot, again threading the classifier-
14947 // supplied keyword; (e) the projection's `Err` short-circuits at
14948 // the first failing matched form; (f) `expand_program` runs BEFORE
14949 // the classifier filter walks — a `(defmacro …)` emitting a named
14950 // form is decoded by the classifier on the EXPANDED head, not the
14951 // macro-call head; (g) the from-source sibling agrees with `read +
14952 // from-forms` on the same source.
14953
14954 #[test]
14955 fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
14956 ) {
14957 // The typed-decoded named primitive's happy path: a closed-set
14958 // classifier decodes head symbols to a typed enum PAIRED with
14959 // its canonical static keyword, and the per-form projection
14960 // receives `(decoded, name, spec_args)` for every matched form
14961 // in source order. Sweep across TWO distinct classifier outcomes
14962 // — `Monitor` / `Notify` — interleaved with a non-matching form
14963 // to pin the typed-witness threading AND the NAME-slot
14964 // projection AND the source-order yield AND the rejection of
14965 // non-classifier forms in ONE assertion.
14966 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14967 enum Kind {
14968 Monitor,
14969 Notify,
14970 }
14971 let src = r#"(defmonitor cpu :threshold 80)
14972 (other-form 99)
14973 (defnotify email :to "ops@example.com")
14974 (defmonitor mem :threshold 90)"#;
14975 let forms = read(src).unwrap();
14976 let mut e = Expander::new();
14977 let rows: Vec<(Kind, String, usize)> = e
14978 .expand_and_collect_named_calls_to_any(
14979 forms,
14980 |h| match h {
14981 "defmonitor" => Some((Kind::Monitor, "defmonitor")),
14982 "defnotify" => Some((Kind::Notify, "defnotify")),
14983 _ => None,
14984 },
14985 |kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
14986 )
14987 .unwrap();
14988 assert_eq!(
14989 rows,
14990 vec![
14991 (Kind::Monitor, "cpu".to_string(), 2),
14992 (Kind::Notify, "email".to_string(), 2),
14993 (Kind::Monitor, "mem".to_string(), 2),
14994 ],
14995 );
14996 }
14997
14998 #[test]
14999 fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
15000 // Soft-projection contract — every shape the typed-decoded
15001 // primitive rejects (non-list, empty list, list whose head is
15002 // not a symbol, list whose head decodes to `None`) skips the
15003 // projection silently. The named gate inside the wrapper
15004 // projection runs ONLY for classifier-matched forms; a non-
15005 // matching form that reaches the projection panics here.
15006 let src = r#":bare-keyword
15007 "bare-string"
15008 42
15009 ()
15010 (foo bar)
15011 (defmonitor cpu :threshold 80)"#;
15012 let forms = read(src).unwrap();
15013 let mut e = Expander::new();
15014 let names: Vec<String> = e
15015 .expand_and_collect_named_calls_to_any(
15016 forms,
15017 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15018 |(), name, _args| Ok(name.to_string()),
15019 )
15020 .unwrap();
15021 assert_eq!(names, vec!["cpu".to_string()]);
15022 }
15023
15024 #[test]
15025 fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
15026 ) {
15027 // `(defmonitor)` matches the classifier but has no NAME slot —
15028 // `split_name_slot` fires `NamedFormMissingName { keyword:
15029 // "defmonitor" }`. The classifier's `&'static str` keyword is
15030 // threaded VERBATIM through the named gate; a regression that
15031 // hardcodes a different keyword (e.g. the matched head text or
15032 // a default) would fail the structural variant identity
15033 // assertion. Fail-before-pass-after: pre-lift the primitive did
15034 // not exist; the inline `expand_and_collect_calls_to_any +
15035 // split_name_slot` composition at the consumer site was the
15036 // only path, with the keyword bound at the consumer's call
15037 // boundary rather than at the named primitive's body.
15038 let forms = read("(defmonitor)").unwrap();
15039 let mut e = Expander::new();
15040 let err = e
15041 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15042 forms,
15043 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15044 |(), _name, _args| Ok(()),
15045 )
15046 .unwrap_err();
15047 assert!(
15048 matches!(
15049 err,
15050 crate::error::LispError::NamedFormMissingName {
15051 keyword: "defmonitor"
15052 }
15053 ),
15054 "expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
15055 );
15056 }
15057
15058 #[test]
15059 fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
15060 ) {
15061 // `(defmonitor 42 :threshold 80)` matches the classifier but
15062 // the NAME slot is an int — `split_name_slot` fires
15063 // `NamedFormNonSymbolName { keyword: "defmonitor", got:
15064 // SexpShape::Int }`. The classifier's `&'static str` keyword is
15065 // threaded through the structural rejection variant verbatim,
15066 // and the typed `SexpShape` projection on the offending slot
15067 // is preserved across the named primitive.
15068 let forms = read("(defmonitor 42 :threshold 80)").unwrap();
15069 let mut e = Expander::new();
15070 let err = e
15071 .expand_and_collect_named_calls_to_any::<(), _, _, ()>(
15072 forms,
15073 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15074 |(), _name, _args| Ok(()),
15075 )
15076 .unwrap_err();
15077 assert!(
15078 matches!(
15079 err,
15080 crate::error::LispError::NamedFormNonSymbolName {
15081 keyword: "defmonitor",
15082 got: crate::error::SexpShape::Int,
15083 }
15084 ),
15085 "expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
15086 );
15087 }
15088
15089 #[test]
15090 fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
15091 // Result projection's short-circuit contract — when the
15092 // projection returns `Err` on a matched named form, the walk
15093 // stops and subsequent matched forms are NOT projected. Pin
15094 // source-order short-circuit via a counter the projection
15095 // increments before deciding to fail: the counter sits at
15096 // exactly the index of the failing form (second match),
15097 // proving the third match never reached the projection.
15098 let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
15099 let mut count = 0usize;
15100 let mut e = Expander::new();
15101 let err = e
15102 .expand_and_collect_named_calls_to_any::<String, _, _, ()>(
15103 forms,
15104 |h| (h == "defmon").then_some(((), "defmon")),
15105 |(), name, _args| {
15106 count += 1;
15107 if name == "b" {
15108 return Err(crate::error::LispError::Missing("test-failure"));
15109 }
15110 Ok(name.to_string())
15111 },
15112 )
15113 .expect_err("projection must short-circuit on first Err");
15114 assert_eq!(
15115 count, 2,
15116 "projection must have run on first matched form AND the failing form, then stopped"
15117 );
15118 assert!(
15119 matches!(err, crate::error::LispError::Missing("test-failure")),
15120 "expected the projection's typed Err verbatim, got: {err:?}"
15121 );
15122 }
15123
15124 #[test]
15125 fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
15126 // Ordering contract — `expand_program` runs BEFORE the
15127 // typed-decoded named walk. A `(defmacro …)` form that emits a
15128 // classifier-decoded named form must have its EXPANDED head
15129 // decoded by the classifier, not the macro-call head. Pin the
15130 // ordering with a macro `(emit-mon n thr)` expanding to
15131 // `(defmonitor ,n :threshold ,thr)` — the classifier sees the
15132 // expanded `defmonitor` head and the NAME slot's symbol value
15133 // from the macro arg.
15134 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15135 (defmonitor cpu :threshold 80)
15136 (emit-mon mem 90)
15137 (other-form 99)
15138 (emit-mon disk 70)"#;
15139 let forms = read(src).unwrap();
15140 let mut e = Expander::new();
15141 let names: Vec<String> = e
15142 .expand_and_collect_named_calls_to_any(
15143 forms,
15144 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15145 |(), name, _args| Ok(name.to_string()),
15146 )
15147 .unwrap();
15148 // `cpu` from the literal call, `mem` from the first macro
15149 // emit, `disk` from the second macro emit. The macro-emitted
15150 // forms are present BECAUSE `expand_program` ran first.
15151 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15152 }
15153
15154 #[test]
15155 fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
15156 // From-source posture parity: feeding source through
15157 // `expand_source_and_collect_named_calls_to_any` is byte-
15158 // identical to feeding `read(src)?` through the from-forms
15159 // sibling on a fresh expander, because the from-source posture
15160 // is `read(src)? + from-forms` by construction. A regression
15161 // that drifts the from-source posture's pipeline from the
15162 // from-forms posture's pipeline would fail here.
15163 let src = r#"(defmonitor cpu :threshold 80)
15164 (defmonitor mem :threshold 90)"#;
15165 let via_src: Vec<String> = Expander::new()
15166 .expand_source_and_collect_named_calls_to_any(
15167 src,
15168 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15169 |(), name, _args| Ok(name.to_string()),
15170 )
15171 .unwrap();
15172 let forms = read(src).unwrap();
15173 let via_forms: Vec<String> = Expander::new()
15174 .expand_and_collect_named_calls_to_any(
15175 forms,
15176 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15177 |(), name, _args| Ok(name.to_string()),
15178 )
15179 .unwrap();
15180 assert_eq!(via_src, via_forms);
15181 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15182 }
15183
15184 #[test]
15185 fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
15186 ) {
15187 // `?`-routing through `read` short-circuits BEFORE the
15188 // classifier or the named gate runs. Unbalanced paren — reader
15189 // error fires; classifier panic-decoder MUST NOT execute,
15190 // proving the read step gates the entire pipeline.
15191 let mut e = Expander::new();
15192 let err = e
15193 .expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
15194 "(defmonitor cpu :threshold 80",
15195 |_h| panic!("classifier must not run when reader fails"),
15196 |(), _name, _args| Ok(()),
15197 )
15198 .unwrap_err();
15199 // The named gate's variants MUST NOT fire — the reader error
15200 // is structurally distinct.
15201 assert!(
15202 !matches!(
15203 err,
15204 crate::error::LispError::NamedFormMissingName { .. }
15205 | crate::error::LispError::NamedFormNonSymbolName { .. }
15206 ),
15207 "expected reader error, not named-gate variant; got: {err:?}"
15208 );
15209 }
15210
15211 // ── expand_and_collect_named_calls_to (from-forms × named × ──────
15212 // constant-keyword × untyped R) ─────────────────────────────────
15213 //
15214 // The (named NAME-then-kwargs × constant-keyword × untyped `R`)
15215 // cell of the `Expander` typed-walk family — pre-lift the cell was
15216 // reachable ONLY through `expand_to_named<T>` with the
15217 // `T: TataraDomain` parameter baking BOTH the `T::KEYWORD` filter
15218 // AND the `T::compile_from_args`-based projection through
15219 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15220 // named_form_projection::<T>)`. Post-lift the cell surfaces as ONE
15221 // method that takes the keyword and the `(name, args) -> R`
15222 // projection as caller-supplied parameters, AND
15223 // `expand_to_named<T>` routes through it as the typed
15224 // `T::KEYWORD`-constant specialization — so the named-form
15225 // `split_name_slot` composition lives at ONE site
15226 // (`expand_and_collect_named_calls_to_any` body) post-lift rather
15227 // than at TWO sites pre-lift (the bare-kwargs path through
15228 // `named_form_projection<T>` AND the classifier path).
15229 //
15230 // The tests below pin: (a) the happy path yields the `(name, args)`
15231 // pair for every matched form in source order; (b) non-matching
15232 // forms are skipped (soft-projection contract inherited from the
15233 // named typed-decoded primitive); (c) the `NamedFormMissingName`
15234 // variant fires for matched forms with no NAME slot, threading the
15235 // primitive's `&'static str` keyword; (d) the
15236 // `NamedFormNonSymbolName` variant fires for matched forms with a
15237 // non-symbol-or-string NAME slot, again threading the primitive's
15238 // keyword; (e) the projection's `Err` short-circuits at the first
15239 // failing matched form; (f) `expand_program` runs BEFORE the
15240 // keyword filter walks — a `(defmacro …)` emitting a named form
15241 // has its EXPANDED head matched by the constant-keyword filter,
15242 // not the macro-call head; (g) the from-source sibling agrees with
15243 // `read + from-forms` on the same source; (h) the constant-
15244 // classifier composition law binds the runtime-keyword cell
15245 // (`expand_and_collect_named_calls_to`) to the typed-decoded
15246 // classifier cell (`expand_and_collect_named_calls_to_any`) via a
15247 // constant-classifier decoder.
15248
15249 #[test]
15250 fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
15251 ) {
15252 // The constant-keyword named primitive's happy path: a runtime
15253 // `&'static str` keyword filters matched forms, and the per-
15254 // form projection receives `(name, spec_args)` for every
15255 // matched form in source order. Three matched forms
15256 // interleaved with a non-matcher pin the source-order yield,
15257 // the NAME-slot projection, AND the rejection of non-keyword
15258 // forms in ONE assertion.
15259 let src = r#"(defmonitor cpu :threshold 80)
15260 (other-form 99)
15261 (defmonitor mem :threshold 90 :unit "MiB")
15262 (defmonitor disk :threshold 70)"#;
15263 let forms = read(src).unwrap();
15264 let mut e = Expander::new();
15265 let rows: Vec<(String, usize)> = e
15266 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
15267 Ok((name.to_string(), spec_args.len()))
15268 })
15269 .unwrap();
15270 assert_eq!(
15271 rows,
15272 vec![
15273 ("cpu".to_string(), 2),
15274 ("mem".to_string(), 4),
15275 ("disk".to_string(), 2),
15276 ],
15277 );
15278 }
15279
15280 #[test]
15281 fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
15282 // Soft-projection contract — every shape the named typed-
15283 // decoded primitive rejects (non-list, empty list, list whose
15284 // head is not a symbol, list whose head doesn't match the
15285 // constant keyword) skips the projection silently. The named
15286 // gate inside the wrapper projection runs ONLY for matched
15287 // forms; a non-matching form that reaches the projection
15288 // panics here.
15289 let src = r#":bare-keyword
15290 "bare-string"
15291 42
15292 ()
15293 (foo bar)
15294 (defmonitor cpu :threshold 80)"#;
15295 let forms = read(src).unwrap();
15296 let mut e = Expander::new();
15297 let names: Vec<String> = e
15298 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15299 Ok(name.to_string())
15300 })
15301 .unwrap();
15302 assert_eq!(names, vec!["cpu".to_string()]);
15303 }
15304
15305 #[test]
15306 fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
15307 // The named-form gate's missing-NAME variant must thread the
15308 // primitive's `&'static str` keyword (NOT a hardcoded literal,
15309 // NOT the projection's own copy) — the `NamedFormMissingName
15310 // { keyword }` slot inside `split_name_slot` is bound at the
15311 // call site BY THE PRIMITIVE itself, via the constant-
15312 // classifier decoder threading `((), keyword)` into
15313 // `expand_and_collect_named_calls_to_any`.
15314 let src = r#"(defmonitor)"#;
15315 let forms = read(src).unwrap();
15316 let mut e = Expander::new();
15317 let err = e
15318 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15319 .unwrap_err();
15320 assert!(
15321 matches!(
15322 err,
15323 crate::error::LispError::NamedFormMissingName {
15324 keyword: "defmonitor"
15325 }
15326 ),
15327 "expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
15328 );
15329 }
15330
15331 #[test]
15332 fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
15333 ) {
15334 // The named-form gate's non-symbol-NAME variant must thread
15335 // the primitive's `&'static str` keyword AND the typed-shape
15336 // witness (`SexpShape::Int` for an integer NAME slot). The
15337 // shape projection through `split_name_slot`'s
15338 // `as_symbol_or_string()` gate fires on the first matched
15339 // form whose NAME slot is not a symbol or string.
15340 let src = r#"(defmonitor 42 :threshold 80)"#;
15341 let forms = read(src).unwrap();
15342 let mut e = Expander::new();
15343 let err = e
15344 .expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
15345 .unwrap_err();
15346 match err {
15347 crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
15348 assert_eq!(keyword, "defmonitor");
15349 assert_eq!(got, crate::error::SexpShape::Int);
15350 }
15351 other => panic!(
15352 "expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
15353 ),
15354 }
15355 }
15356
15357 #[test]
15358 fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
15359 // `Iterator::collect::<Result<Vec<R>, _>>()` short-circuits at
15360 // the first failing projection. The second matched form's
15361 // projection error MUST fire AND the third matched form's
15362 // projection MUST NOT run. Pin both halves with a counter +
15363 // a fail-on-call sentinel.
15364 let src = r#"(defmonitor cpu :threshold 80)
15365 (defmonitor mem :threshold 90)
15366 (defmonitor disk :threshold 70)"#;
15367 let forms = read(src).unwrap();
15368 let mut count: usize = 0;
15369 let mut e = Expander::new();
15370 let err = e
15371 .expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
15372 count += 1;
15373 if name == "mem" {
15374 Err(crate::error::LispError::Compile {
15375 form: "test".into(),
15376 message: format!("rejecting at NAME={name}"),
15377 })
15378 } else {
15379 Ok(name.to_string())
15380 }
15381 })
15382 .unwrap_err();
15383 assert_eq!(count, 2, "third matched form must not be projected");
15384 match err {
15385 crate::error::LispError::Compile { message, .. } => {
15386 assert!(message.contains("NAME=mem"));
15387 }
15388 other => panic!("expected projection-driven Compile error; got: {other:?}"),
15389 }
15390 }
15391
15392 #[test]
15393 fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
15394 // Ordering contract — `expand_program` runs BEFORE the
15395 // constant-keyword named walk. A `(defmacro …)` form that
15396 // emits a matched named form must have its EXPANDED head
15397 // matched by the constant-keyword filter, not the macro-call
15398 // head. Pin the ordering with a macro `(emit-mon n thr)`
15399 // expanding to `(defmonitor ,n :threshold ,thr)` — the
15400 // constant-keyword filter matches the expanded `defmonitor`
15401 // head and the NAME slot projects the symbol-author value.
15402 let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
15403 (defmonitor cpu :threshold 80)
15404 (emit-mon mem 90)
15405 (other-form 99)
15406 (emit-mon disk 70)"#;
15407 let forms = read(src).unwrap();
15408 let mut e = Expander::new();
15409 let names: Vec<String> = e
15410 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15411 Ok(name.to_string())
15412 })
15413 .unwrap();
15414 // `cpu` from the literal call, `mem` from the first macro
15415 // emit, `disk` from the second macro emit. The macro-emitted
15416 // forms are present BECAUSE `expand_program` ran first.
15417 assert_eq!(names, vec!["cpu", "mem", "disk"]);
15418 }
15419
15420 #[test]
15421 fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
15422 // From-source posture parity: feeding source through
15423 // `expand_source_and_collect_named_calls_to` is byte-identical
15424 // to feeding `read(src)?` through the from-forms sibling on a
15425 // fresh expander, because the from-source posture is
15426 // `read(src)? + from-forms` by construction. A regression that
15427 // drifts the from-source posture's pipeline from the from-
15428 // forms posture's pipeline would fail here.
15429 let src = r#"(defmonitor cpu :threshold 80)
15430 (defmonitor mem :threshold 90)"#;
15431 let via_src: Vec<String> = Expander::new()
15432 .expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
15433 Ok(name.to_string())
15434 })
15435 .unwrap();
15436 let forms = read(src).unwrap();
15437 let via_forms: Vec<String> = Expander::new()
15438 .expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
15439 Ok(name.to_string())
15440 })
15441 .unwrap();
15442 assert_eq!(via_src, via_forms);
15443 assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
15444 }
15445
15446 #[test]
15447 fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
15448 ) {
15449 // `?`-routing through `read` short-circuits BEFORE the
15450 // constant-keyword filter, the named gate, or the projection
15451 // runs. Unbalanced paren — reader error fires; the projection
15452 // panic-payload MUST NOT execute, proving the read step gates
15453 // the entire pipeline.
15454 let mut e = Expander::new();
15455 let err = e
15456 .expand_source_and_collect_named_calls_to::<(), _>(
15457 "(defmonitor cpu :threshold 80",
15458 "defmonitor",
15459 |_name, _args| panic!("projection must not run when reader fails"),
15460 )
15461 .unwrap_err();
15462 // The named gate's variants MUST NOT fire — the reader error
15463 // is structurally distinct.
15464 assert!(
15465 !matches!(
15466 err,
15467 crate::error::LispError::NamedFormMissingName { .. }
15468 | crate::error::LispError::NamedFormNonSymbolName { .. }
15469 ),
15470 "expected reader error, not named-gate variant; got: {err:?}",
15471 );
15472 }
15473
15474 #[test]
15475 fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
15476 ) {
15477 // Composition-identity test pinning the runtime-keyword named
15478 // cell (`expand_and_collect_named_calls_to`) to the typed-
15479 // decoded named-classifier cell
15480 // (`expand_and_collect_named_calls_to_any`) via the constant-
15481 // classifier decoder shape. Post-lift the identity:
15482 //
15483 // expand_and_collect_named_calls_to(forms, kw, project) ==
15484 // expand_and_collect_named_calls_to_any(forms,
15485 // |h| (h == kw).then_some(((), kw)),
15486 // |(), name, args| project(name, args))
15487 //
15488 // Pinning the identity here makes the typed-decoded named-
15489 // classifier primitive the CANONICAL composition point the
15490 // runtime-keyword sibling routes through — parallel to how
15491 // `expand_and_collect_calls_to` routes through
15492 // `expand_and_collect_calls_to_any` via a `|h| (h == k).
15493 // then_some(())` decoder on the bare-kwargs axis. A future
15494 // regression that drifts ONE cell's NAME-slot rejection chain
15495 // from the other becomes loudly visible at this assertion.
15496 let src = r#"(defmonitor cpu :threshold 80)
15497 (other-form 99)
15498 (defmonitor mem :threshold 90)"#;
15499 let forms = read(src).unwrap();
15500 let via_constant_keyword: Vec<(String, usize)> = Expander::new()
15501 .expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
15502 Ok((name.to_string(), args.len()))
15503 })
15504 .unwrap();
15505 let via_classifier: Vec<(String, usize)> = Expander::new()
15506 .expand_and_collect_named_calls_to_any(
15507 forms,
15508 |h| (h == "defmonitor").then_some(((), "defmonitor")),
15509 |(), name, args| Ok((name.to_string(), args.len())),
15510 )
15511 .unwrap();
15512 assert_eq!(via_constant_keyword, via_classifier);
15513 assert_eq!(
15514 via_constant_keyword,
15515 vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
15516 );
15517 }
15518
15519 #[test]
15520 fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
15521 ) {
15522 // End-to-end path-uniformity: `Expander::expand_to_named<T>`
15523 // (the typed constant-keyword named cell) must yield the same
15524 // payload as `expand_and_collect_named_calls_to(forms,
15525 // T::KEYWORD, |name, args| { T::compile_from_args(args)? +
15526 // NamedDefinition })` — the lift's structural identity.
15527 //
15528 // Pre-lift `expand_to_named<T>` routed through
15529 // `expand_and_collect_calls_to(forms, T::KEYWORD,
15530 // named_form_projection::<T>)` (the bare-kwargs constant
15531 // primitive with `named_form_projection<T>` doing the NAME
15532 // extraction internally). Post-lift `expand_to_named<T>`
15533 // routes through the named constant-keyword primitive (which
15534 // itself routes through the classifier primitive via a
15535 // constant-classifier decoder) — so the `split_name_slot`
15536 // composition lives at ONE site (the `_any` primitive body)
15537 // post-lift rather than at TWO sites.
15538 use crate::compile::NamedDefinition;
15539 use crate::compiler_spec::CompilerSpec;
15540 let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
15541 (defcompiler beta-compiler :name "y" :dialect "standard")"#;
15542 let forms = read(src).unwrap();
15543 let via_expand_to_named = Expander::new()
15544 .expand_to_named::<CompilerSpec>(forms.clone())
15545 .unwrap();
15546 let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
15547 .expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
15548 let spec =
15549 <CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
15550 Ok(NamedDefinition {
15551 name: name.to_string(),
15552 spec,
15553 })
15554 })
15555 .unwrap();
15556 assert_eq!(via_expand_to_named.len(), 2);
15557 assert_eq!(via_expand_to_named.len(), via_named_constant.len());
15558 for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
15559 assert_eq!(a.name, b.name, "NAME slot must agree across cells");
15560 assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
15561 }
15562 assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
15563 assert_eq!(via_expand_to_named[0].spec.name, "x");
15564 assert_eq!(via_expand_to_named[1].name, "beta-compiler");
15565 assert_eq!(via_expand_to_named[1].spec.name, "y");
15566 }
15567
15568 // ── ExpansionDepthExceeded: runaway macro rejected instead of aborting ──
15569
15570 #[test]
15571 fn expander_default_max_expansion_depth_is_the_module_constant() {
15572 // Both constructors seed `max_expansion_depth` from the module
15573 // constant — a regression that drifts one constructor's default
15574 // from the constant (e.g. a hardcoded `256` at ONE site with the
15575 // other left uninitialized after a `Default` derive addition)
15576 // fails this pin.
15577 assert_eq!(
15578 Expander::new().max_expansion_depth(),
15579 DEFAULT_MAX_EXPANSION_DEPTH
15580 );
15581 assert_eq!(
15582 Expander::new_substitute_only().max_expansion_depth(),
15583 DEFAULT_MAX_EXPANSION_DEPTH
15584 );
15585 assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
15586 }
15587
15588 #[test]
15589 fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
15590 let mut e = Expander::new();
15591 e.set_max_expansion_depth(7);
15592 assert_eq!(e.max_expansion_depth(), 7);
15593 }
15594
15595 #[test]
15596 fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
15597 // `(defmacro loop (x) `(loop ,x))` applied to `(loop hello)`
15598 // pre-lift stack-overflowed the process — a below-floor abort.
15599 // Post-lift the expander returns a typed
15600 // `LispError::ExpansionDepthExceeded` at the ceiling with
15601 // `macro_name` populated from the offending call's head.
15602 let mut e = Expander::new();
15603 e.set_max_expansion_depth(4);
15604 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15605 let err = e.expand_program(forms).unwrap_err();
15606 match err {
15607 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15608 assert_eq!(macro_name, "loop");
15609 assert_eq!(limit, 4);
15610 }
15611 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15612 }
15613 }
15614
15615 #[test]
15616 fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
15617 // The substitute strategy shares the same `expand` outer walker
15618 // with the bytecode strategy, so the ceiling fires at the SAME
15619 // gate regardless of which apply-layer is active. The pin makes
15620 // that shared-outer-walker property structural: a regression
15621 // that ever branches the depth guard on strategy (e.g. moves it
15622 // into `apply_compiled` alone) fails this test on the
15623 // substitute expander.
15624 let mut e = Expander::new_substitute_only();
15625 e.set_max_expansion_depth(4);
15626 let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
15627 let err = e.expand_program(forms).unwrap_err();
15628 match err {
15629 LispError::ExpansionDepthExceeded { macro_name, limit } => {
15630 assert_eq!(macro_name, "loop");
15631 assert_eq!(limit, 4);
15632 }
15633 other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
15634 }
15635 }
15636
15637 #[test]
15638 fn expand_lawful_nested_macros_within_ceiling_succeed() {
15639 // Lawful nested macros (`(twice (twice hey))`) accrete depth in
15640 // the low single digits because the tree-child arm doesn't
15641 // count — only the post-apply re-expansion path bumps the
15642 // counter. A ceiling of 3 is enough for a two-deep nested
15643 // expansion. This pin is the fail-before/pass-after negative
15644 // control for the recursive-macro rejection tests above: the
15645 // depth counter must NOT reject lawful macro-nesting within
15646 // the ceiling, and only reject the runaway shape.
15647 let mut e = Expander::new();
15648 e.set_max_expansion_depth(3);
15649 let forms = read(
15650 "(defmacro twice (x) `(list ,x ,x))
15651 (twice (twice hey))",
15652 )
15653 .unwrap();
15654 let out = e.expand_program(forms).unwrap();
15655 assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
15656 }
15657
15658 #[test]
15659 fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
15660 // A macro-free lawful deep tree — five levels of plain nesting —
15661 // is expanded at a ceiling well below the tree's height. The
15662 // tree-child arm does not accrete depth, so tree height is
15663 // orthogonal to the ceiling; only macro re-expansion counts.
15664 // A regression that ever bumps depth on tree-child descent
15665 // fails this pin.
15666 let mut e = Expander::new();
15667 e.set_max_expansion_depth(2);
15668 let forms = read("(a (b (c (d (e f)))))").unwrap();
15669 let out = e.expand_program(forms).unwrap();
15670 assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
15671 }
15672
15673 #[test]
15674 fn expansion_depth_exceeded_position_is_none() {
15675 // The variant joins the non-positional cohort — the offending
15676 // macro's byte offset is not what a runaway diagnostic anchors
15677 // to; the OFFENDING MACRO NAME (`macro_name`) is. This pin
15678 // keeps the variant in the `position() -> None` cohort so a
15679 // future span-carrying edit lands the field in ONE place
15680 // across every non-positional variant simultaneously.
15681 let err = LispError::ExpansionDepthExceeded {
15682 macro_name: "loop".to_string(),
15683 limit: 256,
15684 };
15685 assert_eq!(err.position(), None);
15686 }
15687
15688 #[test]
15689 fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
15690 // Display projection carries both `macro_name` and `limit`, so
15691 // authoring surfaces that substring-grep the rendered
15692 // diagnostic (`tatara-check`, REPL, LSP) see BOTH halves at
15693 // the variant boundary and never need to substring-parse
15694 // free-form text to recover the identity of the offending
15695 // macro or the ceiling it breached.
15696 let err = LispError::ExpansionDepthExceeded {
15697 macro_name: "loop".to_string(),
15698 limit: 256,
15699 };
15700 let rendered = err.to_string();
15701 assert!(rendered.contains("loop"), "rendered: {rendered}");
15702 assert!(rendered.contains("256"), "rendered: {rendered}");
15703 assert!(
15704 rendered.contains("macro expansion depth exceeded"),
15705 "rendered: {rendered}"
15706 );
15707 }
15708
15709 // ── max_cache_entries: bounded-memoization ceiling ──────────────────
15710
15711 #[test]
15712 fn expander_default_max_cache_entries_is_the_module_constant() {
15713 // Both cache-enabling constructors seed `max_cache_entries` from
15714 // the module constant — a regression that drifts one
15715 // constructor's default from the constant (e.g. a hardcoded
15716 // `8192` at ONE site with the other left uninitialized after a
15717 // `Default` derive addition) fails this pin. The peer
15718 // constructor `new_substitute_only` seeds the same ceiling so
15719 // the field stays initialized under `#[derive(Clone)]` (a
15720 // zero-initialized clone would be a silent bifurcation of the
15721 // cache-ceiling surface).
15722 assert_eq!(
15723 Expander::new().max_cache_entries(),
15724 DEFAULT_MAX_CACHE_ENTRIES
15725 );
15726 assert_eq!(
15727 Expander::new_substitute_only().max_cache_entries(),
15728 DEFAULT_MAX_CACHE_ENTRIES
15729 );
15730 assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
15731 }
15732
15733 #[test]
15734 fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
15735 // Setter mirrors accessor — a regression that ever forgets to
15736 // write through to the field, or writes to a shadow field the
15737 // accessor does not read, fails this pin.
15738 let mut e = Expander::new();
15739 e.set_max_cache_entries(7);
15740 assert_eq!(e.max_cache_entries(), 7);
15741 }
15742
15743 #[test]
15744 fn expand_cache_size_is_bounded_by_max_cache_entries() {
15745 // Five distinct `(name, args)` pairs expanded against a cache
15746 // ceiling of `2` populate the cache with EXACTLY the first two
15747 // insertions and skip the remaining three. Every expansion
15748 // still returns the correct result — the cache is a pure
15749 // PERFORMANCE optimization; the ceiling gates memoization,
15750 // never correctness. LOAD-BEARING true-arm catch on the
15751 // bounded-cache contract: a regression that ignored the
15752 // ceiling (unbounded insert) would land `cache_size() == 5`
15753 // here; a regression that skipped insertion entirely (broken
15754 // cache) would still see correct expansion output but would
15755 // fail the interior `cache_size() == 2` pin.
15756 let mut e = Expander::new();
15757 e.set_max_cache_entries(2);
15758 let src = "
15759 (defmacro id (x) `,x)
15760 (id one)
15761 (id two)
15762 (id three)
15763 (id four)
15764 (id five)
15765 ";
15766 let out = e.expand_program(read(src).unwrap()).unwrap();
15767 assert_eq!(out.len(), 5);
15768 assert_eq!(out[0], parse("one"));
15769 assert_eq!(out[1], parse("two"));
15770 assert_eq!(out[2], parse("three"));
15771 assert_eq!(out[3], parse("four"));
15772 assert_eq!(out[4], parse("five"));
15773 assert_eq!(
15774 e.cache_size(),
15775 2,
15776 "cache grew past the max_cache_entries ceiling",
15777 );
15778 }
15779
15780 #[test]
15781 fn expand_zero_cache_ceiling_disables_caching_effectively() {
15782 // Zero ceiling admits every fresh `(name, args)` pair through
15783 // the compute path and never grows the cache — the same
15784 // observable behavior as `set_cache_enabled(false)` but
15785 // reached through the ceiling knob. Correctness is preserved:
15786 // the miss path always recomputes the same value the cache
15787 // would have returned, so an operator that dials the ceiling
15788 // to `0` (e.g. to hunt a cache-related regression) never
15789 // bifurcates the output surface. Peer to
15790 // `set_max_expansion_depth(0)` one RESOURCE axis over — both
15791 // ceilings admit the zero endpoint as a well-defined
15792 // extremum.
15793 let mut e = Expander::new();
15794 e.set_max_cache_entries(0);
15795 let src = "
15796 (defmacro id (x) `,x)
15797 (id one)
15798 (id two)
15799 ";
15800 let out = e.expand_program(read(src).unwrap()).unwrap();
15801 assert_eq!(out.len(), 2);
15802 assert_eq!(out[0], parse("one"));
15803 assert_eq!(out[1], parse("two"));
15804 assert_eq!(
15805 e.cache_size(),
15806 0,
15807 "zero cache ceiling grew the cache — the ceiling must be respected on every insert",
15808 );
15809 }
15810
15811 #[test]
15812 fn expand_cached_hits_survive_past_the_ceiling() {
15813 // Once a `(name, args)` pair sits IN the cache under the
15814 // ceiling, subsequent calls hit it — the ceiling only gates
15815 // NEW-KEY inserts, never lookups. This test seeds the cache
15816 // with two entries under a ceiling of `2`, then re-issues the
15817 // SAME calls to prove the cached hits still fire (cache_size
15818 // stays at `2` because no new keys are inserted, and the
15819 // expansion output is stable). LOAD-BEARING true-arm catch
15820 // that the ceiling gates the INSERT path alone — a regression
15821 // that ever refused cache LOOKUPS at the ceiling would
15822 // silently bifurcate correctness on the second call.
15823 let mut e = Expander::new();
15824 e.set_max_cache_entries(2);
15825 let src = "
15826 (defmacro id (x) `,x)
15827 (id one)
15828 (id two)
15829 (id one)
15830 (id two)
15831 ";
15832 let out = e.expand_program(read(src).unwrap()).unwrap();
15833 assert_eq!(out.len(), 4);
15834 assert_eq!(out[0], parse("one"));
15835 assert_eq!(out[1], parse("two"));
15836 assert_eq!(out[2], parse("one"));
15837 assert_eq!(out[3], parse("two"));
15838 assert_eq!(
15839 e.cache_size(),
15840 2,
15841 "cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
15842 );
15843 }
15844
15845 #[test]
15846 fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
15847 // After the cache reaches its ceiling and subsequent inserts
15848 // are skipped, `clear_cache` empties the memo and RE-OPENS the
15849 // insert path — the next expansion populates the freshly-
15850 // empty cache without operator intervention on the ceiling
15851 // itself. The pin binds the operator-facing recovery path
15852 // documented on [`DEFAULT_MAX_CACHE_ENTRIES`]: "skip cache
15853 // insertion until the operator clears the cache via
15854 // [`Expander::clear_cache`]".
15855 let mut e = Expander::new();
15856 e.set_max_cache_entries(1);
15857 let src_fill = "
15858 (defmacro id (x) `,x)
15859 (id one)
15860 (id two)
15861 ";
15862 let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
15863 assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
15864 e.clear_cache();
15865 assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
15866 let out = e
15867 .expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
15868 .unwrap();
15869 assert_eq!(out.len(), 1);
15870 assert_eq!(out[0], parse("three"));
15871 assert_eq!(
15872 e.cache_size(),
15873 1,
15874 "clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
15875 );
15876 }
15877
15878 // ── max_expansion_size: bounded-output ceiling ─────────────────────
15879
15880 #[test]
15881 fn expander_default_max_expansion_size_is_the_module_constant() {
15882 // Both constructors seed `max_expansion_size` from the module
15883 // constant — a regression that drifts one constructor's
15884 // default from the constant (e.g. a hardcoded `65_536` at
15885 // ONE site with the other left uninitialized after a
15886 // `Default` derive addition) fails this pin. Peer to the
15887 // `max_expansion_depth` + `max_cache_entries` default-
15888 // coherence pins one RESOURCE-DIMENSION axis over.
15889 assert_eq!(
15890 Expander::new().max_expansion_size(),
15891 DEFAULT_MAX_EXPANSION_SIZE
15892 );
15893 assert_eq!(
15894 Expander::new_substitute_only().max_expansion_size(),
15895 DEFAULT_MAX_EXPANSION_SIZE
15896 );
15897 assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
15898 }
15899
15900 #[test]
15901 fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
15902 // Setter mirrors accessor — a regression that ever forgets to
15903 // write through to the field, or writes to a shadow field the
15904 // accessor does not read, fails this pin. Peer to the
15905 // setter/accessor coherence pins on the depth + cache axes.
15906 let mut e = Expander::new();
15907 e.set_max_expansion_size(64);
15908 assert_eq!(e.max_expansion_size(), 64);
15909 }
15910
15911 #[test]
15912 fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
15913 // `(defmacro bomb (x) `(list ,x ,x ,x ,x))` applied to
15914 // `(bomb hey)` produces `(list hey hey hey hey)` — 6 nodes
15915 // (outer List + 5 children). With a ceiling of `4` this
15916 // crosses the OUTPUT-SIZE threshold at the first apply and
15917 // the expander returns a typed
15918 // `LispError::ExpansionSizeExceeded` with `macro_name`,
15919 // `size`, and `limit` all populated. Pre-lift the runaway
15920 // just accumulated in a growing tree until the process
15921 // heap ran out; post-lift the rejection sits at the same
15922 // call boundary as every other macro-expansion failure.
15923 let mut e = Expander::new();
15924 e.set_max_expansion_size(4);
15925 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15926 let err = e.expand_program(forms).unwrap_err();
15927 match err {
15928 LispError::ExpansionSizeExceeded {
15929 macro_name,
15930 size,
15931 limit,
15932 } => {
15933 assert_eq!(macro_name, "bomb");
15934 assert_eq!(size, 6);
15935 assert_eq!(limit, 4);
15936 }
15937 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15938 }
15939 }
15940
15941 #[test]
15942 fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
15943 // The substitute strategy shares the same `expand_with_depth`
15944 // outer walker with the bytecode strategy, so the ceiling
15945 // fires at the SAME gate regardless of which apply-layer is
15946 // active. The pin makes that shared-outer-walker property
15947 // structural: a regression that ever branches the size guard
15948 // on strategy (e.g. moves it into `apply_compiled` alone)
15949 // fails this test on the substitute expander. Peer to
15950 // `expand_recursive_macro_rejects_at_depth_limit_substitute_path`
15951 // one RESOURCE axis over.
15952 let mut e = Expander::new_substitute_only();
15953 e.set_max_expansion_size(4);
15954 let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
15955 let err = e.expand_program(forms).unwrap_err();
15956 match err {
15957 LispError::ExpansionSizeExceeded {
15958 macro_name,
15959 size,
15960 limit,
15961 } => {
15962 assert_eq!(macro_name, "bomb");
15963 assert_eq!(size, 6);
15964 assert_eq!(limit, 4);
15965 }
15966 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
15967 }
15968 }
15969
15970 #[test]
15971 fn expand_lawful_output_within_size_ceiling_succeeds() {
15972 // `(defmacro twice (x) `(list ,x ,x))` applied to `(twice hey)`
15973 // produces `(list hey hey)` — 4 nodes. With a ceiling of `4`
15974 // the output sits AT the ceiling (`<=` admits equality; only
15975 // `>` rejects), so the expansion succeeds. Fail-before/pass-
15976 // after negative control for the bomb rejection tests: a
15977 // regression that ever rejected at-ceiling output would fail
15978 // this pin. Peer to `expand_lawful_nested_macros_within_ceiling_succeed`
15979 // one RESOURCE axis over.
15980 let mut e = Expander::new();
15981 e.set_max_expansion_size(4);
15982 let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
15983 let out = e.expand_program(forms).unwrap();
15984 assert_eq!(out[0], parse("(list hey hey)"));
15985 }
15986
15987 #[test]
15988 fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
15989 // A macro-free lawful large tree is NOT gated by the size
15990 // ceiling — the ceiling gates macro-`apply` outputs alone,
15991 // not tree-child descents through non-macro forms. A `usize::MAX`
15992 // ceiling would trivially admit anything; this pin uses a
15993 // deliberately small ceiling of `4` on a tree with 6 nodes
15994 // to catch a regression that ever bumps the ceiling on tree
15995 // descent. A tree-only walk should NEVER fire the ceiling.
15996 // Peer to `expand_depth_ceiling_ignores_lawful_tree_nesting_depth`
15997 // one RESOURCE axis over.
15998 let mut e = Expander::new();
15999 e.set_max_expansion_size(4);
16000 // `(a (b (c d)))` — 6 nodes, no macros. Passes cleanly.
16001 let forms = read("(a (b (c d)))").unwrap();
16002 let out = e.expand_program(forms).unwrap();
16003 assert_eq!(out[0], parse("(a (b (c d)))"));
16004 }
16005
16006 #[test]
16007 fn expansion_size_exceeded_position_is_none() {
16008 // The variant joins the non-positional cohort — the offending
16009 // macro's byte offset is not what an expansion-bomb diagnostic
16010 // anchors to; the (offending macro name, observed size,
16011 // configured ceiling) triple is. This pin keeps the variant
16012 // in the `position() -> None` cohort so a future span-carrying
16013 // edit lands the field in ONE place across every non-positional
16014 // variant simultaneously. Peer to
16015 // `expansion_depth_exceeded_position_is_none` one RESOURCE
16016 // axis over.
16017 let err = LispError::ExpansionSizeExceeded {
16018 macro_name: "bomb".to_string(),
16019 size: 512,
16020 limit: 256,
16021 };
16022 assert_eq!(err.position(), None);
16023 }
16024
16025 #[test]
16026 fn expansion_size_exceeded_display_matches_typed_variant_shape() {
16027 // Display projection carries `macro_name`, `size`, AND
16028 // `limit`, so authoring surfaces that substring-grep the
16029 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16030 // THREE halves at the variant boundary and never need to
16031 // substring-parse free-form text to recover the identity of
16032 // the offending macro, the observed size, or the ceiling it
16033 // breached. Peer to `expansion_depth_exceeded_display_matches_typed_variant_shape`
16034 // one RESOURCE axis over.
16035 let err = LispError::ExpansionSizeExceeded {
16036 macro_name: "bomb".to_string(),
16037 size: 512,
16038 limit: 256,
16039 };
16040 let rendered = err.to_string();
16041 assert!(rendered.contains("bomb"), "rendered: {rendered}");
16042 assert!(rendered.contains("512"), "rendered: {rendered}");
16043 assert!(rendered.contains("256"), "rendered: {rendered}");
16044 assert!(
16045 rendered.contains("macro expansion output size exceeded"),
16046 "rendered: {rendered}"
16047 );
16048 }
16049
16050 #[test]
16051 fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
16052 // A lawful outer macro that expands to a call of a bomb inner
16053 // macro rejects at the INNER call — `macro_name` at the
16054 // variant boundary is the name of the macro whose `apply`
16055 // output crossed the ceiling, not the outermost caller.
16056 // Load-bearing name-provenance pin: an operator whose bomb
16057 // sits N levels deep in a compose chain needs to see the
16058 // INNER macro's name in the diagnostic, not the wrapping
16059 // shape. A regression that ever populates `macro_name` from
16060 // the outermost caller's head fails this pin.
16061 let mut e = Expander::new();
16062 e.set_max_expansion_size(4);
16063 // `wrapper` re-expands into `bomb`; `bomb` produces a size-6
16064 // output that crosses the ceiling. Expected: the diagnostic
16065 // names `bomb`, not `wrapper`.
16066 let src = "
16067 (defmacro bomb (x) `(list ,x ,x ,x ,x))
16068 (defmacro wrapper (x) `(bomb ,x))
16069 (wrapper hey)
16070 ";
16071 let err = e.expand_program(read(src).unwrap()).unwrap_err();
16072 match err {
16073 LispError::ExpansionSizeExceeded {
16074 macro_name,
16075 size,
16076 limit,
16077 } => {
16078 assert_eq!(macro_name, "bomb");
16079 assert_eq!(size, 6);
16080 assert_eq!(limit, 4);
16081 }
16082 other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
16083 }
16084 }
16085
16086 // ── max_macro_body_size: REGISTRATION-time body-size ceiling ──────
16087 // Peer to depth (recursion length) + cache (memoization width) +
16088 // expansion size (single-`apply` OUTPUT size) one PIPELINE-STAGE
16089 // axis over — the three prior guards fire at EXPAND time; this
16090 // one fires at REGISTER time. Closes the expander's RESOURCE
16091 // surface at a fourth typed dimension across two pipeline stages.
16092
16093 #[test]
16094 fn expander_default_max_macro_body_size_is_the_module_constant() {
16095 // Both constructors seed `max_macro_body_size` from the
16096 // module constant — a regression that drifts one
16097 // constructor's default from the constant (e.g. a hardcoded
16098 // `16_384` at ONE site with the other left uninitialized
16099 // after a `Default` derive addition) fails this pin. Peer
16100 // to the `expander_default_max_expansion_{depth,size}_is_the_module_constant`
16101 // pins one PIPELINE-STAGE axis over.
16102 assert_eq!(
16103 Expander::new().max_macro_body_size(),
16104 DEFAULT_MAX_MACRO_BODY_SIZE
16105 );
16106 assert_eq!(
16107 Expander::new_substitute_only().max_macro_body_size(),
16108 DEFAULT_MAX_MACRO_BODY_SIZE
16109 );
16110 assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
16111 }
16112
16113 #[test]
16114 fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
16115 // Setter mirrors accessor — a regression that ever forgets to
16116 // write through to the field, or writes to a shadow field the
16117 // accessor does not read, fails this pin. Peer to the
16118 // setter/accessor coherence pins on the depth + cache +
16119 // expansion-size axes.
16120 let mut e = Expander::new();
16121 e.set_max_macro_body_size(64);
16122 assert_eq!(e.max_macro_body_size(), 64);
16123 }
16124
16125 #[test]
16126 fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
16127 // `(defmacro huge (x) `(list a b c d e))` has body
16128 // `` `(list a b c d e) `` — a `Sexp::Quasiquote` wrapping a
16129 // list of 6 nodes (outer List + 5 children); the outer
16130 // quasi-quote wrapper adds one → 8 nodes total for
16131 // `def.body.node_count()`. With a ceiling of `4` this
16132 // crosses the REGISTRATION-time BODY-SIZE threshold and the
16133 // expander returns a typed `LispError::MacroBodySizeExceeded`
16134 // with `macro_name`, `size`, and `limit` all populated. Peer
16135 // to `expand_expansion_bomb_rejects_at_size_limit_bytecode_path`
16136 // one PIPELINE-STAGE axis over.
16137 let mut e = Expander::new();
16138 e.set_max_macro_body_size(4);
16139 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16140 let err = e.expand_program(forms).unwrap_err();
16141 match err {
16142 LispError::MacroBodySizeExceeded {
16143 macro_name,
16144 size,
16145 limit,
16146 } => {
16147 assert_eq!(macro_name, "huge");
16148 assert_eq!(size, 8);
16149 assert_eq!(limit, 4);
16150 }
16151 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16152 }
16153 }
16154
16155 #[test]
16156 fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
16157 // The substitute strategy shares the same
16158 // `register_macro_def` outer registration entry with the
16159 // bytecode strategy, so the REGISTRATION-time BODY-SIZE
16160 // ceiling fires at the SAME gate regardless of which
16161 // apply-layer is active. The pin makes that shared-registration
16162 // property structural: a regression that ever branches the
16163 // body-size guard on strategy (e.g. moves it into the
16164 // `compile_templates` arm alone) fails this test on the
16165 // substitute expander. Peer to
16166 // `expand_expansion_bomb_rejects_at_size_limit_substitute_path`
16167 // one PIPELINE-STAGE axis over.
16168 let mut e = Expander::new_substitute_only();
16169 e.set_max_macro_body_size(4);
16170 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16171 let err = e.expand_program(forms).unwrap_err();
16172 match err {
16173 LispError::MacroBodySizeExceeded {
16174 macro_name,
16175 size,
16176 limit,
16177 } => {
16178 assert_eq!(macro_name, "huge");
16179 assert_eq!(size, 8);
16180 assert_eq!(limit, 4);
16181 }
16182 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16183 }
16184 }
16185
16186 #[test]
16187 fn register_macro_body_at_ceiling_admits() {
16188 // `(defmacro twice (x) `(list ,x))` has body
16189 // `` `(list ,x) `` — `Sexp::Quasiquote` wrapping a 3-node
16190 // list (List + `list` atom + `,x` unquote which is
16191 // 1 + inner atom = 2 nodes) = 1 + (1 + 1 + 2) = 5 nodes.
16192 // With a ceiling of `5` the body sits AT the ceiling
16193 // (`<=` admits equality; only `>` rejects), so the
16194 // registration succeeds and the subsequent expansion
16195 // proceeds normally. Fail-before/pass-after negative
16196 // control for the body-bomb rejection tests: a regression
16197 // that ever rejected at-ceiling bodies would fail this pin.
16198 let mut e = Expander::new();
16199 e.set_max_macro_body_size(5);
16200 let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
16201 let out = e.expand_program(forms).unwrap();
16202 assert_eq!(out[0], parse("(list hey)"));
16203 }
16204
16205 #[test]
16206 fn register_failure_leaves_both_tables_pristine() {
16207 // A rejected registration MUST leave BOTH `self.macros`
16208 // AND `self.templates` exactly as they were — no
16209 // partial-write window in which one table carries the
16210 // over-ceiling entry while the other does not, or in
16211 // which the template pre-compile already ran. This pin
16212 // catches a regression that ever moves the body-size
16213 // gate below `self.templates.insert` or below
16214 // `self.macros.insert`.
16215 let mut e = Expander::new();
16216 e.set_max_macro_body_size(4);
16217 let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
16218 assert!(e.expand_program(forms).is_err());
16219 assert!(
16220 !e.has("huge"),
16221 "macros table must stay pristine after rejection"
16222 );
16223 assert_eq!(e.len(), 0);
16224 }
16225
16226 #[test]
16227 fn macro_body_size_exceeded_position_is_none() {
16228 // The variant joins the non-positional cohort — the
16229 // offending macro's byte offset is not what a body-bomb
16230 // diagnostic anchors to; the (offending macro name,
16231 // observed body size, configured ceiling) triple is.
16232 // This pin keeps the variant in the `position() -> None`
16233 // cohort so a future span-carrying edit lands the field
16234 // in ONE place across every non-positional variant
16235 // simultaneously. Peer to
16236 // `expansion_{depth,size}_exceeded_position_is_none` one
16237 // PIPELINE-STAGE axis over.
16238 let err = LispError::MacroBodySizeExceeded {
16239 macro_name: "huge".to_string(),
16240 size: 512,
16241 limit: 256,
16242 };
16243 assert_eq!(err.position(), None);
16244 }
16245
16246 #[test]
16247 fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
16248 // Display projection carries `macro_name`, `size`, AND
16249 // `limit`, so authoring surfaces that substring-grep the
16250 // rendered diagnostic (`tatara-check`, REPL, LSP) see
16251 // ALL THREE halves at the variant boundary and never need
16252 // to substring-parse free-form text to recover the
16253 // identity of the offending macro, the observed body
16254 // size, or the ceiling it breached. Peer to
16255 // `expansion_size_exceeded_display_matches_typed_variant_shape`
16256 // one PIPELINE-STAGE axis over.
16257 let err = LispError::MacroBodySizeExceeded {
16258 macro_name: "huge".to_string(),
16259 size: 512,
16260 limit: 256,
16261 };
16262 let rendered = err.to_string();
16263 assert!(rendered.contains("huge"), "rendered: {rendered}");
16264 assert!(rendered.contains("512"), "rendered: {rendered}");
16265 assert!(rendered.contains("256"), "rendered: {rendered}");
16266 assert!(
16267 rendered.contains("macro body size exceeded"),
16268 "rendered: {rendered}"
16269 );
16270 }
16271
16272 #[test]
16273 fn lawful_macro_body_within_ceiling_registers_and_expands() {
16274 // A lawful hand-authored macro body sits well below any
16275 // realistic ceiling — the default `DEFAULT_MAX_MACRO_BODY_SIZE`
16276 // is deliberately generous so operators never brush it on
16277 // real authoring workloads. This pin locks the default
16278 // ceiling's non-interference posture: a regression that
16279 // ever lowered the default enough to reject the reference
16280 // `(defmacro when (cond x) `(if ,cond ,x))` shape would
16281 // fail here.
16282 let mut e = Expander::new();
16283 let forms = read(
16284 "(defmacro when (cond x) `(if ,cond ,x))
16285 (when #t hey)",
16286 )
16287 .unwrap();
16288 let out = e.expand_program(forms).unwrap();
16289 assert_eq!(out[0], parse("(if #t hey)"));
16290 }
16291
16292 #[test]
16293 fn register_macro_def_direct_call_respects_body_size_ceiling() {
16294 // `register_macro_def` is the substrate primitive both
16295 // `expand_program`'s `defmacro` recognition AND
16296 // `with_macros`'s bulk load route through. The body-size
16297 // ceiling fires on the primitive itself, not on any one
16298 // consumer — a regression that ever moved the gate into
16299 // `expand_program`'s arm alone would fail this pin.
16300 let mut e = Expander::new();
16301 e.set_max_macro_body_size(4);
16302 let def = MacroDef {
16303 name: "huge".to_string(),
16304 params: MacroParams {
16305 required: vec!["x".to_string()],
16306 optional: Vec::new(),
16307 rest: None,
16308 },
16309 body: Sexp::List(vec![
16310 Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
16311 Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
16312 Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
16313 Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
16314 Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
16315 ]),
16316 };
16317 // Body node_count: List(5 atoms) = 1 + 5 = 6 nodes.
16318 let err = e.register_macro_def(def).unwrap_err();
16319 match err {
16320 LispError::MacroBodySizeExceeded {
16321 macro_name,
16322 size,
16323 limit,
16324 } => {
16325 assert_eq!(macro_name, "huge");
16326 assert_eq!(size, 6);
16327 assert_eq!(limit, 4);
16328 }
16329 other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
16330 }
16331 assert!(!e.has("huge"));
16332 }
16333
16334 // ── max_registered_macros: REGISTRATION-time table-count ceiling ──
16335 // Peer to body-size (REGISTER-time SIZE) one RESOURCE-DIMENSION
16336 // axis over — where body-size bounds a per-registration SIZE,
16337 // this ceiling bounds cumulative registration COUNT. Peer to
16338 // cache-entries (EXPAND-time COUNT) one PIPELINE-STAGE axis over
16339 // — the two entry-count guards close the two pipeline stages
16340 // symmetrically. Closes the expander's RESOURCE surface at a
16341 // FIFTH typed dimension across two pipeline stages: the
16342 // REGISTER-time face is now a two-dimensional (size × count)
16343 // closure symmetric with the EXPAND-time (depth + count + size)
16344 // triple.
16345
16346 #[test]
16347 fn expander_default_max_registered_macros_is_the_module_constant() {
16348 // Both constructors seed `max_registered_macros` from the
16349 // module constant — a regression that drifts one
16350 // constructor's default from the constant (e.g. a hardcoded
16351 // `4096` at ONE site with the other left uninitialized
16352 // after a `Default` derive addition) fails this pin. Peer
16353 // to the four prior default-coherence pins on depth,
16354 // cache-entries, expansion-size, and body-size.
16355 assert_eq!(
16356 Expander::new().max_registered_macros(),
16357 DEFAULT_MAX_REGISTERED_MACROS
16358 );
16359 assert_eq!(
16360 Expander::new_substitute_only().max_registered_macros(),
16361 DEFAULT_MAX_REGISTERED_MACROS
16362 );
16363 assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
16364 }
16365
16366 #[test]
16367 fn set_max_registered_macros_takes_effect_on_subsequent_register() {
16368 // Setter mirrors accessor — a regression that ever forgets
16369 // to write through to the field, or writes to a shadow
16370 // field the accessor does not read, fails this pin. Peer
16371 // to the four prior setter/accessor coherence pins.
16372 let mut e = Expander::new();
16373 e.set_max_registered_macros(64);
16374 assert_eq!(e.max_registered_macros(), 64);
16375 }
16376
16377 #[test]
16378 fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
16379 // With a ceiling of `2`, the first two fresh registrations
16380 // land in the table and the third fresh key rejects with
16381 // `LispError::RegisteredMacrosExceeded { macro_name: "c",
16382 // count: 2, limit: 2 }`. `count` is the pre-insert table
16383 // length (which equals `limit` at rejection in the common
16384 // case); `macro_name` is the OFFENDING call's name (the
16385 // fresh key we could not admit), not the outermost caller.
16386 let mut e = Expander::new();
16387 e.set_max_registered_macros(2);
16388 let forms = read(
16389 "(defmacro a (x) `(list ,x))
16390 (defmacro b (x) `(list ,x))
16391 (defmacro c (x) `(list ,x))",
16392 )
16393 .unwrap();
16394 let err = e.expand_program(forms).unwrap_err();
16395 match err {
16396 LispError::RegisteredMacrosExceeded {
16397 macro_name,
16398 count,
16399 limit,
16400 } => {
16401 assert_eq!(macro_name, "c");
16402 assert_eq!(count, 2);
16403 assert_eq!(limit, 2);
16404 }
16405 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16406 }
16407 // First two admitted; third rejected leaves the table at
16408 // its pre-third-call size.
16409 assert!(e.has("a"));
16410 assert!(e.has("b"));
16411 assert!(!e.has("c"));
16412 assert_eq!(e.len(), 2);
16413 }
16414
16415 #[test]
16416 fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
16417 // The substitute strategy shares the same
16418 // `register_macro_def` outer registration entry with the
16419 // bytecode strategy, so the REGISTRATION-time TABLE-COUNT
16420 // ceiling fires at the SAME gate regardless of which
16421 // apply-layer is active. The pin makes that shared-
16422 // registration property structural: a regression that ever
16423 // branches the table-count guard on strategy (e.g. moves
16424 // it into the `compile_templates` arm alone) fails this
16425 // test on the substitute expander. Peer to the four prior
16426 // shared-registration pins.
16427 let mut e = Expander::new_substitute_only();
16428 e.set_max_registered_macros(2);
16429 let forms = read(
16430 "(defmacro a (x) `(list ,x))
16431 (defmacro b (x) `(list ,x))
16432 (defmacro c (x) `(list ,x))",
16433 )
16434 .unwrap();
16435 let err = e.expand_program(forms).unwrap_err();
16436 match err {
16437 LispError::RegisteredMacrosExceeded {
16438 macro_name,
16439 count,
16440 limit,
16441 } => {
16442 assert_eq!(macro_name, "c");
16443 assert_eq!(count, 2);
16444 assert_eq!(limit, 2);
16445 }
16446 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16447 }
16448 assert!(e.has("a"));
16449 assert!(e.has("b"));
16450 assert!(!e.has("c"));
16451 assert_eq!(e.len(), 2);
16452 }
16453
16454 #[test]
16455 fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
16456 // OVERWRITES (a re-registration of an already-registered
16457 // key) do not grow the table and MUST be admitted at any
16458 // ceiling — operators redefining a macro at capacity is a
16459 // legitimate authoring pattern. This pin catches a
16460 // regression that ever treats overwrites the same as fresh
16461 // keys (a `contains_key` check that inverts, or a
16462 // `set_max_registered_macros(0)` that would reject even
16463 // pre-registered overwrites).
16464 let mut e = Expander::new();
16465 // Land two macros with a permissive ceiling.
16466 e.expand_program(
16467 read(
16468 "(defmacro a (x) `(list ,x))
16469 (defmacro b (x) `(list ,x))",
16470 )
16471 .unwrap(),
16472 )
16473 .unwrap();
16474 assert_eq!(e.len(), 2);
16475 // Drop the ceiling to exactly the current size — a fresh
16476 // third key would now reject, but an overwrite of `a` MUST
16477 // admit.
16478 e.set_max_registered_macros(2);
16479 let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
16480 e.expand_program(forms).unwrap();
16481 assert_eq!(e.len(), 2, "overwrite must not grow the table");
16482 // Re-expand a call to `a` — the NEW definition (two
16483 // required params) is active, and the OLD definition (one
16484 // required param) is gone. Under the new definition,
16485 // `(a 1 2)` expands to `(pair 1 2)`.
16486 let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
16487 assert_eq!(expanded[0], parse("(pair foo bar)"));
16488 }
16489
16490 #[test]
16491 fn register_at_table_ceiling_admits_up_to_but_not_past() {
16492 // Fail-before/pass-after negative control on the exact
16493 // boundary: with a ceiling of `3`, exactly three fresh keys
16494 // admit and the fourth rejects. A regression that ever
16495 // shifted the boundary by one (e.g. keyed on `>` instead of
16496 // `>=`, or checked `len() > cap` before insert instead of
16497 // `len() >= cap`) would fail one of the two arms — either
16498 // the third admission or the fourth rejection.
16499 let mut e = Expander::new();
16500 e.set_max_registered_macros(3);
16501 // Three fresh keys admit.
16502 e.expand_program(
16503 read(
16504 "(defmacro a (x) `(list ,x))
16505 (defmacro b (x) `(list ,x))
16506 (defmacro c (x) `(list ,x))",
16507 )
16508 .unwrap(),
16509 )
16510 .unwrap();
16511 assert_eq!(e.len(), 3);
16512 // Fourth fresh key rejects.
16513 let err = e
16514 .expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
16515 .unwrap_err();
16516 match err {
16517 LispError::RegisteredMacrosExceeded {
16518 macro_name,
16519 count,
16520 limit,
16521 } => {
16522 assert_eq!(macro_name, "d");
16523 assert_eq!(count, 3);
16524 assert_eq!(limit, 3);
16525 }
16526 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16527 }
16528 // Table stays at pre-rejection size — no partial write.
16529 assert_eq!(e.len(), 3);
16530 assert!(!e.has("d"));
16531 }
16532
16533 #[test]
16534 fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
16535 // A rejected registration MUST leave BOTH `self.macros` AND
16536 // `self.templates` exactly as they were — no partial-write
16537 // window in which one table carries the over-ceiling entry
16538 // while the other does not, or in which the template
16539 // pre-compile already ran. Table-count gate fires BEFORE
16540 // body-size gate walks the body AND BEFORE any insert lands,
16541 // so a regression that ever moved the table-count gate
16542 // below `self.templates.insert` or below
16543 // `self.macros.insert` would fail this pin. Peer to
16544 // `register_failure_leaves_both_tables_pristine` on the
16545 // body-size axis.
16546 let mut e = Expander::new();
16547 e.set_max_registered_macros(1);
16548 // Land one macro at ceiling.
16549 e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16550 .unwrap();
16551 assert!(e.has("a"));
16552 // Reject a fresh second key.
16553 let err = e
16554 .expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
16555 .unwrap_err();
16556 assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
16557 // Table stays at pre-rejection state — `b` is NOT present
16558 // in either self.macros OR self.templates.
16559 assert_eq!(e.len(), 1);
16560 assert!(e.has("a"));
16561 assert!(!e.has("b"));
16562 // Expanding a call to `a` still works (templates table for
16563 // `a` untouched by the failed `b` registration).
16564 let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
16565 assert_eq!(expanded[0], parse("(list hey)"));
16566 }
16567
16568 #[test]
16569 fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
16570 // Zero ceiling is the "contract-test that this expander
16571 // accepts no new macros" posture — every fresh key rejects
16572 // at the first `defmacro` because `0 >= 0`. Peer to the
16573 // depth-ceiling zero-admits-and-rejects-every-macro-call
16574 // pin one RESOURCE axis over.
16575 let mut e = Expander::new();
16576 e.set_max_registered_macros(0);
16577 let err = e
16578 .expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
16579 .unwrap_err();
16580 match err {
16581 LispError::RegisteredMacrosExceeded {
16582 macro_name,
16583 count,
16584 limit,
16585 } => {
16586 assert_eq!(macro_name, "a");
16587 assert_eq!(count, 0);
16588 assert_eq!(limit, 0);
16589 }
16590 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16591 }
16592 assert!(e.is_empty());
16593 }
16594
16595 #[test]
16596 fn registered_macros_exceeded_position_is_none() {
16597 // The variant joins the non-positional cohort — the
16598 // offending macro's byte offset is not what a table-count
16599 // diagnostic anchors to; the (offending fresh macro name,
16600 // observed table count, configured ceiling) triple is.
16601 // This pin keeps the variant in the `position() -> None`
16602 // cohort so a future span-carrying edit lands the field
16603 // in ONE place across every non-positional variant
16604 // simultaneously. Peer to the four prior
16605 // `*_exceeded_position_is_none` pins on the resource-
16606 // ceiling cohort.
16607 let err = LispError::RegisteredMacrosExceeded {
16608 macro_name: "fresh-1000".to_string(),
16609 count: 4096,
16610 limit: 4096,
16611 };
16612 assert_eq!(err.position(), None);
16613 }
16614
16615 #[test]
16616 fn registered_macros_exceeded_display_matches_typed_variant_shape() {
16617 // Display projection carries `macro_name`, `count`, AND
16618 // `limit`, so authoring surfaces that substring-grep the
16619 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
16620 // THREE halves at the variant boundary and never need to
16621 // substring-parse free-form text to recover the identity
16622 // of the offending fresh key, the observed table count, or
16623 // the ceiling it breached. Peer to the four prior
16624 // `*_exceeded_display_matches_typed_variant_shape` pins.
16625 let err = LispError::RegisteredMacrosExceeded {
16626 macro_name: "fresh-1000".to_string(),
16627 count: 4096,
16628 limit: 4096,
16629 };
16630 let rendered = err.to_string();
16631 assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
16632 assert!(rendered.contains("4096"), "rendered: {rendered}");
16633 assert!(
16634 rendered.contains("registered macros count exceeded"),
16635 "rendered: {rendered}"
16636 );
16637 }
16638
16639 #[test]
16640 fn lawful_typescape_within_ceiling_registers_and_expands() {
16641 // A lawful hand-authored typescape (a handful of macros)
16642 // sits well below any realistic ceiling — the default
16643 // `DEFAULT_MAX_REGISTERED_MACROS` is deliberately generous
16644 // so operators never brush it on real authoring workloads.
16645 // This pin locks the default ceiling's non-interference
16646 // posture: a regression that ever lowered the default
16647 // enough to reject a small typescape would fail here. Peer
16648 // to `lawful_macro_body_within_ceiling_registers_and_expands`.
16649 let mut e = Expander::new();
16650 let forms = read(
16651 "(defmacro when (cond x) `(if ,cond ,x))
16652 (defmacro twice (x) `(list ,x ,x))
16653 (defmacro pair (x y) `(list ,x ,y))
16654 (when #t (twice hey))",
16655 )
16656 .unwrap();
16657 let out = e.expand_program(forms).unwrap();
16658 assert_eq!(out[0], parse("(if #t (list hey hey))"));
16659 assert_eq!(e.len(), 3);
16660 }
16661
16662 #[test]
16663 fn register_macro_def_direct_call_respects_table_ceiling() {
16664 // `register_macro_def` is the substrate primitive both
16665 // `expand_program`'s `defmacro` recognition AND
16666 // `with_macros`'s bulk load route through. The table-count
16667 // ceiling fires on the primitive itself, not on any one
16668 // consumer — a regression that ever moved the gate into
16669 // `expand_program`'s arm alone would fail this pin. Peer
16670 // to `register_macro_def_direct_call_respects_body_size_ceiling`
16671 // one RESOURCE-DIMENSION axis over.
16672 let mut e = Expander::new();
16673 e.set_max_registered_macros(1);
16674 let def_a = MacroDef {
16675 name: "a".to_string(),
16676 params: MacroParams {
16677 required: vec!["x".to_string()],
16678 optional: Vec::new(),
16679 rest: None,
16680 },
16681 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16682 };
16683 let def_b = MacroDef {
16684 name: "b".to_string(),
16685 params: MacroParams {
16686 required: vec!["x".to_string()],
16687 optional: Vec::new(),
16688 rest: None,
16689 },
16690 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16691 };
16692 // First admits — table sits at ceiling.
16693 e.register_macro_def(def_a).unwrap();
16694 assert!(e.has("a"));
16695 assert_eq!(e.len(), 1);
16696 // Second rejects — fresh key past ceiling.
16697 let err = e.register_macro_def(def_b).unwrap_err();
16698 match err {
16699 LispError::RegisteredMacrosExceeded {
16700 macro_name,
16701 count,
16702 limit,
16703 } => {
16704 assert_eq!(macro_name, "b");
16705 assert_eq!(count, 1);
16706 assert_eq!(limit, 1);
16707 }
16708 other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
16709 }
16710 assert!(!e.has("b"));
16711 assert_eq!(e.len(), 1);
16712 }
16713
16714 #[test]
16715 fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
16716 // Direct `register_macro_def` calls must ALSO admit
16717 // overwrites at ceiling — the overwrite discipline lives
16718 // on the primitive, not on `expand_program`'s arm alone.
16719 // Sibling of the source-level overwrite admission test.
16720 let mut e = Expander::new();
16721 e.set_max_registered_macros(1);
16722 let def_a_v1 = MacroDef {
16723 name: "a".to_string(),
16724 params: MacroParams {
16725 required: vec!["x".to_string()],
16726 optional: Vec::new(),
16727 rest: None,
16728 },
16729 body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
16730 };
16731 let def_a_v2 = MacroDef {
16732 name: "a".to_string(),
16733 params: MacroParams {
16734 required: vec!["x".to_string(), "y".to_string()],
16735 optional: Vec::new(),
16736 rest: None,
16737 },
16738 body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
16739 };
16740 e.register_macro_def(def_a_v1).unwrap();
16741 assert_eq!(e.len(), 1);
16742 // Overwrite of `a` at ceiling admits — no growth.
16743 e.register_macro_def(def_a_v2).unwrap();
16744 assert_eq!(e.len(), 1);
16745 }
16746
16747 // ── `MacroParams::total_arity` — the total-slots projection ──
16748 //
16749 // `total_arity()` names `required.len() + optional.len() +
16750 // rest.is_some() as usize` on the typed `MacroParams` algebra —
16751 // the `Vec`-free peer of `names().len()` AND the axis both
16752 // `MacroParams::bind`'s `Vec::with_capacity` hint AND the
16753 // REGISTER-time arity-ceiling gate consult. These pins cover:
16754 // structural identity vs. `names().len()`, the
16755 // `fixed_arity + rest.is_some() as usize` decomposition, and the
16756 // three canonical shapes (empty, all-required, mixed with rest).
16757 #[test]
16758 fn macro_params_total_arity_matches_names_len() {
16759 // The structural identity `total_arity() == names().len()`
16760 // — the `Vec`-free primitive's projection equals the flat-
16761 // index list's length. A regression that ever miscounts
16762 // one slot (drops rest, double-counts optional) fails this
16763 // pin. Sibling to `fixed_arity`'s peer-arithmetic pins.
16764 let params = MacroParams {
16765 required: vec!["a".into(), "b".into()],
16766 optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
16767 rest: Some("e".into()),
16768 };
16769 assert_eq!(params.total_arity(), params.names().len());
16770 assert_eq!(params.total_arity(), 5);
16771 }
16772
16773 #[test]
16774 fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
16775 // The `fixed_arity + rest.is_some() as usize` decomposition
16776 // — pins the composition on the [`MacroParams`] algebra so
16777 // a future consumer that wants "total slots" via
16778 // `fixed_arity + rest?` binds to the same arithmetic
16779 // `total_arity` names.
16780 let rest_none = MacroParams {
16781 required: vec!["a".into()],
16782 optional: vec![OptionalParam::bare("b")],
16783 rest: None,
16784 };
16785 assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
16786 assert_eq!(rest_none.total_arity(), 2);
16787 let rest_some = MacroParams {
16788 required: vec!["a".into()],
16789 optional: vec![OptionalParam::bare("b")],
16790 rest: Some("r".into()),
16791 };
16792 assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
16793 assert_eq!(rest_some.total_arity(), 3);
16794 }
16795
16796 #[test]
16797 fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
16798 // Nullary macro edge: `(defmacro nullary () BODY)` has
16799 // `total_arity() == 0`. Under a `set_max_macro_arity(0)`
16800 // ceiling the arity gate keys on `>` (equality admits), so
16801 // this nullary registration succeeds while any non-nullary
16802 // one rejects — the exact contract the arity-gate
16803 // zero-admits-nullary test below relies on.
16804 let params = MacroParams::default();
16805 assert_eq!(params.total_arity(), 0);
16806 }
16807
16808 // ── max_macro_arity: REGISTRATION-time param-count ceiling ──
16809 // Peer to body-size (REGISTER-time BODY SIZE) one RESOURCE-
16810 // DIMENSION axis over — where body-size bounds a per-registration
16811 // BODY node count, this ceiling bounds a per-registration PARAM
16812 // slot count. Peer to registered-macros (REGISTER-time TABLE
16813 // COUNT) one RESOURCE-DIMENSION axis over — where
16814 // registered-macros bounds cumulative registration COUNT, this
16815 // ceiling bounds a per-registration PARAM-LIST WIDTH. Together
16816 // the three REGISTER-time ceilings close the (per-body SIZE,
16817 // per-body ARITY, per-table COUNT) three-corner surface.
16818
16819 #[test]
16820 fn expander_default_max_macro_arity_is_the_module_constant() {
16821 // Both constructors seed `max_macro_arity` from the module
16822 // constant — a regression that drifts one constructor's
16823 // default from the constant (e.g. a hardcoded `128` at ONE
16824 // site with the other left uninitialized after a `Default`
16825 // derive addition) fails this pin. Peer to the five prior
16826 // default-coherence pins on depth, cache-entries,
16827 // expansion-size, body-size, and registered-macros.
16828 assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
16829 assert_eq!(
16830 Expander::new_substitute_only().max_macro_arity(),
16831 DEFAULT_MAX_MACRO_ARITY
16832 );
16833 assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
16834 }
16835
16836 #[test]
16837 fn set_max_macro_arity_takes_effect_on_subsequent_register() {
16838 // Setter mirrors accessor — a regression that ever forgets to
16839 // write through to the field, or writes to a shadow field the
16840 // accessor does not read, fails this pin. Peer to the five
16841 // prior setter/accessor coherence pins.
16842 let mut e = Expander::new();
16843 e.set_max_macro_arity(16);
16844 assert_eq!(e.max_macro_arity(), 16);
16845 }
16846
16847 #[test]
16848 fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
16849 // `(defmacro huge (a b c) `,a)` declares 3 required params.
16850 // Under `set_max_macro_arity(2)` the arity gate fires:
16851 // 3 > 2 → `LispError::MacroArityExceeded { macro_name: "huge",
16852 // arity: 3, limit: 2 }`. `arity` is the observed
16853 // `def.params.total_arity()`, NOT `required.len()` alone —
16854 // even an optional-only or rest-only overrun trips the gate
16855 // (pinned separately). Peer to the register-time body-size
16856 // rejection tests one RESOURCE-DIMENSION axis over.
16857 let mut e = Expander::new();
16858 e.set_max_macro_arity(2);
16859 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16860 let err = e.expand_program(forms).unwrap_err();
16861 match err {
16862 LispError::MacroArityExceeded {
16863 macro_name,
16864 arity,
16865 limit,
16866 } => {
16867 assert_eq!(macro_name, "huge");
16868 assert_eq!(arity, 3);
16869 assert_eq!(limit, 2);
16870 }
16871 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16872 }
16873 assert!(!e.has("huge"));
16874 }
16875
16876 #[test]
16877 fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
16878 // The substitute strategy shares the same
16879 // `register_macro_def` outer registration entry with the
16880 // bytecode strategy, so the REGISTRATION-time ARITY ceiling
16881 // fires at the SAME gate regardless of which apply-layer is
16882 // active. Peer to
16883 // `register_arity_bomb_rejects_at_arity_limit_bytecode_path`.
16884 let mut e = Expander::new_substitute_only();
16885 e.set_max_macro_arity(2);
16886 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16887 let err = e.expand_program(forms).unwrap_err();
16888 match err {
16889 LispError::MacroArityExceeded {
16890 macro_name,
16891 arity,
16892 limit,
16893 } => {
16894 assert_eq!(macro_name, "huge");
16895 assert_eq!(arity, 3);
16896 assert_eq!(limit, 2);
16897 }
16898 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16899 }
16900 assert!(!e.has("huge"));
16901 }
16902
16903 #[test]
16904 fn register_arity_gate_counts_optional_slots() {
16905 // `(defmacro foo (a &optional b c) `,a)` has 1 required + 2
16906 // optional + 0 rest = arity 3 via `total_arity`. Under a
16907 // ceiling of `2` the gate rejects, matching the required-only
16908 // arity-bomb — the arity axis is `total_arity()`, not
16909 // `required.len()`. A regression that drops the optional slots
16910 // from the arity computation would ADMIT this shape and fail
16911 // this pin.
16912 let mut e = Expander::new();
16913 e.set_max_macro_arity(2);
16914 let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
16915 let err = e.expand_program(forms).unwrap_err();
16916 match err {
16917 LispError::MacroArityExceeded {
16918 macro_name,
16919 arity,
16920 limit,
16921 } => {
16922 assert_eq!(macro_name, "foo");
16923 assert_eq!(arity, 3);
16924 assert_eq!(limit, 2);
16925 }
16926 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16927 }
16928 }
16929
16930 #[test]
16931 fn register_arity_gate_counts_rest_slot() {
16932 // `(defmacro spread (a b &rest r) `,a)` has 2 required + 0
16933 // optional + 1 rest = arity 3 via `total_arity`. Under a
16934 // ceiling of `2` the gate rejects, matching the required-only
16935 // arity-bomb — the arity axis includes the rest slot as ONE
16936 // named param, not zero. A regression that drops the rest slot
16937 // from the arity computation would ADMIT this shape and fail
16938 // this pin.
16939 let mut e = Expander::new();
16940 e.set_max_macro_arity(2);
16941 let forms = read("(defmacro spread (a b &rest r) `,a)").unwrap();
16942 let err = e.expand_program(forms).unwrap_err();
16943 match err {
16944 LispError::MacroArityExceeded {
16945 macro_name,
16946 arity,
16947 limit,
16948 } => {
16949 assert_eq!(macro_name, "spread");
16950 assert_eq!(arity, 3);
16951 assert_eq!(limit, 2);
16952 }
16953 other => panic!("expected MacroArityExceeded, got: {other:?}"),
16954 }
16955 }
16956
16957 #[test]
16958 fn register_arity_at_ceiling_admits() {
16959 // `(defmacro pair (x y) `(list ,x ,y))` has 2 required = arity
16960 // 2 via `total_arity`. Under a ceiling of `2` the arity gate
16961 // keys on `>` (equality admits; only strict overrun rejects),
16962 // so the registration succeeds and the expansion proceeds
16963 // normally. Fail-before/pass-after negative control on the
16964 // exact boundary: a regression that ever shifted the boundary
16965 // by one (e.g. keyed on `>=` instead of `>`) would fail this
16966 // pin. Peer to `register_macro_body_at_ceiling_admits` on the
16967 // body-size axis.
16968 let mut e = Expander::new();
16969 e.set_max_macro_arity(2);
16970 let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
16971 let out = e.expand_program(forms).unwrap();
16972 assert_eq!(out[0], parse("(list a b)"));
16973 assert_eq!(e.len(), 1);
16974 }
16975
16976 #[test]
16977 fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
16978 // A rejected arity-gated registration MUST leave BOTH
16979 // `self.macros` AND `self.templates` exactly as they were —
16980 // no partial-write window in which one table carries the
16981 // over-ceiling entry while the other does not, or in which
16982 // the template pre-compile already ran. Arity gate fires
16983 // BEFORE the body-size gate walks the body AND BEFORE any
16984 // insert lands, so a regression that ever moved the arity
16985 // gate below `self.templates.insert` or below
16986 // `self.macros.insert` would fail this pin. Peer to
16987 // `register_failure_leaves_both_tables_pristine` (body-size)
16988 // and `register_table_ceiling_leaves_both_tables_pristine_on_rejection`
16989 // (table-count).
16990 let mut e = Expander::new();
16991 e.set_max_macro_arity(1);
16992 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
16993 assert!(e.expand_program(forms).is_err());
16994 assert!(
16995 !e.has("huge"),
16996 "macros table must stay pristine after rejection"
16997 );
16998 assert_eq!(e.len(), 0);
16999 // Expanding a NEW lawful macro afterwards still works — the
17000 // failed arity-gate rejection has not corrupted expander
17001 // state.
17002 let out = e
17003 .expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
17004 .unwrap();
17005 assert_eq!(out[0], parse("hey"));
17006 }
17007
17008 #[test]
17009 fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
17010 // Zero ceiling is the "contract-test that this expander
17011 // accepts only nullary macros" posture — every fresh
17012 // non-nullary key rejects at the arity gate because
17013 // `total_arity() > 0`, while nullary `(defmacro nullary ()
17014 // BODY)` still admits (its arity is exactly 0, and only
17015 // strict overrun rejects). Peer to the body-size zero-rejects-
17016 // every-non-empty-body pin one RESOURCE-DIMENSION axis over
17017 // (though body-size zero rejects even nullary-body shapes,
17018 // since `Sexp::Nil::node_count() == 1 > 0`; arity zero
17019 // preserves the nullary registration surface).
17020 let mut e = Expander::new();
17021 e.set_max_macro_arity(0);
17022 // Nullary admits — arity 0 <= 0.
17023 e.expand_program(read("(defmacro nullary () `unit)").unwrap())
17024 .unwrap();
17025 assert!(e.has("nullary"));
17026 // Non-nullary rejects — arity 1 > 0.
17027 let err = e
17028 .expand_program(read("(defmacro id (x) `,x)").unwrap())
17029 .unwrap_err();
17030 match err {
17031 LispError::MacroArityExceeded {
17032 macro_name,
17033 arity,
17034 limit,
17035 } => {
17036 assert_eq!(macro_name, "id");
17037 assert_eq!(arity, 1);
17038 assert_eq!(limit, 0);
17039 }
17040 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17041 }
17042 assert!(!e.has("id"));
17043 }
17044
17045 #[test]
17046 fn macro_arity_exceeded_position_is_none() {
17047 // The variant joins the non-positional cohort — the offending
17048 // macro's byte offset is not what an arity-bomb diagnostic
17049 // anchors to; the (macro name, observed arity, configured
17050 // ceiling) triple is. This pin keeps the variant in the
17051 // `position() -> None` cohort so a future span-carrying edit
17052 // lands the field in ONE place across every non-positional
17053 // variant simultaneously. Peer to the five prior
17054 // `*_exceeded_position_is_none` pins on the resource-ceiling
17055 // cohort.
17056 let err = LispError::MacroArityExceeded {
17057 macro_name: "huge".to_string(),
17058 arity: 512,
17059 limit: 128,
17060 };
17061 assert_eq!(err.position(), None);
17062 }
17063
17064 #[test]
17065 fn macro_arity_exceeded_display_matches_typed_variant_shape() {
17066 // Display projection carries `macro_name`, `arity`, AND
17067 // `limit`, so authoring surfaces that substring-grep the
17068 // rendered diagnostic (`tatara-check`, REPL, LSP) see ALL
17069 // THREE halves at the variant boundary and never need to
17070 // substring-parse free-form text to recover the identity of
17071 // the offending macro, the observed arity, or the ceiling it
17072 // breached. Peer to the five prior
17073 // `*_exceeded_display_matches_typed_variant_shape` pins.
17074 let err = LispError::MacroArityExceeded {
17075 macro_name: "huge".to_string(),
17076 arity: 512,
17077 limit: 128,
17078 };
17079 let rendered = err.to_string();
17080 assert!(rendered.contains("huge"), "rendered: {rendered}");
17081 assert!(rendered.contains("512"), "rendered: {rendered}");
17082 assert!(rendered.contains("128"), "rendered: {rendered}");
17083 assert!(
17084 rendered.contains("macro arity exceeded"),
17085 "rendered: {rendered}"
17086 );
17087 }
17088
17089 #[test]
17090 fn lawful_macro_arity_within_ceiling_registers_and_expands() {
17091 // A lawful hand-authored macro arity sits well below any
17092 // realistic ceiling — the default `DEFAULT_MAX_MACRO_ARITY`
17093 // is deliberately generous so operators never brush it on
17094 // real authoring workloads. This pin locks the default
17095 // ceiling's non-interference posture: a regression that
17096 // ever lowered the default enough to reject the reference
17097 // multi-slot `(defmacro when (cond x) …)` shape would fail
17098 // here. Peer to `lawful_macro_body_within_ceiling_registers_and_expands`
17099 // and `lawful_typescape_within_ceiling_registers_and_expands`.
17100 let mut e = Expander::new();
17101 let forms = read(
17102 "(defmacro when (cond x) `(if ,cond ,x))
17103 (defmacro triple (x y z) `(list ,x ,y ,z))
17104 (when #t (triple a b c))",
17105 )
17106 .unwrap();
17107 let out = e.expand_program(forms).unwrap();
17108 assert_eq!(out[0], parse("(if #t (list a b c))"));
17109 }
17110
17111 #[test]
17112 fn register_macro_def_direct_call_respects_arity_ceiling() {
17113 // `register_macro_def` is the substrate primitive both
17114 // `expand_program`'s `defmacro` recognition AND `with_macros`'s
17115 // bulk load route through. The arity ceiling fires on the
17116 // primitive itself, not on any one consumer — a regression
17117 // that ever moved the gate into `expand_program`'s arm alone
17118 // would fail this pin. Peer to
17119 // `register_macro_def_direct_call_respects_body_size_ceiling`
17120 // AND `register_macro_def_direct_call_respects_table_ceiling`
17121 // one RESOURCE-DIMENSION axis over.
17122 let mut e = Expander::new();
17123 e.set_max_macro_arity(2);
17124 let def = MacroDef {
17125 name: "huge".to_string(),
17126 params: MacroParams {
17127 required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
17128 optional: Vec::new(),
17129 rest: None,
17130 },
17131 body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
17132 };
17133 // Arity 3 > ceiling 2 — rejected before body-size gate.
17134 let err = e.register_macro_def(def).unwrap_err();
17135 match err {
17136 LispError::MacroArityExceeded {
17137 macro_name,
17138 arity,
17139 limit,
17140 } => {
17141 assert_eq!(macro_name, "huge");
17142 assert_eq!(arity, 3);
17143 assert_eq!(limit, 2);
17144 }
17145 other => panic!("expected MacroArityExceeded, got: {other:?}"),
17146 }
17147 assert!(!e.has("huge"));
17148 }
17149
17150 #[test]
17151 fn arity_gate_fires_before_body_size_gate() {
17152 // ORDERING PIN — the arity gate fires BEFORE the body-size
17153 // gate. With both `set_max_macro_arity(1)` AND
17154 // `set_max_macro_body_size(2)` set below a shape that violates
17155 // BOTH ceilings — `(defmacro dual (a b) `(list ,a ,b))` has
17156 // arity 2 AND body node count > 2 — the arity gate's
17157 // rejection wins, so the returned variant is
17158 // `MacroArityExceeded`, not `MacroBodySizeExceeded`. A
17159 // regression that ever swapped the two gates' order would fail
17160 // this pin. The ordering isn't cosmetic: arity is O(1)
17161 // (three-arm addition on `def.params` fields), body-size is
17162 // O(body_nodes) (`def.body.node_count()` walks the AST), so
17163 // the O(1)-first order minimises the cost of rejecting the
17164 // worst case (a large-body large-arity macro).
17165 let mut e = Expander::new();
17166 e.set_max_macro_arity(1);
17167 e.set_max_macro_body_size(2);
17168 let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
17169 let err = e.expand_program(forms).unwrap_err();
17170 assert!(
17171 matches!(err, LispError::MacroArityExceeded { .. }),
17172 "arity gate must fire before body-size gate; got: {err:?}"
17173 );
17174 }
17175
17176 #[test]
17177 fn arity_gate_fires_after_table_count_gate() {
17178 // ORDERING PIN — the table-count gate fires BEFORE the arity
17179 // gate. Under `set_max_registered_macros(1)` at capacity AND
17180 // `set_max_macro_arity(1)`, a fresh over-arity registration
17181 // trips the table-count gate FIRST (its `HashMap::len`
17182 // comparison is O(1) cache-friendly and gates cumulative
17183 // registration cost); the arity gate is O(1) but on `def`,
17184 // not on shared state, so it sits AFTER the table-count gate
17185 // on the O(1)-first ordering of the REGISTER-time chain. A
17186 // regression that ever swapped the two would fail this pin.
17187 let mut e = Expander::new();
17188 e.set_max_registered_macros(1);
17189 e.set_max_macro_arity(1);
17190 // Land one macro at table ceiling.
17191 e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
17192 .unwrap();
17193 assert_eq!(e.len(), 1);
17194 // Fresh registration with BOTH violations — table-count wins.
17195 let forms = read("(defmacro huge (a b c) `,a)").unwrap();
17196 let err = e.expand_program(forms).unwrap_err();
17197 assert!(
17198 matches!(err, LispError::RegisteredMacrosExceeded { .. }),
17199 "table-count gate must fire before arity gate; got: {err:?}"
17200 );
17201 }
17202
17203 // ── ResourceLimits: bundled resource-ceiling snapshot ────────────
17204
17205 #[test]
17206 fn default_resource_limits_binds_each_field_to_matching_module_constant() {
17207 // Pin the (constant × aggregation) corner: the const-form
17208 // aggregate MUST bind every field to its individually-defined
17209 // module constant. A re-tuning of ONE module constant
17210 // (e.g. `DEFAULT_MAX_EXPANSION_DEPTH` raised from 256 to 512)
17211 // has to propagate through the aggregate at ONE site; this
17212 // test fires the divergence loudly if a future edit sets a
17213 // module constant to a fresh value but forgets to update the
17214 // matching aggregate slot. Field-by-field equality carries
17215 // the message better than one deep-equality assertion.
17216 assert_eq!(
17217 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
17218 DEFAULT_MAX_EXPANSION_DEPTH
17219 );
17220 assert_eq!(
17221 DEFAULT_RESOURCE_LIMITS.max_cache_entries,
17222 DEFAULT_MAX_CACHE_ENTRIES
17223 );
17224 assert_eq!(
17225 DEFAULT_RESOURCE_LIMITS.max_expansion_size,
17226 DEFAULT_MAX_EXPANSION_SIZE
17227 );
17228 assert_eq!(
17229 DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
17230 DEFAULT_MAX_MACRO_BODY_SIZE
17231 );
17232 assert_eq!(
17233 DEFAULT_RESOURCE_LIMITS.max_registered_macros,
17234 DEFAULT_MAX_REGISTERED_MACROS
17235 );
17236 assert_eq!(
17237 DEFAULT_RESOURCE_LIMITS.max_macro_arity,
17238 DEFAULT_MAX_MACRO_ARITY
17239 );
17240 }
17241
17242 #[test]
17243 fn resource_limits_default_impl_matches_const_form() {
17244 // The two projections of the shipped posture — the `const`
17245 // form [`DEFAULT_RESOURCE_LIMITS`] and the runtime
17246 // `Default::default()` form — must reach the SAME value. Pins
17247 // that a future edit which flips one to a new default cannot
17248 // silently drift the other.
17249 assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
17250 }
17251
17252 #[test]
17253 fn expander_new_resource_limits_matches_shipped_defaults() {
17254 // Fresh-expander posture on the bundled-getter surface — the
17255 // six ceilings [`Expander::new`] seeds MUST snapshot to
17256 // [`DEFAULT_RESOURCE_LIMITS`] verbatim. Peer of the six
17257 // individual `expander_default_max_*_is_the_module_constant`
17258 // pins one AGGREGATION axis over — where each individual
17259 // pin catches ONE field drift, this pin catches ANY of the
17260 // six drifting AND the snapshot projection itself losing a
17261 // field.
17262 assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
17263 }
17264
17265 #[test]
17266 fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
17267 // Substitute-only expander seeds the SAME six ceilings from
17268 // the SAME module constants as [`Expander::new`] — the two
17269 // constructors share the resource-ceiling posture even
17270 // though they diverge on `compile_templates` +
17271 // `cache_enabled`. Pins that a future constructor which
17272 // forgets to seed one of the six ceilings from its module
17273 // constant fails loudly through the bundled getter.
17274 assert_eq!(
17275 Expander::new_substitute_only().resource_limits(),
17276 DEFAULT_RESOURCE_LIMITS
17277 );
17278 }
17279
17280 #[test]
17281 fn resource_limits_snapshot_reflects_each_individual_setter() {
17282 // Coherence pin between the six individual setters and the
17283 // bundled getter — flipping ONE ceiling via its individual
17284 // setter must project through [`Expander::resource_limits`]
17285 // to the SAME value the individual getter returns. A
17286 // regression that leaves ONE field out of the snapshot
17287 // projection (a copy-paste omission in the struct literal
17288 // constructor) fires here. Distinct low-non-default values
17289 // so a stray field carrying the default fails loudly.
17290 let mut e = Expander::new();
17291 e.set_max_expansion_depth(3);
17292 e.set_max_cache_entries(5);
17293 e.set_max_expansion_size(7);
17294 e.set_max_macro_body_size(11);
17295 e.set_max_registered_macros(13);
17296 e.set_max_macro_arity(17);
17297 let snap = e.resource_limits();
17298 assert_eq!(snap.max_expansion_depth, 3);
17299 assert_eq!(snap.max_cache_entries, 5);
17300 assert_eq!(snap.max_expansion_size, 7);
17301 assert_eq!(snap.max_macro_body_size, 11);
17302 assert_eq!(snap.max_registered_macros, 13);
17303 assert_eq!(snap.max_macro_arity, 17);
17304 // And the individual getters see the same values — the
17305 // snapshot is a projection, not a divergent copy.
17306 assert_eq!(e.max_expansion_depth(), 3);
17307 assert_eq!(e.max_cache_entries(), 5);
17308 assert_eq!(e.max_expansion_size(), 7);
17309 assert_eq!(e.max_macro_body_size(), 11);
17310 assert_eq!(e.max_registered_macros(), 13);
17311 assert_eq!(e.max_macro_arity(), 17);
17312 }
17313
17314 #[test]
17315 fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
17316 // Sibling pin to `resource_limits_snapshot_reflects_...` on the
17317 // WRITE axis — bulk-setting via [`Expander::set_resource_limits`]
17318 // must reach every individual `max_*` getter. A regression
17319 // that drops one field from the destructuring assignment in
17320 // the bulk setter fires here. Distinct low-non-default values
17321 // so a stray field carrying the previous value fails loudly.
17322 let mut e = Expander::new();
17323 e.set_resource_limits(ResourceLimits {
17324 max_expansion_depth: 2,
17325 max_cache_entries: 4,
17326 max_expansion_size: 8,
17327 max_macro_body_size: 16,
17328 max_registered_macros: 32,
17329 max_macro_arity: 64,
17330 });
17331 assert_eq!(e.max_expansion_depth(), 2);
17332 assert_eq!(e.max_cache_entries(), 4);
17333 assert_eq!(e.max_expansion_size(), 8);
17334 assert_eq!(e.max_macro_body_size(), 16);
17335 assert_eq!(e.max_registered_macros(), 32);
17336 assert_eq!(e.max_macro_arity(), 64);
17337 }
17338
17339 #[test]
17340 fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
17341 // Round-trip theorem — for any starting expander, the
17342 // composition `set_resource_limits(resource_limits())` is
17343 // the identity on the resource-ceiling surface. This is the
17344 // ONE typed statement that pins the (getter, setter) pair as
17345 // strict projections of the same six-field surface, no
17346 // hidden lossy field, no ordering asymmetry between the
17347 // struct literal and the destructuring pattern.
17348 let mut e = Expander::new();
17349 e.set_max_expansion_depth(9);
17350 e.set_max_cache_entries(19);
17351 e.set_max_expansion_size(29);
17352 e.set_max_macro_body_size(39);
17353 e.set_max_registered_macros(49);
17354 e.set_max_macro_arity(59);
17355 let snap = e.resource_limits();
17356 // Reset to defaults so any leftover state would show up in
17357 // the post-round-trip snapshot as the default rather than
17358 // the intended value.
17359 e.set_resource_limits(ResourceLimits::default());
17360 assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
17361 // Apply the captured snapshot — the expander must land at
17362 // the same six values `snap` recorded.
17363 e.set_resource_limits(snap);
17364 assert_eq!(e.resource_limits(), snap);
17365 }
17366
17367 #[test]
17368 fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
17369 // The `Copy` posture on [`ResourceLimits`] gives callers the
17370 // cheap ergonomic of a struct-update literal — take the
17371 // shipped posture and override ONE ceiling — that a chain of
17372 // six independent setters cannot express in ONE typed expression.
17373 // Pins the intended use-shape so a future refactor which
17374 // removes `Copy` from the struct fails to compile at THIS
17375 // site rather than at every downstream consumer.
17376 let mut e = Expander::new();
17377 e.set_resource_limits(ResourceLimits {
17378 max_macro_arity: 4,
17379 ..DEFAULT_RESOURCE_LIMITS
17380 });
17381 // Overridden field carries the fresh value.
17382 assert_eq!(e.max_macro_arity(), 4);
17383 // Every other field carries the shipped default — the
17384 // struct-update literal is a full projection, not a
17385 // selective mutation.
17386 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17387 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17388 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17389 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17390 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17391 }
17392
17393 #[test]
17394 fn expander_default_derives_resource_limits_from_bundled_field_default() {
17395 // Post the [`Expander::limits`]-field consolidation the derived
17396 // `Default` on [`Expander`] projects through [`ResourceLimits`]'s
17397 // own `Default` — which returns [`DEFAULT_RESOURCE_LIMITS`] — so
17398 // `Expander::default().resource_limits()` lands at the shipped
17399 // ceiling posture verbatim. Pre-consolidation the six independent
17400 // `max_*: usize` fields defaulted to the bare `usize` zero, so
17401 // `Expander::default()` yielded a below-floor posture with every
17402 // ceiling clamped to `0` — a shape no consumer wanted and every
17403 // test path avoided by routing through [`Expander::new`] instead.
17404 // The consolidation is what turned that latent below-floor
17405 // derived-`Default` shape into a lawful shipped posture on the
17406 // structural axis. Peer of
17407 // `expander_new_resource_limits_matches_shipped_defaults` one
17408 // CONSTRUCTOR axis over — where that test pins the explicit
17409 // [`Expander::new`] constructor, this test pins the derived-
17410 // [`Default`] constructor route to the SAME six values through
17411 // the SAME bundled projection. Sibling of
17412 // `resource_limits_default_impl_matches_const_form` one
17413 // TYPE-LEVEL axis over — that test pins [`ResourceLimits::default`]
17414 // to [`DEFAULT_RESOURCE_LIMITS`]; this test pins [`Expander`]'s
17415 // derived `Default` to project through it.
17416 assert_eq!(
17417 Expander::default().resource_limits(),
17418 DEFAULT_RESOURCE_LIMITS
17419 );
17420 }
17421
17422 #[test]
17423 fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
17424 // Behavioral pin — the bundled setter is not a decorative
17425 // getter/setter pair; the six ceilings it propagates must
17426 // reach the SAME check sites the individual setters do. Set
17427 // `max_macro_arity` to 1 via the bulk setter and confirm the
17428 // arity gate fires on a two-arg macro registration. A
17429 // regression that wired the bulk setter to a shadow field
17430 // rather than the actual `Expander` state fails here.
17431 let mut e = Expander::new();
17432 e.set_resource_limits(ResourceLimits {
17433 max_macro_arity: 1,
17434 ..DEFAULT_RESOURCE_LIMITS
17435 });
17436 let forms = read("(defmacro two (a b) `,a)").unwrap();
17437 let err = e.expand_program(forms).unwrap_err();
17438 assert!(
17439 matches!(
17440 err,
17441 LispError::MacroArityExceeded {
17442 arity: 2,
17443 limit: 1,
17444 ..
17445 }
17446 ),
17447 "bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
17448 );
17449 }
17450
17451 // ── Expander::with_limits — at-construction resource-posture ───────
17452
17453 #[test]
17454 fn expander_with_limits_seeds_the_provided_resource_posture() {
17455 // Round-trip pin at CONSTRUCTION time — the bundle the caller
17456 // passes into [`Expander::with_limits`] MUST project back
17457 // through [`Expander::resource_limits`] verbatim. Peer of the
17458 // `set_resource_limits(l); resource_limits() == l` round-trip
17459 // one CONSTRUCTOR-STAGE axis over — that pin covers the
17460 // POST-construction assignment; this pin covers the
17461 // AT-construction assignment. A regression that dropped the
17462 // `limits` argument on the floor (a copy-paste that left
17463 // `limits: DEFAULT_RESOURCE_LIMITS` in the struct literal
17464 // instead of `limits`) fires here on every non-default field.
17465 // Distinct low-non-default values so a stray field defaulting
17466 // fails loudly.
17467 let want = ResourceLimits {
17468 max_expansion_depth: 3,
17469 max_cache_entries: 5,
17470 max_expansion_size: 7,
17471 max_macro_body_size: 11,
17472 max_registered_macros: 13,
17473 max_macro_arity: 17,
17474 };
17475 assert_eq!(Expander::with_limits(want).resource_limits(), want);
17476 }
17477
17478 #[test]
17479 fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
17480 // Delegation identity — [`Expander::new`] delegates to
17481 // [`Expander::with_limits`] with [`DEFAULT_RESOURCE_LIMITS`] so
17482 // the two constructors MUST agree on the resource-ceiling
17483 // projection. Pins the "no drift between the two paths to a
17484 // fresh default-posture expander" theorem: a future refactor
17485 // that breaks the delegation (an inline field literal reappearing
17486 // in [`Expander::new`], an accidental hardcoded const at ONE
17487 // site with the other unchanged) fires here. Peer of
17488 // `resource_limits_default_impl_matches_const_form` one
17489 // TYPE-LEVEL axis over — that test pins the two projections of
17490 // the shipped posture on [`ResourceLimits`] itself; this test
17491 // pins the two constructor paths on [`Expander`] to reach the
17492 // SAME six values through the SAME bundle.
17493 assert_eq!(
17494 Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
17495 Expander::new().resource_limits()
17496 );
17497 }
17498
17499 #[test]
17500 fn expander_with_limits_composes_with_struct_update_override() {
17501 // The `Copy` posture on [`ResourceLimits`] lets a caller compose
17502 // "the shipped posture with ONE ceiling overridden" as ONE
17503 // struct-update literal AND thread that composition through
17504 // [`Expander::with_limits`] as ONE typed call — the ergonomic
17505 // shape that six independent setter invocations cannot express
17506 // in ONE expression. Overridden ceiling carries the fresh value,
17507 // every other ceiling carries its shipped default. A future
17508 // refactor that removes `Copy` from [`ResourceLimits`] fails to
17509 // compile at THIS site rather than at every downstream consumer.
17510 let e = Expander::with_limits(ResourceLimits {
17511 max_macro_arity: 4,
17512 ..DEFAULT_RESOURCE_LIMITS
17513 });
17514 assert_eq!(e.max_macro_arity(), 4);
17515 assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
17516 assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
17517 assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
17518 assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
17519 assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
17520 }
17521
17522 #[test]
17523 fn expander_with_limits_carries_the_new_execution_strategy() {
17524 // The (compile_templates, cache_enabled) execution-strategy pair
17525 // MUST carry the [`Expander::new`] default posture — bytecode +
17526 // cache both on — regardless of which resource posture the
17527 // caller passes in. Cache-count observation is the least-
17528 // invasive check available on the public surface: a fresh
17529 // `with_limits` expander that expands a repeat-call site must
17530 // populate at least one cache entry (bytecode + cache both on),
17531 // and a substitute-only sibling would leave `cache_size()` at
17532 // zero even after every call site expanded. A regression that
17533 // wired the wrong execution strategy into `with_limits`
17534 // (compile_templates: false, cache_enabled: false) fires here.
17535 let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
17536 let forms = read(
17537 "(defmacro id (x) `,x)
17538 (id one)
17539 (id one)",
17540 )
17541 .unwrap();
17542 e.expand_program(forms).unwrap();
17543 assert!(
17544 e.cache_size() >= 1,
17545 "with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
17546 e.cache_size()
17547 );
17548 }
17549
17550 #[test]
17551 fn expander_with_limits_fires_the_arity_gate_from_construction() {
17552 // Behavioral pin — the at-construction bundle assignment MUST
17553 // reach the SAME check sites the individual + bulk POST-
17554 // construction setters do. Construct an expander with
17555 // `max_macro_arity: 1` directly and confirm the arity gate
17556 // fires on a two-arg macro registration. Sibling of
17557 // `resource_limits_bulk_setter_keeps_the_arity_gate_in_effect`
17558 // one CONSTRUCTOR-STAGE axis over — that pin covers the POST-
17559 // construction setter reaching the gate; this pin covers the
17560 // AT-construction assignment reaching the SAME gate through
17561 // the SAME `self.limits.max_macro_arity` load. A regression
17562 // that wired `with_limits` to a shadow field rather than
17563 // `self.limits` fails here.
17564 let mut e = Expander::with_limits(ResourceLimits {
17565 max_macro_arity: 1,
17566 ..DEFAULT_RESOURCE_LIMITS
17567 });
17568 let forms = read("(defmacro two (a b) `,a)").unwrap();
17569 let err = e.expand_program(forms).unwrap_err();
17570 assert!(
17571 matches!(
17572 err,
17573 LispError::MacroArityExceeded {
17574 arity: 2,
17575 limit: 1,
17576 ..
17577 }
17578 ),
17579 "at-construction arity ceiling must reach the register-time gate; got: {err:?}"
17580 );
17581 }
17582
17583 // ── UNBOUNDED_RESOURCE_LIMITS — ceiling-lifted preset posture ──────
17584
17585 #[test]
17586 fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
17587 // Field-level pin — the ceiling-lifted preset carries
17588 // [`usize::MAX`] on every axis of the six-field surface. Pinned
17589 // as six independent field asserts (rather than one struct
17590 // equality) so a drift on ONE field carries a distinct-name
17591 // failure that names the drifting axis. A future extension
17592 // that adds a SEVENTH ceiling requires an additional field
17593 // assert here in lockstep with the const literal — rustc's
17594 // field-exhaustiveness on the const forces the new field to
17595 // appear, and this pin exercises it.
17596 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
17597 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
17598 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
17599 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
17600 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
17601 assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
17602 }
17603
17604 #[test]
17605 fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
17606 // Structural-distinctness pin — the two constants
17607 // [`DEFAULT_RESOURCE_LIMITS`] and [`UNBOUNDED_RESOURCE_LIMITS`]
17608 // sit at DISTINCT points on the posture axis of the (posture ×
17609 // aggregation) grid, and the disagreement is EXHAUSTIVE on the
17610 // six-field surface — every shipped `DEFAULT_MAX_*` module
17611 // constant is a concrete positive value strictly less than
17612 // [`usize::MAX`]. Pins that a future re-tuning which cranked one
17613 // shipped default up to [`usize::MAX`] (collapsing the two
17614 // presets on that field) fires here loudly rather than
17615 // silently letting the two presets become indistinguishable
17616 // on that axis. A regression which flipped ONE unbounded-const
17617 // field back to its default value also fires here — either
17618 // half of the disagreement breaks the theorem.
17619 assert_ne!(
17620 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
17621 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
17622 );
17623 assert_ne!(
17624 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
17625 DEFAULT_RESOURCE_LIMITS.max_cache_entries
17626 );
17627 assert_ne!(
17628 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
17629 DEFAULT_RESOURCE_LIMITS.max_expansion_size
17630 );
17631 assert_ne!(
17632 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
17633 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
17634 );
17635 assert_ne!(
17636 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
17637 DEFAULT_RESOURCE_LIMITS.max_registered_macros
17638 );
17639 assert_ne!(
17640 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
17641 DEFAULT_RESOURCE_LIMITS.max_macro_arity
17642 );
17643 }
17644
17645 #[test]
17646 fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
17647 // Round-trip pin at CONSTRUCTION time — an expander built at
17648 // the ceiling-lifted preset MUST project back through
17649 // [`Expander::resource_limits`] to the SAME constant verbatim.
17650 // Peer of `expander_with_limits_seeds_the_provided_resource_posture`
17651 // one PRESET-POSTURE axis over — that pin exercises the
17652 // constructor's carry via a distinct-low-value bundle; this
17653 // pin exercises it via the ceiling-lifted preset constant.
17654 // Cross-pin the six-field snapshot equals the ceiling-lifted
17655 // constant so a regression that broke either the constant OR
17656 // the constructor's carry fires here.
17657 assert_eq!(
17658 Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
17659 UNBOUNDED_RESOURCE_LIMITS
17660 );
17661 }
17662
17663 #[test]
17664 fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
17665 // Behavioral pin — the ceiling-lifted preset MUST reach the
17666 // register-time body-size gate as an actually-lifted ceiling.
17667 // Register a macro whose body node count strictly exceeds
17668 // [`DEFAULT_MAX_MACRO_BODY_SIZE`] (a body which under the
17669 // shipped default would fail [`register_macro_def`] with a
17670 // [`LispError::MacroBodySizeExceeded`]) and confirm the
17671 // registration succeeds under the ceiling-lifted preset. Only
17672 // the body-size ceiling being lifted lets a body of that
17673 // magnitude land; the arity ceiling is left at 1 param via
17674 // struct-update override to prove the preset's other five
17675 // ceilings STILL project cleanly at [`usize::MAX`] alongside
17676 // the ceiling under test. A regression that failed to lift
17677 // ONE ceiling across the preset (a stale `DEFAULT_MAX_*` field
17678 // in the const literal) fires here on the axis it dropped.
17679 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17680 // Build a huge quasi-quoted list body: `(k k k k k … k) whose
17681 // node_count() eclipses DEFAULT_MAX_MACRO_BODY_SIZE = 16384.
17682 let mut body = String::from("(defmacro huge (k) `(");
17683 for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
17684 body.push_str(",k ");
17685 }
17686 body.push_str("))");
17687 let forms = read(&body).unwrap();
17688 e.expand_program(forms)
17689 .expect("unbounded body-size ceiling must admit a body over the default limit");
17690 assert!(e.has("huge"));
17691 }
17692
17693 #[test]
17694 fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
17695 // Behavioral pin sibling of the body-size pin above one
17696 // RESOURCE-DIMENSION axis over on the REGISTER-time surface —
17697 // the ceiling-lifted preset MUST also reach the arity gate as
17698 // an actually-lifted ceiling. Register a macro whose param
17699 // list strictly exceeds [`DEFAULT_MAX_MACRO_ARITY`] (a
17700 // 129-slot lambda list which under the shipped default fails
17701 // with [`LispError::MacroArityExceeded`]) and confirm the
17702 // registration succeeds under the ceiling-lifted preset.
17703 use std::fmt::Write as _;
17704 let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17705 let mut src = String::from("(defmacro many-arity (");
17706 for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
17707 write!(src, "a-{i} ").unwrap();
17708 }
17709 src.push_str(") `,a-0)");
17710 let forms = read(&src).unwrap();
17711 e.expand_program(forms)
17712 .expect("unbounded arity ceiling must admit a param list over the default limit");
17713 assert!(e.has("many-arity"));
17714 }
17715
17716 #[test]
17717 fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
17718 // Ergonomic pin — the `Copy` posture on [`ResourceLimits`] lets
17719 // a caller build "every ceiling lifted EXCEPT this ONE" as ONE
17720 // struct-update literal on the ceiling-lifted preset AND thread
17721 // that composition through [`Expander::with_limits`] as ONE
17722 // typed call. This is the "only THIS ceiling gates" fixture
17723 // shape a chain of six independent setters cannot express in
17724 // ONE expression, and the shape the last commit named as a
17725 // future benefit of the (getter, setter, at-construction)
17726 // three-corner face closing at ONE bundle.
17727 //
17728 // Set only `max_macro_arity: 1`; confirm the arity gate fires
17729 // on a two-arg registration while the five other lifted
17730 // ceilings project through the getter as [`usize::MAX`]. A
17731 // regression that broke the ceiling-lifted preset would EITHER
17732 // fail to project [`usize::MAX`] on one of the five other
17733 // fields OR fail to gate the isolated arity ceiling at 1.
17734 let mut e = Expander::with_limits(ResourceLimits {
17735 max_macro_arity: 1,
17736 ..UNBOUNDED_RESOURCE_LIMITS
17737 });
17738 assert_eq!(e.max_macro_arity(), 1);
17739 assert_eq!(e.max_expansion_depth(), usize::MAX);
17740 assert_eq!(e.max_cache_entries(), usize::MAX);
17741 assert_eq!(e.max_expansion_size(), usize::MAX);
17742 assert_eq!(e.max_macro_body_size(), usize::MAX);
17743 assert_eq!(e.max_registered_macros(), usize::MAX);
17744 let forms = read("(defmacro two (a b) `,a)").unwrap();
17745 let err = e.expand_program(forms).unwrap_err();
17746 assert!(
17747 matches!(
17748 err,
17749 LispError::MacroArityExceeded {
17750 arity: 2,
17751 limit: 1,
17752 ..
17753 }
17754 ),
17755 "struct-update override on the ceiling-lifted preset must \
17756 leave the isolated arity ceiling gating; got: {err:?}"
17757 );
17758 }
17759
17760 #[test]
17761 fn unbounded_resource_limits_carries_through_the_bulk_setter() {
17762 // Coherence pin between the AT-construction constructor and
17763 // the POST-construction bulk setter — both paths that reach the
17764 // ceiling-lifted preset must land at the SAME six values on
17765 // the expander state. Pre-lift a caller wanting the unbounded
17766 // posture composed six individual setter invocations at the
17767 // call site; post-lift both `Expander::with_limits(UNBOUNDED_
17768 // RESOURCE_LIMITS)` and `e.set_resource_limits(UNBOUNDED_
17769 // RESOURCE_LIMITS)` reach the SAME snapshot through the SAME
17770 // constant. A regression that wired the bulk setter to a
17771 // shadow field, or the ceiling-lifted preset to a stale field
17772 // literal, fires here on the constructor-vs-setter identity.
17773 let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
17774 let mut b = Expander::new();
17775 b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
17776 assert_eq!(a.resource_limits(), b.resource_limits());
17777 assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
17778 }
17779
17780 // ── ResourceLimits::strictest / ::most_permissive — meet/join ──────
17781
17782 /// A hand-authored asymmetric posture used across the lattice-law
17783 /// tests. Every field is a distinct positive value strictly less
17784 /// than [`usize::MAX`] and distinct from every corresponding
17785 /// [`DEFAULT_MAX_*`] module constant, so composition witnesses
17786 /// (meet, join, absorption, distributivity) discriminate across
17787 /// all three postures unambiguously.
17788 const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
17789 max_expansion_depth: 7,
17790 max_cache_entries: 11,
17791 max_expansion_size: 13,
17792 max_macro_body_size: 17,
17793 max_registered_macros: 19,
17794 max_macro_arity: 23,
17795 };
17796
17797 /// A second asymmetric posture whose per-axis values are on both
17798 /// sides of `HAND_AUTHORED_MID_POSTURE`'s values — three axes
17799 /// smaller (so the meet picks THIS posture on those axes and the
17800 /// join picks `MID` on those axes) and three axes larger (mirror).
17801 /// Together the two postures exercise every ordering combination
17802 /// on the six-axis pointwise `min`/`max` cascade.
17803 const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
17804 max_expansion_depth: 3, // smaller than MID's 7
17805 max_cache_entries: 29, // larger than MID's 11
17806 max_expansion_size: 5, // smaller than MID's 13
17807 max_macro_body_size: 31, // larger than MID's 17
17808 max_registered_macros: 2, // smaller than MID's 19
17809 max_macro_arity: 41, // larger than MID's 23
17810 };
17811
17812 #[test]
17813 fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
17814 // Concrete-preset pin — the meet of the (shipped default,
17815 // ceiling-lifted unbounded) preset pair is the DEFAULT preset
17816 // structurally: every `DEFAULT_MAX_*` module constant is a
17817 // concrete positive value strictly less than [`usize::MAX`],
17818 // so on every axis the pointwise-`min` picks the DEFAULT side.
17819 // Peer of `most_permissive_of_default_and_unbounded_projects_the_unbounded`
17820 // one COMBINATOR axis over on the same shipped-preset-pair
17821 // surface.
17822 assert_eq!(
17823 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17824 DEFAULT_RESOURCE_LIMITS,
17825 );
17826 assert_eq!(
17827 UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17828 DEFAULT_RESOURCE_LIMITS,
17829 );
17830 }
17831
17832 #[test]
17833 fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
17834 // Concrete-preset pin — the join of the (shipped default,
17835 // ceiling-lifted unbounded) preset pair is the UNBOUNDED
17836 // preset structurally: every axis's pointwise-`max` picks the
17837 // [`usize::MAX`] side over any concrete default. Peer of
17838 // `strictest_of_default_and_unbounded_projects_the_default`.
17839 assert_eq!(
17840 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17841 UNBOUNDED_RESOURCE_LIMITS,
17842 );
17843 assert_eq!(
17844 UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17845 UNBOUNDED_RESOURCE_LIMITS,
17846 );
17847 }
17848
17849 #[test]
17850 fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
17851 // Field-level pin — the meet operation projects the pointwise
17852 // `min` across the six-field surface. Pinned as six
17853 // independent field asserts (rather than one struct equality)
17854 // so a drift on ONE axis carries a distinct-name failure that
17855 // names the drifting axis; a future SEVENTH ceiling extension
17856 // requires an additional field assert here in lockstep.
17857 let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
17858 assert_eq!(m.max_expansion_depth, 3);
17859 assert_eq!(m.max_cache_entries, 11);
17860 assert_eq!(m.max_expansion_size, 5);
17861 assert_eq!(m.max_macro_body_size, 17);
17862 assert_eq!(m.max_registered_macros, 2);
17863 assert_eq!(m.max_macro_arity, 23);
17864 }
17865
17866 #[test]
17867 fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
17868 // Field-level pin — the join operation projects the pointwise
17869 // `max` across the six-field surface. Pinned as six
17870 // independent field asserts so a drift on ONE axis carries a
17871 // distinct-name failure that names the drifting axis.
17872 let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
17873 assert_eq!(j.max_expansion_depth, 7);
17874 assert_eq!(j.max_cache_entries, 29);
17875 assert_eq!(j.max_expansion_size, 13);
17876 assert_eq!(j.max_macro_body_size, 31);
17877 assert_eq!(j.max_registered_macros, 19);
17878 assert_eq!(j.max_macro_arity, 41);
17879 }
17880
17881 #[test]
17882 fn resource_limits_strictest_is_idempotent() {
17883 // Lattice law — `a ∧ a = a`. Every posture is a fixed point of
17884 // its own meet with itself; a regression that changed the
17885 // per-axis primitive to something other than pointwise `min`
17886 // (a `min - 1` off-by-one, or an arbitrary tie-breaker on
17887 // equal values) fires here.
17888 assert_eq!(
17889 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
17890 DEFAULT_RESOURCE_LIMITS,
17891 );
17892 assert_eq!(
17893 UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
17894 UNBOUNDED_RESOURCE_LIMITS,
17895 );
17896 assert_eq!(
17897 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17898 HAND_AUTHORED_MID_POSTURE,
17899 );
17900 }
17901
17902 #[test]
17903 fn resource_limits_most_permissive_is_idempotent() {
17904 // Lattice law — `a ∨ a = a`. Sibling of the strictest
17905 // idempotence pin one COMBINATOR axis over.
17906 assert_eq!(
17907 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
17908 DEFAULT_RESOURCE_LIMITS,
17909 );
17910 assert_eq!(
17911 UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
17912 UNBOUNDED_RESOURCE_LIMITS,
17913 );
17914 assert_eq!(
17915 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17916 HAND_AUTHORED_MID_POSTURE,
17917 );
17918 }
17919
17920 #[test]
17921 fn resource_limits_strictest_is_commutative() {
17922 // Lattice law — `a ∧ b = b ∧ a`. Pointwise `min` is symmetric
17923 // on every axis, so the composition inherits commutativity
17924 // structurally. A regression that broke ordering (e.g. a
17925 // per-axis "prefer left on tie") fires here.
17926 assert_eq!(
17927 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
17928 HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
17929 );
17930 assert_eq!(
17931 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
17932 HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
17933 );
17934 }
17935
17936 #[test]
17937 fn resource_limits_most_permissive_is_commutative() {
17938 // Lattice law — `a ∨ b = b ∨ a`. Sibling of the strictest
17939 // commutativity pin one COMBINATOR axis over.
17940 assert_eq!(
17941 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
17942 HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
17943 );
17944 assert_eq!(
17945 DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
17946 HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
17947 );
17948 }
17949
17950 #[test]
17951 fn resource_limits_strictest_is_associative() {
17952 // Lattice law — `(a ∧ b) ∧ c = a ∧ (b ∧ c)`. Pointwise `min`
17953 // is associative on every axis, so the composition inherits
17954 // associativity structurally. A regression that broke
17955 // grouping (e.g. an accidental accumulator that skewed the
17956 // order of `min` invocations) fires here.
17957 let a = DEFAULT_RESOURCE_LIMITS;
17958 let b = HAND_AUTHORED_MID_POSTURE;
17959 let c = HAND_AUTHORED_OTHER_POSTURE;
17960 assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
17961 }
17962
17963 #[test]
17964 fn resource_limits_most_permissive_is_associative() {
17965 // Lattice law — `(a ∨ b) ∨ c = a ∨ (b ∨ c)`. Sibling of the
17966 // strictest associativity pin one COMBINATOR axis over.
17967 let a = DEFAULT_RESOURCE_LIMITS;
17968 let b = HAND_AUTHORED_MID_POSTURE;
17969 let c = HAND_AUTHORED_OTHER_POSTURE;
17970 assert_eq!(
17971 a.most_permissive(b).most_permissive(c),
17972 a.most_permissive(b.most_permissive(c)),
17973 );
17974 }
17975
17976 #[test]
17977 fn resource_limits_meet_and_join_satisfy_absorption() {
17978 // Lattice absorption identities — `a ∧ (a ∨ b) = a` AND
17979 // `a ∨ (a ∧ b) = a`. Together with commutativity, idempotence,
17980 // and associativity they define the lattice axioms; the two
17981 // absorption laws are the operator PAIR's interlock (each
17982 // undoes the other's contribution on the shared operand).
17983 // Any per-axis primitive pair for which `min(a, max(a, b)) = a`
17984 // AND `max(a, min(a, b)) = a` inherits this at the pointwise
17985 // composition; pointwise-`min`/`max` on [`usize`] satisfy both.
17986 let a = HAND_AUTHORED_MID_POSTURE;
17987 let b = HAND_AUTHORED_OTHER_POSTURE;
17988 assert_eq!(a.strictest(a.most_permissive(b)), a);
17989 assert_eq!(a.most_permissive(a.strictest(b)), a);
17990 }
17991
17992 #[test]
17993 fn resource_limits_meet_distributes_over_join() {
17994 // Distributive-lattice pin — `a ∧ (b ∨ c) = (a ∧ b) ∨ (a ∧ c)`.
17995 // Pointwise-`min` distributes over pointwise-`max` on [`usize`]
17996 // on every axis, so the composition inherits distributivity
17997 // structurally. A regression that broke the pointwise
17998 // structure (e.g. a per-axis operation that used SUBTRACTION
17999 // rather than MIN/MAX) fires here — subtraction does NOT
18000 // distribute over addition and would break the law on the
18001 // asymmetric-posture witness pair.
18002 let a = DEFAULT_RESOURCE_LIMITS;
18003 let b = HAND_AUTHORED_MID_POSTURE;
18004 let c = HAND_AUTHORED_OTHER_POSTURE;
18005 assert_eq!(
18006 a.strictest(b.most_permissive(c)),
18007 a.strictest(b).most_permissive(a.strictest(c)),
18008 );
18009 }
18010
18011 #[test]
18012 fn resource_limits_join_distributes_over_meet() {
18013 // Distributive-lattice pin, sibling of the meet-over-join
18014 // distributivity pin one COMBINATOR axis over — `a ∨ (b ∧ c) =
18015 // (a ∨ b) ∧ (a ∨ c)`. In a distributive lattice BOTH
18016 // distributivity identities hold; pin both so a regression
18017 // that broke ONE without the other cannot slip through.
18018 let a = DEFAULT_RESOURCE_LIMITS;
18019 let b = HAND_AUTHORED_MID_POSTURE;
18020 let c = HAND_AUTHORED_OTHER_POSTURE;
18021 assert_eq!(
18022 a.most_permissive(b.strictest(c)),
18023 a.most_permissive(b).strictest(a.most_permissive(c)),
18024 );
18025 }
18026
18027 #[test]
18028 fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
18029 // Meet-lower-bound pin — the meet lies at or below BOTH
18030 // operands on every axis. This is the structural GLB property
18031 // of the meet in the pointwise partial order (tighter-first):
18032 // any admissible input under the meet is admissible under
18033 // both operands, so the meet's ceiling must not exceed either
18034 // operand's ceiling on any axis.
18035 let a = HAND_AUTHORED_MID_POSTURE;
18036 let b = HAND_AUTHORED_OTHER_POSTURE;
18037 let m = a.strictest(b);
18038 assert!(m.max_expansion_depth <= a.max_expansion_depth);
18039 assert!(m.max_expansion_depth <= b.max_expansion_depth);
18040 assert!(m.max_cache_entries <= a.max_cache_entries);
18041 assert!(m.max_cache_entries <= b.max_cache_entries);
18042 assert!(m.max_expansion_size <= a.max_expansion_size);
18043 assert!(m.max_expansion_size <= b.max_expansion_size);
18044 assert!(m.max_macro_body_size <= a.max_macro_body_size);
18045 assert!(m.max_macro_body_size <= b.max_macro_body_size);
18046 assert!(m.max_registered_macros <= a.max_registered_macros);
18047 assert!(m.max_registered_macros <= b.max_registered_macros);
18048 assert!(m.max_macro_arity <= a.max_macro_arity);
18049 assert!(m.max_macro_arity <= b.max_macro_arity);
18050 }
18051
18052 #[test]
18053 fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
18054 // Join-upper-bound pin — the join lies at or above BOTH
18055 // operands on every axis. Sibling of the strictest-lower-
18056 // bound pin one COMBINATOR axis over; the structural LUB
18057 // property of the join in the pointwise partial order.
18058 let a = HAND_AUTHORED_MID_POSTURE;
18059 let b = HAND_AUTHORED_OTHER_POSTURE;
18060 let j = a.most_permissive(b);
18061 assert!(j.max_expansion_depth >= a.max_expansion_depth);
18062 assert!(j.max_expansion_depth >= b.max_expansion_depth);
18063 assert!(j.max_cache_entries >= a.max_cache_entries);
18064 assert!(j.max_cache_entries >= b.max_cache_entries);
18065 assert!(j.max_expansion_size >= a.max_expansion_size);
18066 assert!(j.max_expansion_size >= b.max_expansion_size);
18067 assert!(j.max_macro_body_size >= a.max_macro_body_size);
18068 assert!(j.max_macro_body_size >= b.max_macro_body_size);
18069 assert!(j.max_registered_macros >= a.max_registered_macros);
18070 assert!(j.max_registered_macros >= b.max_registered_macros);
18071 assert!(j.max_macro_arity >= a.max_macro_arity);
18072 assert!(j.max_macro_arity >= b.max_macro_arity);
18073 }
18074
18075 #[test]
18076 fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
18077 // Const-fn pin — the meet is evaluable in const context, so a
18078 // caller can stitch a new `pub const` preset from two shipped
18079 // presets without deferring the composition to runtime. Pinned
18080 // as a `const` binding at the item level so the compiler
18081 // exercises the const-fn path structurally; the runtime
18082 // assertion is redundant for a `const` value that already
18083 // evaluated at compile time, but pins the identity as a typed
18084 // theorem alongside every OTHER `strictest` pin above.
18085 const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
18086 DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
18087 assert_eq!(
18088 DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
18089 min_usize(
18090 DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18091 HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
18092 ),
18093 );
18094 // Extra check: the composition MUST project the MID posture's
18095 // small value on every axis where MID is smaller than DEFAULT
18096 // — MID's per-axis values (7, 11, 13, 17, 19, 23) are all
18097 // strictly less than every `DEFAULT_MAX_*` constant, so the
18098 // meet projects MID verbatim.
18099 assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
18100 }
18101
18102 #[test]
18103 fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
18104 // Const-fn pin sibling of the strictest const-composition pin
18105 // one COMBINATOR axis over — the join is also `const fn`, so
18106 // a caller can stitch a new `pub const` preset via the LUB
18107 // combinator at compile time.
18108 const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
18109 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
18110 assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
18111 }
18112
18113 // ── ResourceLimits::leq — pointwise partial order ─────────────────
18114
18115 #[test]
18116 fn resource_limits_leq_is_pointwise_field_conjunction() {
18117 // Field-level pin — the leq relation projects the pointwise
18118 // `<=` conjunction across the six-field surface. `LOOSER` is
18119 // `MID + 1` on every axis, so `MID.leq(LOOSER)` holds; a
18120 // per-axis tighten on ANY field alone flips the relation to
18121 // false. Pinned as six independent tighten-per-axis asserts so
18122 // a drift on ONE axis carries a distinct-name failure that
18123 // names the drifting axis; a future SEVENTH ceiling extension
18124 // requires an additional per-axis tighten assert here in
18125 // lockstep.
18126 const LOOSER: ResourceLimits = ResourceLimits {
18127 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18128 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18129 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18130 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18131 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18132 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18133 };
18134 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18135 assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
18136
18137 // Per-axis exceed pins: raise MID's field ONE axis above LOOSER
18138 // on that axis; the leq must flip to false. Every other axis
18139 // stays at MID, so LOOSER still dominates on those axes —
18140 // isolating the single-axis violation.
18141 let exceed_depth = ResourceLimits {
18142 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18143 ..HAND_AUTHORED_MID_POSTURE
18144 };
18145 assert!(!exceed_depth.leq(LOOSER));
18146 let exceed_cache = ResourceLimits {
18147 max_cache_entries: LOOSER.max_cache_entries + 1,
18148 ..HAND_AUTHORED_MID_POSTURE
18149 };
18150 assert!(!exceed_cache.leq(LOOSER));
18151 let exceed_expansion = ResourceLimits {
18152 max_expansion_size: LOOSER.max_expansion_size + 1,
18153 ..HAND_AUTHORED_MID_POSTURE
18154 };
18155 assert!(!exceed_expansion.leq(LOOSER));
18156 let exceed_body = ResourceLimits {
18157 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18158 ..HAND_AUTHORED_MID_POSTURE
18159 };
18160 assert!(!exceed_body.leq(LOOSER));
18161 let exceed_registered = ResourceLimits {
18162 max_registered_macros: LOOSER.max_registered_macros + 1,
18163 ..HAND_AUTHORED_MID_POSTURE
18164 };
18165 assert!(!exceed_registered.leq(LOOSER));
18166 let exceed_arity = ResourceLimits {
18167 max_macro_arity: LOOSER.max_macro_arity + 1,
18168 ..HAND_AUTHORED_MID_POSTURE
18169 };
18170 assert!(!exceed_arity.leq(LOOSER));
18171 }
18172
18173 #[test]
18174 fn resource_limits_leq_is_reflexive() {
18175 // Partial-order law — `a ≤ a` for every posture. Reflexivity
18176 // is the identity axiom on any lattice's underlying partial
18177 // order; a regression that changed the per-axis primitive to
18178 // strict `<` (or that added an accidental "prefer other on
18179 // tie" branch) fires here on every shipped posture.
18180 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18181 assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18182 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18183 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18184 }
18185
18186 #[test]
18187 fn resource_limits_leq_is_antisymmetric() {
18188 // Partial-order law — `a ≤ b ∧ b ≤ a ⇒ a = b`. The mutual-
18189 // dominance witness on a partial order pins the two operands
18190 // to structural equality. On the pointwise `<=` composition
18191 // this holds because `a.max_X <= b.max_X && b.max_X <= a.max_X
18192 // ⇒ a.max_X == b.max_X` on every axis, and struct equality
18193 // is field-conjunction. Sibling to reflexivity one AXIOM axis
18194 // over on the partial-order axiom surface.
18195 //
18196 // Two distinct constructions of the SAME six-field posture
18197 // (one via struct literal, one via `..DEFAULT_RESOURCE_LIMITS`
18198 // spread of the shipped preset) — mutual leq holds; struct
18199 // equality holds; antisymmetry pin closes.
18200 let a = DEFAULT_RESOURCE_LIMITS;
18201 let b = ResourceLimits {
18202 max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
18203 max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
18204 max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
18205 max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
18206 max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
18207 max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
18208 };
18209 assert!(a.leq(b));
18210 assert!(b.leq(a));
18211 assert_eq!(a, b);
18212 }
18213
18214 #[test]
18215 fn resource_limits_leq_is_transitive() {
18216 // Partial-order law — `a ≤ b ∧ b ≤ c ⇒ a ≤ c`. Transitivity
18217 // is the composition axiom on any lattice's partial order.
18218 // Pointwise `<=` on [`usize`] is transitive per axis, so the
18219 // six-field conjunction inherits it structurally.
18220 //
18221 // Chain witness: MID's every-axis `+1` shift is LOOSER, and
18222 // LOOSER's every-axis `+1` shift is LOOSEST; the chain MID
18223 // ≤ LOOSER ≤ LOOSEST implies MID ≤ LOOSEST.
18224 const LOOSER: ResourceLimits = ResourceLimits {
18225 max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
18226 max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
18227 max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
18228 max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
18229 max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
18230 max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
18231 };
18232 const LOOSEST: ResourceLimits = ResourceLimits {
18233 max_expansion_depth: LOOSER.max_expansion_depth + 1,
18234 max_cache_entries: LOOSER.max_cache_entries + 1,
18235 max_expansion_size: LOOSER.max_expansion_size + 1,
18236 max_macro_body_size: LOOSER.max_macro_body_size + 1,
18237 max_registered_macros: LOOSER.max_registered_macros + 1,
18238 max_macro_arity: LOOSER.max_macro_arity + 1,
18239 };
18240 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
18241 assert!(LOOSER.leq(LOOSEST));
18242 assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
18243 }
18244
18245 #[test]
18246 fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
18247 // Concrete-preset pin — the leq relation on the shipped
18248 // preset pair is STRICT: DEFAULT ≤ UNBOUNDED holds (every
18249 // `DEFAULT_MAX_*` module constant is a concrete positive
18250 // value at most [`usize::MAX`]) AND UNBOUNDED ≤ DEFAULT
18251 // fails (every `DEFAULT_MAX_*` is strictly less than
18252 // [`usize::MAX`], so on every axis [`usize::MAX`] exceeds
18253 // the matching default). Peer of
18254 // `strictest_of_default_and_unbounded_projects_the_default`
18255 // one PRIMITIVE-KIND axis over on the same shipped-preset-
18256 // pair surface: where the meet composition projects DEFAULT,
18257 // this relation asserts DEFAULT is the tighter of the two.
18258 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18259 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18260 }
18261
18262 #[test]
18263 fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
18264 // Partial-order NON-total pin — the two hand-authored asymmetric
18265 // postures have MID smaller on three axes (depth, expansion,
18266 // registered) and OTHER smaller on the other three (cache,
18267 // body, arity); neither dominates the other, so BOTH
18268 // directions of the leq relation fail. The pin discriminates
18269 // a regression that promoted `leq` from partial to total
18270 // (e.g. an accidental `||` swap in the conjunction, which
18271 // would make every pair comparable).
18272 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18273 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
18274 }
18275
18276 #[test]
18277 fn resource_limits_leq_agrees_with_meet() {
18278 // Lattice cross-check axiom — `a ≤ b ⇔ a ⊓ b = a`. Every
18279 // classification lattice one crate over
18280 // (`tatara-lattice/src/lib.rs`'s preamble) declares this
18281 // agreement axiom binding the partial order to the meet
18282 // combinator, and the default-impl body on
18283 // `tatara_lattice::Lattice::leq` derives `leq` FROM `meet`
18284 // through it. This pin discharges the same axiom on the
18285 // ResourceLimits algebra as a two-direction structural
18286 // equality across the strict-order preset pair AND the
18287 // reflexivity witness.
18288 //
18289 // Forward (a ≤ b ⇒ a ⊓ b = a):
18290 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18291 assert_eq!(
18292 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18293 DEFAULT_RESOURCE_LIMITS,
18294 );
18295 // Reverse (a ⊓ b = a ⇒ a ≤ b), reflexivity witness:
18296 assert_eq!(
18297 DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18298 DEFAULT_RESOURCE_LIMITS,
18299 );
18300 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18301
18302 // Non-agreement witness — the incomparable asymmetric postures
18303 // have meet distinct from EITHER operand, and leq false in
18304 // both directions; agreement holds on the negative side too.
18305 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18306 assert_ne!(
18307 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18308 HAND_AUTHORED_MID_POSTURE,
18309 );
18310 }
18311
18312 #[test]
18313 fn resource_limits_leq_agrees_with_join() {
18314 // Lattice cross-check axiom, sibling of the meet-agreement
18315 // pin one COMBINATOR axis over — `a ≤ b ⇔ a ⊔ b = b`.
18316 //
18317 // Forward (a ≤ b ⇒ a ⊔ b = b):
18318 assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18319 assert_eq!(
18320 DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18321 UNBOUNDED_RESOURCE_LIMITS,
18322 );
18323 // Reverse (a ⊔ b = b ⇒ a ≤ b), reflexivity witness:
18324 assert_eq!(
18325 DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18326 DEFAULT_RESOURCE_LIMITS,
18327 );
18328 assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18329
18330 // Non-agreement witness — the incomparable asymmetric postures
18331 // have join distinct from EITHER operand.
18332 assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
18333 assert_ne!(
18334 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18335 HAND_AUTHORED_OTHER_POSTURE,
18336 );
18337 }
18338
18339 #[test]
18340 fn resource_limits_strictest_is_leq_both_operands() {
18341 // Meet-lower-bound pin, typed lattice-relation companion to
18342 // `resource_limits_strictest_is_dominated_by_both_operands_pointwise`
18343 // one PRIMITIVE-KIND axis over — where the per-axis pin
18344 // discharged the six independent `<=` asserts inline, this pin
18345 // routes through the typed `leq` primitive, so the
18346 // greatest-lower-bound property is stated in the LATTICE
18347 // vocabulary (meet-is-leq-both-operands) rather than the
18348 // six-fold field-conjunction vocabulary.
18349 let a = HAND_AUTHORED_MID_POSTURE;
18350 let b = HAND_AUTHORED_OTHER_POSTURE;
18351 let m = a.strictest(b);
18352 assert!(m.leq(a));
18353 assert!(m.leq(b));
18354 }
18355
18356 #[test]
18357 fn resource_limits_most_permissive_is_geq_both_operands() {
18358 // Join-upper-bound pin, typed lattice-relation companion to
18359 // `resource_limits_most_permissive_dominates_both_operands_pointwise`
18360 // one PRIMITIVE-KIND axis over. Stated in the LATTICE
18361 // vocabulary as `a ≤ a ⊔ b ∧ b ≤ a ⊔ b` — sibling of the
18362 // meet-lower-bound pin one COMBINATOR axis over.
18363 let a = HAND_AUTHORED_MID_POSTURE;
18364 let b = HAND_AUTHORED_OTHER_POSTURE;
18365 let j = a.most_permissive(b);
18366 assert!(a.leq(j));
18367 assert!(b.leq(j));
18368 }
18369
18370 #[test]
18371 fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
18372 // Const-fn pin — the leq relation is evaluable in const
18373 // context, so a caller can pin a preset-relation identity at
18374 // compile time. Sibling of the const-fn composition pins on
18375 // `strictest` / `most_permissive` one PRIMITIVE-KIND axis
18376 // over: those two operations compose two presets into a
18377 // const-time third, and this operation decides a const-time
18378 // yes/no on two presets. `const _: ()` at item scope is a
18379 // compile-time proof — the compiler rejects the module if
18380 // either identity does not hold; a runtime `assert!` over the
18381 // same expression would fire at test time on the same value
18382 // clippy's `assertions_on_constants` correctly names as a
18383 // redundant late check on a compile-time constant.
18384 //
18385 // Strict-order preset-pair pin — DEFAULT ≤ UNBOUNDED (every
18386 // `DEFAULT_MAX_*` module constant is at most [`usize::MAX`]):
18387 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18388 // Reverse direction fails — [`usize::MAX`] exceeds every
18389 // concrete `DEFAULT_MAX_*` constant, so the partial order is
18390 // STRICT on the shipped preset pair:
18391 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18392 }
18393
18394 // ── EMPTY_RESOURCE_LIMITS — bounded-lattice bottom preset ─────────
18395
18396 #[test]
18397 fn empty_resource_limits_binds_every_ceiling_to_zero() {
18398 // Field-level pin — the zero preset carries `0` on every axis
18399 // of the six-field surface. Pinned as six independent field
18400 // asserts (rather than one struct equality) so a drift on ONE
18401 // field carries a distinct-name failure that names the drifting
18402 // axis. A future extension that adds a SEVENTH ceiling requires
18403 // an additional field assert here in lockstep with the const
18404 // literal — rustc's field-exhaustiveness on the const forces
18405 // the new field to appear, and this pin exercises the seed
18406 // value. Structural peer of
18407 // `unbounded_resource_limits_binds_every_ceiling_to_usize_max`
18408 // one LATTICE-POLE axis over on the (bottom, top)
18409 // bounded-lattice preset-pair surface.
18410 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
18411 assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
18412 assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
18413 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
18414 assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
18415 assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
18416 }
18417
18418 #[test]
18419 fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
18420 // Structural-distinctness pin — the two constants
18421 // [`DEFAULT_RESOURCE_LIMITS`] and [`EMPTY_RESOURCE_LIMITS`] sit
18422 // at DISTINCT points on the preset-posture axis, and the
18423 // disagreement is EXHAUSTIVE on the six-field surface — every
18424 // shipped `DEFAULT_MAX_*` module constant is a concrete
18425 // strictly-positive value, so every axis distinguishes DEFAULT
18426 // from the all-zero bottom. Peer of
18427 // `unbounded_resource_limits_disagrees_with_default_on_every_ceiling`
18428 // one LATTICE-POLE axis over.
18429 assert_ne!(
18430 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18431 DEFAULT_RESOURCE_LIMITS.max_expansion_depth
18432 );
18433 assert_ne!(
18434 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18435 DEFAULT_RESOURCE_LIMITS.max_cache_entries
18436 );
18437 assert_ne!(
18438 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18439 DEFAULT_RESOURCE_LIMITS.max_expansion_size
18440 );
18441 assert_ne!(
18442 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18443 DEFAULT_RESOURCE_LIMITS.max_macro_body_size
18444 );
18445 assert_ne!(
18446 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18447 DEFAULT_RESOURCE_LIMITS.max_registered_macros
18448 );
18449 assert_ne!(
18450 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18451 DEFAULT_RESOURCE_LIMITS.max_macro_arity
18452 );
18453 }
18454
18455 #[test]
18456 fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
18457 // Diagonal-distinctness pin — the (BOTTOM, TOP) preset pair
18458 // disagrees on every axis (`0 != usize::MAX` on every field).
18459 // Structural companion to the DEFAULT-vs-EMPTY and
18460 // UNBOUNDED-vs-DEFAULT disagreement pins one PRESET-PAIR axis
18461 // over — together the three pin the six-field surface at the
18462 // full diagonal of the (EMPTY, DEFAULT, UNBOUNDED) triple.
18463 assert_ne!(
18464 EMPTY_RESOURCE_LIMITS.max_expansion_depth,
18465 UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
18466 );
18467 assert_ne!(
18468 EMPTY_RESOURCE_LIMITS.max_cache_entries,
18469 UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
18470 );
18471 assert_ne!(
18472 EMPTY_RESOURCE_LIMITS.max_expansion_size,
18473 UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
18474 );
18475 assert_ne!(
18476 EMPTY_RESOURCE_LIMITS.max_macro_body_size,
18477 UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
18478 );
18479 assert_ne!(
18480 EMPTY_RESOURCE_LIMITS.max_registered_macros,
18481 UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
18482 );
18483 assert_ne!(
18484 EMPTY_RESOURCE_LIMITS.max_macro_arity,
18485 UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
18486 );
18487 }
18488
18489 #[test]
18490 fn empty_resource_limits_is_the_join_identity() {
18491 // Lattice-identity law — `a ⊔ ⊥ = a`. Every posture's
18492 // pointwise `max` against 0 returns the posture verbatim, so
18493 // [`EMPTY_RESOURCE_LIMITS`] acts as the identity element of
18494 // the join monoid. Peer of
18495 // `empty_resource_limits_is_the_meet_annihilator` one
18496 // COMBINATOR axis over on the (meet, join) surface.
18497 //
18498 // Cross-preset exhaustion — verify the identity against every
18499 // named preset (DEFAULT, UNBOUNDED, both hand-authored
18500 // asymmetric postures) so a regression that broke ONE axis of
18501 // the algebra fires here on the operand whose field crosses
18502 // that axis's boundary.
18503 assert_eq!(
18504 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18505 DEFAULT_RESOURCE_LIMITS,
18506 );
18507 assert_eq!(
18508 EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
18509 DEFAULT_RESOURCE_LIMITS,
18510 );
18511 assert_eq!(
18512 UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18513 UNBOUNDED_RESOURCE_LIMITS,
18514 );
18515 assert_eq!(
18516 EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
18517 UNBOUNDED_RESOURCE_LIMITS,
18518 );
18519 assert_eq!(
18520 HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18521 HAND_AUTHORED_MID_POSTURE,
18522 );
18523 assert_eq!(
18524 HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
18525 HAND_AUTHORED_OTHER_POSTURE,
18526 );
18527 // Idempotence at the identity itself — sibling witness of the
18528 // `most_permissive_is_idempotent` general pin one PRESET axis
18529 // over.
18530 assert_eq!(
18531 EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18532 EMPTY_RESOURCE_LIMITS,
18533 );
18534 }
18535
18536 #[test]
18537 fn empty_resource_limits_is_the_meet_annihilator() {
18538 // Lattice-annihilator law — `a ⊓ ⊥ = ⊥`. Every posture's
18539 // pointwise `min` against 0 returns 0 on every axis, so
18540 // [`EMPTY_RESOURCE_LIMITS`] acts as the annihilator (absorbing
18541 // element) of the meet operation. Peer of
18542 // `empty_resource_limits_is_the_join_identity` one COMBINATOR
18543 // axis over — the identity on one side of the bounded lattice
18544 // is the annihilator on the other side.
18545 assert_eq!(
18546 DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18547 EMPTY_RESOURCE_LIMITS,
18548 );
18549 assert_eq!(
18550 EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
18551 EMPTY_RESOURCE_LIMITS,
18552 );
18553 assert_eq!(
18554 UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
18555 EMPTY_RESOURCE_LIMITS,
18556 );
18557 assert_eq!(
18558 EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18559 EMPTY_RESOURCE_LIMITS,
18560 );
18561 assert_eq!(
18562 HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18563 EMPTY_RESOURCE_LIMITS,
18564 );
18565 assert_eq!(
18566 HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
18567 EMPTY_RESOURCE_LIMITS,
18568 );
18569 }
18570
18571 #[test]
18572 fn empty_resource_limits_is_the_lattice_minimum() {
18573 // Partial-order minimum law — `⊥ ≤ a` for every `a`. Every
18574 // posture's field is a `usize` and `0 <= x` holds for every
18575 // `usize x`, so [`EMPTY_RESOURCE_LIMITS`] sits at or below
18576 // every posture on every axis of the pointwise `<=` conjunction.
18577 // Peer of the UNBOUNDED-is-maximum property on the
18578 // partial-order face one LATTICE-POLE axis over — those two
18579 // pin the (min, max) bounds of the bounded lattice.
18580 assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18581 assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18582 assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18583 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
18584 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
18585 // Reverse direction FAILS on every non-EMPTY preset (each has
18586 // at least one field strictly greater than 0). The partial
18587 // order is STRICT between EMPTY and every other named preset,
18588 // structurally exhausting the diagonal of the
18589 // (EMPTY, DEFAULT, UNBOUNDED, MID, OTHER) 5-preset ordering
18590 // face at the BOTTOM row.
18591 assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18592 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18593 assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18594 assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
18595 }
18596
18597 #[test]
18598 fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
18599 // Const-fn pin — the bounded-lattice identity + annihilator +
18600 // minimum laws hold at COMPILE time. Sibling of
18601 // `resource_limits_leq_evaluates_at_compile_time_via_const_fn`
18602 // one LATTICE-STRUCTURE axis over — that pin fixed the strict
18603 // order of the (DEFAULT, UNBOUNDED) preset pair at compile
18604 // time; this pin fixes the bounded-lattice pole role of the
18605 // (EMPTY, UNBOUNDED) preset pair. `const _: ()` at item scope
18606 // is a compile-time proof — a regression that broke either
18607 // identity element or the bottom-is-minimum invariant fires at
18608 // rustc time, not test-run time.
18609 //
18610 // Bottom-is-minimum: EMPTY ≤ DEFAULT ≤ UNBOUNDED (the
18611 // three-preset ordering chain on the shipped preset diagonal).
18612 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
18613 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
18614 // Reverse strictness — neither DEFAULT nor UNBOUNDED sits
18615 // below EMPTY (each has a strictly-positive field).
18616 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18617 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
18618 // Join-identity + meet-annihilator laws cross-pinned through
18619 // the `leq` primitive at const time — the round-trip identity
18620 // (`a ⊔ ⊥ = a ⇔ ⊥ ≤ a`, `a ⊓ ⊥ = ⊥ ⇔ ⊥ ≤ a`) means the two
18621 // leq asserts above discharge both algebraic laws by the
18622 // `leq_agrees_with_meet` + `leq_agrees_with_join` lattice
18623 // cross-check axiom already pinned in this cohort.
18624 }
18625
18626 #[test]
18627 fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
18628 // Fold-identity behavioral pin — a slice of postures folded
18629 // through [`ResourceLimits::most_permissive`] with EMPTY as
18630 // the seed produces the pointwise least upper bound across
18631 // the slice on every axis. The join-identity property
18632 // (`a.most_permissive(EMPTY) == a`) guarantees the seed
18633 // contributes nothing to the fold's result — every field's
18634 // pointwise `max` against the seed's `0` returns the field's
18635 // value verbatim, so the fold reduces to the joined-across-
18636 // -operands upper bound. This is the destination usage the
18637 // constant's docstring names as its fold-identity role.
18638 //
18639 // A regression that seeded from ANY non-empty posture (e.g.
18640 // `DEFAULT_RESOURCE_LIMITS`) would inflate the fold's result
18641 // on every axis where the seed's field exceeds the pointwise
18642 // max of the slice — fires as a distinct-axis inequality here.
18643 let postures: [ResourceLimits; 3] = [
18644 HAND_AUTHORED_MID_POSTURE,
18645 HAND_AUTHORED_OTHER_POSTURE,
18646 DEFAULT_RESOURCE_LIMITS,
18647 ];
18648 let joined = postures
18649 .iter()
18650 .copied()
18651 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
18652 // Every operand is `leq` the joined result — the least
18653 // upper bound dominates each contributor on every axis.
18654 assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
18655 assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
18656 assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
18657 // Field-level pin — each axis is the pointwise `max` of the
18658 // three sources' values. Six independent field asserts so a
18659 // drift on ONE axis carries a distinct-name failure.
18660 assert_eq!(
18661 joined.max_expansion_depth,
18662 HAND_AUTHORED_MID_POSTURE
18663 .max_expansion_depth
18664 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18665 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18666 );
18667 assert_eq!(
18668 joined.max_cache_entries,
18669 HAND_AUTHORED_MID_POSTURE
18670 .max_cache_entries
18671 .max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18672 .max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18673 );
18674 assert_eq!(
18675 joined.max_expansion_size,
18676 HAND_AUTHORED_MID_POSTURE
18677 .max_expansion_size
18678 .max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18679 .max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18680 );
18681 assert_eq!(
18682 joined.max_macro_body_size,
18683 HAND_AUTHORED_MID_POSTURE
18684 .max_macro_body_size
18685 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18686 .max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18687 );
18688 assert_eq!(
18689 joined.max_registered_macros,
18690 HAND_AUTHORED_MID_POSTURE
18691 .max_registered_macros
18692 .max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18693 .max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18694 );
18695 assert_eq!(
18696 joined.max_macro_arity,
18697 HAND_AUTHORED_MID_POSTURE
18698 .max_macro_arity
18699 .max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18700 .max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18701 );
18702 // Empty-slice edge case — folding over an EMPTY slice returns
18703 // the seed verbatim (the identity element as the null aggregate).
18704 // A wrapper-type approach (`Option<ResourceLimits>` for the
18705 // aggregate) would need a distinct absent branch here; the
18706 // typed identity-element approach resolves it structurally.
18707 let empty_slice: [ResourceLimits; 0] = [];
18708 assert_eq!(
18709 empty_slice
18710 .iter()
18711 .copied()
18712 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18713 EMPTY_RESOURCE_LIMITS,
18714 );
18715 }
18716
18717 #[test]
18718 fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
18719 // Dual fold-identity pin — a slice of postures folded through
18720 // [`ResourceLimits::strictest`] with UNBOUNDED as the seed
18721 // produces the pointwise greatest lower bound across the slice
18722 // on every axis. Peer of the EMPTY-seeds-most_permissive-fold
18723 // pin one COMBINATOR axis over — the identity element for
18724 // meet is the join's ANNIHILATOR (`UNBOUNDED`), and the
18725 // identity element for join is the meet's annihilator
18726 // (`EMPTY`); the two together bind the bounded-lattice's
18727 // aggregation surface at BOTH poles.
18728 let postures: [ResourceLimits; 3] = [
18729 HAND_AUTHORED_MID_POSTURE,
18730 HAND_AUTHORED_OTHER_POSTURE,
18731 DEFAULT_RESOURCE_LIMITS,
18732 ];
18733 let met = postures
18734 .iter()
18735 .copied()
18736 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
18737 // The greatest lower bound sits `leq` every operand.
18738 assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
18739 assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
18740 assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
18741 // Field-level pin — each axis is the pointwise `min` of the
18742 // three sources' values (the UNBOUNDED seed's `usize::MAX`
18743 // never wins the min against a concrete positive value).
18744 assert_eq!(
18745 met.max_expansion_depth,
18746 HAND_AUTHORED_MID_POSTURE
18747 .max_expansion_depth
18748 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
18749 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
18750 );
18751 assert_eq!(
18752 met.max_cache_entries,
18753 HAND_AUTHORED_MID_POSTURE
18754 .max_cache_entries
18755 .min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
18756 .min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
18757 );
18758 assert_eq!(
18759 met.max_expansion_size,
18760 HAND_AUTHORED_MID_POSTURE
18761 .max_expansion_size
18762 .min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
18763 .min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
18764 );
18765 assert_eq!(
18766 met.max_macro_body_size,
18767 HAND_AUTHORED_MID_POSTURE
18768 .max_macro_body_size
18769 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
18770 .min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
18771 );
18772 assert_eq!(
18773 met.max_registered_macros,
18774 HAND_AUTHORED_MID_POSTURE
18775 .max_registered_macros
18776 .min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
18777 .min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
18778 );
18779 assert_eq!(
18780 met.max_macro_arity,
18781 HAND_AUTHORED_MID_POSTURE
18782 .max_macro_arity
18783 .min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
18784 .min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
18785 );
18786 // Empty-slice edge — folding over an empty slice returns the
18787 // UNBOUNDED seed verbatim (the meet-identity as the null
18788 // aggregate). Symmetric to the EMPTY-seed empty-slice pin.
18789 let empty_slice: [ResourceLimits; 0] = [];
18790 assert_eq!(
18791 empty_slice
18792 .iter()
18793 .copied()
18794 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18795 UNBOUNDED_RESOURCE_LIMITS,
18796 );
18797 }
18798
18799 #[test]
18800 fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
18801 // Null-aggregate pin — the N-ary meet over an empty slice is
18802 // the meet-identity element [`UNBOUNDED_RESOURCE_LIMITS`]. The
18803 // typed identity-element approach resolves the empty-input case
18804 // at the algebra layer; a wrapper-type approach
18805 // (`Option<ResourceLimits>` for the aggregate) would need a
18806 // distinct absent branch here, and a panic-on-empty seed-from-
18807 // -first-element approach would abort the caller.
18808 assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
18809 }
18810
18811 #[test]
18812 fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
18813 // Null-aggregate dual pin — the N-ary join over an empty slice
18814 // is the join-identity element [`EMPTY_RESOURCE_LIMITS`]. Peer
18815 // to `strictest_of_empty_slice` one COMBINATOR axis over on the
18816 // (N-ary meet, N-ary join) surface — the identity element for
18817 // join is the meet's ANNIHILATOR and vice versa; the two
18818 // together bind both bounded-lattice poles as null aggregates.
18819 assert_eq!(
18820 ResourceLimits::most_permissive_of(&[]),
18821 EMPTY_RESOURCE_LIMITS,
18822 );
18823 }
18824
18825 #[test]
18826 fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
18827 // Meet-identity absorption pin at N=1 — the N-ary meet of ONE
18828 // input is the input verbatim, since the meet-identity seed
18829 // (`UNBOUNDED_RESOURCE_LIMITS`) satisfies
18830 // `UNBOUNDED.strictest(a) == a` by the pointwise `min` behavior
18831 // against [`usize::MAX`]. This is the identity-law-at-N=1
18832 // pin: the fold's seed does not distort the 1-input case.
18833 assert_eq!(
18834 ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
18835 HAND_AUTHORED_MID_POSTURE,
18836 );
18837 assert_eq!(
18838 ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18839 HAND_AUTHORED_OTHER_POSTURE,
18840 );
18841 assert_eq!(
18842 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
18843 DEFAULT_RESOURCE_LIMITS,
18844 );
18845 }
18846
18847 #[test]
18848 fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
18849 // Join-identity absorption pin at N=1 — dual of
18850 // `strictest_of_single_element`, using the join-identity seed
18851 // (`EMPTY_RESOURCE_LIMITS`) whose per-axis `0` satisfies
18852 // `EMPTY.most_permissive(a) == a` against any non-zero value.
18853 assert_eq!(
18854 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
18855 HAND_AUTHORED_MID_POSTURE,
18856 );
18857 assert_eq!(
18858 ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
18859 HAND_AUTHORED_OTHER_POSTURE,
18860 );
18861 assert_eq!(
18862 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
18863 DEFAULT_RESOURCE_LIMITS,
18864 );
18865 }
18866
18867 #[test]
18868 fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
18869 // N=2 reduction pin — the N-ary meet on a 2-element slice
18870 // reduces to the pairwise combinator, since
18871 // `UNBOUNDED.strictest(a).strictest(b) == a.strictest(b)`. The
18872 // strictest_of(&[a, b]) call MUST agree with a.strictest(b) on
18873 // every input pair the algebra distinguishes.
18874 assert_eq!(
18875 ResourceLimits::strictest_of(
18876 &[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
18877 ),
18878 HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
18879 );
18880 assert_eq!(
18881 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
18882 DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
18883 );
18884 }
18885
18886 #[test]
18887 fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
18888 // N=2 reduction dual pin — the N-ary join on a 2-element slice
18889 // reduces to the pairwise combinator.
18890 assert_eq!(
18891 ResourceLimits::most_permissive_of(&[
18892 HAND_AUTHORED_MID_POSTURE,
18893 HAND_AUTHORED_OTHER_POSTURE,
18894 ]),
18895 HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
18896 );
18897 assert_eq!(
18898 ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
18899 DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
18900 );
18901 }
18902
18903 #[test]
18904 fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
18905 // Method-agrees-with-fold pin — the sole behavioral claim the
18906 // N-ary aggregator makes is that it IS the inline
18907 // `.iter().copied().fold(UNBOUNDED, strictest)` cascade the
18908 // pre-lift caller composed at every consumption site. A drift
18909 // between the method's implementation and the inline fold
18910 // would break this equality on every non-empty slice.
18911 let postures: [ResourceLimits; 3] = [
18912 HAND_AUTHORED_MID_POSTURE,
18913 HAND_AUTHORED_OTHER_POSTURE,
18914 DEFAULT_RESOURCE_LIMITS,
18915 ];
18916 assert_eq!(
18917 ResourceLimits::strictest_of(&postures),
18918 postures
18919 .iter()
18920 .copied()
18921 .fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
18922 );
18923 }
18924
18925 #[test]
18926 fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
18927 // Method-agrees-with-fold dual pin.
18928 let postures: [ResourceLimits; 3] = [
18929 HAND_AUTHORED_MID_POSTURE,
18930 HAND_AUTHORED_OTHER_POSTURE,
18931 DEFAULT_RESOURCE_LIMITS,
18932 ];
18933 assert_eq!(
18934 ResourceLimits::most_permissive_of(&postures),
18935 postures
18936 .iter()
18937 .copied()
18938 .fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
18939 );
18940 }
18941
18942 #[test]
18943 fn resource_limits_strictest_of_is_order_independent() {
18944 // Order-independence pin — the fold inherits commutativity AND
18945 // associativity of the pairwise `strictest`, so any permutation
18946 // of the input slice yields the same aggregate. Reversal is
18947 // the maximal permutation for a 3-element input (indices
18948 // 0,1,2 → 2,1,0); an aggregator that leaked fold order would
18949 // discriminate here.
18950 let forward = ResourceLimits::strictest_of(&[
18951 HAND_AUTHORED_MID_POSTURE,
18952 HAND_AUTHORED_OTHER_POSTURE,
18953 DEFAULT_RESOURCE_LIMITS,
18954 ]);
18955 let reversed = ResourceLimits::strictest_of(&[
18956 DEFAULT_RESOURCE_LIMITS,
18957 HAND_AUTHORED_OTHER_POSTURE,
18958 HAND_AUTHORED_MID_POSTURE,
18959 ]);
18960 assert_eq!(forward, reversed);
18961 }
18962
18963 #[test]
18964 fn resource_limits_most_permissive_of_is_order_independent() {
18965 let forward = ResourceLimits::most_permissive_of(&[
18966 HAND_AUTHORED_MID_POSTURE,
18967 HAND_AUTHORED_OTHER_POSTURE,
18968 DEFAULT_RESOURCE_LIMITS,
18969 ]);
18970 let reversed = ResourceLimits::most_permissive_of(&[
18971 DEFAULT_RESOURCE_LIMITS,
18972 HAND_AUTHORED_OTHER_POSTURE,
18973 HAND_AUTHORED_MID_POSTURE,
18974 ]);
18975 assert_eq!(forward, reversed);
18976 }
18977
18978 #[test]
18979 fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
18980 // Compile-time-composition pin — `strictest_of` is `const fn`,
18981 // so a caller binds an N-ary aggregate to a `pub const` and the
18982 // fold evaluates at compile time. Every `DEFAULT_MAX_*` is
18983 // strictly less than `usize::MAX`, so the meet of `[DEFAULT,
18984 // UNBOUNDED]` picks DEFAULT on every axis — a preset-pair
18985 // identity already pinned pairwise, verified here through the
18986 // N-ary method's const-context evaluation. The N-ary const-fn
18987 // peer of the pairwise const-fn pins on
18988 // [`ResourceLimits::strictest`]; a regression that turned
18989 // `strictest_of` into a runtime `fn` would break the `const`
18990 // binding below at compile time.
18991 const AGGREGATED: ResourceLimits =
18992 ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
18993 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
18994 }
18995
18996 #[test]
18997 fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
18998 // Compile-time-composition dual pin — every `DEFAULT_MAX_*` is
18999 // strictly greater than `0`, so the join of `[EMPTY, DEFAULT]`
19000 // picks DEFAULT on every axis.
19001 const AGGREGATED: ResourceLimits =
19002 ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
19003 assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
19004 }
19005
19006 #[test]
19007 fn resource_limits_strictest_of_result_is_leq_every_operand() {
19008 // Lattice-relation pin — the N-ary meet sits `leq` every
19009 // operand. This is the N-ary extension of the pairwise
19010 // `strictest_is_leq_both_operands` pin; the meet aggregate
19011 // dominates from below (the greatest lower bound of the slice
19012 // sits at-or-below each input on every axis).
19013 let postures: [ResourceLimits; 3] = [
19014 HAND_AUTHORED_MID_POSTURE,
19015 HAND_AUTHORED_OTHER_POSTURE,
19016 DEFAULT_RESOURCE_LIMITS,
19017 ];
19018 let met = ResourceLimits::strictest_of(&postures);
19019 for p in postures {
19020 assert!(met.leq(p));
19021 }
19022 }
19023
19024 #[test]
19025 fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
19026 // Lattice-relation dual pin — every operand sits `leq` the
19027 // N-ary join. The join aggregate is the least upper bound of
19028 // the slice.
19029 let postures: [ResourceLimits; 3] = [
19030 HAND_AUTHORED_MID_POSTURE,
19031 HAND_AUTHORED_OTHER_POSTURE,
19032 DEFAULT_RESOURCE_LIMITS,
19033 ];
19034 let joined = ResourceLimits::most_permissive_of(&postures);
19035 for p in postures {
19036 assert!(p.leq(joined));
19037 }
19038 }
19039
19040 #[test]
19041 fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
19042 // Behavioral pin — an expander constructed from the N-ary meet
19043 // of a slice of postures MUST gate at the tightest ceiling
19044 // across the slice on every axis. Peer of
19045 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19046 // one AGGREGATION-ARITY axis over: where the pairwise pin
19047 // composes exactly two postures, this pin composes THREE
19048 // through the N-ary aggregator, confirming the arity extension
19049 // preserves the "meet gates at tightest" behavioral contract.
19050 //
19051 // The slice tightens macro_arity to 1 (the tightest across the
19052 // three postures) via the middle operand; every other axis
19053 // stays at usize::MAX (the outer two operands supply
19054 // usize::MAX; the middle operand's struct-update literal
19055 // preserves usize::MAX everywhere except macro_arity).
19056 let tightened = ResourceLimits::strictest_of(&[
19057 UNBOUNDED_RESOURCE_LIMITS,
19058 ResourceLimits {
19059 max_macro_arity: 1,
19060 ..UNBOUNDED_RESOURCE_LIMITS
19061 },
19062 UNBOUNDED_RESOURCE_LIMITS,
19063 ]);
19064 assert_eq!(tightened.max_macro_arity, 1);
19065 assert_eq!(tightened.max_expansion_depth, usize::MAX);
19066 assert_eq!(tightened.max_cache_entries, usize::MAX);
19067 assert_eq!(tightened.max_expansion_size, usize::MAX);
19068 assert_eq!(tightened.max_macro_body_size, usize::MAX);
19069 assert_eq!(tightened.max_registered_macros, usize::MAX);
19070 let mut e = Expander::with_limits(tightened);
19071 let forms = read("(defmacro two (a b) `,a)").unwrap();
19072 let err = e.expand_program(forms).unwrap_err();
19073 assert!(
19074 matches!(
19075 err,
19076 LispError::MacroArityExceeded {
19077 arity: 2,
19078 limit: 1,
19079 ..
19080 }
19081 ),
19082 "expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
19083 );
19084 }
19085
19086 #[test]
19087 fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
19088 // Behavioral pin — an expander constructed from the meet of
19089 // two postures MUST gate at the tighter of the two ceilings
19090 // on every axis. Build the meet of the ceiling-lifted preset
19091 // and a hand-authored posture whose `max_macro_arity` sits at
19092 // 1; the meet inherits the 1-slot arity ceiling (tighter than
19093 // [`usize::MAX`]) AND the [`usize::MAX`] ceiling on every
19094 // other axis (tighter than the hand-authored posture's small
19095 // values — no, wait: the OTHER posture's small values ARE
19096 // tighter. So the meet inherits 1 on arity and the OTHER
19097 // small values elsewhere.) Confirm the constructed expander
19098 // rejects a 2-arg macro registration with the arity gate.
19099 let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
19100 max_macro_arity: 1,
19101 ..UNBOUNDED_RESOURCE_LIMITS
19102 });
19103 assert_eq!(tightened.max_macro_arity, 1);
19104 // Other five axes remain at `usize::MAX` (both operands carry
19105 // `usize::MAX` on those, so the pointwise `min` is `usize::MAX`).
19106 assert_eq!(tightened.max_expansion_depth, usize::MAX);
19107 assert_eq!(tightened.max_cache_entries, usize::MAX);
19108 assert_eq!(tightened.max_expansion_size, usize::MAX);
19109 assert_eq!(tightened.max_macro_body_size, usize::MAX);
19110 assert_eq!(tightened.max_registered_macros, usize::MAX);
19111 let mut e = Expander::with_limits(tightened);
19112 let forms = read("(defmacro two (a b) `,a)").unwrap();
19113 let err = e.expand_program(forms).unwrap_err();
19114 assert!(
19115 matches!(
19116 err,
19117 LispError::MacroArityExceeded {
19118 arity: 2,
19119 limit: 1,
19120 ..
19121 }
19122 ),
19123 "the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
19124 );
19125 }
19126
19127 // ── ResourceLimits::clamp — bounded-lattice bracket combinator ────
19128
19129 /// A tighter-than-MID lower-bracket witness — every axis strictly
19130 /// smaller than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the
19131 /// pointwise `max(mid, floor) == mid` on every axis (`floor.leq(mid)`
19132 /// holds structurally). Peer of `HAND_AUTHORED_CLAMP_CEILING` one
19133 /// BRACKET-BOUND axis over on the (floor, ceiling) bracket-witness
19134 /// pair.
19135 const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
19136 max_expansion_depth: 2,
19137 max_cache_entries: 3,
19138 max_expansion_size: 5,
19139 max_macro_body_size: 7,
19140 max_registered_macros: 11,
19141 max_macro_arity: 13,
19142 };
19143
19144 /// A looser-than-MID upper-bracket witness — every axis strictly larger
19145 /// than `HAND_AUTHORED_MID_POSTURE`'s matching axis, so the pointwise
19146 /// `min(mid, ceiling) == mid` on every axis (`mid.leq(ceiling)` holds
19147 /// structurally). Peer of `HAND_AUTHORED_CLAMP_FLOOR` one BRACKET-
19148 /// BOUND axis over. Together with `HAND_AUTHORED_CLAMP_FLOOR` the two
19149 /// bracket `HAND_AUTHORED_MID_POSTURE` strictly on every axis:
19150 /// `FLOOR.leq(MID) && MID.leq(CEILING) && FLOOR != MID && MID !=
19151 /// CEILING`.
19152 const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
19153 max_expansion_depth: 47,
19154 max_cache_entries: 53,
19155 max_expansion_size: 59,
19156 max_macro_body_size: 61,
19157 max_registered_macros: 67,
19158 max_macro_arity: 71,
19159 };
19160
19161 #[test]
19162 fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
19163 // In-range identity — when the input already sits within the
19164 // `[lower, upper]` bracket (`lower.leq(a) && a.leq(upper)` per-
19165 // axis), the clamp is the identity function: the pointwise
19166 // `max(a, lower) == a` (since `lower ≤ a` per-axis picks `a`),
19167 // then the pointwise `min(a, upper) == a` (since `a ≤ upper`
19168 // per-axis picks `a`). The primary invariant on the "input
19169 // already satisfies the bracket" arm of the three-arm bracket-
19170 // membership surface.
19171 //
19172 // Prerequisite pins — the two hand-authored bounds bracket
19173 // `MID` strictly on every axis (structural check that discharges
19174 // the "already in range" premise). A regression that inflated
19175 // `FLOOR` or deflated `CEILING` past `MID` would fail these
19176 // asserts before the clamp identity was tested, making the
19177 // premise's failure name itself rather than showing up as a
19178 // downstream misleading clamp mismatch.
19179 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19180 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19181 let clamped =
19182 HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19183 assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
19184 }
19185
19186 #[test]
19187 fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
19188 // Below-lower floor — when the input is tighter than the
19189 // bracket floor on every axis (`a.leq(lower)` per-axis), the
19190 // clamp lifts the input up to `lower`: the pointwise `max(a,
19191 // lower) == lower` (since `a ≤ lower` per-axis picks `lower`),
19192 // then the pointwise `min(lower, upper) == lower` (since
19193 // `lower ≤ upper` per-axis picks `lower`). Peer of
19194 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19195 // one BRACKET-POSITION axis over on the (in-range, below-lower,
19196 // above-upper) three-arm bracket-membership surface.
19197 //
19198 // `EMPTY_RESOURCE_LIMITS` sits `leq` every posture (bounded-
19199 // lattice bottom), so `EMPTY.leq(FLOOR)` holds structurally
19200 // for any FLOOR; a clamp of `EMPTY` into `[FLOOR, CEILING]`
19201 // MUST return `FLOOR`.
19202 assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
19203 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19204 let clamped =
19205 EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19206 assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
19207 }
19208
19209 #[test]
19210 fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
19211 // Above-upper ceiling — when the input is looser than the
19212 // bracket ceiling on every axis (`upper.leq(a)` per-axis), the
19213 // clamp lowers the input down to `upper`: the pointwise
19214 // `max(a, lower) == a` (since `lower ≤ upper ≤ a` per-axis picks
19215 // `a`), then the pointwise `min(a, upper) == upper` (since
19216 // `upper ≤ a` per-axis picks `upper`). Peer of the
19217 // below-lower arm one BRACKET-POSITION axis over on the three-
19218 // arm bracket-membership surface.
19219 //
19220 // `UNBOUNDED_RESOURCE_LIMITS` sits `geq` every posture (bounded-
19221 // lattice top), so `CEILING.leq(UNBOUNDED)` holds structurally
19222 // for any CEILING; a clamp of `UNBOUNDED` into `[FLOOR, CEILING]`
19223 // MUST return `CEILING`.
19224 assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
19225 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19226 let clamped =
19227 UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19228 assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
19229 }
19230
19231 #[test]
19232 fn resource_limits_clamp_result_sits_within_the_bracket() {
19233 // Bracket-membership contract — the DEFINING invariant on the
19234 // clamp primitive: for every well-formed bracket
19235 // (`lower.leq(upper)`) and every input `a`, the result sits
19236 // within the bracket `lower.leq(a.clamp(lower, upper)) &&
19237 // a.clamp(lower, upper).leq(upper)`. Exercised on ALL FOUR arm
19238 // positions of the bracket-membership surface via the four
19239 // known-preset inputs (below-lower → in-range → above-upper
19240 // → asymmetric-crossing) so a regression that inflated the
19241 // ceiling arm or deflated the floor arm exhausts all four
19242 // input positions and names the failing arm.
19243 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19244 let inputs = [
19245 EMPTY_RESOURCE_LIMITS, // below-lower
19246 HAND_AUTHORED_MID_POSTURE, // in-range
19247 UNBOUNDED_RESOURCE_LIMITS, // above-upper
19248 HAND_AUTHORED_OTHER_POSTURE, // asymmetric (crosses the bracket)
19249 ];
19250 for a in inputs {
19251 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19252 assert!(
19253 HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
19254 "clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
19255 );
19256 assert!(
19257 clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
19258 "clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
19259 );
19260 }
19261 }
19262
19263 #[test]
19264 fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
19265 // Extrema-bracket identity — clamping against the bounded-
19266 // lattice extrema (`EMPTY_RESOURCE_LIMITS` as the leq-minimum,
19267 // `UNBOUNDED_RESOURCE_LIMITS` as the leq-maximum) is the
19268 // identity function on every posture: every posture already
19269 // sits within the widest possible bracket. Cross-checks the
19270 // clamp primitive against the bounded-lattice identity elements
19271 // previously lifted; a regression that swapped the two extrema
19272 // in the impl would collapse every input to
19273 // `UNBOUNDED.strictest(EMPTY) == EMPTY` and fail this pin on
19274 // every non-EMPTY input.
19275 for a in [
19276 DEFAULT_RESOURCE_LIMITS,
19277 HAND_AUTHORED_MID_POSTURE,
19278 HAND_AUTHORED_OTHER_POSTURE,
19279 EMPTY_RESOURCE_LIMITS,
19280 UNBOUNDED_RESOURCE_LIMITS,
19281 ] {
19282 assert_eq!(
19283 a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19284 a,
19285 "clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
19286 );
19287 }
19288 }
19289
19290 #[test]
19291 fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
19292 // Degenerate-bracket collapse — a zero-width bracket
19293 // (`lower == upper`) collapses every input to the single point.
19294 // The pointwise `max(a, x)` lifts every axis to at least `x`,
19295 // then the pointwise `min(., x)` clamps every axis to exactly
19296 // `x` regardless of `a`'s per-axis position (since `x ≤ max(a,
19297 // x) ≤ max(x, x) == x` when `a.leq(x)` and `min(max(a, x), x)
19298 // == x` when `x.leq(a)`).
19299 for x in [
19300 HAND_AUTHORED_MID_POSTURE,
19301 HAND_AUTHORED_OTHER_POSTURE,
19302 DEFAULT_RESOURCE_LIMITS,
19303 ] {
19304 for a in [
19305 EMPTY_RESOURCE_LIMITS,
19306 HAND_AUTHORED_MID_POSTURE,
19307 HAND_AUTHORED_OTHER_POSTURE,
19308 UNBOUNDED_RESOURCE_LIMITS,
19309 ] {
19310 assert_eq!(
19311 a.clamp(x, x),
19312 x,
19313 "zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
19314 );
19315 }
19316 }
19317 }
19318
19319 #[test]
19320 fn resource_limits_clamp_is_idempotent() {
19321 // Idempotence — re-applying the same bracket to an already-
19322 // clamped posture returns the same result. The clamp primitive
19323 // is idempotent because its output already sits within the
19324 // `[lower, upper]` bracket (pinned by
19325 // `resource_limits_clamp_result_sits_within_the_bracket`), so
19326 // the second application hits the in-range identity arm. Peer
19327 // of the idempotence pins on `strictest` and `most_permissive`
19328 // one ARITY axis over (2-input primitive → 3-input bracket) on
19329 // the lattice-algebra idempotence-law surface.
19330 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19331 for a in [
19332 EMPTY_RESOURCE_LIMITS,
19333 HAND_AUTHORED_MID_POSTURE,
19334 HAND_AUTHORED_OTHER_POSTURE,
19335 UNBOUNDED_RESOURCE_LIMITS,
19336 ] {
19337 let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19338 let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19339 assert_eq!(
19340 once, twice,
19341 "clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
19342 );
19343 }
19344 }
19345
19346 #[test]
19347 fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
19348 // Const-fn pin — the bracket combinator is evaluable in const
19349 // context, so a caller can pre-clamp a shipped preset against
19350 // an operator-supplied policy bracket at compile time. Sibling
19351 // of the const-fn composition pins on `strictest` /
19352 // `most_permissive` / `strictest_of` / `most_permissive_of`
19353 // one PRIMITIVE-KIND axis over: those four combinators compose
19354 // presets at const time, and this bracket combinator projects
19355 // a preset into a const-time bracket.
19356 //
19357 // A regression to a runtime `fn` here would fail the `pub
19358 // const` binding below at compile time — a stronger guarantee
19359 // than a runtime `assert!` because the const binding is
19360 // evaluated once at compile time, not per test invocation.
19361 const CLAMPED_DEFAULT: ResourceLimits =
19362 DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
19363 assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
19364
19365 // Compile-time bracket-membership pin — const `assert!` on the
19366 // lattice-relation contract discharges the "clamp result sits
19367 // within the bracket" invariant at compile time on the
19368 // (DEFAULT, EMPTY, UNBOUNDED) preset triple.
19369 const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
19370 const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
19371 }
19372
19373 #[test]
19374 fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
19375 // Structural-equivalence pin — the sole behavioral claim the
19376 // clamp primitive makes is that it IS the
19377 // `self.most_permissive(lower).strictest(upper)` two-step
19378 // cascade the pre-lift caller composed. A drift between the
19379 // method and the inline cascade would break this equality on
19380 // every input; the pin exhausts the four bracket-position arms
19381 // of the input surface.
19382 for a in [
19383 EMPTY_RESOURCE_LIMITS,
19384 HAND_AUTHORED_MID_POSTURE,
19385 HAND_AUTHORED_OTHER_POSTURE,
19386 UNBOUNDED_RESOURCE_LIMITS,
19387 ] {
19388 let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19389 let via_cascade = a
19390 .most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
19391 .strictest(HAND_AUTHORED_CLAMP_CEILING);
19392 assert_eq!(via_method, via_cascade);
19393 }
19394 }
19395
19396 #[test]
19397 fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
19398 // Behavioral pin — an expander constructed from a preset
19399 // clamped into `[EMPTY, TIGHT_BRACKET]` MUST gate at the
19400 // bracket ceiling's arity axis. Confirms the clamp primitive
19401 // reaches through to the runtime gates that consume
19402 // `ResourceLimits`, so a future consumer that pre-clamps a
19403 // preset against a policy bracket inherits the bracket's
19404 // ceilings at every guard. Peer of
19405 // `expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling`
19406 // one COMBINATOR-ARITY axis over on the (pairwise-meet,
19407 // N-ary-meet, bracket) combinator-arity face.
19408 //
19409 // The bracket ceiling tightens `max_macro_arity` to 1; every
19410 // other axis stays at `usize::MAX` (the CEILING preset carries
19411 // `usize::MAX` on those, and the input UNBOUNDED preset also
19412 // carries `usize::MAX` on every axis — the clamp's `strictest`
19413 // arm picks `usize::MAX` from either side).
19414 let bracket_ceiling = ResourceLimits {
19415 max_macro_arity: 1,
19416 ..UNBOUNDED_RESOURCE_LIMITS
19417 };
19418 let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19419 assert_eq!(clamped.max_macro_arity, 1);
19420 assert_eq!(clamped.max_expansion_depth, usize::MAX);
19421 assert_eq!(clamped.max_cache_entries, usize::MAX);
19422 assert_eq!(clamped.max_expansion_size, usize::MAX);
19423 assert_eq!(clamped.max_macro_body_size, usize::MAX);
19424 assert_eq!(clamped.max_registered_macros, usize::MAX);
19425 let mut e = Expander::with_limits(clamped);
19426 let forms = read("(defmacro two (a b) `,a)").unwrap();
19427 let err = e.expand_program(forms).unwrap_err();
19428 assert!(
19429 matches!(
19430 err,
19431 LispError::MacroArityExceeded {
19432 arity: 2,
19433 limit: 1,
19434 ..
19435 }
19436 ),
19437 "the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
19438 );
19439 }
19440
19441 // ── ResourceLimits::within — bracket-membership predicate ─────────
19442
19443 #[test]
19444 fn resource_limits_within_of_posture_in_range_is_true() {
19445 // In-range identity — when the input already sits within the
19446 // `[lower, upper]` bracket on every axis (`lower.leq(a) &&
19447 // a.leq(upper)`), the predicate returns `true`. Peer of
19448 // `resource_limits_clamp_of_posture_already_in_range_returns_the_posture`
19449 // one PRIMITIVE-KIND axis over on the (combinator, predicate)
19450 // face of the bracket-primitive surface.
19451 //
19452 // Prerequisite pins — the two hand-authored bounds bracket
19453 // `MID` strictly on every axis, structurally discharging the
19454 // "already in range" premise.
19455 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
19456 assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
19457 assert!(HAND_AUTHORED_MID_POSTURE
19458 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19459 }
19460
19461 #[test]
19462 fn resource_limits_within_of_posture_below_lower_is_false() {
19463 // Below-lower rejection — when the input is strictly tighter
19464 // than the bracket floor on some axis (`!lower.leq(a)`), the
19465 // predicate returns `false`. `EMPTY_RESOURCE_LIMITS` has every
19466 // axis at `0`, strictly below `HAND_AUTHORED_CLAMP_FLOOR`'s
19467 // per-axis ceilings; so `FLOOR.leq(EMPTY)` fails on every axis,
19468 // making the conjunction reject regardless of `EMPTY`'s relation
19469 // to `CEILING`. Peer of
19470 // `resource_limits_clamp_of_posture_below_lower_returns_lower`
19471 // one PRIMITIVE-KIND axis over.
19472 assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
19473 assert!(
19474 !EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
19475 );
19476 }
19477
19478 #[test]
19479 fn resource_limits_within_of_posture_above_upper_is_false() {
19480 // Above-upper rejection — when the input is strictly looser
19481 // than the bracket ceiling on some axis (`!a.leq(upper)`), the
19482 // predicate returns `false`. `UNBOUNDED_RESOURCE_LIMITS` has
19483 // every axis at `usize::MAX`, strictly above
19484 // `HAND_AUTHORED_CLAMP_CEILING`'s per-axis ceilings; so
19485 // `UNBOUNDED.leq(CEILING)` fails on every axis. Peer of
19486 // `resource_limits_clamp_of_posture_above_upper_returns_upper`
19487 // one PRIMITIVE-KIND axis over.
19488 assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
19489 assert!(!UNBOUNDED_RESOURCE_LIMITS
19490 .within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
19491 }
19492
19493 #[test]
19494 fn resource_limits_within_with_lattice_extrema_is_true() {
19495 // Extrema-bracket identity — every posture sits within the
19496 // widest possible bracket `[EMPTY_RESOURCE_LIMITS,
19497 // UNBOUNDED_RESOURCE_LIMITS]`. Cross-checks the predicate
19498 // against the bounded-lattice identity elements (`EMPTY` is
19499 // the leq-minimum, `UNBOUNDED` is the leq-maximum). Peer of
19500 // `resource_limits_clamp_with_lattice_extrema_returns_the_input`
19501 // one PRIMITIVE-KIND axis over — the predicate view of the
19502 // "clamp is the identity" property.
19503 for a in [
19504 DEFAULT_RESOURCE_LIMITS,
19505 HAND_AUTHORED_MID_POSTURE,
19506 HAND_AUTHORED_OTHER_POSTURE,
19507 EMPTY_RESOURCE_LIMITS,
19508 UNBOUNDED_RESOURCE_LIMITS,
19509 ] {
19510 assert!(
19511 a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19512 "every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
19513 );
19514 }
19515 }
19516
19517 #[test]
19518 fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
19519 // Degenerate-bracket collapse — a zero-width bracket admits
19520 // only the single point at the bounds. `a.within(x, x)` holds
19521 // iff `x.leq(a) && a.leq(x)` iff `a == x` by antisymmetry of
19522 // `leq`. Peer of `resource_limits_clamp_with_equal_bounds_returns_the_bound`
19523 // one PRIMITIVE-KIND axis over: the clamp collapses to `x`
19524 // unconditionally, and the predicate distinguishes the "was
19525 // already equal" case from the "would have been clamped" case.
19526 let bounds = [
19527 HAND_AUTHORED_MID_POSTURE,
19528 HAND_AUTHORED_OTHER_POSTURE,
19529 DEFAULT_RESOURCE_LIMITS,
19530 EMPTY_RESOURCE_LIMITS,
19531 UNBOUNDED_RESOURCE_LIMITS,
19532 ];
19533 for x in bounds {
19534 for a in bounds {
19535 assert_eq!(
19536 a.within(x, x),
19537 a == x,
19538 "a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
19539 );
19540 }
19541 }
19542 }
19543
19544 #[test]
19545 fn resource_limits_within_of_self_is_reflexive() {
19546 // Reflexive-bracket identity — every posture sits within the
19547 // zero-width bracket at itself, since `a.leq(a)` holds by
19548 // reflexivity of `leq`. The reflexive-bracket peer of the
19549 // reflexive-leq pin (`resource_limits_leq_is_reflexive`) one
19550 // ARITY axis over (pairwise → three-input).
19551 for a in [
19552 EMPTY_RESOURCE_LIMITS,
19553 DEFAULT_RESOURCE_LIMITS,
19554 HAND_AUTHORED_MID_POSTURE,
19555 HAND_AUTHORED_OTHER_POSTURE,
19556 UNBOUNDED_RESOURCE_LIMITS,
19557 ] {
19558 assert!(
19559 a.within(a, a),
19560 "a.within(a, a) must hold by reflexivity of leq; a={a:?}",
19561 );
19562 }
19563 }
19564
19565 #[test]
19566 fn resource_limits_within_agrees_with_clamp_fixed_point() {
19567 // Clamp fixed-point theorem — `a.within(lower, upper) ⇔
19568 // a.clamp(lower, upper) == a`. The CANONICAL cross-check axiom
19569 // binding the `within` predicate to the `clamp` combinator,
19570 // analogous to the meet-agreement (`a.leq(b) ⇔ a.strictest(b)
19571 // == a`, pinned by `resource_limits_leq_agrees_with_meet`) and
19572 // join-agreement (`a.leq(b) ⇔ a.most_permissive(b) == b`,
19573 // pinned by `resource_limits_leq_agrees_with_join`) axioms
19574 // binding the `leq` relation to its combinator peers.
19575 //
19576 // Exercised on all four bracket-position arms of the input
19577 // surface (below-lower / in-range / above-upper / asymmetric-
19578 // crossing) so a regression that broke either direction of the
19579 // fixed-point equivalence exhausts every position and names the
19580 // failing arm.
19581 assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
19582 for a in [
19583 EMPTY_RESOURCE_LIMITS, // below-lower — clamp != a, within false
19584 HAND_AUTHORED_MID_POSTURE, // in-range — clamp == a, within true
19585 UNBOUNDED_RESOURCE_LIMITS, // above-upper — clamp != a, within false
19586 HAND_AUTHORED_OTHER_POSTURE, // asymmetric — clamp != a, within false
19587 ] {
19588 let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19589 let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19590 let clamp_fixed = clamped == a;
19591 assert_eq!(
19592 within_holds, clamp_fixed,
19593 "within(lower, upper) must agree with clamp fixed-point; \
19594 a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
19595 );
19596 }
19597 }
19598
19599 #[test]
19600 fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
19601 // Structural-equivalence pin — the sole behavioral claim the
19602 // predicate makes is that it IS the `lower.leq(a) && a.leq(upper)`
19603 // two-primitive conjunction the pre-lift caller composed. A drift
19604 // between the method and the inline conjunction would break this
19605 // equality on some input; the pin exhausts the four bracket-
19606 // position arms of the input surface. Peer of
19607 // `resource_limits_clamp_agrees_with_direct_two_step_cascade`
19608 // one PRIMITIVE-KIND axis over (combinator ↔ predicate).
19609 for a in [
19610 EMPTY_RESOURCE_LIMITS,
19611 HAND_AUTHORED_MID_POSTURE,
19612 HAND_AUTHORED_OTHER_POSTURE,
19613 UNBOUNDED_RESOURCE_LIMITS,
19614 ] {
19615 let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
19616 let via_conjunction =
19617 HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
19618 assert_eq!(via_method, via_conjunction, "a={a:?}");
19619 }
19620 }
19621
19622 #[test]
19623 fn resource_limits_within_composes_at_compile_time_via_const_fn() {
19624 // Const-fn pin — the bracket predicate is evaluable in const
19625 // context, so a caller can pin a preset-bracket-membership
19626 // identity at compile time. Sibling of the const-fn evaluability
19627 // pins on `leq` and `clamp` one PRIMITIVE-KIND axis over: the
19628 // relation and combinator both evaluate at const time, and the
19629 // predicate closing the (combinator, predicate) row does too.
19630 //
19631 // A regression to a runtime `fn` here would fail the `const _:
19632 // () = assert!(...)` bindings below at compile time.
19633 const _: () = assert!(
19634 DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
19635 );
19636 const _: () = assert!(
19637 DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
19638 );
19639 const _: () = assert!(
19640 !UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
19641 );
19642 }
19643
19644 #[test]
19645 fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
19646 // Behavioral pin — a caller that CHECKS bracket-membership
19647 // before constructing an expander can safely reach through to
19648 // the runtime gates: if `within` reports true, `clamp` is a
19649 // no-op (by the fixed-point theorem), so the expander's
19650 // ceilings are the input posture's own. Peer of
19651 // `expander_built_at_clamped_preset_gates_at_the_bracket_ceiling`
19652 // one PRIMITIVE-KIND axis over — the predicate view of the
19653 // "clamp reaches through to the runtime gates" property.
19654 //
19655 // The candidate sits at the bracket ceiling (arity=1, other
19656 // axes at usize::MAX), so it MUST sit within the [EMPTY,
19657 // bracket_ceiling] bracket — the maximal within-membership arm
19658 // of the fixed-point theorem. Every other axis at usize::MAX
19659 // clears the register-time gates, so the arity gate at 1 is
19660 // the one that trips on a 2-parameter macro registration.
19661 let bracket_ceiling = ResourceLimits {
19662 max_macro_arity: 1,
19663 ..UNBOUNDED_RESOURCE_LIMITS
19664 };
19665 let candidate = ResourceLimits {
19666 max_macro_arity: 1,
19667 ..UNBOUNDED_RESOURCE_LIMITS
19668 };
19669 assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
19670 // Since candidate sits within the bracket, clamp is the
19671 // identity; the expander inherits candidate's ceilings verbatim.
19672 let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
19673 assert_eq!(clamped, candidate);
19674 let mut e = Expander::with_limits(clamped);
19675 let forms = read("(defmacro two (a b) `,a)").unwrap();
19676 let err = e.expand_program(forms).unwrap_err();
19677 assert!(
19678 matches!(
19679 err,
19680 LispError::MacroArityExceeded {
19681 arity: 2,
19682 limit: 1,
19683 ..
19684 }
19685 ),
19686 "the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
19687 );
19688 }
19689
19690 // ── ResourceLimits::is_lower_bound_of / is_upper_bound_of ─────────
19691 // N-ary boolean-conjunction peers of the pairwise `leq` relation,
19692 // closing the (combinator, predicate) row on the N-ary-aggregation
19693 // face of the lattice-algebra surface. See the docstring on
19694 // `ResourceLimits::is_lower_bound_of` for the (pairwise, N-ary,
19695 // bracket) × (combinator, predicate) placement.
19696
19697 #[test]
19698 fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
19699 // Empty-conjunction identity — every posture is trivially a
19700 // lower bound of the empty set. Peer of `strictest_of(&[]) ==
19701 // UNBOUNDED_RESOURCE_LIMITS`'s empty-slice identity one
19702 // PRIMITIVE-KIND axis over: the empty slice aggregates to the
19703 // identity element of the underlying operation (`true` for
19704 // boolean conjunction, `UNBOUNDED` for pointwise `min`).
19705 assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
19706 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19707 assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19708 assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19709 }
19710
19711 #[test]
19712 fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
19713 // Empty-conjunction dual — every posture is trivially an upper
19714 // bound of the empty set. Peer of
19715 // `is_lower_bound_of_empty_slice_is_vacuously_true` one
19716 // COMBINATOR-DIRECTION axis over, AND peer of
19717 // `most_permissive_of(&[]) == EMPTY_RESOURCE_LIMITS`'s empty-
19718 // slice identity one PRIMITIVE-KIND axis over.
19719 assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
19720 assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19721 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19722 assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19723 }
19724
19725 #[test]
19726 fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
19727 // 1-input identity — the singleton predicate reduces to the
19728 // pairwise partial-order relation. The N-ary predicate on a
19729 // 1-element slice is the pairwise `leq` verbatim, mirroring
19730 // `strictest_of(&[a]) == a`'s reduction on the combinator side.
19731 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19732 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19733 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19734 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19735 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19736 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19737 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19738 ];
19739 for (a, b) in cases {
19740 assert_eq!(
19741 a.is_lower_bound_of(&[b]),
19742 a.leq(b),
19743 "1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
19744 );
19745 }
19746 }
19747
19748 #[test]
19749 fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
19750 // 1-input dual identity — the singleton upper-bound predicate
19751 // is the pairwise `leq` with the direction flipped
19752 // (`b.leq(a)`), since `self` sits ABOVE the operand rather than
19753 // BELOW it.
19754 let cases: [(ResourceLimits, ResourceLimits); 6] = [
19755 (EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19756 (UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
19757 (DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
19758 (UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
19759 (HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
19760 (HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
19761 ];
19762 for (a, b) in cases {
19763 assert_eq!(
19764 a.is_upper_bound_of(&[b]),
19765 b.leq(a),
19766 "1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
19767 );
19768 }
19769 }
19770
19771 #[test]
19772 fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
19773 // Definitional bridge — the N-ary meet is always a lower bound
19774 // of the slice it aggregates. Pins the link between the N-ary
19775 // COMBINATOR (`strictest_of`) and the N-ary PREDICATE
19776 // (`is_lower_bound_of`): the meet is an ELEMENT of the lower-
19777 // bound set the predicate CHARACTERIZES.
19778 let postures: [ResourceLimits; 3] = [
19779 HAND_AUTHORED_MID_POSTURE,
19780 HAND_AUTHORED_OTHER_POSTURE,
19781 DEFAULT_RESOURCE_LIMITS,
19782 ];
19783 let met = ResourceLimits::strictest_of(&postures);
19784 assert!(met.is_lower_bound_of(&postures));
19785 }
19786
19787 #[test]
19788 fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
19789 // Dual definitional bridge — the N-ary join is always an upper
19790 // bound of the slice it aggregates.
19791 let postures: [ResourceLimits; 3] = [
19792 HAND_AUTHORED_MID_POSTURE,
19793 HAND_AUTHORED_OTHER_POSTURE,
19794 DEFAULT_RESOURCE_LIMITS,
19795 ];
19796 let joined = ResourceLimits::most_permissive_of(&postures);
19797 assert!(joined.is_upper_bound_of(&postures));
19798 }
19799
19800 #[test]
19801 fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
19802 // Universal-bottom witness — the lattice bottom is a common
19803 // lower bound of every slice, since `EMPTY.leq(a) == true` for
19804 // every posture by the lattice-bottom axiom (`0 ≤ x` per-axis).
19805 // The bounded-lattice specialization of the general "the meet-
19806 // identity is a lower bound of every set" theorem.
19807 let postures: [ResourceLimits; 4] = [
19808 HAND_AUTHORED_MID_POSTURE,
19809 HAND_AUTHORED_OTHER_POSTURE,
19810 DEFAULT_RESOURCE_LIMITS,
19811 UNBOUNDED_RESOURCE_LIMITS,
19812 ];
19813 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
19814 // The three preset-carried postures each individually — the
19815 // universal-bottom witness holds on the singleton slice too.
19816 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19817 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19818 assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19819 }
19820
19821 #[test]
19822 fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
19823 // Universal-top witness — the lattice top is a common upper
19824 // bound of every slice, since `a.leq(UNBOUNDED) == true` for
19825 // every posture by the lattice-top axiom (`x ≤ usize::MAX`
19826 // per-axis).
19827 let postures: [ResourceLimits; 4] = [
19828 HAND_AUTHORED_MID_POSTURE,
19829 HAND_AUTHORED_OTHER_POSTURE,
19830 DEFAULT_RESOURCE_LIMITS,
19831 EMPTY_RESOURCE_LIMITS,
19832 ];
19833 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
19834 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19835 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19836 assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
19837 }
19838
19839 #[test]
19840 fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
19841 // Any-operand short-circuit — if `self` fails `leq` against ANY
19842 // operand in the slice, the N-ary conjunction rejects. The dual
19843 // of the "meet is a lower bound" witness on the false side:
19844 // moving `self` STRICTLY ABOVE an operand on any axis costs the
19845 // lower-bound property.
19846 //
19847 // MID sits above the join (element-wise) on some axes and below
19848 // on others, so MID is NEITHER a lower bound of {MID, OTHER}
19849 // (fails against OTHER's smaller-axis values) NOR is OTHER a
19850 // lower bound of {MID, OTHER} (fails against MID's smaller-axis
19851 // values).
19852 assert!(!HAND_AUTHORED_MID_POSTURE
19853 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19854 assert!(!HAND_AUTHORED_OTHER_POSTURE
19855 .is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19856 // A UNIVERSAL rejection — the lattice top is a lower bound
19857 // ONLY of the empty set OR of slices whose every element equals
19858 // UNBOUNDED. Given a strictly-smaller preset in the slice, the
19859 // top's per-axis usize::MAX loses `leq` on every axis whose
19860 // preset value is strictly smaller.
19861 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19862 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19863 }
19864
19865 #[test]
19866 fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
19867 // Dual any-operand short-circuit — if ANY operand fails `leq`
19868 // against `self`, the N-ary conjunction rejects.
19869 assert!(!HAND_AUTHORED_MID_POSTURE
19870 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19871 assert!(!HAND_AUTHORED_OTHER_POSTURE
19872 .is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
19873 // The bottom cannot bound anything from above except the empty
19874 // set OR a slice of EMPTY-only postures.
19875 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
19876 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19877 }
19878
19879 #[test]
19880 fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
19881 // Method-vs-scaffold cross-check — the lifted N-ary predicate
19882 // agrees with the pre-lift two-primitive
19883 // `postures.iter().all(|p| self.leq(*p))` scaffolding on every
19884 // preset-carried posture across the shipped bounded-lattice
19885 // extrema AND the two hand-authored asymmetric witnesses. Pins
19886 // the lift is a semantic no-op — post-lift the consumer routes
19887 // through ONE name instead of composing the conjunction inline,
19888 // but the two paths compute the same boolean.
19889 let candidates: [ResourceLimits; 5] = [
19890 EMPTY_RESOURCE_LIMITS,
19891 DEFAULT_RESOURCE_LIMITS,
19892 UNBOUNDED_RESOURCE_LIMITS,
19893 HAND_AUTHORED_MID_POSTURE,
19894 HAND_AUTHORED_OTHER_POSTURE,
19895 ];
19896 let slice: [ResourceLimits; 3] = [
19897 HAND_AUTHORED_MID_POSTURE,
19898 HAND_AUTHORED_OTHER_POSTURE,
19899 DEFAULT_RESOURCE_LIMITS,
19900 ];
19901 for c in candidates {
19902 let direct = slice.iter().all(|p| c.leq(*p));
19903 assert_eq!(
19904 c.is_lower_bound_of(&slice),
19905 direct,
19906 "is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
19907 );
19908 }
19909 }
19910
19911 #[test]
19912 fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
19913 // Dual method-vs-scaffold cross-check.
19914 let candidates: [ResourceLimits; 5] = [
19915 EMPTY_RESOURCE_LIMITS,
19916 DEFAULT_RESOURCE_LIMITS,
19917 UNBOUNDED_RESOURCE_LIMITS,
19918 HAND_AUTHORED_MID_POSTURE,
19919 HAND_AUTHORED_OTHER_POSTURE,
19920 ];
19921 let slice: [ResourceLimits; 3] = [
19922 HAND_AUTHORED_MID_POSTURE,
19923 HAND_AUTHORED_OTHER_POSTURE,
19924 DEFAULT_RESOURCE_LIMITS,
19925 ];
19926 for c in candidates {
19927 let direct = slice.iter().all(|p| p.leq(c));
19928 assert_eq!(
19929 c.is_upper_bound_of(&slice),
19930 direct,
19931 "is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
19932 );
19933 }
19934 }
19935
19936 #[test]
19937 fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
19938 // Const-fn pin — the N-ary lower-bound predicate is evaluable
19939 // in const context, so a caller can pin an N-ary-bound-
19940 // membership identity at compile time. Sibling of the const-fn
19941 // evaluability pins on `leq` and `within` one PRIMITIVE-KIND
19942 // axis over, AND sibling of `strictest_of`'s const-fn pin one
19943 // COMBINATOR-KIND axis over.
19944 //
19945 // A regression to a runtime `fn` here would fail the `const _:
19946 // () = assert!(...)` bindings below at compile time.
19947 const _: () = assert!(EMPTY_RESOURCE_LIMITS
19948 .is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
19949 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
19950 const _: () =
19951 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
19952 }
19953
19954 #[test]
19955 fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
19956 // Const-fn dual pin.
19957 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
19958 .is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
19959 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
19960 const _: () =
19961 assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
19962 }
19963
19964 /// The full canonical preset roster the strict-order pins sweep
19965 /// through — the (bottom, middle, top) bounded-lattice preset
19966 /// triple plus the two hand-authored incomparable asymmetric
19967 /// postures. Every strict-order law below quantifies over this
19968 /// slice, so a regression that breaks strict `<` on any preset
19969 /// pair discriminates at ONE named fixture rather than at a
19970 /// per-test ad-hoc literal.
19971 const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
19972 EMPTY_RESOURCE_LIMITS,
19973 DEFAULT_RESOURCE_LIMITS,
19974 UNBOUNDED_RESOURCE_LIMITS,
19975 HAND_AUTHORED_MID_POSTURE,
19976 HAND_AUTHORED_OTHER_POSTURE,
19977 ];
19978
19979 #[test]
19980 fn resource_limits_lt_is_irreflexive() {
19981 // Strict partial-order law — `a < a == false` for every
19982 // posture. Irreflexivity is the strict-relation axiom peer of
19983 // reflexivity on `leq` one STRICTNESS axis over on the
19984 // partial-order axiom surface; a regression that dropped the
19985 // antisymmetric leg (`!other.leq(self)`) — leaving the body at
19986 // `self.leq(other)` — would fire here on every preset since
19987 // `leq` is reflexive.
19988 for &a in STRICT_ORDER_ROSTER {
19989 assert!(!a.lt(a), "irreflexivity failed on {a:?}");
19990 }
19991 }
19992
19993 #[test]
19994 fn resource_limits_lt_is_asymmetric() {
19995 // Strict partial-order law — `a < b ⇒ ¬(b < a)` for every
19996 // pair. The two directions of the strict relation cannot both
19997 // hold, since `a.lt(b)` implies `!b.leq(a)` and `b.lt(a)`
19998 // requires `b.leq(a)`. Sweeps every ordered pair in the
19999 // canonical preset roster; discriminates a regression that
20000 // dropped the antisymmetric leg in either direction.
20001 for &a in STRICT_ORDER_ROSTER {
20002 for &b in STRICT_ORDER_ROSTER {
20003 if a.lt(b) {
20004 assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
20005 }
20006 }
20007 }
20008 }
20009
20010 #[test]
20011 fn resource_limits_lt_is_transitive() {
20012 // Strict partial-order law — `a < b ∧ b < c ⇒ a < c`. Sweeps
20013 // every ordered triple in the canonical preset roster; each
20014 // consequence is checked whenever both antecedents hold. On
20015 // the (bottom, middle, top) diagonal the canonical witness
20016 // `EMPTY < DEFAULT < UNBOUNDED` composes to
20017 // `EMPTY < UNBOUNDED`, exercising the transitivity chain on
20018 // the bounded-lattice diagonal explicitly; the sweep pins the
20019 // same law on every other transitively-related triple in the
20020 // roster.
20021 for &a in STRICT_ORDER_ROSTER {
20022 for &b in STRICT_ORDER_ROSTER {
20023 for &c in STRICT_ORDER_ROSTER {
20024 if a.lt(b) && b.lt(c) {
20025 assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20026 }
20027 }
20028 }
20029 }
20030 }
20031
20032 #[test]
20033 fn resource_limits_lt_refines_leq() {
20034 // Refinement axiom — `a < b ⇒ a ≤ b`. The strict relation is
20035 // a refinement of the non-strict one; a witness of strict `<`
20036 // is also a witness of `≤`. Sweeps every ordered pair in the
20037 // canonical preset roster.
20038 for &a in STRICT_ORDER_ROSTER {
20039 for &b in STRICT_ORDER_ROSTER {
20040 if a.lt(b) {
20041 assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
20042 }
20043 }
20044 }
20045 }
20046
20047 #[test]
20048 fn resource_limits_lt_agrees_with_leq_minus_equality() {
20049 // Antisymmetry consequence — on ANY antisymmetric partial
20050 // order (which the pointwise `≤` on `ResourceLimits` is),
20051 // `a < b ⇔ a ≤ b ∧ a ≠ b`. Cross-checks the shipped
20052 // antisymmetric-leq encoding against the leq-and-not-equal
20053 // encoding across the full 5×5 preset matrix; a regression to
20054 // a non-antisymmetric preorder encoding would fire here.
20055 for &a in STRICT_ORDER_ROSTER {
20056 for &b in STRICT_ORDER_ROSTER {
20057 let shipped = a.lt(b);
20058 let alt = a.leq(b) && a != b;
20059 assert_eq!(
20060 shipped, alt,
20061 "encoding cross-check failed on ({a:?}, {b:?})",
20062 );
20063 }
20064 }
20065 }
20066
20067 #[test]
20068 fn resource_limits_lt_of_bottom_diagonal_pinned() {
20069 // Concrete-preset pin — the strict chain across the (bottom,
20070 // middle, top) preset triple on the bounded-lattice diagonal.
20071 // Every `DEFAULT_MAX_*` module constant is a concrete positive
20072 // value strictly less than [`usize::MAX`] and strictly greater
20073 // than `0`, so on every axis the three endpoints are strictly
20074 // ordered; the six-axis pointwise conjunction lifts the
20075 // per-axis strict chain to the posture-level strict chain.
20076 //
20077 // Peer of `resource_limits_leq_of_default_and_unbounded_is_a_
20078 // strict_order` one STRICTNESS axis over — where the non-
20079 // strict pin asserted "leq holds one way and not the other"
20080 // via two `leq` calls, this pin asserts the strict-chain
20081 // conclusion via one `lt` call per link and pinpoints the
20082 // three-preset chain on the bounded-lattice diagonal.
20083 assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20084 assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20085 assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20086 }
20087
20088 #[test]
20089 fn resource_limits_lt_rejects_incomparable_postures() {
20090 // Partial-order non-total pin — the two hand-authored
20091 // asymmetric postures have MID smaller on three axes and OTHER
20092 // smaller on the other three, so neither `leq` direction
20093 // holds; strict `<` therefore fails in BOTH directions. The
20094 // pin discriminates a regression that promoted strict `<`
20095 // from partial to total (e.g. treating incomparable pairs as
20096 // strictly `<` in some canonical direction), and pairs with
20097 // `resource_limits_leq_is_not_total_on_asymmetric_postures`
20098 // one STRICTNESS axis over.
20099 assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
20100 assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
20101 }
20102
20103 #[test]
20104 fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
20105 // Direct-encoding cross-check — `a.lt(b) == (a.leq(b) &&
20106 // !b.leq(a))` at every pair by definition. Sibling of the
20107 // `leq_minus_equality` pin one ENCODING axis over: that pin
20108 // routes through the equality-based encoding (an
20109 // antisymmetry consequence), this pin routes through the
20110 // definitional antisymmetric-leq encoding. A regression that
20111 // shipped a different (e.g. axis-wise strict) encoding of
20112 // `lt` diverges from this cross-check on the incomparable
20113 // asymmetric-preset pair.
20114 for &a in STRICT_ORDER_ROSTER {
20115 for &b in STRICT_ORDER_ROSTER {
20116 let shipped = a.lt(b);
20117 let direct = a.leq(b) && !b.leq(a);
20118 assert_eq!(
20119 shipped, direct,
20120 "direct-encoding cross-check failed on ({a:?}, {b:?})",
20121 );
20122 }
20123 }
20124 }
20125
20126 #[test]
20127 fn resource_limits_gt_is_dual_of_lt() {
20128 // Direction-flip pin — `a.gt(b) == b.lt(a)` at every pair. The
20129 // shipped `gt` body delegates to `lt` verbatim so this
20130 // agreement holds definitionally, but the pin discriminates a
20131 // regression that re-implemented `gt` independently and
20132 // dropped the direction flip. Sweeps the full 5×5 preset
20133 // matrix so incomparable + strictly-ordered + equal pairs
20134 // are all exercised.
20135 for &a in STRICT_ORDER_ROSTER {
20136 for &b in STRICT_ORDER_ROSTER {
20137 assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
20138 }
20139 }
20140 }
20141
20142 #[test]
20143 fn resource_limits_gt_of_top_diagonal_pinned() {
20144 // Concrete-preset pin — the strict chain across the (top,
20145 // middle, bottom) preset triple on the bounded-lattice
20146 // diagonal, walked in the "above" direction. Peer of
20147 // `resource_limits_lt_of_bottom_diagonal_pinned` one
20148 // DIRECTION axis over.
20149 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20150 assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20151 assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20152 }
20153
20154 #[test]
20155 fn resource_limits_gt_is_irreflexive() {
20156 // Strict partial-order law dual — `a > a == false` for every
20157 // posture. Inherits from the irreflexivity of `lt` via the
20158 // shipped `gt` body's delegation, but pinned explicitly so a
20159 // future non-delegating re-implementation of `gt` still has
20160 // its irreflexivity gated at the type-level test cohort.
20161 for &a in STRICT_ORDER_ROSTER {
20162 assert!(!a.gt(a), "irreflexivity failed on {a:?}");
20163 }
20164 }
20165
20166 #[test]
20167 fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
20168 // Const-fn pin — the strict `<` relation is evaluable in
20169 // const context, so a caller can pin a preset-strict-order
20170 // identity at compile time. Sibling of the const-fn
20171 // evaluability pin on `leq` one STRICTNESS axis over.
20172 //
20173 // A regression to a runtime `fn` here would fail the `const
20174 // _: () = assert!(...)` bindings below at compile time.
20175 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20176 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20177 const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
20178 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
20179 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
20180 }
20181
20182 #[test]
20183 fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
20184 // Const-fn dual pin — sibling of `lt`'s const-fn pin one
20185 // DIRECTION axis over.
20186 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20187 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20188 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
20189 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
20190 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
20191 }
20192
20193 #[test]
20194 fn resource_limits_geq_is_dual_of_leq() {
20195 // Direction-flip pin — `a.geq(b) == b.leq(a)` at every pair.
20196 // The shipped `geq` body delegates to `leq` verbatim so this
20197 // agreement holds definitionally, but the pin discriminates a
20198 // regression that re-implemented `geq` independently and
20199 // dropped the direction flip. Sweeps the full 5×5 preset
20200 // matrix so incomparable + strictly-ordered + equal pairs are
20201 // all exercised. Peer of `resource_limits_gt_is_dual_of_lt`
20202 // one STRICTNESS axis over on the (below, above) × (non-strict,
20203 // strict) 2×2 pairwise partial-order face.
20204 for &a in STRICT_ORDER_ROSTER {
20205 for &b in STRICT_ORDER_ROSTER {
20206 assert_eq!(
20207 a.geq(b),
20208 b.leq(a),
20209 "geq/leq duality failed on ({a:?}, {b:?})",
20210 );
20211 }
20212 }
20213 }
20214
20215 #[test]
20216 fn resource_limits_geq_is_reflexive() {
20217 // Non-strict partial-order law — `a ≥ a == true` for every
20218 // posture. Reflexivity is the non-strict-relation axiom peer of
20219 // irreflexivity on `gt` one STRICTNESS axis over on the
20220 // partial-order axiom surface; a regression that hardened the
20221 // delegation into a strict `!other.leq(self)` would fire here
20222 // on every preset since the reflexive pair collapses.
20223 for &a in STRICT_ORDER_ROSTER {
20224 assert!(a.geq(a), "reflexivity failed on {a:?}");
20225 }
20226 }
20227
20228 #[test]
20229 fn resource_limits_geq_is_antisymmetric() {
20230 // Non-strict partial-order law — `a ≥ b ∧ b ≥ a ⇒ a == b`.
20231 // Antisymmetry is inherited from `leq` via the shipped `geq`
20232 // body's `other.leq(self)` delegation (which combined with
20233 // `b.geq(a)` — i.e. `a.leq(b)` — gives `leq` antisymmetry on
20234 // the pair). Sweeps the full 5×5 preset matrix; the pin fires
20235 // on any regression that let two DISTINCT postures satisfy
20236 // `geq` in both directions.
20237 for &a in STRICT_ORDER_ROSTER {
20238 for &b in STRICT_ORDER_ROSTER {
20239 if a.geq(b) && b.geq(a) {
20240 assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
20241 }
20242 }
20243 }
20244 }
20245
20246 #[test]
20247 fn resource_limits_geq_is_transitive() {
20248 // Non-strict partial-order law — `a ≥ b ∧ b ≥ c ⇒ a ≥ c`.
20249 // Inherits from the transitivity of `leq` via the shipped
20250 // delegation; sweeps every ordered triple in the canonical
20251 // preset roster. Peer of `resource_limits_lt_is_transitive`
20252 // one STRICTNESS axis over and `resource_limits_leq_is_
20253 // transitive` one DIRECTION axis over.
20254 for &a in STRICT_ORDER_ROSTER {
20255 for &b in STRICT_ORDER_ROSTER {
20256 for &c in STRICT_ORDER_ROSTER {
20257 if a.geq(b) && b.geq(c) {
20258 assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
20259 }
20260 }
20261 }
20262 }
20263 }
20264
20265 #[test]
20266 fn resource_limits_geq_refines_gt() {
20267 // Refinement axiom peer — `a > b ⇒ a ≥ b`. The strict
20268 // relation is a refinement of the non-strict one in the
20269 // "above" direction, exactly as `lt` refines `leq` in the
20270 // "below" direction (pinned by
20271 // `resource_limits_lt_refines_leq` one DIRECTION axis over).
20272 // A witness of strict `>` is also a witness of `≥`.
20273 for &a in STRICT_ORDER_ROSTER {
20274 for &b in STRICT_ORDER_ROSTER {
20275 if a.gt(b) {
20276 assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
20277 }
20278 }
20279 }
20280 }
20281
20282 #[test]
20283 fn resource_limits_geq_agrees_with_gt_plus_equality() {
20284 // Antisymmetry consequence peer — on ANY antisymmetric partial
20285 // order, `a ≥ b ⇔ a > b ∨ a == b`. Cross-checks the shipped
20286 // delegation against the strict-or-equal encoding across the
20287 // full 5×5 preset matrix; a regression that shipped a non-
20288 // antisymmetric preorder encoding for `geq` fires here. Peer
20289 // of `resource_limits_lt_agrees_with_leq_minus_equality` one
20290 // DIRECTION-AND-STRICTNESS axis over.
20291 for &a in STRICT_ORDER_ROSTER {
20292 for &b in STRICT_ORDER_ROSTER {
20293 let shipped = a.geq(b);
20294 let alt = a.gt(b) || a == b;
20295 assert_eq!(
20296 shipped, alt,
20297 "encoding cross-check failed on ({a:?}, {b:?})",
20298 );
20299 }
20300 }
20301 }
20302
20303 #[test]
20304 fn resource_limits_geq_of_top_diagonal_pinned() {
20305 // Concrete-preset pin — the non-strict chain across the (top,
20306 // middle, bottom) preset triple on the bounded-lattice
20307 // diagonal, walked in the "above" direction. Peer of
20308 // `resource_limits_gt_of_top_diagonal_pinned` one STRICTNESS
20309 // axis over: the non-strict pin also holds at the reflexive
20310 // diagonal, so every posture sits above ITSELF via `geq` in
20311 // addition to sitting above the strictly-tighter presets.
20312 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20313 assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20314 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20315 assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20316 assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20317 assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20318 }
20319
20320 #[test]
20321 fn resource_limits_geq_rejects_incomparable_postures() {
20322 // Partial-order non-total pin — the two hand-authored
20323 // asymmetric postures have MID smaller on three axes and OTHER
20324 // smaller on the other three, so neither `leq` direction holds
20325 // and therefore neither `geq` direction holds. The pin
20326 // discriminates a regression that promoted `geq` from partial
20327 // to total, and pairs with
20328 // `resource_limits_lt_rejects_incomparable_postures` one
20329 // STRICTNESS-AND-DIRECTION axis over.
20330 assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
20331 assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
20332 }
20333
20334 #[test]
20335 fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
20336 // Arity-reduction pin — `a.geq(b) == a.is_upper_bound_of(&[b])`
20337 // at every pair. The pairwise "above" relation is exactly the
20338 // 1-input specialization of the N-ary upper-bound predicate;
20339 // the pin cross-checks that the two named entries agree on
20340 // the single-element case, which discriminates a regression
20341 // that drifted one from the other. Peer of the single-element
20342 // identity docstring on `is_upper_bound_of` one ARITY axis
20343 // over.
20344 for &a in STRICT_ORDER_ROSTER {
20345 for &b in STRICT_ORDER_ROSTER {
20346 assert_eq!(
20347 a.geq(b),
20348 a.is_upper_bound_of(&[b]),
20349 "arity-reduction failed on ({a:?}, {b:?})",
20350 );
20351 }
20352 }
20353 }
20354
20355 #[test]
20356 fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
20357 // Const-fn pin — the non-strict `≥` relation is evaluable in
20358 // const context, so a caller can pin a preset-non-strict-order
20359 // identity at compile time. Sibling of the const-fn evaluability
20360 // pins on `leq` one DIRECTION axis over AND on `gt` one
20361 // STRICTNESS axis over.
20362 //
20363 // A regression to a runtime `fn` here would fail the `const _:
20364 // () = assert!(...)` bindings below at compile time.
20365 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20366 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20367 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
20368 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
20369 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
20370 }
20371
20372 #[test]
20373 fn resource_limits_is_incomparable_is_irreflexive() {
20374 // Antichain-corner reflexivity pin — `a.is_incomparable(a) ==
20375 // false` on every posture. Every posture is COMPARABLE to
20376 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so the
20377 // conjunction's `!self.leq(other)` leg forces the verdict to
20378 // `false` at every diagonal input. Discriminates a regression
20379 // that dropped the `!` on `self.leq(other)` (leaving the body
20380 // at `self.leq(other) && !self.geq(other)`) — that would fold
20381 // the diagonal to `false` on the reflexive `leq` arm but
20382 // silently flip the antichain corner on non-diagonal cells.
20383 for &a in STRICT_ORDER_ROSTER {
20384 assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
20385 }
20386 }
20387
20388 #[test]
20389 fn resource_limits_is_incomparable_is_symmetric() {
20390 // Antichain-corner symmetry pin — `a.is_incomparable(b) ==
20391 // b.is_incomparable(a)` on every pair. The composition
20392 // `!self.leq(other) && !self.geq(other)` folds through the
20393 // `self.leq(other) == other.geq(self)` dual identity on `geq`
20394 // and the conjunction's commutativity into an order-
20395 // independent projection on its two inputs. Sweeps every
20396 // ordered pair in the canonical preset roster;
20397 // discriminates a regression that broke either leg's dual
20398 // identity — one direction would fold to `false` on cells the
20399 // other still folded to `true`.
20400 for &a in STRICT_ORDER_ROSTER {
20401 for &b in STRICT_ORDER_ROSTER {
20402 assert_eq!(
20403 a.is_incomparable(b),
20404 b.is_incomparable(a),
20405 "symmetry failed on ({a:?}, {b:?})",
20406 );
20407 }
20408 }
20409 }
20410
20411 #[test]
20412 fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
20413 // De Morgan dual pin — `a.is_incomparable(b) == !(a.leq(b) ||
20414 // a.geq(b))` on every pair. The antichain corner is the SET-
20415 // COMPLEMENT of the union of the two pointwise-domination
20416 // directions on the (comparable, incomparable) two-cell
20417 // partition of the ordered pair × verdict surface. Sweeps
20418 // every ordered pair in the canonical preset roster;
20419 // discriminates a regression that drifted the antichain
20420 // characterization off the substrate's `!leq && !geq`
20421 // composition (e.g., a shortcut through `!self.leq(other) ||
20422 // !self.geq(other)` — the OR-form falsely reports
20423 // incomparability on non-equal comparable pairs).
20424 for &a in STRICT_ORDER_ROSTER {
20425 for &b in STRICT_ORDER_ROSTER {
20426 let via_named = a.is_incomparable(b);
20427 let via_composition = !(a.leq(b) || a.geq(b));
20428 assert_eq!(
20429 via_named, via_composition,
20430 "De Morgan dual failed on ({a:?}, {b:?})",
20431 );
20432 }
20433 }
20434 }
20435
20436 #[test]
20437 fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
20438 // Two-cell partition pin — on every ordered pair EITHER
20439 // (a.leq(b) || a.geq(b)) OR a.is_incomparable(b), and NEVER
20440 // both. The (comparable, incomparable) partition of the
20441 // ordered pair × verdict surface is exhaustive-and-disjoint —
20442 // a substrate-level EXCLUSIVITY THEOREM the projection pins
20443 // once. Sweeps every ordered pair in the canonical preset
20444 // roster; discriminates a regression that let the two cells
20445 // overlap (e.g., an implementation that reported the antichain
20446 // corner as `true` on the comparable diagonal).
20447 for &a in STRICT_ORDER_ROSTER {
20448 for &b in STRICT_ORDER_ROSTER {
20449 let comparable = a.leq(b) || a.geq(b);
20450 let incomparable = a.is_incomparable(b);
20451 assert!(
20452 comparable != incomparable,
20453 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20454 );
20455 }
20456 }
20457 }
20458
20459 #[test]
20460 fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
20461 // Bottom-pole absorption pin — `a.is_incomparable(
20462 // EMPTY_RESOURCE_LIMITS) == false` on every posture `a`.
20463 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20464 // `EMPTY.leq(a)` holds on every axis (usize zero is the
20465 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20466 // dual identity, which falsifies the `!self.geq(other)` leg.
20467 // Sweeps the canonical preset roster; pairs with the top-
20468 // pole absorption sibling one POLE axis over.
20469 for &a in STRICT_ORDER_ROSTER {
20470 assert!(
20471 !a.is_incomparable(EMPTY_RESOURCE_LIMITS),
20472 "bottom-pole absorption failed on {a:?}",
20473 );
20474 assert!(
20475 !EMPTY_RESOURCE_LIMITS.is_incomparable(a),
20476 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20477 );
20478 }
20479 }
20480
20481 #[test]
20482 fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
20483 // Top-pole absorption pin — `a.is_incomparable(
20484 // UNBOUNDED_RESOURCE_LIMITS) == false` on every posture `a`.
20485 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20486 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20487 // usize::MAX` unconditionally), which falsifies the
20488 // `!self.leq(other)` leg. Sweeps the canonical preset
20489 // roster; pairs with the bottom-pole absorption sibling one
20490 // POLE axis over.
20491 for &a in STRICT_ORDER_ROSTER {
20492 assert!(
20493 !a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
20494 "top-pole absorption failed on {a:?}",
20495 );
20496 assert!(
20497 !UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
20498 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20499 );
20500 }
20501 }
20502
20503 #[test]
20504 fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
20505 // Antichain load-bearing arm — the two hand-authored
20506 // asymmetric postures sit on distinct branches (each is
20507 // smaller on three axes and larger on the other three), so
20508 // neither pointwise-domination direction closes and the
20509 // antichain verdict HOLDS in both orderings. Pairs with
20510 // `resource_limits_geq_rejects_incomparable_postures` +
20511 // `resource_limits_lt_rejects_incomparable_postures` one COVER
20512 // axis over: those pin the two comparable-direction predicates
20513 // FALSIFY on this pair; THIS pin confirms the antichain
20514 // verdict HOLDS on the SAME pair — the two are the negation-
20515 // paired sides of the (comparable, incomparable) two-cell
20516 // partition.
20517 assert!(
20518 HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
20519 "antichain arm failed on (MID, OTHER)",
20520 );
20521 assert!(
20522 HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
20523 "antichain arm failed on flipped (OTHER, MID)",
20524 );
20525 }
20526
20527 #[test]
20528 fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
20529 // Shipped preset comparability pin — every ordered pair drawn
20530 // from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset triangle
20531 // is COMPARABLE (in both directions the pairwise partial-order
20532 // relation gives a verdict). The three shipped presets form a
20533 // TOTALLY ORDERED chain on the pointwise partial-order (EMPTY
20534 // <= DEFAULT <= UNBOUNDED on every field), so the antichain
20535 // corner MUST fold to `false` on every ordered pair drawn from
20536 // them. Discriminates a regression that promoted a shipped
20537 // preset off the total-order chain by drifting ONE field
20538 // silently.
20539 const TRIANGLE: &[ResourceLimits] = &[
20540 EMPTY_RESOURCE_LIMITS,
20541 DEFAULT_RESOURCE_LIMITS,
20542 UNBOUNDED_RESOURCE_LIMITS,
20543 ];
20544 for &a in TRIANGLE {
20545 for &b in TRIANGLE {
20546 assert!(
20547 !a.is_incomparable(b),
20548 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20549 );
20550 }
20551 }
20552 }
20553
20554 #[test]
20555 fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
20556 // Const-fn pin — the antichain characterization is evaluable
20557 // in const context, so a caller can pin an antichain-
20558 // disagreement identity at compile time. Sibling of the
20559 // const-fn evaluability pins on `leq` / `geq` / `lt` / `gt`
20560 // one COVER axis over on the pairwise-relation surface.
20561 //
20562 // A regression to a runtime `fn` here would fail the `const _:
20563 // () = assert!(...)` bindings below at compile time.
20564 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20565 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20566 const _: () =
20567 assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20568 const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
20569 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20570 const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
20571 const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
20572 }
20573
20574 // ── ResourceLimits::is_comparable ─────────────────────────────────
20575 //
20576 // The COMPARABLE CORNER on the pairwise-relation surface — the
20577 // DIRECT POSITIVE DUAL of `is_incomparable` one COVER-COMPLEMENT
20578 // axis over on the (comparable, incomparable) two-cell partition
20579 // of the ordered pair × verdict surface. See `ResourceLimits::is_comparable`
20580 // for the algebra; the pins below fix each documented property
20581 // exactly once so a regression on any leg fires on a NAMED test
20582 // rather than an ad-hoc downstream break.
20583
20584 #[test]
20585 fn resource_limits_is_comparable_is_reflexive() {
20586 // Comparable-corner reflexivity pin — `a.is_comparable(a) ==
20587 // true` on every posture. Every posture is COMPARABLE to
20588 // itself via `leq`'s reflexivity (`a.leq(a) == true`), so
20589 // the disjunction's `self.leq(other)` leg holds at every
20590 // diagonal input. Sibling of `is_incomparable`'s
20591 // irreflexivity one CELL axis over on the (comparable,
20592 // incomparable) partition — the diagonal MUST fold to `true`
20593 // here iff it folds to `false` there.
20594 for &a in STRICT_ORDER_ROSTER {
20595 assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
20596 }
20597 }
20598
20599 #[test]
20600 fn resource_limits_is_comparable_is_symmetric() {
20601 // Comparable-corner symmetry pin — `a.is_comparable(b) ==
20602 // b.is_comparable(a)` on every pair. The composition
20603 // `self.leq(other) || self.geq(other)` folds through the
20604 // `self.leq(other) == other.geq(self)` dual identity on
20605 // `geq` and the disjunction's commutativity into an order-
20606 // independent projection on its two inputs. Both cells of an
20607 // EXHAUSTIVE-AND-DISJOINT partition of a symmetric surface
20608 // must themselves be symmetric — the two verdicts flip in
20609 // lockstep on argument swap.
20610 for &a in STRICT_ORDER_ROSTER {
20611 for &b in STRICT_ORDER_ROSTER {
20612 assert_eq!(
20613 a.is_comparable(b),
20614 b.is_comparable(a),
20615 "symmetry failed on ({a:?}, {b:?})",
20616 );
20617 }
20618 }
20619 }
20620
20621 #[test]
20622 fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
20623 // De Morgan dual pin — `a.is_comparable(b) ==
20624 // !a.is_incomparable(b)` on every pair. The comparable corner
20625 // is the SET-COMPLEMENT of the antichain corner on the
20626 // (comparable, incomparable) two-cell partition of the
20627 // ordered pair × verdict surface. The DIRECT dual of
20628 // `resource_limits_is_incomparable_is_de_morgan_dual_of_comparable`
20629 // one CELL axis over: that test pins the antichain corner as
20630 // the negation of the `leq || geq` disjunction; THIS test
20631 // pins the comparability corner as the negation of the
20632 // antichain projection. Together the two pin BOTH SIDES of
20633 // the partition, closing it as a substrate-level
20634 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM.
20635 for &a in STRICT_ORDER_ROSTER {
20636 for &b in STRICT_ORDER_ROSTER {
20637 let via_named = a.is_comparable(b);
20638 let via_negated_dual = !a.is_incomparable(b);
20639 assert_eq!(
20640 via_named, via_negated_dual,
20641 "De Morgan dual failed on ({a:?}, {b:?})",
20642 );
20643 }
20644 }
20645 }
20646
20647 #[test]
20648 fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
20649 // Direct composition pin — `a.is_comparable(b) == a.leq(b) ||
20650 // a.geq(b)` on every pair. Discriminates a regression that
20651 // drifted the comparability body off the substrate's `leq ||
20652 // geq` composition (e.g., a shortcut through `a.leq(b) &&
20653 // a.geq(b)` — the AND-form falsely reports comparability
20654 // only on the equality diagonal, dropping the strict-order
20655 // cells). Anchors the two-primitive delegation as a
20656 // substrate-level identity.
20657 for &a in STRICT_ORDER_ROSTER {
20658 for &b in STRICT_ORDER_ROSTER {
20659 let via_named = a.is_comparable(b);
20660 let via_composition = a.leq(b) || a.geq(b);
20661 assert_eq!(
20662 via_named, via_composition,
20663 "leq||geq composition failed on ({a:?}, {b:?})",
20664 );
20665 }
20666 }
20667 }
20668
20669 #[test]
20670 fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
20671 // Bottom-pole absorption pin — `a.is_comparable(
20672 // EMPTY_RESOURCE_LIMITS) == true` on every posture `a`.
20673 // `EMPTY_RESOURCE_LIMITS` is the bounded-lattice bottom, so
20674 // `EMPTY.leq(a)` holds on every axis (`usize` zero is the
20675 // minimum), which gives `a.geq(EMPTY)` through the `geq`
20676 // dual identity, which satisfies the `self.geq(other)` leg.
20677 // The DIRECT COMPLEMENT of
20678 // `resource_limits_is_incomparable_folds_false_at_the_bottom_pole`
20679 // one CELL axis over on the SAME preset roster — where the
20680 // antichain verdict FOLDS FALSE at the bottom pole, the
20681 // comparability verdict FOLDS TRUE.
20682 for &a in STRICT_ORDER_ROSTER {
20683 assert!(
20684 a.is_comparable(EMPTY_RESOURCE_LIMITS),
20685 "bottom-pole absorption failed on {a:?}",
20686 );
20687 assert!(
20688 EMPTY_RESOURCE_LIMITS.is_comparable(a),
20689 "bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
20690 );
20691 }
20692 }
20693
20694 #[test]
20695 fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
20696 // Top-pole absorption pin — `a.is_comparable(
20697 // UNBOUNDED_RESOURCE_LIMITS) == true` on every posture `a`.
20698 // `UNBOUNDED_RESOURCE_LIMITS` is the bounded-lattice top, so
20699 // `a.leq(UNBOUNDED)` holds on every axis (`a.max_* <=
20700 // usize::MAX` unconditionally), which satisfies the
20701 // `self.leq(other)` leg. The DIRECT COMPLEMENT of
20702 // `resource_limits_is_incomparable_folds_false_at_the_top_pole`
20703 // one CELL axis over on the SAME preset roster.
20704 for &a in STRICT_ORDER_ROSTER {
20705 assert!(
20706 a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
20707 "top-pole absorption failed on {a:?}",
20708 );
20709 assert!(
20710 UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
20711 "top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
20712 );
20713 }
20714 }
20715
20716 #[test]
20717 fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
20718 // Antichain load-bearing arm — the two hand-authored
20719 // asymmetric postures sit on distinct branches (each is
20720 // smaller on three axes and larger on the other three), so
20721 // NEITHER pointwise-domination direction closes and the
20722 // comparability verdict FALSIFIES in both orderings. The
20723 // DIRECT NEGATION of
20724 // `resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair`
20725 // one CELL axis over on the SAME hand-authored pair — the
20726 // two are the negation-paired sides of the (comparable,
20727 // incomparable) two-cell partition, and pinning BOTH cells'
20728 // verdicts on the SAME load-bearing antichain pair closes
20729 // the partition as a substrate-level EXCLUSIVITY THEOREM.
20730 assert!(
20731 !HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
20732 "antichain arm failed on (MID, OTHER)",
20733 );
20734 assert!(
20735 !HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
20736 "antichain arm failed on flipped (OTHER, MID)",
20737 );
20738 }
20739
20740 #[test]
20741 fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
20742 // Shipped preset comparability pin — every ordered pair
20743 // drawn from the (EMPTY, DEFAULT, UNBOUNDED) shipped preset
20744 // triangle is COMPARABLE (in at least one direction the
20745 // pairwise partial-order relation gives a verdict). The
20746 // three shipped presets form a TOTALLY ORDERED chain on the
20747 // pointwise partial-order (EMPTY <= DEFAULT <= UNBOUNDED on
20748 // every field), so the comparability corner MUST fold to
20749 // `true` on every ordered pair drawn from them. The DIRECT
20750 // COMPLEMENT of
20751 // `resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle`
20752 // one CELL axis over on the SAME triangle. Discriminates a
20753 // regression that promoted a shipped preset off the total-
20754 // order chain by drifting ONE field silently.
20755 const TRIANGLE: &[ResourceLimits] = &[
20756 EMPTY_RESOURCE_LIMITS,
20757 DEFAULT_RESOURCE_LIMITS,
20758 UNBOUNDED_RESOURCE_LIMITS,
20759 ];
20760 for &a in TRIANGLE {
20761 for &b in TRIANGLE {
20762 assert!(
20763 a.is_comparable(b),
20764 "shipped preset triangle comparability failed on ({a:?}, {b:?})",
20765 );
20766 }
20767 }
20768 }
20769
20770 #[test]
20771 fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
20772 // Exhaustive-and-disjoint partition pin — on every ordered
20773 // pair EXACTLY ONE of `a.is_comparable(b)` OR
20774 // `a.is_incomparable(b)` holds. The (comparable,
20775 // incomparable) partition of the ordered pair × verdict
20776 // surface is exhaustive-and-disjoint — a substrate-level
20777 // EXCLUSIVITY-AND-EXHAUSTIVENESS THEOREM the two projections
20778 // together pin. Complements
20779 // `resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable`
20780 // one CELL axis over: that test pins the partition against
20781 // the raw `a.leq(b) || a.geq(b)` composition; THIS test pins
20782 // the partition against the NAMED comparability projection —
20783 // a regression that drifted `is_comparable` off the
20784 // composition without breaking the composition-based test
20785 // would still fire here.
20786 for &a in STRICT_ORDER_ROSTER {
20787 for &b in STRICT_ORDER_ROSTER {
20788 let comparable = a.is_comparable(b);
20789 let incomparable = a.is_incomparable(b);
20790 assert!(
20791 comparable != incomparable,
20792 "partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
20793 );
20794 }
20795 }
20796 }
20797
20798 #[test]
20799 fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
20800 // Const-fn pin — the comparability characterization is
20801 // evaluable in const context, so a caller can pin a
20802 // comparability-agreement identity at compile time. Sibling
20803 // of the const-fn evaluability pins on `leq` / `geq` / `lt`
20804 // / `gt` / `is_incomparable` one COVER axis over on the
20805 // pairwise-relation surface.
20806 //
20807 // A regression to a runtime `fn` here would fail the `const _:
20808 // () = assert!(...)` bindings below at compile time.
20809 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20810 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20811 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20812 const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
20813 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20814 const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
20815 const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
20816 }
20817
20818 // ── ResourceLimits::partial_cmp ───────────────────────────────────
20819 //
20820 // The `Option<Ordering>` FULL-VERDICT corner on the pairwise-
20821 // relation surface — the JOINT LIFT of the (comparable, incomparable)
20822 // two-cell partition together with the ordering-direction refinement
20823 // on the comparable arm. See `ResourceLimits::partial_cmp` for the
20824 // algebra; the pins below fix each documented property exactly once
20825 // so a regression on any leg fires on a NAMED test rather than an
20826 // ad-hoc downstream break.
20827
20828 #[test]
20829 fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
20830 // Diagonal-equality pin — `a.partial_cmp(a) ==
20831 // Some(Ordering::Equal)` on every posture. The `leq`
20832 // reflexivity closes both arms of the equality-guarding
20833 // conjunction, so the diagonal cell of the (Less, Equal,
20834 // Greater, None) 4-tile partition pins to the Equal tile.
20835 // Sibling of `is_comparable_is_reflexive` one PROJECTION
20836 // axis over — where the projection folds TRUE at the
20837 // diagonal, THIS method refines the verdict to `Equal`.
20838 for &a in STRICT_ORDER_ROSTER {
20839 assert_eq!(
20840 a.partial_cmp(a),
20841 Some(Ordering::Equal),
20842 "diagonal-equality failed on {a:?}",
20843 );
20844 }
20845 }
20846
20847 #[test]
20848 fn resource_limits_partial_cmp_reverses_under_argument_swap() {
20849 // Antisymmetric-swap pin — `a.partial_cmp(b) ==
20850 // b.partial_cmp(a).map(Ordering::reverse)` on every pair.
20851 // Swapping the argument pair FLIPS the ordering-direction
20852 // verdict (`Less <-> Greater`), fixes the diagonal-equality
20853 // arm, and fixes the antichain arm. Discriminates
20854 // `is_incomparable` / `is_comparable` (both SYMMETRIC on
20855 // argument swap) one POSTURE axis over — the FULL verdict
20856 // carries directional content that inverts under swap where
20857 // the projections do not.
20858 for &a in STRICT_ORDER_ROSTER {
20859 for &b in STRICT_ORDER_ROSTER {
20860 let forward = a.partial_cmp(b);
20861 let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
20862 assert_eq!(
20863 forward, swapped_reversed,
20864 "swap-reverse failed on ({a:?}, {b:?})",
20865 );
20866 }
20867 }
20868 }
20869
20870 #[test]
20871 fn resource_limits_partial_cmp_none_iff_incomparable() {
20872 // Antichain-arm identity pin — `a.partial_cmp(b).is_none()
20873 // == a.is_incomparable(b)` on every pair. The `None` arm of
20874 // the return coincides EXACTLY with the antichain-corner
20875 // projection. Anchors the joint-lift claim: THIS method
20876 // subsumes `is_incomparable` at its `is_none()` projection
20877 // without drifting the antichain characterization.
20878 for &a in STRICT_ORDER_ROSTER {
20879 for &b in STRICT_ORDER_ROSTER {
20880 let via_partial_cmp = a.partial_cmp(b).is_none();
20881 let via_is_incomparable = a.is_incomparable(b);
20882 assert_eq!(
20883 via_partial_cmp, via_is_incomparable,
20884 "none-iff-incomparable failed on ({a:?}, {b:?})",
20885 );
20886 }
20887 }
20888 }
20889
20890 #[test]
20891 fn resource_limits_partial_cmp_some_iff_comparable() {
20892 // Comparability-arm identity pin — `a.partial_cmp(b).is_some()
20893 // == a.is_comparable(b)` on every pair. The `Some(_)` arm of
20894 // the return coincides EXACTLY with the comparability-corner
20895 // projection. The DUAL of the antichain-arm pin one CELL axis
20896 // over on the (comparable, incomparable) partition — together
20897 // the two pins close the JOINT LIFT of both cells at THIS
20898 // method.
20899 for &a in STRICT_ORDER_ROSTER {
20900 for &b in STRICT_ORDER_ROSTER {
20901 let via_partial_cmp = a.partial_cmp(b).is_some();
20902 let via_is_comparable = a.is_comparable(b);
20903 assert_eq!(
20904 via_partial_cmp, via_is_comparable,
20905 "some-iff-comparable failed on ({a:?}, {b:?})",
20906 );
20907 }
20908 }
20909 }
20910
20911 #[test]
20912 fn resource_limits_partial_cmp_less_iff_lt() {
20913 // Strict-less agreement pin — `a.partial_cmp(b) ==
20914 // Some(Ordering::Less)` iff `a.lt(b)`. The strict-less arm of
20915 // the (Less, Equal, Greater, None) 4-tile partition binds to
20916 // the named `lt` companion primitive at exactly one dispatch
20917 // site. A regression that drifted the arm off `lt` (e.g., to
20918 // `leq` — including the equality diagonal) would fire here.
20919 for &a in STRICT_ORDER_ROSTER {
20920 for &b in STRICT_ORDER_ROSTER {
20921 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
20922 let via_lt = a.lt(b);
20923 assert_eq!(
20924 via_partial_cmp, via_lt,
20925 "less-iff-lt failed on ({a:?}, {b:?})",
20926 );
20927 }
20928 }
20929 }
20930
20931 #[test]
20932 fn resource_limits_partial_cmp_greater_iff_gt() {
20933 // Strict-greater agreement pin — `a.partial_cmp(b) ==
20934 // Some(Ordering::Greater)` iff `a.gt(b)`. Dual of the strict-
20935 // less agreement pin one DIRECTION axis over on the (Less,
20936 // Greater) strict-order pair — a regression that dropped the
20937 // asymmetry between the two arms (e.g., collapsed both to
20938 // `Less`) would fire here.
20939 for &a in STRICT_ORDER_ROSTER {
20940 for &b in STRICT_ORDER_ROSTER {
20941 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
20942 let via_gt = a.gt(b);
20943 assert_eq!(
20944 via_partial_cmp, via_gt,
20945 "greater-iff-gt failed on ({a:?}, {b:?})",
20946 );
20947 }
20948 }
20949 }
20950
20951 #[test]
20952 fn resource_limits_partial_cmp_equal_iff_eq() {
20953 // Equality agreement pin — `a.partial_cmp(b) ==
20954 // Some(Ordering::Equal)` iff `a == b`. The equality arm of the
20955 // 4-tile partition binds through the pointwise antisymmetric-
20956 // equality characterization (`a.leq(b) && b.leq(a) → a == b`,
20957 // pinned by `resource_limits_leq_is_antisymmetric`) to the
20958 // `PartialEq::eq` companion. Discriminates a regression that
20959 // drifted the Equal arm off the antisymmetric conjunction.
20960 for &a in STRICT_ORDER_ROSTER {
20961 for &b in STRICT_ORDER_ROSTER {
20962 let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
20963 let via_eq = a == b;
20964 assert_eq!(
20965 via_partial_cmp, via_eq,
20966 "equal-iff-eq failed on ({a:?}, {b:?})",
20967 );
20968 }
20969 }
20970 }
20971
20972 #[test]
20973 fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
20974 // Bottom-to-top-pole ordering pin — every strictly-ascending
20975 // pair on the shipped preset chain (EMPTY <= DEFAULT <=
20976 // UNBOUNDED) folds to `Some(Ordering::Less)`. The three
20977 // shipped presets form a TOTALLY ORDERED chain on the
20978 // pointwise partial-order (every DEFAULT_MAX_* is a concrete
20979 // positive value strictly less than usize::MAX), so the
20980 // strict-less arm fires on every ordered pair drawn in
20981 // ascending direction.
20982 assert_eq!(
20983 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
20984 Some(Ordering::Less),
20985 );
20986 assert_eq!(
20987 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20988 Some(Ordering::Less),
20989 );
20990 assert_eq!(
20991 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
20992 Some(Ordering::Less),
20993 );
20994 }
20995
20996 #[test]
20997 fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
20998 // Top-to-bottom-pole ordering pin — every strictly-descending
20999 // pair on the shipped preset chain folds to
21000 // `Some(Ordering::Greater)`. The DUAL of the ascending pin
21001 // one DIRECTION axis over — together the two pins close the
21002 // (ascending, descending) direction pair on the shipped
21003 // preset chain at the ordering-direction refinement.
21004 assert_eq!(
21005 DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21006 Some(Ordering::Greater),
21007 );
21008 assert_eq!(
21009 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21010 Some(Ordering::Greater),
21011 );
21012 assert_eq!(
21013 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21014 Some(Ordering::Greater),
21015 );
21016 }
21017
21018 #[test]
21019 fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
21020 // Antichain load-bearing arm — the two hand-authored
21021 // asymmetric postures sit on distinct branches (each is
21022 // smaller on three axes and larger on the other three), so
21023 // NEITHER pointwise-domination direction closes and the
21024 // fall-through `None` arm fires in both orderings. The DIRECT
21025 // LIFT of `is_incomparable_holds_on_the_hand_authored_antichain_pair`
21026 // one PROJECTION axis over on the SAME hand-authored pair —
21027 // where the projection folds TRUE, THIS method folds to
21028 // `None`. The two are the negation-paired sides of the
21029 // (Some(_), None) two-cell partition of `Option<Ordering>`.
21030 assert_eq!(
21031 HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
21032 None,
21033 );
21034 assert_eq!(
21035 HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
21036 None,
21037 );
21038 }
21039
21040 #[test]
21041 fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
21042 // Const-fn pin — the full-verdict projection is evaluable in
21043 // const context, so a caller can pin an ordering-direction
21044 // identity at compile time. Sibling of the const-fn
21045 // evaluability pins on `leq` / `geq` / `lt` / `gt` /
21046 // `is_incomparable` / `is_comparable` one COVER axis over on
21047 // the pairwise-relation surface.
21048 //
21049 // `matches!` in const context requires the pattern's variants
21050 // to be const-constructible — `Ordering::Less` / `Equal` /
21051 // `Greater` are plain enum constructors, so the assertions
21052 // below stay inside stable const-fn scope. A regression to a
21053 // runtime `fn` here would fail the `const _: () = assert!(
21054 // matches!(...))` bindings below at compile time.
21055 const _: () = assert!(matches!(
21056 EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21057 Some(Ordering::Equal),
21058 ));
21059 const _: () = assert!(matches!(
21060 EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21061 Some(Ordering::Less),
21062 ));
21063 const _: () = assert!(matches!(
21064 UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
21065 Some(Ordering::Greater),
21066 ));
21067 const _: () = assert!(matches!(
21068 DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
21069 Some(Ordering::Less),
21070 ));
21071 const _: () = assert!(matches!(
21072 EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
21073 Some(Ordering::Less),
21074 ));
21075 }
21076
21077 #[test]
21078 fn resource_limits_is_chain_empty_slice_is_vacuously_true() {
21079 // Empty-slice vacuous truth — the empty conjunction is vacuously
21080 // true; the empty slice contains no distinct pairs to reject.
21081 // Peer of `is_lower_bound_of(&[]) == true`'s empty-slice identity
21082 // one PRIMITIVE-KIND axis over on the (leq, comparable) ×
21083 // (pairwise, N-ary) primitive surface.
21084 assert!(ResourceLimits::is_chain(&[]));
21085 }
21086
21087 #[test]
21088 fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
21089 // Empty-slice vacuous truth — the empty conjunction is vacuously
21090 // true; the empty slice contains no distinct pairs to reject.
21091 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21092 // COVER-COMPLEMENT axis over: both cells of the (chain, antichain)
21093 // set-level pair AGREE at the empty slice on the vacuous-truth
21094 // verdict — the two cells coincide at the empty face of the
21095 // set-level verdict surface.
21096 assert!(ResourceLimits::is_antichain(&[]));
21097 }
21098
21099 #[test]
21100 fn resource_limits_is_chain_singleton_is_vacuously_true() {
21101 // Singleton vacuous truth — a one-element slice contains no
21102 // distinct pairs, so the outer-and-inner-index conjunction never
21103 // enters the inner loop and returns `true` vacuously. Pinned on
21104 // every canonical preset AND on both hand-authored asymmetric
21105 // postures so the vacuous-truth verdict holds regardless of the
21106 // singleton element's lattice position.
21107 assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21108 assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
21109 assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
21110 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
21111 assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
21112 }
21113
21114 #[test]
21115 fn resource_limits_is_antichain_singleton_is_vacuously_true() {
21116 // Singleton vacuous truth — a one-element slice contains no
21117 // distinct pairs, so the outer-and-inner-index conjunction never
21118 // enters the inner loop and returns `true` vacuously. Peer of
21119 // `is_chain`'s singleton-vacuous-truth identity: both cells AGREE
21120 // at singleton slices — the (chain, antichain) verdict surface
21121 // coincides at every slice with strictly fewer than two distinct
21122 // pairs.
21123 assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21124 assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
21125 assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
21126 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
21127 assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
21128 }
21129
21130 #[test]
21131 fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
21132 // Diagonal-duplicate identity — `is_comparable` is REFLEXIVE
21133 // (`a.is_comparable(a) == true`), so the single distinct index
21134 // pair `(0, 1)` binds `a.is_comparable(a) == true` and the
21135 // conjunction holds at every diagonal-duplicate slice.
21136 // Distinguishes it from `is_antichain` one COVER-COMPLEMENT axis
21137 // over, whose diagonal-duplicate verdict is `false` via
21138 // `is_incomparable`'s IRREFLEXIVITY.
21139 assert!(ResourceLimits::is_chain(&[
21140 DEFAULT_RESOURCE_LIMITS,
21141 DEFAULT_RESOURCE_LIMITS,
21142 ]));
21143 assert!(ResourceLimits::is_chain(&[
21144 HAND_AUTHORED_MID_POSTURE,
21145 HAND_AUTHORED_MID_POSTURE,
21146 ]));
21147 }
21148
21149 #[test]
21150 fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
21151 // Diagonal-duplicate rejection — `is_incomparable` is
21152 // IRREFLEXIVE (`a.is_incomparable(a) == false`), so the single
21153 // distinct index pair `(0, 1)` binds `a.is_incomparable(a) ==
21154 // false` and the conjunction rejects at every diagonal-duplicate
21155 // slice. The reflexivity/irreflexivity divergence between the
21156 // two pair-level projections PROPAGATES to the set-level
21157 // projections at any slice with a duplicated element — the two
21158 // set-level cells DIVERGE at every diagonal-duplicate slice, the
21159 // mirror of their AGREEMENT at every empty-or-singleton slice
21160 // one CARDINALITY axis over.
21161 assert!(!ResourceLimits::is_antichain(&[
21162 DEFAULT_RESOURCE_LIMITS,
21163 DEFAULT_RESOURCE_LIMITS,
21164 ]));
21165 assert!(!ResourceLimits::is_antichain(&[
21166 HAND_AUTHORED_MID_POSTURE,
21167 HAND_AUTHORED_MID_POSTURE,
21168 ]));
21169 }
21170
21171 #[test]
21172 fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
21173 // Ordered-chain closure — the shipped-preset triple on the
21174 // bounded-lattice diagonal is an ascending chain (`EMPTY <=
21175 // DEFAULT <= UNBOUNDED` pointwise), so every distinct pair among
21176 // the three is comparable. Pinned in every permutation of the
21177 // triple so the doubly-indexed all-pairs walk closes on every
21178 // enumeration of the same chain.
21179 assert!(ResourceLimits::is_chain(&[
21180 EMPTY_RESOURCE_LIMITS,
21181 DEFAULT_RESOURCE_LIMITS,
21182 UNBOUNDED_RESOURCE_LIMITS,
21183 ]));
21184 assert!(ResourceLimits::is_chain(&[
21185 UNBOUNDED_RESOURCE_LIMITS,
21186 DEFAULT_RESOURCE_LIMITS,
21187 EMPTY_RESOURCE_LIMITS,
21188 ]));
21189 assert!(ResourceLimits::is_chain(&[
21190 DEFAULT_RESOURCE_LIMITS,
21191 EMPTY_RESOURCE_LIMITS,
21192 UNBOUNDED_RESOURCE_LIMITS,
21193 ]));
21194 }
21195
21196 #[test]
21197 fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
21198 // Chain rejection — the shipped-preset pair on the bounded-
21199 // lattice diagonal is a strict chain (`EMPTY.lt(DEFAULT)`), so
21200 // the single distinct pair is COMPARABLE and the antichain
21201 // conjunction rejects. The DIRECT FALSIFICATION of
21202 // `is_chain_holds_on_the_shipped_preset_triple` one CELL axis
21203 // over on the SAME lattice diagonal — the two set-level cells
21204 // carry the negation-paired verdict at every strictly-chained
21205 // slice, the mirror of their AGREEMENT at every empty-or-
21206 // singleton slice one CARDINALITY axis over.
21207 assert!(!ResourceLimits::is_antichain(&[
21208 EMPTY_RESOURCE_LIMITS,
21209 DEFAULT_RESOURCE_LIMITS,
21210 ]));
21211 assert!(!ResourceLimits::is_antichain(&[
21212 DEFAULT_RESOURCE_LIMITS,
21213 UNBOUNDED_RESOURCE_LIMITS,
21214 ]));
21215 assert!(!ResourceLimits::is_antichain(&[
21216 EMPTY_RESOURCE_LIMITS,
21217 DEFAULT_RESOURCE_LIMITS,
21218 UNBOUNDED_RESOURCE_LIMITS,
21219 ]));
21220 }
21221
21222 #[test]
21223 fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
21224 // Antichain rejection — the two hand-authored asymmetric
21225 // postures sit on distinct branches (each is smaller on three
21226 // axes and larger on the other three), so neither pointwise-
21227 // domination direction closes and the pair is INCOMPARABLE. The
21228 // chain conjunction rejects at the first incomparable pair.
21229 // Pinned in both orderings so the doubly-indexed pair walk
21230 // rejects regardless of index order (the diagonal-adjacent
21231 // pair-level rejection is symmetric on argument swap).
21232 assert!(!ResourceLimits::is_chain(&[
21233 HAND_AUTHORED_MID_POSTURE,
21234 HAND_AUTHORED_OTHER_POSTURE,
21235 ]));
21236 assert!(!ResourceLimits::is_chain(&[
21237 HAND_AUTHORED_OTHER_POSTURE,
21238 HAND_AUTHORED_MID_POSTURE,
21239 ]));
21240 }
21241
21242 #[test]
21243 fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
21244 // Hand-authored antichain closure — the two hand-authored
21245 // asymmetric postures sit on distinct branches (each is smaller
21246 // on three axes and larger on the other three), so neither
21247 // pointwise-domination direction closes and the pair is
21248 // INCOMPARABLE. The antichain conjunction accepts every distinct
21249 // pair of the slice. DIRECT LIFT of `is_chain`'s antichain
21250 // rejection one CELL axis over on the SAME hand-authored pair —
21251 // pinning BOTH cells' verdicts on the SAME load-bearing
21252 // antichain pair closes the (chain, antichain) two-cell face on
21253 // arity-2 slices as a substrate-level EXCLUSIVITY THEOREM.
21254 assert!(ResourceLimits::is_antichain(&[
21255 HAND_AUTHORED_MID_POSTURE,
21256 HAND_AUTHORED_OTHER_POSTURE,
21257 ]));
21258 assert!(ResourceLimits::is_antichain(&[
21259 HAND_AUTHORED_OTHER_POSTURE,
21260 HAND_AUTHORED_MID_POSTURE,
21261 ]));
21262 }
21263
21264 #[test]
21265 fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
21266 // Mixed-set rejection — both `(DEFAULT, MID)` and `(DEFAULT,
21267 // OTHER)` are comparable pairs (DEFAULT's shipped `DEFAULT_MAX_*`
21268 // per-axis values are all much larger than either hand-authored
21269 // posture's, so DEFAULT dominates both pointwise), but the
21270 // `(MID, OTHER)` pair is the hand-authored INCOMPARABLE pair.
21271 // The set-level chain verdict is stricter than the union of its
21272 // pair-level verdicts — one antichain pair anywhere in the
21273 // slice falsifies the whole projection.
21274 assert!(!ResourceLimits::is_chain(&[
21275 DEFAULT_RESOURCE_LIMITS,
21276 HAND_AUTHORED_MID_POSTURE,
21277 HAND_AUTHORED_OTHER_POSTURE,
21278 ]));
21279 }
21280
21281 #[test]
21282 fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
21283 // Mixed-set rejection — even though the second-and-third pair
21284 // is incomparable (the hand-authored antichain pair), the first
21285 // pair (`DEFAULT`-and-`MID`) is comparable, so the whole-set
21286 // conjunction rejects. Pins the three-cell (chain, antichain,
21287 // mixed) set-level face at the mixed-set cell: the same slice
21288 // both `is_chain` AND `is_antichain` reject at, and the third
21289 // cell of the three-cell face the two set-level projections
21290 // carry the ends of.
21291 assert!(!ResourceLimits::is_antichain(&[
21292 DEFAULT_RESOURCE_LIMITS,
21293 HAND_AUTHORED_MID_POSTURE,
21294 HAND_AUTHORED_OTHER_POSTURE,
21295 ]));
21296 }
21297
21298 #[test]
21299 fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
21300 // De Morgan mirror at arity 2 — on any two-element slice `[a,
21301 // b]` with `a != b`, the set-level (chain, antichain) two-cell
21302 // face reduces to the pair-level (comparable, incomparable)
21303 // two-cell partition and the two cells become MUTUALLY
21304 // EXCLUSIVE (the mixed cell requires ≥3 elements to open). The
21305 // set-level De Morgan mirror of the pair-level identity
21306 // `a.is_comparable(b) == !a.is_incomparable(b)` `is_comparable`
21307 // pins one CARDINALITY axis over.
21308 let presets: [ResourceLimits; 5] = [
21309 EMPTY_RESOURCE_LIMITS,
21310 DEFAULT_RESOURCE_LIMITS,
21311 UNBOUNDED_RESOURCE_LIMITS,
21312 HAND_AUTHORED_MID_POSTURE,
21313 HAND_AUTHORED_OTHER_POSTURE,
21314 ];
21315 for a in presets {
21316 for b in presets {
21317 if a == b {
21318 continue;
21319 }
21320 let pair = [a, b];
21321 let chain = ResourceLimits::is_chain(&pair);
21322 let antichain = ResourceLimits::is_antichain(&pair);
21323 assert_ne!(
21324 chain, antichain,
21325 "distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
21326 );
21327 }
21328 }
21329 }
21330
21331 #[test]
21332 fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
21333 // Empty-and-singleton agreement — the two set-level projections
21334 // carry the SAME vacuous-truth verdict at every slice with
21335 // strictly fewer than two distinct pairs (empty AND singleton).
21336 // Pinned as a substrate-level identity so a future rewrite of
21337 // either projection cannot silently drift from the shared
21338 // vacuous-truth verdict.
21339 assert_eq!(
21340 ResourceLimits::is_chain(&[]),
21341 ResourceLimits::is_antichain(&[]),
21342 );
21343 let presets: [ResourceLimits; 5] = [
21344 EMPTY_RESOURCE_LIMITS,
21345 DEFAULT_RESOURCE_LIMITS,
21346 UNBOUNDED_RESOURCE_LIMITS,
21347 HAND_AUTHORED_MID_POSTURE,
21348 HAND_AUTHORED_OTHER_POSTURE,
21349 ];
21350 for a in presets {
21351 let single = [a];
21352 assert_eq!(
21353 ResourceLimits::is_chain(&single),
21354 ResourceLimits::is_antichain(&single),
21355 "singleton agreement failed on {a:?}",
21356 );
21357 }
21358 }
21359
21360 #[test]
21361 fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
21362 // Const-fn pin — the set-level chain projection is evaluable in
21363 // const context, so a caller can pin a chain-membership
21364 // identity at compile time. Sibling of the const-fn
21365 // evaluability pins on `is_comparable` one CARDINALITY axis
21366 // over and on `is_lower_bound_of` / `is_upper_bound_of` one
21367 // PRIMITIVE-KIND axis over on the N-ary aggregation surface.
21368 const _: () = assert!(ResourceLimits::is_chain(&[]));
21369 const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
21370 const _: () = assert!(ResourceLimits::is_chain(&[
21371 EMPTY_RESOURCE_LIMITS,
21372 DEFAULT_RESOURCE_LIMITS,
21373 UNBOUNDED_RESOURCE_LIMITS,
21374 ]));
21375 }
21376
21377 #[test]
21378 fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
21379 // Const-fn pin — the set-level antichain projection is
21380 // evaluable in const context, so a caller can pin an antichain-
21381 // membership identity at compile time. Sibling of the const-fn
21382 // evaluability pins on `is_incomparable` one CARDINALITY axis
21383 // over and on `is_chain` one COVER-COMPLEMENT axis over.
21384 const _: () = assert!(ResourceLimits::is_antichain(&[]));
21385 const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
21386 const _: () = assert!(ResourceLimits::is_antichain(&[
21387 HAND_AUTHORED_MID_POSTURE,
21388 HAND_AUTHORED_OTHER_POSTURE,
21389 ]));
21390 }
21391
21392 #[test]
21393 fn resource_limits_is_mixed_empty_slice_is_false() {
21394 // Empty-slice contract — is_chain and is_antichain BOTH return
21395 // `true` vacuously, so `!true && !true == false`. The empty
21396 // slice is NOT mixed. Sibling posture to the vacuous-truth
21397 // verdicts of is_chain + is_antichain one CELL axis over on the
21398 // (chain, antichain, mixed) three-cell face.
21399 assert!(!ResourceLimits::is_mixed(&[]));
21400 }
21401
21402 #[test]
21403 fn resource_limits_is_mixed_singleton_is_false() {
21404 // Singleton contract — a one-element slice contains no distinct
21405 // pairs; is_chain + is_antichain BOTH return `true` vacuously.
21406 // Pinned on every canonical preset AND on both hand-authored
21407 // asymmetric postures so the false-at-singleton verdict holds
21408 // regardless of the singleton element's lattice position.
21409 assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21410 assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
21411 assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
21412 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
21413 assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
21414 }
21415
21416 #[test]
21417 fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
21418 // Diagonal-duplicate contract — is_chain returns `true` via
21419 // is_comparable's REFLEXIVITY; is_antichain returns `false` via
21420 // is_incomparable's IRREFLEXIVITY; the conjunction
21421 // `!true && !false == false`. The diagonal-duplicate slice is a
21422 // CHAIN (per the reflexivity arm), not mixed.
21423 assert!(!ResourceLimits::is_mixed(&[
21424 DEFAULT_RESOURCE_LIMITS,
21425 DEFAULT_RESOURCE_LIMITS,
21426 ]));
21427 assert!(!ResourceLimits::is_mixed(&[
21428 HAND_AUTHORED_MID_POSTURE,
21429 HAND_AUTHORED_MID_POSTURE,
21430 ]));
21431 }
21432
21433 #[test]
21434 fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
21435 // Chain rejection — the shipped-preset triple on the bounded-
21436 // lattice diagonal is an ascending chain, is_chain returns
21437 // `true`, and `!true && …` short-circuits to `false`. A PURE
21438 // chain is not mixed. Pinned in every permutation of the triple
21439 // so the projection factors through the ordering-agnostic
21440 // sibling is_chain verdict on the same chain.
21441 assert!(!ResourceLimits::is_mixed(&[
21442 EMPTY_RESOURCE_LIMITS,
21443 DEFAULT_RESOURCE_LIMITS,
21444 UNBOUNDED_RESOURCE_LIMITS,
21445 ]));
21446 assert!(!ResourceLimits::is_mixed(&[
21447 UNBOUNDED_RESOURCE_LIMITS,
21448 DEFAULT_RESOURCE_LIMITS,
21449 EMPTY_RESOURCE_LIMITS,
21450 ]));
21451 }
21452
21453 #[test]
21454 fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
21455 // Antichain rejection — the hand-authored antichain pair binds
21456 // is_antichain to `true`, and `!… && !true == false`. A PURE
21457 // antichain is not mixed. Pinned in both orderings so the
21458 // projection factors through the argument-swap-symmetric
21459 // sibling is_antichain verdict on the same antichain.
21460 assert!(!ResourceLimits::is_mixed(&[
21461 HAND_AUTHORED_MID_POSTURE,
21462 HAND_AUTHORED_OTHER_POSTURE,
21463 ]));
21464 assert!(!ResourceLimits::is_mixed(&[
21465 HAND_AUTHORED_OTHER_POSTURE,
21466 HAND_AUTHORED_MID_POSTURE,
21467 ]));
21468 }
21469
21470 #[test]
21471 fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
21472 ) {
21473 // Mixed-set closure — LOAD-BEARING `true`-arm catch. The
21474 // (DEFAULT, MID) pair is comparable (falsifying is_antichain)
21475 // while the (MID, OTHER) pair is incomparable (falsifying
21476 // is_chain); the conjunction `!false && !false == true`. This
21477 // is the SAME fixture the sibling is_chain + is_antichain BOTH
21478 // reject at — the DISCRIMINATING positive-arm catch for the
21479 // (mixed) cell of the three-cell face the two set-level
21480 // projections carry the ends of.
21481 assert!(ResourceLimits::is_mixed(&[
21482 DEFAULT_RESOURCE_LIMITS,
21483 HAND_AUTHORED_MID_POSTURE,
21484 HAND_AUTHORED_OTHER_POSTURE,
21485 ]));
21486 }
21487
21488 #[test]
21489 fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
21490 ) {
21491 // Trichotomy exhaustiveness at arity ≥2 with pairwise-distinct
21492 // elements — EXACTLY ONE of is_chain, is_antichain, is_mixed
21493 // returns `true` on every pair OR triple drawn from the shipped
21494 // presets whose elements are pairwise distinct. The three cells
21495 // partition the set-level verdict surface at every non-
21496 // degenerate slice. Substrate-level identity: pinning it here
21497 // catches a future rewrite of any of the three projections that
21498 // silently drifts from the partition contract.
21499 let presets: [ResourceLimits; 5] = [
21500 EMPTY_RESOURCE_LIMITS,
21501 DEFAULT_RESOURCE_LIMITS,
21502 UNBOUNDED_RESOURCE_LIMITS,
21503 HAND_AUTHORED_MID_POSTURE,
21504 HAND_AUTHORED_OTHER_POSTURE,
21505 ];
21506 // Arity-2 sweep — the mixed cell CANNOT open at arity 2 (only
21507 // one distinct pair), so is_mixed is always false and the
21508 // partition reduces to the (chain, antichain) two-cell face
21509 // that is_chain + is_antichain already pinned MUTUALLY
21510 // EXCLUSIVE. Cross-check that the three-cell projection
21511 // AGREES with the two-cell projection at arity 2.
21512 for a in presets {
21513 for b in presets {
21514 if a == b {
21515 continue;
21516 }
21517 let pair = [a, b];
21518 let chain = ResourceLimits::is_chain(&pair);
21519 let antichain = ResourceLimits::is_antichain(&pair);
21520 let mixed = ResourceLimits::is_mixed(&pair);
21521 let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21522 assert_eq!(
21523 true_count, 1,
21524 "trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21525 );
21526 assert!(
21527 !mixed,
21528 "is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
21529 );
21530 }
21531 }
21532 // Arity-3 sweep on distinct triples — the mixed cell opens
21533 // whenever the triple carries both a comparable and an
21534 // incomparable pair (e.g. DEFAULT + MID + OTHER above).
21535 for a in presets {
21536 for b in presets {
21537 for c in presets {
21538 if a == b || b == c || a == c {
21539 continue;
21540 }
21541 let triple = [a, b, c];
21542 let chain = ResourceLimits::is_chain(&triple);
21543 let antichain = ResourceLimits::is_antichain(&triple);
21544 let mixed = ResourceLimits::is_mixed(&triple);
21545 let true_count =
21546 usize::from(chain) + usize::from(antichain) + usize::from(mixed);
21547 assert_eq!(
21548 true_count, 1,
21549 "trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
21550 );
21551 }
21552 }
21553 }
21554 }
21555
21556 #[test]
21557 fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
21558 // Const-fn pin — the set-level mixed projection is evaluable
21559 // in const context, so a caller can pin a mixed-set identity
21560 // at compile time. Sibling of the const-fn evaluability pins
21561 // on is_chain + is_antichain one CELL axis over.
21562 const _: () = assert!(!ResourceLimits::is_mixed(&[]));
21563 const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
21564 const _: () = assert!(ResourceLimits::is_mixed(&[
21565 DEFAULT_RESOURCE_LIMITS,
21566 HAND_AUTHORED_MID_POSTURE,
21567 HAND_AUTHORED_OTHER_POSTURE,
21568 ]));
21569 }
21570
21571 // ── ResourceLimits::is_ascending / ::is_descending — sequence-level ──
21572
21573 #[test]
21574 fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
21575 // Empty-slice vacuous truth — the empty conjunction is vacuously
21576 // true; the empty slice contains no consecutive pairs to reject.
21577 // Peer of `is_chain(&[]) == true`'s empty-slice identity one
21578 // CONSECUTIVE-VS-ALL-PAIRS axis over: both cells of the (set-
21579 // level, sequence-level) projection pair AGREE at the empty slice
21580 // on the vacuous-truth verdict.
21581 assert!(ResourceLimits::is_ascending(&[]));
21582 }
21583
21584 #[test]
21585 fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
21586 // Peer of is_ascending's empty-slice identity one PAIR-LEVEL-
21587 // PRIMITIVE axis over: both cells of the (ascending, descending)
21588 // sequence-level pair AGREE at the empty slice.
21589 assert!(ResourceLimits::is_descending(&[]));
21590 }
21591
21592 #[test]
21593 fn resource_limits_is_ascending_singleton_is_vacuously_true() {
21594 // Singleton vacuous truth — a one-element slice contains no
21595 // consecutive pairs, so the walk never enters the loop and
21596 // returns `true` vacuously. Pinned on every canonical preset AND
21597 // on both hand-authored asymmetric postures.
21598 assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21599 assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
21600 assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
21601 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
21602 assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
21603 }
21604
21605 #[test]
21606 fn resource_limits_is_descending_singleton_is_vacuously_true() {
21607 assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21608 assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
21609 assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
21610 assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
21611 assert!(ResourceLimits::is_descending(&[
21612 HAND_AUTHORED_OTHER_POSTURE
21613 ]));
21614 }
21615
21616 #[test]
21617 fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
21618 // Diagonal-duplicate identity — `leq` is REFLEXIVE
21619 // (`a.leq(a) == true` via each axis's `<=` reflexivity), so the
21620 // single consecutive pair `(0, 1)` binds `a.leq(a) == true` and
21621 // the conjunction holds at every diagonal-duplicate slice.
21622 // AGREES with is_chain's diagonal-duplicate verdict — the two
21623 // projections diverge only where the ORDER matters, and
21624 // duplicated elements carry no ordering information.
21625 assert!(ResourceLimits::is_ascending(&[
21626 DEFAULT_RESOURCE_LIMITS,
21627 DEFAULT_RESOURCE_LIMITS,
21628 ]));
21629 assert!(ResourceLimits::is_ascending(&[
21630 HAND_AUTHORED_MID_POSTURE,
21631 HAND_AUTHORED_MID_POSTURE,
21632 ]));
21633 }
21634
21635 #[test]
21636 fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
21637 // Diagonal-duplicate identity — `geq` is REFLEXIVE, so the
21638 // single consecutive pair `(0, 1)` binds `a.geq(a) == true`.
21639 // AGREES with is_ascending's diagonal-duplicate verdict — both
21640 // leq and geq carry reflexivity, so the sequence-level (T, T)
21641 // corner opens at every diagonal-duplicate slice, distinct from
21642 // the set-level (T, F) verdict is_chain / is_antichain carry on
21643 // the same slice.
21644 assert!(ResourceLimits::is_descending(&[
21645 DEFAULT_RESOURCE_LIMITS,
21646 DEFAULT_RESOURCE_LIMITS,
21647 ]));
21648 assert!(ResourceLimits::is_descending(&[
21649 HAND_AUTHORED_MID_POSTURE,
21650 HAND_AUTHORED_MID_POSTURE,
21651 ]));
21652 }
21653
21654 #[test]
21655 fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
21656 // Ordered ascending closure — the shipped-preset triple on the
21657 // bounded-lattice diagonal enumerated leq-ascending has
21658 // `EMPTY.leq(DEFAULT) == true` and `DEFAULT.leq(UNBOUNDED) ==
21659 // true`, so every consecutive pair passes the conjunction.
21660 // Pinned on the arity-2 prefix AND on the full arity-3 chain so
21661 // the walk closes at every non-degenerate ascending enumeration
21662 // length.
21663 assert!(ResourceLimits::is_ascending(&[
21664 EMPTY_RESOURCE_LIMITS,
21665 DEFAULT_RESOURCE_LIMITS,
21666 ]));
21667 assert!(ResourceLimits::is_ascending(&[
21668 DEFAULT_RESOURCE_LIMITS,
21669 UNBOUNDED_RESOURCE_LIMITS,
21670 ]));
21671 assert!(ResourceLimits::is_ascending(&[
21672 EMPTY_RESOURCE_LIMITS,
21673 DEFAULT_RESOURCE_LIMITS,
21674 UNBOUNDED_RESOURCE_LIMITS,
21675 ]));
21676 }
21677
21678 #[test]
21679 fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
21680 // Ordered descending closure — the reversed enumeration is a
21681 // geq-monotone descending sequence. The DIRECT MIRROR of
21682 // is_ascending's ascending closure one PERMUTATION axis over.
21683 assert!(ResourceLimits::is_descending(&[
21684 DEFAULT_RESOURCE_LIMITS,
21685 EMPTY_RESOURCE_LIMITS,
21686 ]));
21687 assert!(ResourceLimits::is_descending(&[
21688 UNBOUNDED_RESOURCE_LIMITS,
21689 DEFAULT_RESOURCE_LIMITS,
21690 ]));
21691 assert!(ResourceLimits::is_descending(&[
21692 UNBOUNDED_RESOURCE_LIMITS,
21693 DEFAULT_RESOURCE_LIMITS,
21694 EMPTY_RESOURCE_LIMITS,
21695 ]));
21696 }
21697
21698 #[test]
21699 fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
21700 // Order-sensitivity — the reversed enumeration of the same
21701 // shipped-preset chain has `UNBOUNDED.leq(DEFAULT) == false` at
21702 // the first consecutive pair. is_chain accepts BOTH orderings
21703 // (order-INSENSITIVE); this projection accepts only the leq-
21704 // sorted one — the DISCRIMINATING arm between the sequence-level
21705 // and set-level projections.
21706 assert!(!ResourceLimits::is_ascending(&[
21707 UNBOUNDED_RESOURCE_LIMITS,
21708 DEFAULT_RESOURCE_LIMITS,
21709 EMPTY_RESOURCE_LIMITS,
21710 ]));
21711 }
21712
21713 #[test]
21714 fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
21715 // The direct falsification of is_ascending's ascending closure
21716 // one CELL axis over on the SAME slice.
21717 assert!(!ResourceLimits::is_descending(&[
21718 EMPTY_RESOURCE_LIMITS,
21719 DEFAULT_RESOURCE_LIMITS,
21720 UNBOUNDED_RESOURCE_LIMITS,
21721 ]));
21722 }
21723
21724 #[test]
21725 fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
21726 // Non-monotone chain permutation — [DEFAULT, EMPTY, UNBOUNDED]
21727 // is a permutation of the shipped-preset chain, so it IS a chain
21728 // (is_chain accepts it), but the first consecutive pair binds
21729 // `DEFAULT.leq(EMPTY) == false`, so the sequence-level ascending
21730 // walk rejects. Also is_descending rejects — the third
21731 // consecutive pair binds `EMPTY.geq(UNBOUNDED) == false`. Pins
21732 // the DISCRIMINATING arm between the sequence-level and set-
21733 // level projections at a slice that is a chain but neither
21734 // ascending nor descending.
21735 let slice = [
21736 DEFAULT_RESOURCE_LIMITS,
21737 EMPTY_RESOURCE_LIMITS,
21738 UNBOUNDED_RESOURCE_LIMITS,
21739 ];
21740 assert!(ResourceLimits::is_chain(&slice));
21741 assert!(!ResourceLimits::is_ascending(&slice));
21742 assert!(!ResourceLimits::is_descending(&slice));
21743 }
21744
21745 #[test]
21746 fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
21747 // The two hand-authored asymmetric postures are incomparable
21748 // (neither MID.leq(OTHER) nor OTHER.leq(MID)), so the single
21749 // consecutive pair rejects. Pinned in both orderings — the pair-
21750 // level leq verdict rejects on the antichain pair regardless of
21751 // which side is `self`.
21752 assert!(!ResourceLimits::is_ascending(&[
21753 HAND_AUTHORED_MID_POSTURE,
21754 HAND_AUTHORED_OTHER_POSTURE,
21755 ]));
21756 assert!(!ResourceLimits::is_ascending(&[
21757 HAND_AUTHORED_OTHER_POSTURE,
21758 HAND_AUTHORED_MID_POSTURE,
21759 ]));
21760 }
21761
21762 #[test]
21763 fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
21764 assert!(!ResourceLimits::is_descending(&[
21765 HAND_AUTHORED_MID_POSTURE,
21766 HAND_AUTHORED_OTHER_POSTURE,
21767 ]));
21768 assert!(!ResourceLimits::is_descending(&[
21769 HAND_AUTHORED_OTHER_POSTURE,
21770 HAND_AUTHORED_MID_POSTURE,
21771 ]));
21772 }
21773
21774 #[test]
21775 fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
21776 // Transitivity theorem — for every slice `postures`,
21777 // `is_ascending(postures) == true` implies `is_chain(postures)
21778 // == true`. The pointwise partial order is TRANSITIVE, so a
21779 // consecutive-pair leq-chain closes under transitive composition
21780 // into the all-pairs leq-chain. Substrate-level THEOREM: the
21781 // sequence-level ascending verdict is strictly STRONGER than the
21782 // set-level chain verdict. Same identity for is_descending —
21783 // both sequence-level projections imply the set-level chain
21784 // projection.
21785 let presets: [ResourceLimits; 5] = [
21786 EMPTY_RESOURCE_LIMITS,
21787 DEFAULT_RESOURCE_LIMITS,
21788 UNBOUNDED_RESOURCE_LIMITS,
21789 HAND_AUTHORED_MID_POSTURE,
21790 HAND_AUTHORED_OTHER_POSTURE,
21791 ];
21792 // Every arity-2 slice from the 5×5 preset matrix witnesses the
21793 // implication in both directions of the (ascending, descending)
21794 // pair.
21795 for a in presets {
21796 for b in presets {
21797 let slice = [a, b];
21798 if ResourceLimits::is_ascending(&slice) {
21799 assert!(
21800 ResourceLimits::is_chain(&slice),
21801 "is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
21802 );
21803 }
21804 if ResourceLimits::is_descending(&slice) {
21805 assert!(
21806 ResourceLimits::is_chain(&slice),
21807 "is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
21808 );
21809 }
21810 }
21811 }
21812 // Load-bearing arity-3 witness — the shipped ascending chain
21813 // triple satisfies is_ascending AND is_chain; the descending
21814 // reversal satisfies is_descending AND is_chain.
21815 let asc = [
21816 EMPTY_RESOURCE_LIMITS,
21817 DEFAULT_RESOURCE_LIMITS,
21818 UNBOUNDED_RESOURCE_LIMITS,
21819 ];
21820 assert!(ResourceLimits::is_ascending(&asc));
21821 assert!(ResourceLimits::is_chain(&asc));
21822 let desc = [
21823 UNBOUNDED_RESOURCE_LIMITS,
21824 DEFAULT_RESOURCE_LIMITS,
21825 EMPTY_RESOURCE_LIMITS,
21826 ];
21827 assert!(ResourceLimits::is_descending(&desc));
21828 assert!(ResourceLimits::is_chain(&desc));
21829 }
21830
21831 #[test]
21832 fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
21833 // Empty-and-singleton agreement — the two sequence-level
21834 // projections carry the SAME vacuous-truth verdict at every
21835 // slice with fewer than two consecutive pairs. Pinned as a
21836 // substrate-level identity so a future rewrite of either
21837 // projection cannot silently drift from the shared vacuous-
21838 // truth verdict.
21839 assert_eq!(
21840 ResourceLimits::is_ascending(&[]),
21841 ResourceLimits::is_descending(&[]),
21842 );
21843 let presets: [ResourceLimits; 5] = [
21844 EMPTY_RESOURCE_LIMITS,
21845 DEFAULT_RESOURCE_LIMITS,
21846 UNBOUNDED_RESOURCE_LIMITS,
21847 HAND_AUTHORED_MID_POSTURE,
21848 HAND_AUTHORED_OTHER_POSTURE,
21849 ];
21850 for a in presets {
21851 let single = [a];
21852 assert_eq!(
21853 ResourceLimits::is_ascending(&single),
21854 ResourceLimits::is_descending(&single),
21855 "empty-and-singleton agreement failed on singleton {a:?}",
21856 );
21857 let dup = [a, a];
21858 assert_eq!(
21859 ResourceLimits::is_ascending(&dup),
21860 ResourceLimits::is_descending(&dup),
21861 "diagonal-duplicate agreement failed on {a:?}",
21862 );
21863 }
21864 }
21865
21866 #[test]
21867 fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
21868 // Const-fn pin — the sequence-level ascending projection is
21869 // evaluable in const context, so a caller can pin an ascending-
21870 // sequence identity at compile time.
21871 const _: () = assert!(ResourceLimits::is_ascending(&[]));
21872 const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
21873 const _: () = assert!(ResourceLimits::is_ascending(&[
21874 EMPTY_RESOURCE_LIMITS,
21875 DEFAULT_RESOURCE_LIMITS,
21876 UNBOUNDED_RESOURCE_LIMITS,
21877 ]));
21878 const _: () = assert!(!ResourceLimits::is_ascending(&[
21879 UNBOUNDED_RESOURCE_LIMITS,
21880 EMPTY_RESOURCE_LIMITS,
21881 ]));
21882 }
21883
21884 #[test]
21885 fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
21886 // Const-fn pin — sibling of is_ascending one PAIR-LEVEL-
21887 // PRIMITIVE axis over.
21888 const _: () = assert!(ResourceLimits::is_descending(&[]));
21889 const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
21890 const _: () = assert!(ResourceLimits::is_descending(&[
21891 UNBOUNDED_RESOURCE_LIMITS,
21892 DEFAULT_RESOURCE_LIMITS,
21893 EMPTY_RESOURCE_LIMITS,
21894 ]));
21895 const _: () = assert!(!ResourceLimits::is_descending(&[
21896 EMPTY_RESOURCE_LIMITS,
21897 UNBOUNDED_RESOURCE_LIMITS,
21898 ]));
21899 }
21900}